Skip to content

feat(aws_tools): add S3 File Uploader and S3 File Download tools#3273

Merged
crazywoola merged 2 commits into
langgenius:mainfrom
leoou331:add-s3-file-uploader-and-download
Jun 11, 2026
Merged

feat(aws_tools): add S3 File Uploader and S3 File Download tools#3273
crazywoola merged 2 commits into
langgenius:mainfrom
leoou331:add-s3-file-uploader-and-download

Conversation

@leoou331

@leoou331 leoou331 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Adds two new builtin tools to the aws_tools plugin (tools/aws):

  • s3_file_uploader — takes a file variable from an upstream workflow node (Start file input, LLM output, another tool, etc.) and uploads it to a configurable S3 bucket/key. Optionally returns a presigned GET URL.
  • s3_file_download — takes an s3://bucket/key URI and emits the object as a Dify file (via create_blob_message) plus structured metadata for downstream nodes.

Why

The existing s3_operator is text-only:

  • input is text_content: string
  • output is UTF-8 decoded string

It cannot be wired to a Start node's file input, nor consume the file output of nodes like Frame Extractor / Nova Canvas. These two new tools close that gap and keep the same UX as the rest of the plugin (provider-level credentials, optional per-tool overrides, three-language labels).

A typical workflow now looks like:

[Start: file input] -> [s3_file_uploader] -> ... -> [s3_file_download] -> [LLM/Code/...]

Files

tools/aws/
├── manifest.yaml                          # version 0.0.26 -> 0.0.27
├── README.md                              # +2 lines (Features list)
├── provider/aws_tools.yaml                # +2 lines (register both tools)
└── tools/
    ├── s3_file_uploader.py                # 200 lines
    ├── s3_file_uploader.yaml              # 139 lines
    ├── s3_file_download.py                # 174 lines
    └── s3_file_download.yaml              # 78 lines

Both Python modules are self-contained — credential-resolution helpers (_resolve_aws_credentials, _build_boto3_client_kwargs, _reset_clients_on_credential_change) are inlined so this PR does not introduce a shared utils/ module that the rest of the plugin doesn't use.

Tool surface

s3_file_uploader (form parameters)

Param Type Required Notes
input_file file yes Bound to upstream file variable.
bucket_name string yes Without s3://.
key_prefix string no Folder-style prefix, e.g. workflow-outputs.
object_key string no Override final key; defaults to incoming filename.
aws_region string no Per-tool override of provider default.
aws_access_key_id / aws_secret_access_key / aws_session_token string no Per-tool override / STS support.
generate_presign_url boolean no Default false.
presign_expiry number no Default 3600 seconds.

Outputs three messages: text = s3://bucket/key or presigned URL; json = {bucket_name, object_key, s3_uri, presigned_url?, presign_expiry?}; no files.

s3_file_download

Param Type Required Notes
s3_uri string (LLM-fillable) yes s3://bucket/key.
aws_region / aws_access_key_id / aws_secret_access_key / aws_session_token string no Same overrides as uploader.

Outputs files = [<Dify file>], json = {bucket, key, content_type, content_length, etag, last_modified, s3_uri}, and a key: value text block of the same metadata.

Validation

Static:

  • python -m py_compile on both .py files — ✅
  • yaml.safe_load on the new + modified yaml files — ✅
  • Verified extra.python.source paths resolve to existing files — ✅
  • black --check -l 100 and ruff check both clean on the new files — ✅
  • Confirmed label / description languages match the rest of the plugin (en_US / zh_Hans / pt_BR) — ✅
  • No new dependencies (boto3/botocore already pinned in pyproject.toml) — ✅

End-to-end (real run, not dry validation):

  1. Built a .difypkg from tools/aws/ on this branch.
  2. Installed it on a self-hosted Dify 1.14.2 Community Edition.
  3. Imported a workflow [Start (file input) -> s3_file_uploader -> s3_file_download -> End], pointed at S3 bucket in cn-northwest-1.
  4. Triggered the workflow via the Service API with text/plain (hello.txt, 244 B) and image/png (50×50 RGBA, 144 B).
  5. Result for both: status = succeeded, total_steps = 4, elapsed ~0.4-0.8s.
  6. Pulled both objects back from S3 with aws s3 cp and compared SHA-256 — byte-for-byte identical with the local source.

