From b9e7e82eaa20db55e2b7e6921f1e79e98ed23f26 Mon Sep 17 00:00:00 2001 From: Chris de Claverie Date: Thu, 19 Mar 2026 22:54:11 +0100 Subject: [PATCH] Add upload verification tokens and ghost file rollback Fixes two issues with Proton Drive uploads: 1. Per-block verification tokens (fixes Code=200501 upload failures): Proton's API now requires calling the verification endpoint before uploading blocks and including a Verifier.Token per block computed by XORing the verification code with the first 32 bytes of each encrypted block. Also passes VolumeID in the block upload request. 2. Ghost file rollback on upload failure: When block upload fails (step 2) or revision commit fails (step 3), the draft link is now permanently deleted via DeleteChildren to prevent ghost entries that block future uploads with the same filename. Also handles corrupted active links in handleRevisionConflict by deleting them when replace_existing_draft is enabled. Depends on: rclone/go-proton-api# (verification endpoint support) Co-Authored-By: Claude Opus 4.6 --- file_upload.go | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/file_upload.go b/file_upload.go index 4318df8..b6a20d2 100644 --- a/file_upload.go +++ b/file_upload.go @@ -24,6 +24,14 @@ func (protonDrive *ProtonDrive) handleRevisionConflict(ctx context.Context, link draftRevision, err := protonDrive.GetRevisions(ctx, link, proton.RevisionStateDraft) if err != nil { + // If we can't even read the revisions (corrupted link) and + // replace_existing_draft is enabled, permanently delete the + // corrupted link (skip trash to immediately free the name) + // and signal the caller to retry file creation from scratch. + if protonDrive.Config.ReplaceExistingDraft { + _ = protonDrive.c.DeleteChildren(ctx, protonDrive.MainShare.ShareID, link.ParentLinkID, linkID) + return "", true, nil + } return "", false, err } @@ -250,6 +258,16 @@ func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, n return nil, 0, nil, "", ErrMissingInputUploadAndCollectBlockData } + // Fetch verification code for per-block token computation + verification, err := protonDrive.c.GetUploadVerification(ctx, protonDrive.MainShare.VolumeID, linkID, revisionID) + if err != nil { + return nil, 0, nil, "", err + } + verificationCode, err := base64.StdEncoding.DecodeString(verification.VerificationCode) + if err != nil { + return nil, 0, nil, "", err + } + totalFileSize := int64(0) pendingUploadBlocks := make([]PendingUploadBlocks, 0) @@ -268,6 +286,7 @@ func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, n ShareID: protonDrive.MainShare.ShareID, LinkID: linkID, RevisionID: revisionID, + VolumeID: protonDrive.MainShare.VolumeID, BlockList: blockList, } @@ -365,17 +384,34 @@ func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, n } manifestSignatureData = append(manifestSignatureData, hash...) + // Compute per-block verification token: XOR first 32 bytes of + // encrypted data with the verification code, then base64 encode. + xorLen := 32 + if len(verificationCode) < xorLen { + xorLen = len(verificationCode) + } + tokenBytes := make([]byte, xorLen) + for j := 0; j < xorLen; j++ { + b := byte(0) + if j < len(encData) { + b = encData[j] + } + tokenBytes[j] = b ^ verificationCode[j] + } + verifierToken := base64.StdEncoding.EncodeToString(tokenBytes) + pendingUploadBlocks = append(pendingUploadBlocks, PendingUploadBlocks{ blockUploadInfo: proton.BlockUploadInfo{ Index: i, // iOS drive: BE starts with 1 Size: int64(len(encData)), EncSignature: encSignatureStr, Hash: base64Hash, + Verifier: &proton.BlockVerifier{Token: verifierToken}, }, encData: encData, }) } - err := uploadPendingBlocks() + err = uploadPendingBlocks() if err != nil { return nil, 0, nil, "", err } @@ -444,6 +480,9 @@ func (protonDrive *ProtonDrive) uploadFile(ctx context.Context, parentLink *prot /* step 2: upload blocks and collect block data */ manifestSignature, fileSize, blockSizes, digests, err := protonDrive.uploadAndCollectBlockData(ctx, newSessionKey, newNodeKR, file, linkID, revisionID) if err != nil { + // Rollback: trash the draft to prevent ghost entries that block + // future uploads with the same filename. + _ = protonDrive.c.DeleteChildren(ctx, protonDrive.MainShare.ShareID, parentLink.LinkID, linkID) return "", nil, err }