From ca6631746a7a8c113eb00ed0bd8efde313fa815c Mon Sep 17 00:00:00 2001 From: wiserain Date: Tue, 19 Aug 2025 22:00:23 +0900 Subject: [PATCH] pikpak: add file name integrity check during upload This commit introduces a new validation step to ensure data integrity during file uploads. - The API's returned file name (new.File.Name) is now verified against the requested file name (leaf) immediately after the initial upload ticket is created. - If a mismatch is detected, the upload process is aborted with an error, and the defer cleanup logic is triggered to delete any partially created file. - This addresses an unexpected API behavior where numbered suffixes might be appended to filenames even without conflicts. - This change prevents corrupted or misnamed files from being uploaded without client-side awareness. --- backend/pikpak/pikpak.go | 41 ++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/backend/pikpak/pikpak.go b/backend/pikpak/pikpak.go index 8470928e7..e8638af39 100644 --- a/backend/pikpak/pikpak.go +++ b/backend/pikpak/pikpak.go @@ -1508,8 +1508,30 @@ func (f *Fs) upload(ctx context.Context, in io.Reader, leaf, dirID, gcid string, } if new.File == nil { return nil, fmt.Errorf("invalid response: %+v", new) - } else if new.File.Phase == api.PhaseTypeComplete { - // early return; in case of zero-byte objects + } + + defer atexit.OnError(&err, func() { + fs.Debugf(leaf, "canceling upload: %v", err) + if cancelErr := f.deleteObjects(ctx, []string{new.File.ID}, false); cancelErr != nil { + fs.Logf(leaf, "failed to cancel upload: %v", cancelErr) + } + if new.Task != nil { + if cancelErr := f.deleteTask(ctx, new.Task.ID, false); cancelErr != nil { + fs.Logf(leaf, "failed to cancel upload: %v", cancelErr) + } + fs.Debugf(leaf, "waiting %v for the cancellation to be effective", taskWaitTime) + time.Sleep(taskWaitTime) + } + })() + + // Note: The API might automatically append a numbered suffix to the filename, + // even if a file with the same name does not exist in the target directory. + if upName := f.opt.Enc.ToStandardName(new.File.Name); leaf != upName { + return nil, fserrors.NoRetryError(fmt.Errorf("uploaded file name mismatch: expected %q, got %q", leaf, upName)) + } + + // early return; in case of zero-byte objects or uploaded by matched gcid + if new.File.Phase == api.PhaseTypeComplete { if acc, ok := in.(*accounting.Account); ok && acc != nil { // if `in io.Reader` is still in type of `*accounting.Account` (meaning that it is unused) // it is considered as a server side copy as no incoming/outgoing traffic occur at all @@ -1519,18 +1541,6 @@ func (f *Fs) upload(ctx context.Context, in io.Reader, leaf, dirID, gcid string, return new.File, nil } - defer atexit.OnError(&err, func() { - fs.Debugf(leaf, "canceling upload: %v", err) - if cancelErr := f.deleteObjects(ctx, []string{new.File.ID}, false); cancelErr != nil { - fs.Logf(leaf, "failed to cancel upload: %v", cancelErr) - } - if cancelErr := f.deleteTask(ctx, new.Task.ID, false); cancelErr != nil { - fs.Logf(leaf, "failed to cancel upload: %v", cancelErr) - } - fs.Debugf(leaf, "waiting %v for the cancellation to be effective", taskWaitTime) - time.Sleep(taskWaitTime) - })() - if uploadType == api.UploadTypeForm && new.Form != nil { err = f.uploadByForm(ctx, in, req.Name, size, new.Form, options...) } else if uploadType == api.UploadTypeResumable && new.Resumable != nil { @@ -1542,6 +1552,9 @@ func (f *Fs) upload(ctx context.Context, in io.Reader, leaf, dirID, gcid string, if err != nil { return nil, fmt.Errorf("failed to upload: %w", err) } + if new.Task == nil { + return new.File, nil + } return new.File, f.waitTask(ctx, new.Task.ID) }