A companion run on the equivalent aws-samples/dify-aws-tool#168 PR (same code) additionally covered:

  • PDF binary payload — succeeded, SHA-256 identical
  • generate_presign_url=true — URL contains correct SigV4 fields, custom X-Amz-Expires honored, curl fetch returned HTTP 200 with byte-identical content
  • STS aws_session_token — temporary credentials path round-tripped successfully

Out of scope

  • Multipart upload / streaming for large files
  • Any change to existing s3_operator behavior
  • Shared utils/ module — left for a follow-up if more tools want the helpers

Add two new builtin tools to tools/aws so that Dify workflows can move
file objects (not just text) between workflow nodes and S3:

- s3_file_uploader: takes a file variable from an upstream node and
  uploads it to a configurable bucket/key, optionally returning a
  presigned URL.
- s3_file_download: takes an s3://bucket/key URI and emits a Dify file
  (via create_blob_message) plus structured metadata for downstream
  consumption.

Why
---
The existing s3_operator only handles text payloads (text_content in,
UTF-8 text out), so it can't be wired directly to a Start node 'file'
input or to any tool that emits binary file variables. These two tools
close that gap with the same UX and parameter conventions as
s3_operator.

Implementation notes
--------------------
- Both tools are self-contained (credential-resolution helpers are
  inlined) so this PR does not introduce a shared utils/ module.
- They reuse the existing aws_tools provider's credentials_for_provider
  schema (Access Key / Secret Key / Region) and additionally accept a
  per-invocation aws_session_token for STS / role-assumption use cases.
- Three-language labels (en_US / zh_Hans / pt_BR) match the rest of the
  plugin's tools; identity.author follows existing convention (AWS).
- Bumped manifest.yaml version from 0.0.26 to 0.0.27.
- README.md Features section updated.
- Code formatted with black (-l 100); ruff check passes clean.
- No new dependencies (boto3/botocore already in pyproject.toml).

Validation
----------
Static:
- python -m py_compile on both .py files
- yaml.safe_load on all touched yaml files
- Verified extra.python.source paths resolve correctly
- black --check + ruff check both clean on the new files

End-to-end (real run, not dry validation):
- Built a .difypkg from tools/aws/ on this branch
- Installed it on a self-hosted Dify 1.14.2 Community Edition
- Imported a workflow [Start file -> s3_file_uploader -> s3_file_download
  -> End], pointed at S3 in cn-northwest-1
- Triggered the workflow via the Service API with text/PNG payloads
- Result: status=succeeded, total_steps=4, elapsed ~0.4-0.8s
- Pulled both objects back from S3 via aws s3 cp and SHA-256 verified
  byte-for-byte identical with the local source files
- (Companion regression run on aws-samples/dify-aws-tool#168 also covered
  PDF binary, generate_presign_url=true, and STS aws_session_token paths
  with the same code; all green and SHA-256 identical.)

Origin / attribution
--------------------
Implementation derived from the public s3_file_uploader.py /
s3_file_download.py in r3-yamauchi/dify-my-aws-tools-plugin
(Apache-2.0). The author has confirmed he is happy for these two tools
to be contributed upstream to langgenius/dify-official-plugins with
no attribution requirement; comments translated to English to match
surrounding files. The companion aws-samples/dify-aws-tool PR langgenius#168
contains the same code.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces two new AWS S3 tools: AWS S3 File Uploader and AWS S3 File Download, enabling workflows to upload files to S3 (with optional presigned URLs) and download S3 objects as Dify file variables. The review feedback identifies a critical thread-safety issue where caching the s3_client as an instance attribute could lead to race conditions or credential leakage across concurrent executions. It is recommended to initialize the client locally within the _invoke method instead. Additionally, the feedback suggests improving error handling for S3 bucket/key exceptions and safely parsing the presign_expiry parameter to prevent runtime crashes.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread tools/aws/tools/s3_file_download.py
Comment thread tools/aws/tools/s3_file_uploader.py
Apply concrete code-review feedback from gemini-code-assist on PR langgenius#3273:

