Skip to content

fix(drive): resumable downloads + async folder-zip build - #359

Merged
safwansamsudeen merged 4 commits into
developfrom
fix/drive-resumable-downloads
Jul 28, 2026
Merged

fix(drive): resumable downloads + async folder-zip build#359
safwansamsudeen merged 4 commits into
developfrom
fix/drive-resumable-downloads

Conversation

@safwansamsudeen

Copy link
Copy Markdown
Collaborator

Serve download bytes from storage (S3 presigned URL / nginx / disk) with HTTP Range support instead of streaming through a gunicorn worker.

  • Fixes large folder zips truncating into a corrupt archive when the transfer outran the worker timeout.
  • Fixes large single files restarting from zero on a dropped connection.

Folders build in a background job to a temp artifact; the client polls a capability token and downloads the finished, Content-Length'd archive. Artifacts expire hourly. Local-disk deploys can opt into nginx X-Accel-Redirect via drive_xaccel_prefix; otherwise send_file streams off disk with Range support.

🤖 Generated with Claude Code

Large downloads were piped through a gunicorn worker, which caused two
failures:

- Folder/multi-file zips were streamed with the central directory written
  last. When a transfer outran the worker timeout the connection was cut
  mid-stream, leaving a truncated archive with no central directory that
  unzips as corrupt. The cut is time-bound, so different clients stopped at
  different sizes.
- Single large files held a worker for the whole transfer with no Range
  support, so a dropped connection on a flaky link restarted from zero.

Serve bytes from storage instead of the worker, with HTTP Range/resume:
- S3: redirect to a short-lived presigned GET URL.
- Local disk: nginx X-Accel-Redirect when `drive_xaccel_prefix` is set,
  otherwise send_file streams off disk with conditional Range support
  (no more BytesIO(fh.read()) buffering the whole file into memory).

Folders now build in a background job to a temp artifact (disk or S3);
the client polls a capability-token status and downloads the finished,
Content-Length'd archive over the same resumable path. Archives live for
an hour and are swept hourly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@safwansamsudeen

Copy link
Copy Markdown
Collaborator Author

@greptileai can you review this PR?

safwansamsudeen and others added 3 commits July 28, 2026 11:24
Large downloads were piped through a gunicorn worker, causing two failures:

- Folder/multi-file zips streamed with the central directory written last;
  when a transfer outran the worker timeout the connection was cut mid-stream,
  leaving a truncated archive that unzips as corrupt. The cut is time-bound,
  so different clients stopped at different sizes.
- Single large files held a worker for the whole transfer with no Range
  support, so a dropped connection on a flaky link restarted from zero.

Serve bytes from storage with HTTP Range/resume instead of the worker:
- S3: redirect to a short-lived presigned GET URL.
- Disk: nginx X-Accel-Redirect when drive_xaccel_prefix is set, else send_file
  streams off disk with conditional Range support (Accept-Ranges advertised on
  the 200 so browsers resume rather than restart).

Folders build in a background job to a temp artifact; the client polls a
capability-token status and downloads the finished, Content-Length'd archive
over the same resumable path. Archives expire in an hour and are swept hourly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d lifecycle

- percent-encode the X-Accel-Redirect URI (raw UTF-8/special chars in a
  header value 500 under WSGI's latin-1 rule and confuse nginx)
- replace secure_filename in Content-Disposition (S3 presigned + X-Accel)
  with RFC 6266/5987 encoding; secure_filename strips non-Latin names to
  nothing, regressing what send_file handled correctly
- rate-limit download_folder and dedupe identical selections onto the
  in-flight/ready token: it's guest-reachable and each call cost an
  hour-long queue job plus a multi-GB artifact
- give building cache entries a 3h TTL (1h expired mid-queue-wait and
  404'd polls on still-running builds) and raise the client poll timeout
  past the job's 1h timeout

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
frappe.enqueue(build_download_archive, ...) pickles the raw function
object into the job's kwargs. Unpickling it re-imports
suite.drive.api.files before frappe.init(site) runs inside execute_job,
so the module-level `frappe.qb.DocType("File")` in drive/utils/__init__
crashes with "object is not bound" — every build failed this way, and
the RQ failure callback hit the same import crash trying to log it, so
the cache entry never left "building" and the client polled forever.
Pass the dotted string instead, matching every other async
frappe.enqueue call in this codebase — get_attr() resolves it after
frappe.init(site) has already run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@safwansamsudeen
safwansamsudeen merged commit cf9e478 into develop Jul 28, 2026
4 checks passed
@safwansamsudeen
safwansamsudeen deleted the fix/drive-resumable-downloads branch July 28, 2026 06:52
@safwansamsudeen
safwansamsudeen restored the fix/drive-resumable-downloads branch July 28, 2026 06:52
@safwansamsudeen
safwansamsudeen deleted the fix/drive-resumable-downloads branch July 28, 2026 06:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant