Skip to content

Send large files from a script

A large upload from a script goes to Dew in chunks, so a dropped connection costs one chunk instead of the whole file: run the script again and it carries on where it stopped. At the end you get the file’s shared link, the same as with a single-request upload.

Use this for files of a few gigabytes and up, such as video, disk images or database exports. Smaller files can go in one request, as in Send files from another system.

Before you start, create a service account and token with the Files: Upload and share permission. A personal token works too, for your own files.

An upload takes three calls, and the script below makes them for you:

  1. POST /api/upload-session with the file’s name and size. Dew answers with the upload’s session_id and how to send it: in one request when chunk_size is 0, otherwise in total_chunks chunks of chunk_size bytes.
  2. Send the bytes: PUT /api/upload-session/<session_id>/files/<session_id>/fast for one request, or PATCH /api/upload-session/<session_id>/files/<session_id>/chunk/<n> for each chunk, numbered from 1.
  3. POST /api/upload-session/<session_id>/finalize. The response carries the shared link.

An unfinished upload stays open for at least 12 hours after the last chunk arrived. Within that time, GET /api/upload-session/<session_id>/files/<session_id>/chunks lists the chunks Dew already has, so only the missing ones are sent again.

Save it as dew-upload.sh and make it executable (chmod +x dew-upload.sh). Change DEW to your Dew address. It runs on Linux or macOS, needs curl 7.76 or later and jq, and reads the token from the DEW_TOKEN environment variable.

#!/bin/sh
# Usage: dew-upload.sh <file>
DEW="https://files.example.com"
[ -n "$DEW_TOKEN" ] || { echo "Set DEW_TOKEN to the token" >&2; exit 1; }

file="$1"
[ -f "$file" ] || { echo "Usage: $0 <file>" >&2; exit 1; }
name=$(basename "$file")
size=$(wc -c < "$file" | tr -d ' ')
state="$file.dew-upload"
chunk="$file.dew-chunk"

api() { curl -sS --fail-with-body -H "Authorization: Bearer $DEW_TOKEN" "$@"; }
field() { printf '%s\n' "$1" | jq -r "$2"; }
fail() { echo "$1" >&2; exit 1; }

if [ -f "$state" ]; then
  # Resume: ask which chunks Dew already has.
  session=$(cat "$state")
  plan=$(api "$DEW/api/upload-session/$session/files/$session/chunks") ||
    fail "Can't resume: $plan. Delete $state to start again."
  [ "$(field "$plan" .data.total_bytes)" = "$size" ] ||
    fail "$file changed since the upload started. Delete $state to start again."
  have=$(field "$plan" '.data.completed_chunks[]')
  chunk_size=$(field "$plan" .data.chunk_size)
  total=$(field "$plan" .data.total_chunks)
else
  body=$(jq -n --arg n "$name" --argjson s "$size" \
    '{session_type: "single", files: [{filename: $n, relative_path: $n, size_bytes: $s, mime_type: "application/octet-stream"}]}')
  plan=$(api -X POST -H "Content-Type: application/json" -d "$body" "$DEW/api/upload-session") ||
    fail "Couldn't start the upload: $plan"
  session=$(field "$plan" .data.session_id)
  echo "$session" > "$state"
  have=""
  chunk_size=$(field "$plan" .data.artifact.chunk_size)
  total=$(field "$plan" .data.artifact.total_chunks)
fi
case "$chunk_size$total" in ''|*[!0-9]*) fail "Unexpected answer from Dew: $plan" ;; esac

if [ "$chunk_size" = 0 ]; then
  out=$(api -X PUT -T "$file" "$DEW/api/upload-session/$session/files/$session/fast") ||
    fail "Upload failed: $out. Run the script again to retry."
else
  n=1
  while [ "$n" -le "$total" ]; do
    if ! printf '%s\n' "$have" | grep -qx "$n"; then
      dd if="$file" of="$chunk" bs="$chunk_size" skip=$((n - 1)) count=1 2>/dev/null ||
        fail "Couldn't read chunk $n of $file"
      out=$(api --retry 5 --retry-all-errors -X PATCH -T "$chunk" \
        -H "X-Chunk-Offset: $(((n - 1) * chunk_size))" \
        "$DEW/api/upload-session/$session/files/$session/chunk/$n") ||
        fail "Chunk $n failed: $out. Run the script again to resume."
      echo "Sent chunk $n of $total"
    fi
    n=$((n + 1))
  done
  rm -f "$chunk"
fi

result=$(api --retry 5 -X POST -H "Content-Type: application/json" -d '{}' \
  "$DEW/api/upload-session/$session/finalize") ||
  fail "Couldn't finish the upload: $result. Run the script again to retry."
url=$(field "$result" '.data.files[0].url // empty')
[ -n "$url" ] || fail "Unexpected answer from Dew: $result"
rm -f "$state"
echo "$url"

Run it with the file to send:

DEW_TOKEN="dewsa_<token>" ./dew-upload.sh backup-2026-10-04.tar

It prints each chunk as it goes, then the shared link, and exits with 0 only when the upload finished. While an upload is unfinished, the script keeps its session_id in a file next to the upload, named after it with .dew-upload added, so it needs permission to write in that folder. Each chunk it sends is retried up to 5 times before the script stops.

To put the link on another of the service account’s domains, add domain: "files.example.com", before files: in the jq -n line.

The link is open to anyone who has it, unless you protect it when the upload starts. To protect it afterwards, email it to the recipient or change when it expires, use the file’s ID, the last part of the link (/f/<id>, or /p/<id> for a ZIP), with the steps in Send files from another system.

To protect it from the start, add link_type and protection before files: in the script’s jq -n line. For example, to have Dew email the link to the recipient once the upload finishes and ask them for a code sent to their address:

link_type: "protected", protection: {method: "email", email_recipients: [{email: "anna@customer.com"}], email_message: "Your backup is ready"},

For a password Dew chooses, use protection: {method: "password", auto_generate: true}. Dew returns the password only once, as data.generated_password when the upload starts, so add this line after session=$(field "$plan" .data.session_id) to print it:

echo "Password: $(field "$plan" .data.generated_password)"

Open the link in a private browser window: it shows the file’s name and size and downloads it. To try resuming, stop the script with Ctrl+C partway through and run it again: it skips the chunks already sent.

What you see What to do
“Couldn’t start the upload” with 413 The file is larger than the domain allows. Ask your admin to raise the domain’s upload limit.
“Couldn’t start the upload” with 507 The server is out of storage space. Ask your admin to free space or add storage.
“Can’t resume” with “This upload has expired. Start it again.” The upload was left unfinished for more than 12 hours. Delete the .dew-upload file and run the script again.
“Can’t resume” with “Session not found” The upload was canceled or removed. Delete the .dew-upload file and run the script again.
“Can’t resume” or “Chunk n failed” with “Session is not active” The upload was already finished, or it failed. Delete the .dew-upload file and run the script again.
“changed since the upload started” The file was changed or replaced after the upload began. Delete the .dew-upload file and run the script again.
“Couldn’t finish the upload” with “This upload is still being finished.” Dew is still finishing it. Run the script again in a moment: it returns the link.
403 with “This API token doesn’t have permission for this action.” Add Files: Upload and share to the service account.

Every endpoint used here is in the Uploads API reference, which also covers sending several files or a folder behind one link.