1. Thread safety / credential leakage (high-priority)
   - Move boto3 client construction from cached `self.s3_client` to a
     local variable inside `_invoke`. Tool instances are reused by the
     plugin runtime across concurrent invocations, so a cached client
     tied to one tenant's credentials must never leak into another
     execution. Creating an S3 client is lightweight (no network I/O)
     so there is no real cost to building it per invocation.
   - Drop the now-unused `_reset_clients_on_credential_change` and
     `_credential_signature` helpers (and the `Iterable` import).
     They tried to address the same race but were inherently fragile
     under concurrency.

2. Standardised exception handling in s3_file_download
   - Switch from `self.s3_client.exceptions.NoSuchBucket` /
     `NoSuchKey` (which depended on the cached instance attribute) to
     standard `ClientError` error-code matching via
     `exc.response["Error"]["Code"]`.

3. Robust filename extraction in s3_file_download
   - Tolerate trailing slashes in the S3 key (e.g. `s3://bucket/foo/`)
     so the emitted Dify file's `filename` is never empty.

4. Safe presign_expiry parsing in s3_file_uploader
   - Extracted a small `_parse_presign_expiry` helper that tolerates
     None / empty string / non-numeric input and falls back to the
     default of 3600 seconds, instead of letting `int(None)` raise
     TypeError when the optional Dify number field is left blank.

Validation
----------
- black -l 100 + ruff check both clean.
- End-to-end re-validation on a fresh self-hosted Dify 1.14.2: built a
  .difypkg from this branch, installed it, and ran the regression
  matrix again - text/plain, image/png, application/pdf, generate_presign_url
  with curl-fetch, and STS aws_session_token via `aws sts get-session-token`.
  All six runs returned status=succeeded; SHA-256 byte-for-byte identical
  on every round-trip. Unit-tested `_parse_presign_expiry` against
  None / "" / 600 / "600" / "not a number" / 3.14 / custom-default;
  all 7 cases produce the expected fall-back behaviour.

Refs
----
PR review: langgenius#3273 (review)
leoou331 pushed a commit to leoou331/dify-aws-tool that referenced this pull request Jun 10, 2026
…ins#3273

