File Download Architecture#
Dify uses five distinct download strategies depending on whether content is sourced from an external datasource plugin, a single uploaded document, a batch of documents, an app package export, or an app package import from files or URLs. Each strategy has a different data path, I/O model, and truncation risk profile.
1. Azure Blob / Large Binary Blob Assembly (Datasource Streaming)#
Entry point: DatasourceManager.stream_online_results β stream_node_events
When a workflow node invokes an ONLINE_DRIVE datasource (e.g., Azure Blob), the plugin yields a generator of DatasourceMessage objects. AzureBlobDataSource._download_file selects one of three sub-strategies based on auth method and file size:
| Sub-strategy | Trigger | Mechanism |
|---|---|---|
_download_via_sas_http | auth_method == "sas_token" | requests.get(stream=True), 8 MB HTTP chunks; files β€ 50 MB yielded at once, larger as 100 MB partial blobs |
_download_small_blob | blob β€ 50 MB (SDK auth) | download_blob().readall() in a single call |
_download_large_blob | blob > 50 MB (SDK auth) | 8 MB range-request chunks accumulated in a bytearray, flushed every 100 MB as partial blobs with is_partial=True, remainder yielded with is_partial=False |
Blob chunk assembly β merge_blob_chunks: The raw generator is wrapped by merge_blob_chunks, which accumulates BLOB_CHUNK messages keyed by chunk ID until the final chunk arrives, then emits one complete BLOB message. The maximum assembled size is controlled by dify_config.PLUGIN_MAX_FILE_SIZE (default 50 MB) . Exceeding this limit raises a ValueError and drops the accumulated buffer β this is the primary truncation risk for large datasource files. PR #34662 extended this merging to the datasource path (previously only the tool path in api/core/plugin/impl/tool.py had it; the datasource path in api/core/plugin/impl/datasource.py was unprotected).
Downstream transformation: After merging, DatasourceFileMessageTransformer.transform_datasource_invoke_messages saves the assembled blob to storage via ToolFileManager.create_file_by_raw and returns a BINARY_LINK or IMAGE_LINK message with a URL of the form /files/datasources/{tool_file_id}.
2. Single-Document Signed URL Download#
Entry point: GET /datasets/<dataset_id>/documents/<document_id>/download β DocumentDownloadApi.get
This path returns a signed URL directly from storage β no bytes are proxied through the API server.
DocumentService.get_document_download_url(document, session)
βββ file_helpers.get_signed_file_url(upload_file_id=..., as_attachment=True)
The URL is generated by FileService.get_file_presigned_url, which calls storage.generate_presigned_url(file_key, expires_in=dify_config.FILES_ACCESS_TIMEOUT, ...). The client browser downloads directly from the storage backend (S3, Azure Blob, local, etc.).
Truncation risk: Near-zero under normal conditions. The file is already at rest in storage; the download is a direct pre-signed fetch with no intermediate assembly. Failure modes are storage unavailability and URL expiry (FILES_ACCESS_TIMEOUT).
3. Batch ZIP File Download#
Entry point: POST /datasets/<dataset_id>/documents/download-zip β DocumentBatchDownloadZipApi.post
For downloading multiple upload-file documents at once, Dify builds and streams a single ZIP archive to avoid browser multi-download limits:
DocumentService.prepare_document_batch_download_zipresolvesUploadFilerows for the requested document IDs after checking dataset permissions.FileService.build_upload_files_zip_tempfilewrites the ZIP into aNamedTemporaryFile:- Entry names are sanitized against path traversal (
_sanitize_zip_entry_name) and deduplicated (_dedupe_zip_entry_name). - File bytes are streamed chunk-by-chunk from storage (
storage.load(..., stream=True)) usingZIP_DEFLATEDcompression. - The temp file path is yielded to the caller; the file stays on disk until the response finishes.
- Entry names are sanitized against path traversal (
- Flask's
send_file(zip_path, as_attachment=True)serves the ZIP.response.call_on_close(cleanup.close)deletes the tempfile after streaming completes .
Truncation risk: Moderate. ZIP assembly is synchronous in the API process. Large batches increase disk pressure on the temp directory. There is no total-batch size cap at the service layer β only per-file upload limits enforced at ingest time.
4. App Package Export (.ifpkg)#
Entry point: GET /console/api/apps/{app_id}/export β AppExportApi.get β RosterAgentPackageExporter.export
The App export route accepts optional query parameters:
format:yamlorifpkg. All app types (Workflow, Chatflow, Agent) default to.ifpkgformat; YAML export remains available as an explicit option (format=yaml).version_id: Optional published Agent version UUID. When specified, the export uses the selected version's configuration and workspace skill bindings instead of the active configuration.
The export implementation in AppExportApi.get streams .ifpkg archives with Content-Type: application/zip using Flask's send_file, while YAML exports use AppDslService and return JSON-wrapped DSL.
Version selection behavior:
- When
version_idis not specified, the export uses the editable shared draft. If no draft exists, the export falls back to the active published snapshot. - When
version_idis specified:- The export uses the selected published version's configuration snapshot and workspace skill bindings.
- Requires a paid plan on Cloud (sandbox users must upgrade).
- The version must be owned by the current workspace.
- Historical version visibility must be enabled.
- Validation is performed by
AgentRosterService.get_visible_agent_version_snapshot. - Raises
AgentVersionNotFoundErrorif the version is unavailable or not visible in history.
For exporting applications as portable .ifpkg archives:
- Agent Apps:
RosterAgentPackageExporter.exportcollects the Roster Agent configuration (based onversion_idselection or active draft/snapshot fallback), resolves all referenced skills and files fromToolFileandUploadFiletables, and gathers workspace skills viaSkillManagementService.list_runtime_agent_skill_archives.RosterAgentPackageExporter._build_archiveconstructs a versioned ZIP archive with:manifest.yaml: Package metadata and resource index (format: "dify.roster-agent") (RosterAgentPackageManifest)app.yaml: Agent application configuration with portable references (AgentAppDsl)- Skill packages (
s_NNNNNN.zip): Agent-scoped and workspace skills - Config files (
f_NNNNNN.*): Agent configuration files with preserved extensions - Image icons (
i_NNNNNN.*): App and Agent image icons with preserved extensions
- Workflow and Chatflow Apps:
AppPackageService.exportcallsAppDslService.export_datato serialize the workflow DSL, collecting inline Agent resources from node-bound snapshots viaAgentPackageResourceExporter. The ZIP archive includes:manifest.yaml: Package metadata (format: "dify.app") with Agent resource indexapp.yaml: Workflow/Chatflow DSL with portable Agent package references (capped at the sameDSL_MAX_SIZElimit as YAML imports: 10 MiB)- Skill packages (
s_NNNNNN.zip): Agent-scoped and workspace skills from each node's bound snapshot - Config files (
f_NNNNNN.*): Agent configuration files with preserved extensions - Image icons (
i_NNNNNN.*): App and Agent image icons with preserved extensions
- Package validation and assembly:
-
Agent packages validate each member against configurable limits defined in
FileUploadConfig:Limit Config key Default Maximum single skill package size UPLOAD_SKILL_FILE_SIZE_LIMIT50 MB Maximum total package size AGENT_PACKAGE_MAX_BYTES512 MB Maximum YAML document size (manifest/app) AGENT_PACKAGE_MAX_MANIFEST_BYTES5 MB Maximum number of archive entries AGENT_PACKAGE_MAX_ENTRIES5000 Archive assembly is streamed into a
SpooledTemporaryFile(spooled at 16 MB). Each embedded skill package is inspected viaSkillPackageService.inspectto enforce nested uncompressed size limits (SKILL_PACKAGE_MAX_UNCOMPRESSED_BYTES, default 200 MB). The exporter computes SHA-256 digests and sizes for each member, populating the manifest resource index. The completed archive is yielded as aRosterAgentPackageExportwith a sanitized filename (<agent-name>.ifpkg).
Image icons are validated againstUPLOAD_IMAGE_FILE_SIZE_LIMITand deduplicated in the manifest (shared icons stored once). Icon file references are converted to portable resource IDs (i_NNNNNN) in the exported metadata. -
Workflow/Chatflow packages enforce the
DSL_MAX_SIZElimit (10 MiB) on the serialized DSL during export. The archive is built into aSpooledTemporaryFile(spooled at 16 MB). When inline Agents are present, the resource exporter collects each Agent's configuration snapshot, skill packages, and config files from the boundWorkflowAgentNodeBindingand materializes them using the same validation and assembly path as Roster Agent packages. The completed archive is validated by re-reading through the same import validation path and yielded with a sanitized filename (<app-name>.ifpkg).
-
- Errors are surfaced through dedicated exceptions in
api/services/agent/errors.py:RosterAgentPackageTooLargeError,RosterAgentPackageExportFailedError,InvalidRosterAgentPackageError.
Truncation risk: Low to moderate. Configurable size and entry-count limits prevent unbounded resource consumption. Skill package inspection adds validation overhead but shares the same boundary framework as skill uploads, reducing inconsistency. Disk pressure depends on the 16 MB spool threshold and total package size.
5. App Package Import (File and URL)#
Entry points:
- File upload:
POST /console/api/apps/importswithmode=package-fileand multipartfileβAppImportApi._import_package - URL import:
POST /console/api/apps/importswithmode=yaml-urlandyaml_urlβAppImportApi._import_urlβdownload_app_import_source
When the /apps/imports endpoint receives a package file or a yaml_url parameter pointing to an app package rather than a YAML/DSL file, the import flow attempts format auto-detection before importing:
- URL import only:
download_app_import_sourceperforms a single bounded HTTP download:- Supports extensionless download URLs and GitHub URLs (automatically normalized from
github.com/org/repo/blob/...toraw.githubusercontent.com/org/repo/...) - Uses the existing
remote_fetcherinfrastructure (bounded downloads, SSRF protection, timeout enforcement) - Rejects compressed HTTP responses to prevent memory exhaustion from decompression
- Enforces a size limit of
max(AGENT_PACKAGE_MAX_BYTES, DSL_MAX_SIZE)(default 512 MB for packages, 10 MiB for DSL) - Downloads into a
SpooledTemporaryFile(16 MB spool threshold) - Single download attempt β no retries on failure
- Supports extensionless download URLs and GitHub URLs (automatically normalized from
AppPackageService.read_packageattempts format auto-detection:- Inspects the package as a ZIP archive and reads the
manifest.yamlto determine the package format - If
format == "dify.roster-agent", returnsNoneto signal the stream should be rewound and dispatched to_import_agent_packagefor Roster Agent import viaRosterAgentPackageImporter - If
format == "dify.app", validates the package structure, extractsapp.yaml, and returns aPreparedAppPackagecontaining the DSL and validated inline Agent resources - Validates archive integrity, member path safety, Agent resource ownership, and uncompressed size against the shared
AGENT_PACKAGE_MAX_BYTESlimit (512 MB) - Enforces the
DSL_MAX_SIZElimit (10 MiB) on the extracted DSL - Rewinds the source stream before returning, ensuring the caller can re-read for Agent package import if needed
- Inspects the package as a ZIP archive and reads the
- Ordinary app packages (Workflow/Chatflow): The prepared package is passed to
AppDslService.import_app, which materializes bundled Agent resources viaAgentPackageResourceImporterbefore database writes:- Resource materialization reads skill archives and config files from the prepared package, uploads them to the target workspace storage, and creates
ToolFileandUploadFilerecords. The updated DSL is validated and passed to the existing app creation/overwrite flow. - Icon files are restored as destination-owned uploads with file references updated to the new workspace's file IDs.
- When resources cannot be restored (e.g., damaged skill archives), the importer records missing-resource warnings and marks the references as
is_missing=Truein the imported Agent configuration- Legacy Agent DSL asset validation: When importing a legacy Agent DSL package, skill and file references are validated within the target tenant. Unavailable or invalid references (IDs that don't exist or aren't accessible in the target tenant) are cleared from the imported configuration and marked as missing. These missing resources generate import warnings that are returned to the user. Valid resources that have been restored from archives remain usable. This validation is performed by a private helper (
_mark_missing_package_assetsinAgentDslService) to ensure references resolve correctly in the target environment.
- Legacy Agent DSL asset validation: When importing a legacy Agent DSL package, skill and file references are validated within the target tenant. Unavailable or invalid references (IDs that don't exist or aren't accessible in the target tenant) are cleared from the imported configuration and marked as missing. These missing resources generate import warnings that are returned to the user. Valid resources that have been restored from archives remain usable. This validation is performed by a private helper (
- Overwrite validation (
_validate_workflow_overwrite) ensures compatibility between the target app mode and the imported DSL (both must be Workflow or Advanced Chat) - Additional node-type validation prevents importing workflows with incompatible node types (e.g., Answer nodes in Advanced Chat apps)
- For each inline Agent node, the import creates a new
Agentrow, aWorkflowAgentNodeBindinglinking the node to the materialized snapshot, and anAgentConfigSnapshotwith remapped resource IDs
- Resource materialization reads skill archives and config files from the prepared package, uploads them to the target workspace storage, and creates
- Agent packages: Dispatched to
RosterAgentPackageImporterfor skill installation and Agent app creation (overwrite not supported for Agent packages)- Icon files are restored as destination-owned uploads with file references updated to the new workspace's file IDs. Packages without icons remain compatible.
- Errors are surfaced through
InvalidRosterAgentPackageErrorandRosterAgentPackageTooLargeError
Truncation risk: Low to moderate. Bounded downloads enforce package-specific quotas through the existing remote-file fetcher. The 16 MB spool threshold matches package export behavior. Single-attempt download (no retries) reduces resource consumption on transient failures. Format detection prevents double downloads. Ordinary app packages share the 10 MiB DSL import limit with YAML imports.
Truncation Risk Summary#
| Download Mode | Risk | Limiting Factor |
|---|---|---|
| Azure Blob / datasource streaming | High | PLUGIN_MAX_FILE_SIZE (default 50 MB); files exceeding the limit raise before yielding |
| Single signed URL | Low | Storage availability; URL expiry (FILES_ACCESS_TIMEOUT) |
| Batch ZIP | Moderate | API server disk/memory; no enforced total-batch size cap |
| App Package Export (.ifpkg) | Low to Moderate | Configurable size/entry limits (AGENT_PACKAGE_MAX_BYTES, AGENT_PACKAGE_MAX_ENTRIES, DSL_MAX_SIZE); nested skill validation for Agent packages |
| App Package Import (File/URL) | Low to Moderate | Bounded remote download (AGENT_PACKAGE_MAX_BYTES, DSL_MAX_SIZE); single-attempt fetch; format detection; 10 MiB DSL limit for ordinary apps |
Key Files#
| File | Purpose |
|---|---|
| azure_blob.py | Azure Blob datasource: browse + three-path download logic (small, large, SAS) |
| datasource_manager.py | Orchestrates datasource streaming; bridges plugin messages to workflow events |
| datasets_document.py | REST endpoints: /download (signed URL) and /download-zip (batch ZIP) |
| file_service.py | build_upload_files_zip_tempfile β ZIP packing; get_file_presigned_url β signed URL generation |
| dataset_service.py | get_document_download_url, prepare_document_batch_download_zip β service layer |
| chunk_merger.py | merge_blob_chunks β reassembles partial BLOB_CHUNK messages into complete blobs |
| message_transformer.py | Saves assembled blobs to storage; emits BINARY_LINK / IMAGE_LINK messages |
| app.py | AppExportApi.get β format negotiation and .ifpkg streaming for all app types |
| roster_package_exporter.py | RosterAgentPackageExporter β builds versioned .ifpkg archives for Roster Agents |
| app_package_service.py | AppPackageService β exports/imports .ifpkg archives for Workflow/Chatflow apps; format auto-detection; read_package validates and prepares inline Agent resources |
| package_resource_exporter.py | AgentPackageResourceExporter β collects inline Agent resources from workflow node bindings; streams skills, files, and icons into archive members |
| package_resource_importer.py | AgentPackageResourceImporter β materializes bundled resources into target workspace; validates and uploads skills/files/icons; reports missing-resource warnings |
| roster_package_entities.py | Data contract for .ifpkg manifests, resources, and validation |
| roster_package_reader.py | RosterAgentPackageReader β shared ZIP validation and bounded I/O primitives |
| errors.py | Package errors: RosterAgentPackageTooLargeError, RosterAgentPackageExportFailedError, InvalidRosterAgentPackageError |
| app_import.py | AppImportApi._import_package, _import_url β dispatches imports to YAML, ordinary app package, or Agent package flows after format detection |
| app_import_source.py | download_app_import_source β bounded URL download |
| app_dsl_service.py | AppDslService.import_app β DSL validation and app creation/overwrite; _validate_workflow_overwrite β compatibility checks |
| dsl_service.py | AgentDslService._mark_missing_package_assets β validates legacy Agent DSL skill and file references during import; clears unavailable IDs and returns warnings |