mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-07-19 12:11:02 -04:00
[PR #339] fix: stream ZIP downloads without buffering archives #330
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/vxcontrol/pentagi/pull/339
Author: @mason5052
Created: 6/5/2026
Status: 🔄 Open
Base:
main← Head:codex/issue-338-stream-zip-downloads📝 Commits (2)
556692afix: stream zip downloads without buffering archive41524bdfix: return structured error when zip build fails before streaming📊 Changes
4 files changed (+156 additions, -49 deletions)
View changed files
📝
backend/pkg/server/services/flow_files.go(+11 -26)📝
backend/pkg/server/services/flow_files_test.go(+3 -0)📝
backend/pkg/server/services/resources.go(+53 -23)📝
backend/pkg/server/services/resources_test.go(+89 -0)📄 Description
Summary
Directory and multi-path ZIP downloads built the entire archive in a
bytes.Bufferbefore sending it, so the API process held the whole compressed archive on the heap before writing any response byte. This streams the archive straight to the HTTP response writer instead, keeping memory proportional to a single file's copy buffer rather than the full archive size.Problem
DownloadResource(GET /resources/download) andDownloadFlowFile(GET /flows/{flowID}/files/download) both did, for every directory / multi-path request:The full archive was materialized in
bufpurely so aContent-Lengthcould be derived frombuf.Len(). Combined with the 2 GB upload/pull limits, an authenticatedUser-role account can store (or pull) a large incompressible directory and request it as a ZIP, forcing an allocation roughly the size of the archive. A handful of concurrent requests can exhaust process/host memory and crash or severely degrade the API. The reporter's harness showed heap growth proportional to archive size throughbytes.growSlice -> bytes.(*Buffer).Write -> archive/zip -> io.Copy -> resources.ZipResources.Single-file downloads were already fine (they stream from disk); only the directory / multi-path ZIP paths were affected.
Solution
The underlying helpers (
resources.ZipResources,flowfiles.ZipDirectory,flowfiles.ZipRelativePaths) already accept anio.Writerand stream each entry withio.Copy, so no archiving logic changed. A small shared helper writes the archive directly toc.Writer:All four buffering call sites (two per handler) route through it. All pre-archive validation (auth, path sanitization, resource lookup, single-file 404) still runs before any byte is written and still returns normal error responses. Errors that occur after streaming has started (for example a blob that vanished from disk) are logged and the request is aborted, since the 200 status is already committed.
Filenames, archive entry names, ordering, permissions, and filtering are unchanged. The only observable differences: ZIP responses are now chunked (no
Content-Length), and process memory no longer scales with archive size.User Impact
Content-Length). Browsers still download normally viaContent-Disposition: attachment; clients that relied on a ZIPContent-Lengthfor a progress indicator will no longer receive one.Test Plan
go build ./...(CGO_ENABLED=0) - passes.go vet ./pkg/server/services/...- passes.TestStreamZipArchive(no DB dependency) - passes locally:Content-Type: application/zip, asserts noContent-Length, and parses the body back into the expected entry;buildthat fails after a partial flush propagates the error and aborts the request.TestResourceService_DownloadResourceScenariosandTestFlowFileService_DownloadFlowFileScenarios: every ZIP case now also asserts the response carries noContent-Length(locking in streaming), while keeping the existing valid-ZIP-content assertions.CGO_ENABLED=0). They compile cleanly and run in CI on Linux; locally they fail only atgorm.Open("sqlite3", ...), before the handler is invoked.Closes #338
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.