Same set of fixes applied to the companion PR on the upstream
langgenius/dify-official-plugins repo (#3273), surfaced by
gemini-code-assist review:

1. Thread safety: replace cached self.s3_client with a local boto3
   client created inside each _invoke. Drops the helper functions
   _reset_clients_on_credential_change and _credential_signature.
2. Standardised ClientError error-code matching for NoSuchBucket /
   NoSuchKey (no longer relies on the dropped instance-attribute
   exceptions namespace).
3. Tolerate trailing slashes in the S3 key when deriving filename.
4. Safe presign_expiry parsing (None / empty / non-numeric all fall
   back to 3600 instead of crashing with TypeError).

Re-validated end to end: TXT / PNG / PDF / presign URL / STS session
token paths all succeed with byte-for-byte SHA-256 match.
@leoou331

Copy link
Copy Markdown
Contributor Author

Thanks for the review @gemini-code-assist[bot] — all four points accepted and applied in fafc4c4:

# Point Resolution
1 self.s3_client cache is not thread-safe / can leak credentials Removed. s3_client is now a local variable inside _invoke.
2 Drop the now-redundant _reset_clients_on_credential_change / _credential_signature helpers Done; also removed the Iterable import.
3 Use standard ClientError error-code matching in s3_file_download Done: exc.response["Error"]["Code"]NoSuchBucket / NoSuchKey.
4 Robust filename when the S3 key has a trailing slash Done: key.rstrip("/").split("/")[-1] + empty-string guard.
5 Safe presign_expiry parsing Extracted _parse_presign_expiry() that tolerates None / "" / non-numeric input, falls back to 3600.

Re-validation

black --check -l 100 and ruff check are both clean on the new files.

End-to-end re-run on a fresh self-hosted Dify 1.14.2 with this branch packaged into a .difypkg:

Path Result
text/plain round-trip ✅ succeeded, SHA-256 identical
image/png round-trip ✅ succeeded, SHA-256 identical
application/pdf round-trip ✅ succeeded, SHA-256 identical
generate_presign_url=true + curl fetch ✅ HTTP 200, SHA-256 identical
STS aws_session_token (via aws sts get-session-token) ✅ succeeded, SHA-256 identical

Plus a small unit-style check for _parse_presign_expiry: None, "", 600, "600", "not a number", 3.14, custom default — all 7 cases produce the expected fall-back behaviour.

The companion AWS samples PR (aws-samples/dify-aws-tool#168) has been updated with the same fixes.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

That is excellent news, @leoou331. Thank you for the comprehensive re-validation and for applying the requested improvements so thoroughly. The extracted _parse_presign_expiry helper and the robust filename handling look great. This PR is in excellent shape.

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Jun 11, 2026
@crazywoola crazywoola merged commit a1232aa into langgenius:main Jun 11, 2026
3 checks passed
@leoou331

Copy link
Copy Markdown
Contributor Author

Follow-up: batch counterparts

Pushed commit 00c03330 adding s3_files_uploader (input_files: files) and s3_files_download (s3_uris: array) so a single workflow node can process N files at once — Iteration-node wrapping around the single-file tools was the only previous option.

Tool surface (new tools only)

Tool Required input Output
s3_files_uploader input_files: files + bucket_name json = {count, ok, failed, results: [{object_key, s3_uri, presigned_url?, status, error?}, ...]} + per-line text summary
s3_files_download s3_uris: array of s3://... json = {count, ok, failed, results: [...]} + per-line text summary + one Dify file blob per successful URI in input order

Design choices

  • No object_key override on the batch uploader (a single override cannot apply to N files). Final key per file is {key_prefix}/{filename}; the uploader auto-disambiguates duplicate filenames in the same batch (image.png / image-1.png / image-2.png) so concurrent upstream branches with identical filenames don't silently overwrite each other.
  • Per-entry failure isolation: a single bad file/URI does not abort the batch — status = ok | failed (+ error) is captured per entry in results[]. The whole invocation only emits a top-level error message when every entry fails.
  • Same inline credential helpers as the single-file tools (no shared utils/ module introduced).

Validation

  • python -m py_compile, yaml.safe_load, black --check -l 100, ruff check — all clean.
  • 10 mock-boto3 unit tests covering basic batch, dedup, partial failure (ClientError), all-fail top-level error, empty input, presign error isolation, partial download (NoSuchKey), invalid URI: 10/10 pass.
  • End-to-end on Dify 1.14.2 Community Edition + real S3 (cn-northwest-1):
    • Workflow [Start file-list → s3_files_uploader → extract URIs → s3_files_download → summarize → End], 3 files (txt 40 B + png 220 B + pdf 540 B), elapsed ~1.2 s, all 6 steps succeeded.
    • SHA-256 round-trip byte-identical for all 3 files (pulled back via aws s3 cp and compared with the source).
      • a.txt c377b72e7343c1642a35c7ff5108fef6c14fc5fba3aecce89c18d9ac526e4de8
      • img.png 5a2fe18dbec51b2426f8aa31f6424b0efff246497646f1aa2314abe8d09b7aec
      • doc.pdf 7b6fed1b75159c5cbc633e04f9011a1a9e4f22efce2621b8e14646064cf8c6fa
    • Each presigned URL returned by the uploader fetched via curl and verified byte-identical to the source.
    • Partial-failure run (2 valid + 1 bogus s3:// URI): downloader returned count=3, ok=2, failed=1 with a NoSuchKey error string for the bogus URI and exactly 2 file blobs yielded in input order.

Files

tools/aws/
├── manifest.yaml                          # 0.0.27 → 0.0.28
├── README.md                              # +2 Features lines
├── provider/aws_tools.yaml                # +2 lines (register both new tools)
└── tools/
    ├── s3_files_uploader.py               # 251 lines
    ├── s3_files_uploader.yaml             # 134 lines
    ├── s3_files_download.py               # 209 lines
    └── s3_files_download.yaml             # 80 lines

Out of scope (kept for a follow-up)

  • Multipart upload / streaming for large files
  • Any change to existing s3_operator / s3_file_uploader / s3_file_download
  • Shared utils/ module

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants