Skip to content

fix(upload): resume chunked uploads from the parts the server already has - #6827

Open
ToteMeiSter wants to merge 7 commits into
nextcloud:masterfrom
ToteMeiSter:fix/chunked-upload-resume
Open

ToteMeiSter wants to merge 7 commits into
nextcloud:masterfrom
ToteMeiSter:fix/chunked-upload-resume

Conversation

@ToteMeiSter

@ToteMeiSter ToteMeiSter commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #6825. Part of #6826. Followed by #6828.

Problem

A chunked upload (files larger than 1 MiB) starts from zero after every interruption, although the parts it sent are still on the server. In UploadAndShareFilesWorker and ChunkedFileUploader on current master I see four causes:

  • The PROPFIND on the upload folder requests no properties. isFile is set only from a resourcetype property, so it stays false for every member and the list of uploaded parts is always empty. Resume never finds a part.
  • onStopped() calls abortUpload() on any stop (network lost, screen off, system), which deletes the parts on the server.
  • The folder of the parts is the md5 of name + modification time + length of the file. With "compress images" on, each run of the worker copies and compresses the file again. The new file has a new modification time, so a retry uses another folder.
  • upload() catches every exception and returns false, so a network error never reaches the retry in the worker.

To reproduce: send a video larger than a few MiB to a chat and switch on airplane mode during the upload for a minute. After the network returns, the upload starts from the first part.

Solution

ChunkedFileUploader

  • PROPFIND asks for resourcetype, getcontentlength and displayname.
  • A part counts as uploaded only when its name is <start>-<end> (16 digits each) and its size matches the name. Other entries (.file, foreign files, wrong size) are ignored or uploaded again.
  • Only the missing parts are sent.
  • The assembling MOVE sends OC-Total-Length.
  • If the server rejects the MOVE with 400, the parts are removed once and the file is sent again. The caller supplies the "already restarted" mark (isRestarted / markRestarted), so this happens once per upload.
  • stop() interrupts the upload between two parts and keeps the parts. abortUpload() still removes them.
  • upload() does not swallow exceptions any more. An HTTP error is thrown as HttpException, a network error as IOException. The parts stay on the server. A network error while listing the parts is thrown too, instead of being taken for "no parts".

UploadAndShareFilesWorker

  • The file is copied and compressed once, into cacheDir/uploads/<work id> (new UploadWorkspace). Every run uploads the same file, so the folder key is stable. The workspace is kept after a stop or before a retry and removed when the work ends. Workspaces of finished or unknown works are removed before the next upload.
  • A run stopped during the preparation does not store the half-finished file. A copy of a content uri that could not be read completely is not used.
  • onStopped() removes the parts only when the user cancelled the upload. Any other stop calls stop().
  • The existing retry (IOException, MAX_UPLOAD_ATTEMPTS = 4) is unchanged: this PR makes the retry resume instead of restart. HTTP errors still end the upload as failed.

Not in this PR (see #6828): foreground service, the attempt limit for network errors, interrupting a request that is already waiting on a dead network, removing the parts when the user cancels a work that waits for a retry.

Relation to #6805

#6805 rethrows IOException from ChunkedFileUploader.upload(). This PR changes the same lines: upload() rethrows every exception (IOException and HttpException) and the worker handles both as before. The two PRs conflict in ChunkedFileUploader.upload().

So this PR covers what #6805 does. If this PR is accepted, #6805 can be closed; if #6805 is merged first, this PR needs a rebase of upload().

Tests

Run:

./gradlew :app:testGplayDebugUnitTest --tests "*ChunkedFileUploaderResumeTest" --tests "*ChunkedFileUploaderAbortTest" --tests "*UploadWorkspaceTest" --tests "*FileUtilsContentResolverTest"
  • ChunkedFileUploaderResumeTest (MockWebServer): only missing parts are sent, nothing is sent when all parts exist, a part of a wrong size or foreign names are handled, OC-Total-Length is sent, a failing request throws and keeps the parts, a lost network while listing or sending parts throws IOException, stop() keeps the parts, abortUpload() removes them, a rejected assembly restarts once.
  • UploadWorkspaceTest: the file is prepared once and the folder key stays stable, a vanished file is prepared again, leftovers of an interrupted preparation are removed, finished workspaces are removed and living or locked ones kept, one lock holder.
  • FileUtilsContentResolverTest: a copy of an empty source or of a source whose read grant was revoked is not used (null).

testGplayDebugUnitTest passes (785 tests, 0 failures, 1 skipped), ktlint is clean, detekt reports no new findings against master.

Tested by the author on a Huawei DEL-LX9 with a fork build that contains #6825, this PR and the follow-up foreground PR: large file, screen off, airplane mode — the upload resumed and completed.

🏁 Checklist

  • ⛑️ Tests (unit and/or integration) are included or not needed
  • 🔖 Capability is checked or not needed
  • 🔙 Backport requests are created or not needed: /backport to stable-xx.x
  • 📅 Milestone is set
  • 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

The analysis, the code and the tests were prepared with an AI assistant (Claude Code). The commits carry an Assisted-by trailer. I reviewed the change and tested it on a device.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4275e538-dd74-4890-89c6-935f12b9fcac
📥 Commits

Reviewing files that changed from the base of the PR and between 575c8df and 5e48fbc.

📒 Files selected for processing (6)
  • app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
  • app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt
  • app/src/main/java/com/nextcloud/talk/utils/FileUtils.kt
  • app/src/test/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploaderAbortTest.kt
  • app/src/test/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploaderResumeTest.kt
  • app/src/test/java/com/nextcloud/talk/utils/FileUtilsContentResolverTest.kt
🚧 Files skipped from review as they are similar to previous changes (6)
  • app/src/test/java/com/nextcloud/talk/utils/FileUtilsContentResolverTest.kt
  • app/src/test/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploaderAbortTest.kt
  • app/src/main/java/com/nextcloud/talk/utils/FileUtils.kt
  • app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
  • app/src/test/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploaderResumeTest.kt
  • app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

📝 Walkthrough

The upload worker now prepares files in work-specific workspaces and reuses them across retries. Chunked uploads validate existing parts, upload missing parts, and can retry assembly once after an HTTP 400 response. The worker distinguishes system stops from user cancellation when handling uploads. File copy and compression utilities can write to a specified directory. Tests cover workspace behavior, chunk upload behavior, and content URI copying.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 5e48f

A narrowly timed cancellation can leave upload parts on the server. This is a bounded cleanup risk to address or explicitly accept before merging.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to dc2b1

Resuming uploads improves interruption handling, but two bounded issues need attention: attachment names can collide with recovery records, and cancellation can lose its server-cleanup decision. The existing account and server authorization flows remain unchanged.

Retained concerns

  • Medium · security · inferred: Provider-controlled attachment basenames share the new workspace's trusted control-file namespace. Without successful compression, a file named prepared can be replaced by the workspace's path/name record before transmission; lock selects an existing control file instead of reading the source; and restarted makes recovery state appear already consumed. These are introduced attachment-integrity and recovery-control failures within the selected work request, not a demonstrated arbitrary-file or cross-account access path.
  • Medium · security · inferred: Explicit cancellation can lose its remote cleanup decision if doWork finishes before onStopped. The worker observes cancellation, elects to delete its workspace, and then removes the cancellation marker in finally. A later onStopped consequently calls stop rather than abortUpload, leaving server-held parts after local recovery state has been removed. Base attempted abort regardless of that marker. The late-callback ordering is supported by an earlier dependency-source inspection but was not independently reproduced here; server-side eventual expiry is unknown.
Security review details

Security Blast Radius

  • inferred — The identified failures affect a selected upload's attachment bytes, local recovery records, or chunks retained under its authenticated server account. Filename manipulation requires supplying an attachment through a content provider and having it selected for upload; the cancellation issue requires interruption of an active upload. The inspected paths do not establish additional tenant access, credential disclosure, or service-wide authority.

Security Findings and Attack Paths

  • inferred — A provider basename passes through FileUtils into the same directory as workspace control records. With the name prepared and no successful compression, metadata writing replaces the selected payload, and the normal uploader reads the resulting file. The expected outcome is transmission of the workspace path/name record instead of the attachment. A basename of restarted also controls the existence-based recovery flag.
  • inferred — The cancellation-marker lifetime can end before the callback that decides whether remote parts must be deleted. Under the reported late-callback ordering, explicit cancellation becomes indistinguishable from a resumable system stop at that decision point. This weakens cancellation cleanup without demonstrating that another account can read the remaining parts.

Trust Boundaries and Controls

  • observed — Provider names are reduced to their final slash-separated component, content copies are checked against the target directory, and file URIs retain their canonical app-cache restriction. These controls bound path selection but do not separate untrusted payload names from the new workspace's trusted record names.

Resilience and Maintainability Implications

  • observed — Publishing the volatile uploader before the final stop check covers stops around uploader startup. stop only sets an interruption flag, while abortUpload attempts remote deletion and contains request exceptions. This improves interruption containment, but deletion remains best effort and is distinct from local workspace cleanup.

Hardening Proposals

  • proposed — Separate payload storage from control records using application-generated local payload names while preserving display names only as metadata. Give explicit cancellation a cleanup state whose lifetime extends through remote abort completion or a recoverable cleanup handoff, rather than through doWork alone.
Pre-merge checks · Passed 4 · Failed 1

Failed Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 23.20% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 125 functions across 11 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: resuming chunked uploads from parts already stored on the server.
Description check Passed The description explains the problem, solution, scope, tests, dependencies, follow-up work, and checklist status. The template's screenshots and TODO sections are absent, but the description is otherw…
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.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2c57445d-534c-49a2-8d09-4fd90df47d34
📥 Commits

Reviewing files that changed from the base of the PR and between f3c9097 and 7bdb8a8.

📒 Files selected for processing (10)
  • app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
  • app/src/main/java/com/nextcloud/talk/upload/UploadWorkspace.kt
  • app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt
  • app/src/main/java/com/nextcloud/talk/utils/FileUtils.kt
  • app/src/main/java/com/nextcloud/talk/utils/ImageCompressor.kt
  • app/src/main/java/com/nextcloud/talk/utils/VideoCompressor.kt
  • app/src/test/java/com/nextcloud/talk/upload/UploadWorkspaceTest.kt
  • app/src/test/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploaderAbortTest.kt
  • app/src/test/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploaderResumeTest.kt
  • app/src/test/java/com/nextcloud/talk/utils/FileUtilsContentResolverTest.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

Comment thread app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt Outdated
@ToteMeiSter

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Coordinate cancellation with chunked-uploader startup. · UploadAndShareFilesWorker.kt:502-510

app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt:502-510
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Coordinate cancellation with chunked-uploader startup.

On the plain chunked-upload path, cancellation can arrive after doUpload checks it but before uploadFile assigns chunkedFileUploader. onStopped() then sees null and cannot abort the upload. The worker thread continues, so the new uploader can send chunks and assemble the remote file. doUpload checks cancellation only after uploadFile returns, then skips sharing. Prevent uploader startup when cancellation wins this interval.


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 16e921b3-0e02-48c4-9162-5a3c6d098833
📥 Commits

Reviewing files that changed from the base of the PR and between 7bdb8a8 and 7abef2d.

📒 Files selected for processing (4)
  • app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
  • app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt
  • app/src/test/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploaderAbortTest.kt
  • app/src/test/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploaderResumeTest.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.

@ToteMeiSter

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai. Fixed the outside-diff finding on UploadAndShareFilesWorker (cancellation before the chunked uploader is assigned) in dc2b18e: the worker assigns the uploader first and then checks for a stop, so an earlier stop is seen by the check and a later one reaches the uploader in onStopped(). The same fix is in #6828 (469e2bb), where a cancelled work now finds its parts by the prepared file.

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Latch cancellation before doWork() clears the marker. · UploadAndShareFilesWorker.kt:517-520

app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt:517-520
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Latch cancellation before doWork() clears the marker.

If a chunk request throws IOException after cancellation is marked but before WorkManager calls onStopped(), doUpload() returns stoppedResult() and doWork() removes the marker in finally. WorkManager 2.11.2 can call onStopped() after startWork() completes if cancellation wins before the wrapper processes that completion. The callback then selects stop(), which retains uploaded server parts; the failed request does not assemble or delete them. Latch cancellation in isCancelled() so onStopped() still selects abortUpload().

Suggested fix
@@ -119,2 +119,4 @@
     private var referenceId: String? = null
+    @Volatile
+    private var cancellationObserved = false
     private var internalConversationId: String? = null
@@ -133,1 +135,6 @@
-    private fun isCancelled(): Boolean = referenceId?.let { cancelledReferenceIds.contains(it) } == true
+    private fun isCancelled(): Boolean {
+        if (referenceId?.let { cancelledReferenceIds.contains(it) } == true) {
+            cancellationObserved = true
+        }
+        return cancellationObserved
+    }

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: cb8b7c3e-4bf1-4c10-92ee-9a9f12549962
📥 Commits

Reviewing files that changed from the base of the PR and between 7abef2d and dc2b18e.

📒 Files selected for processing (1)
  • app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.

@ToteMeiSter

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Already reviewed the last commit. Use @coderabbitai full review to rerun a review of the entire changeset.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Oct 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd -t f '^UploadAndShareFilesWorker\.kt$' | while IFS= read -r file; do
  ast-grep outline "$file" --items all --match 'doUpload|onStopped|stoppedResult|upload' --view expanded
  rg -n -C 18 'chunkedFileUploader|ChunkedFileUploader\(|fun onStopped\(|fun doUpload\(' "$file"
done
git show --format=short --stat dc2b18ec9

Length of output: 8216


@ToteMeiSter Thanks for the fix. I verified that startChunkedUpload() assigns chunkedFileUploader before the final stop check. An earlier stop is caught by the check. A later stop reaches the uploader through onStopped(). This addresses the cancellation race I flagged.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@ToteMeiSter

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai. Fixed the outside-diff finding (latch the cancel before doWork() removes its marker) in 8e21659: isCancelled() latches a seen cancel, and doWork() checks once more right before it removes the marker, so a later onStopped() still calls abortUpload(). #6828 does not need it: there the cancel is a flag in the workspace on disk and is not removed by doWork().

@coderabbitai

coderabbitai Bot commented Oct 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd -t f '^UploadAndShareFilesWorker\.kt$' | while IFS= read -r file; do
  ast-grep outline "$file" --items all --match 'isCancelled|doWork|onStopped|stoppedResult' --view expanded
  rg -n -C 16 'isCancelled|cancelled|cancelMarker|remove.*ancel|abortUpload|override.*doWork|override.*onStopped' "$file"
done
git show --format=short 8e2165951 -- app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt

Length of output: 22232


@ToteMeiSter Thanks for the fix and the clarification. I verified 8e2165951: isCancelled() latches an observed cancellation in @Volatile cancelSeen, and doWork() checks it before removing the marker. A later onStopped() can therefore still select abortUpload(). This addresses the finding about losing an observed cancellation.

Your explanation also distinguishes #6828: its workspace cancellation flag remains on disk after doWork().


✏️ Learnings added
Learnt from: ToteMeiSter
URL: https://github.com/nextcloud/talk-android/pull/6827

Timestamp: 2026-10-05T21:16:18.174Z
Learning: In nextcloud/talk-android PR #6828, cancellation is represented by a flag in the upload workspace on disk. UploadAndShareFilesWorker.doWork() does not remove that flag, so the in-memory cancellation latch needed in PR #6827 to preserve an observed cancellation after removing cancelledReferenceIds is not needed for that reason in #6828.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@ToteMeiSter

Copy link
Copy Markdown
Contributor Author

This is a bug fix and all CodeRabbit review threads are resolved. Once it is approved and merged, would it be possible to backport it to stable-25.0.x (e.g. for 25.0.3)? The bug is present in v25.0.2.

Suggested command for a maintainer after merge: /backport to stable-25.0.x

This comment was drafted with AI assistance (Claude Code).

@ToteMeiSter
ToteMeiSter force-pushed the fix/chunked-upload-resume branch from 8e21659 to 575c8df Compare October 12, 2026 20:32
@ToteMeiSter

Copy link
Copy Markdown
Contributor Author

Rebased onto current master; the conflicts with #6805 are resolved.

@coderabbitai coderabbitai 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.

Actionable comments posted: 7


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 46ac8e7e-f6b9-43dc-acec-8c1935f7f6da
📥 Commits

Reviewing files that changed from the base of the PR and between dc2b18e and 575c8df.

📒 Files selected for processing (4)
  • app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
  • app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt
  • app/src/main/java/com/nextcloud/talk/utils/FileUtils.kt
  • app/src/test/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploaderTest.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

Comment thread app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt Outdated
Comment thread app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt Outdated
Comment thread app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt Outdated
Comment thread app/src/main/java/com/nextcloud/talk/utils/FileUtils.kt
… has

A chunked upload restarted from zero after every interruption, although the
parts it had sent were still on the server:

- The PROPFIND on the upload folder named no properties. isFile is set only
  from a resourcetype property, so it stayed false for every member and the
  list of uploaded parts was always empty. The request now asks for
  resourcetype, getcontentlength and displayname.
- Parts are accepted only when their name is well formed and their size
  matches the name. A wrong or foreign entry is uploaded again or ignored.
- Only the missing parts are sent.
- The assembling MOVE sends OC-Total-Length, so the server can check that
  the parts add up to the file.
- A MOVE the server rejects with 400 removes the parts once and sends the
  file again. The caller supplies the "already restarted" mark.
- stop() interrupts the upload between two parts and keeps the parts on the
  server. Only abortUpload() removes them.
- upload() no longer swallows exceptions. A failed request is thrown (an
  HttpException for an HTTP error, an IOException for a network error) and
  the parts stay on the server. A lost network while listing the parts is
  thrown too instead of being taken for "no parts", which would send the
  whole file again.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-sonnet-5-5
The folder of the parts on the server is keyed by the name, the modification
time and the length of the file. UploadAndShareFilesWorker copied the content
uri to the cache and compressed it again on every run. Each run produced a
file with a new modification time, so a retry used another folder and sent
the whole file again.

The worker now prepares the file once into a directory of its own work
(cacheDir/uploads/<work id>) and uploads the same file on every run:

- UploadWorkspace keeps the prepared file and the "parts were removed once"
  mark, and holds a lock, because WorkManager does not interrupt the thread
  of a stopped worker and an old run can still be busy.
- A workspace stays after a stop or before a retry. It is removed when the
  work ends, and the workspaces of finished or unknown works are removed
  before the next upload.
- A run that is stopped while the file is prepared does not store the
  half-finished file.
- A copy of a content uri that could not be read completely is not used as
  the file to upload.
- onStopped() keeps the parts on the server when the system or the network
  stopped the work. The parts are removed only when the user cancelled it.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-sonnet-5-5
Remove the rejected parts before marking the upload as restarted: when
the DELETE fails, the retry must still be allowed to remove them.

Keep the upload workspace in noBackupFilesDir instead of cacheDir. The
system may clear the cache under storage pressure while an upload waits
for the network. A new copy of the file changes its resume key and
leaves the parts on the server orphaned, and after a process restart
the content URI may no longer be readable. Finished workspaces are
still removed by the worker.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
A stop that came after the last check of doUpload() and before the
chunked uploader was assigned found no uploader in onStopped(), so the
upload ran on to the end. Assign the uploader first and check for a
stop after that: an earlier stop is seen by the check, a later one
reaches the uploader.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
doWork() removes the cancel marker of its reference id when it ends,
but WorkManager may call onStopped() after that. onStopped() then saw
no cancel and only stopped the uploader, so the parts stayed on the
server. Latch the cancel once it is seen, and check it once more right
before the marker is removed.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
- Delete the workspace of a cancelled run even when it was marked to be kept.
- Copy a file:// source into the workspace, so the prepared upload does not
  depend on the caller's cache file.
- Fix the upload folder before the uploader is published, so a cancel that
  arrives early can still remove the parts; abort at once if it already came.
- Restart after HTTP 400 only when the assembly was rejected, not a part.
- Sort the listed parts before looking for gaps.
- Treat 401, 403, 408 and 429 while listing the parts as errors, not as an
  empty folder.
- Delete a partial copy when the stream breaks off.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-sonnet-5-5
copyFileToCache returned any existing target, including the leftover of a
copy that died half way, and removed the target only on IOException.

- Copy into a temporary file next to the target and rename it after a
  complete copy; remove the temporary file on any exception.
- Treat an empty existing target as no cache hit and copy again.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
@ToteMeiSter
ToteMeiSter force-pushed the fix/chunked-upload-resume branch from 43d0b1d to 4107b52 Compare October 15, 2026 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3. to review Waiting for reviews AI assisted

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants