Skip to content

TF-4410 Propose ADR attach drive as attachment#4688

Open
tddang-linagora wants to merge 4 commits into
masterfrom
feature/TF-4410-ADR-Attach-drive-as-attachment
Open

TF-4410 Propose ADR attach drive as attachment#4688
tddang-linagora wants to merge 4 commits into
masterfrom
feature/TF-4410-ADR-Attach-drive-as-attachment

Conversation

@tddang-linagora

@tddang-linagora tddang-linagora commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Issue

Summary by CodeRabbit

  • Documentation
    • Added an architecture decision record detailing how Drive documents are attached to email messages.
    • Documented behavior differences between sharingLink (HTML link insertion) and downloadLink (download+attach), including progress behavior across environments.
    • Explained shared upload validation, a per-file transfer concurrency limit, and related tradeoffs/risks.

@tddang-linagora tddang-linagora self-assigned this Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 22a9eb4d-d5c5-413e-ab5c-41c7b7313b3d

📥 Commits

Reviewing files that changed from the base of the PR and between b4df3d2 and 1af6c9e.

📒 Files selected for processing (1)
  • docs/adr/0103-attach-drive-file-as-attachment.md
✅ Files skipped from review due to trivial changes (1)
  • docs/adr/0103-attach-drive-file-as-attachment.md

Walkthrough

This pull request adds ADR-0103 documenting Drive attachment behavior. It defines the sharingLink versus downloadLink split, the streamed IO flow and buffered web flow, DriveAttachmentHandler partitioning, reusable upload validation via AttachmentUploadValidator and CompositeAttachmentUploadValidator, per-file concurrency limits, progress chip behavior, and listed tradeoffs and risks.

Possibly related PRs

Suggested labels: attach-from-drive

Suggested reviewers: chibenwa, zatteo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change by proposing an ADR about attaching Drive files as email attachments.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/TF-4410-ADR-Attach-drive-as-attachment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

codescene-delta-analysis[bot]

This comment was marked as outdated.

Comment thread docs/adr/0103-attach-drive-file-as-attachment.md
@tddang-linagora tddang-linagora requested a review from dab246 July 7, 2026 07:33
codescene-delta-analysis[bot]

This comment was marked as outdated.

@Arsnael

Arsnael commented Jul 7, 2026

Copy link
Copy Markdown
Member

Docker image published for this PR: linagora/tmail-web-pr:4688

1 similar comment
@Arsnael

Arsnael commented Jul 7, 2026

Copy link
Copy Markdown
Member

Docker image published for this PR: linagora/tmail-web-pr:4688

Comment thread docs/adr/0103-attach-drive-file-as-attachment.md Outdated
Comment thread docs/adr/0103-attach-drive-file-as-attachment.md Outdated
Comment thread docs/adr/0103-attach-drive-file-as-attachment.md Outdated
Comment thread docs/adr/0103-attach-drive-file-as-attachment.md Outdated
Comment thread docs/adr/0103-attach-drive-file-as-attachment.md
Comment thread docs/adr/0103-attach-drive-file-as-attachment.md Outdated
codescene-delta-analysis[bot]

This comment was marked as outdated.

dab246
dab246 previously approved these changes Jul 7, 2026

@dab246 dab246 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@Arsnael

Arsnael commented Jul 7, 2026

Copy link
Copy Markdown
Member

Docker image published for this PR: linagora/tmail-web-pr:4688


## Decision

**Partitioning:** `DriveAttachmentHandler` splits picked `DriveDocument`s per-doc: `sharingLink != null` → HTML link (unchanged); `downloadLink != null` → download + attach (new). **Invariant:** both fields may be present on one doc (per ADR-0095's payload); when they are, `downloadLink` wins — download+attach only, enforced in the partition step, not call-site order.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IMO, sharingLink will win if both exists, user have more benefit from it than download.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated


**Download (`workplace` package), streamed:** `DriveFileDatasourceImpl.openFileForUpload(doc)` branches on `kIsWeb`. **IO**: `Dio.get(responseType: stream)` piped directly as the upload request body (`FileInfo.byteStream`) — zero-copy. **Web**: XHR can't stream a body, so web buffers into `FileInfo.bytes`. A bounded worker pool (`kMaxConcurrentDriveTransfers = 3`, new constant, not tied to any existing limit) runs per-file pipelines with no "download all, then upload all" batching. A failing file logs/removes its chip; others continue.

**Upload pipeline contract:** the worker-isolate upload path (`FileUploader.uploadAttachment` → `UploadFileArguments`) only carries `filePath`/`bytes` across the isolate boundary — a one-shot `Stream<List<int>>` can't cross it. So **drive stream uploads run on the main isolate** instead, bypassing the worker-isolate branch. Local-file/paste/drop uploads are unaffected.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

what happen if upload has token expired? can we retry?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, already in Retry policy section.


**Download (`workplace` package), streamed:** `DriveFileDatasourceImpl.openFileForUpload(doc)` branches on `kIsWeb`. **IO**: `Dio.get(responseType: stream)` piped directly as the upload request body (`FileInfo.byteStream`) — zero-copy. **Web**: XHR can't stream a body, so web buffers into `FileInfo.bytes`. A bounded worker pool (`kMaxConcurrentDriveTransfers = 3`, new constant, not tied to any existing limit) runs per-file pipelines with no "download all, then upload all" batching. A failing file logs/removes its chip; others continue.

**Upload pipeline contract:** the worker-isolate upload path (`FileUploader.uploadAttachment` → `UploadFileArguments`) only carries `filePath`/`bytes` across the isolate boundary — a one-shot `Stream<List<int>>` can't cross it. So **drive stream uploads run on the main isolate** instead, bypassing the worker-isolate branch. Local-file/paste/drop uploads are unaffected.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

in case of running in main isolate, what will be impacted?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Explaination added.

- A new attachment-eligibility condition is one new class + one line, not a change to every call site.
- Memory stays flat on IO regardless of file size/concurrent count (zero-copy pipe); web still buffers per file (XHR limitation).
- User sees live per-file progress immediately instead of a blocking wait.
- A streamed IO upload isn't 401-retry-safe (one-shot body can't be rebuilt) — accepted and made explicit via `nonRetryableUploadBody`: that file's chip fails and is removed, others continue (see Retry Policy above).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

so should we use temp file?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

To follow zero-copy pipe, no. Explaination added.


**Progress/cancel model:** `UploadFileStatus` gains `downloading`; `AttachmentUploadState` gains a matching `DownloadingAttachmentUploadState`; the composer progress widget gains a render branch for it. A single `CancelToken` per file spans both the download and upload calls, so cancelling (including deleting the chip mid-transfer) aborts whichever stage is active.

**Validation — minimal, condition-agnostic abstraction:**

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am not still clear about the flow starting from the time we got the document list? validators will be run first -> download -> upload? can you more detail about the flow?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated


**Progress/cancel model:** `UploadFileStatus` gains `downloading`; `AttachmentUploadState` gains a matching `DownloadingAttachmentUploadState`; the composer progress widget gains a render branch for it. A single `CancelToken` per file spans both the download and upload calls, so cancelling (including deleting the chip mid-transfer) aborts whichever stage is active.

**Validation — minimal, condition-agnostic abstraction:**

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how we handle the case:

  • maxFileSize of a document is valid, but total size of files not valid
  • totalSize of files is valid, but one file have not valid in file size

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated


**Visible progress:** each drive file gets a composer attachment chip immediately (`UploadFileStatus.downloading`, color-distinct from `uploading`), fed by `onDownloadProgress`/Dio's existing `onSendProgress`. IO shows one continuous phase (download+upload coupled by the pipe); web shows two sequential phases (download then upload).

**Progress/cancel model:** `UploadFileStatus` gains `downloading`; `AttachmentUploadState` gains a matching `DownloadingAttachmentUploadState`; the composer progress widget gains a render branch for it. A single `CancelToken` per file spans both the download and upload calls, so cancelling (including deleting the chip mid-transfer) aborts whichever stage is active.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

not clear to me, how to indicate the download/upload in one progress bar, how to display exactly the progress?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Answered by Visible progress section


**Partitioning:** `DriveAttachmentHandler` splits picked `DriveDocument`s per-doc: `sharingLink != null` → HTML link (unchanged); `downloadLink != null` → download + attach (new). **Invariant:** both fields may be present on one doc (per ADR-0095's payload); when they are, `downloadLink` wins — download+attach only, enforced in the partition step, not call-site order.

**Download (`workplace` package), streamed:** `DriveFileDatasourceImpl.openFileForUpload(doc)` branches on `kIsWeb`. **IO**: `Dio.get(responseType: stream)` piped directly as the upload request body (`FileInfo.byteStream`) — zero-copy. **Web**: XHR can't stream a body, so web buffers into `FileInfo.bytes`. A bounded worker pool (`kMaxConcurrentDriveTransfers = 3`, new constant, not tied to any existing limit) runs per-file pipelines with no "download all, then upload all" batching. A failing file logs/removes its chip; others continue.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how about create an interface for DriveFileDownloader then implement for mobile (stream/file temp/ ....) and web?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

YAGNI at the moment. Explaination added.

- IO shows only a single-phase "downloading"-styled bar for the whole transfer (download/upload are coupled in lockstep) — accepted over forcing IO to buffer for a two-phase visual.
- Stream receive-timeout is uncapped (`Duration.zero`); a stalled server relies on the user's manual cancel action. **Correction after checking Dio's actual IO adapter source (pinned `dio: 5.2.0`, and current 5.9.0):** `receiveTimeout` was never a whole-transfer watchdog on IO in the first place — it only guards time-to-first-byte/headers (`request.close()`), so the 10s default wasn't going to "kill a slow transfer mid-stream" either way. Setting `Duration.zero` still avoids a spurious timeout before a slow drive file starts streaming, just not for the reason originally stated.
- Concurrency fixed at 3 concurrent transfers, not user/config-configurable (YAGNI for now).
- **Web buffering is a browser platform limit, not a Dio-specific gap.** Verified via `dio_web_adapter` 2.1.1 source (current, ships with `dio: 5.9.0`) — still XHR-based, still fully buffers the request stream before `xhr.send()`. A hand-rolled `fetch()` client wouldn't help either: streaming *upload* request bodies only works on Chromium (Chrome/Edge/Opera, v105+, and only over HTTP/2+HTTPS with `duplex: 'half'`, always CORS-preflighted); Firefox and desktop Safari still buffer the full request body as of current stable channels (WebKit is adding it under Interop 2026, landed in a Safari 26.4 beta, not GA). Fetch response-body streaming (download side) is broadly supported, but doesn't remove the need to buffer before upload on non-Chromium engines. Conclusion: the web buffered fallback is correct for all three engines today, not an XHR-only workaround to revisit later.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

keep it as risk with current choice is not right. WDYT?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated


**Partitioning:** `DriveAttachmentHandler` splits picked `DriveDocument`s per-doc: `sharingLink != null` → HTML link (unchanged); `downloadLink != null` → download + attach (new). **Invariant:** both fields may be present on one doc (per ADR-0095's payload); when they are, `downloadLink` wins — download+attach only, enforced in the partition step, not call-site order.

**Download (`workplace` package), streamed:** `DriveFileDatasourceImpl.openFileForUpload(doc)` branches on `kIsWeb`. **IO**: `Dio.get(responseType: stream)` piped directly as the upload request body (`FileInfo.byteStream`) — zero-copy. **Web**: XHR can't stream a body, so web buffers into `FileInfo.bytes`. A bounded worker pool (`kMaxConcurrentDriveTransfers = 3`, new constant, not tied to any existing limit) runs per-file pipelines with no "download all, then upload all" batching. A failing file logs/removes its chip; others continue.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how about in web we use Origin Private File System instead of memory buffer for file?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Considered but not adopted. Explaination added.

@codescene-delta-analysis codescene-delta-analysis Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No application code in the PR — skipped Code Health checks.

See analysis details in CodeScene

Quality Gate Profile: The Bare Minimum
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

@Arsnael

Arsnael commented Jul 9, 2026

Copy link
Copy Markdown
Member

Docker image published for this PR: linagora/tmail-web-pr:4688

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.

4 participants