fix(dav): upsert upload matches server-converted ext (xls→xlsx) (#82)
Release / GoReleaser (push) Skipped
Tests / Secret scan (gitleaks) (push) Skipped
Tests / Test (Go 1.25) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Tests / Secret scan (gitleaks) (pull_request) Successful in 5s
Tests / Test (Go 1.25) (pull_request) Successful in 1m3s
Tests / Test (Go stable) (pull_request) Successful in 1m11s
Release / GoReleaser (push) Skipped
Tests / Secret scan (gitleaks) (push) Skipped
Tests / Test (Go 1.25) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Tests / Secret scan (gitleaks) (pull_request) Successful in 5s
Tests / Test (Go 1.25) (pull_request) Successful in 1m3s
Tests / Test (Go stable) (pull_request) Successful in 1m11s
OnlyOffice converts legacy binary Office uploads (.xls/.doc/.ppt) into OOXML (.xlsx/.docx/.pptx) on the server. UploadToFolderReplacing matched by the exact stem|ext via FindFilesByDedupKey, so a repeated `oo dav upload FOLDER f.xls --replace` never found the stored f.xlsx and appended a second file (live: ids 3799+3887, 3800+3888). - EquivalentUploadExt / FindFilesByStemExt: match by stem with a legacy↔OOXML extension equivalence, so .xls finds the saved .xlsx. - planUploadReplacement: pick the surviving file and the redundant duplicate ids for a replacing upload. - UploadToFolderReplacing updates the existing file in place (UpdateFile, stable id, no delete window), removes extra duplicates, and falls back to conversion-aware delete + upload when the portal rejects the update. - AssertNoFileConflict (--no-replace) uses the same conversion-aware matching so a .xls upload conflicts with an existing .xlsx. - Offline tests cover the matcher, the plan and the repeated-.xls regression; pdf/xlsx behaviour unchanged.
This commit is contained in:
+77
-7
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -121,7 +122,9 @@ func (c *Client) DeleteFilesByStem(ctx context.Context, folderID, stem string) (
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// AssertNoFileConflict reports ErrFileExists when localPath stem|ext is already in folderID.
|
||||
// AssertNoFileConflict reports ErrFileExists when localPath logical name is
|
||||
// already in folderID. Matching is conversion-aware, so a .xls upload also
|
||||
// conflicts with an existing .xlsx and does not create a hidden duplicate.
|
||||
func (c *Client) AssertNoFileConflict(ctx context.Context, folderID, localPath string) error {
|
||||
files, err := c.FolderFiles(ctx, folderID)
|
||||
if err != nil {
|
||||
@@ -129,7 +132,7 @@ func (c *Client) AssertNoFileConflict(ctx context.Context, folderID, localPath s
|
||||
}
|
||||
stem := UploadStemFromLocal(localPath)
|
||||
ext := UploadExtFromLocal(localPath)
|
||||
matches := FindFilesByDedupKey(files, stem, ext)
|
||||
matches := FindFilesByStemExt(files, stem, ext)
|
||||
if len(matches) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -153,14 +156,81 @@ func (c *Client) UploadProjectFileNoClobber(ctx context.Context, projectID, loca
|
||||
return c.UploadProjectFile(ctx, projectID, localPath)
|
||||
}
|
||||
|
||||
// UploadToFolderReplacing deletes same stem+ext files then uploads localPath.
|
||||
// uploadReplacementPlan is how a replacing upload reconciles with a folder.
|
||||
type uploadReplacementPlan struct {
|
||||
// UpdateID is the existing file to overwrite in place; empty when no file
|
||||
// matches the upload stem and (converted) extension.
|
||||
UpdateID string
|
||||
// DeleteIDs are redundant duplicate ids to remove after the update.
|
||||
DeleteIDs []int
|
||||
}
|
||||
|
||||
// planUploadReplacement matches an incoming local upload (stem + ext) against
|
||||
// the files already in a folder and decides between a fresh upload, an in-place
|
||||
// update and duplicate cleanup. Matching tolerates the legacy→OOXML conversion
|
||||
// OnlyOffice performs on upload, so a repeated upload of f.xls finds the saved
|
||||
// f.xlsx instead of creating a second file.
|
||||
func planUploadReplacement(files []*FileEntry, stem, ext string) uploadReplacementPlan {
|
||||
matches := FindFilesByStemExt(files, stem, ext)
|
||||
if len(matches) == 0 {
|
||||
return uploadReplacementPlan{}
|
||||
}
|
||||
keep, remove := pickDuplicateKeeper(matches, false)
|
||||
plan := uploadReplacementPlan{}
|
||||
if id := FileEntryNumericID(keep); id != 0 {
|
||||
plan.UpdateID = strconv.FormatInt(id, 10)
|
||||
}
|
||||
for _, f := range remove {
|
||||
if id := int(FileEntryNumericID(f)); id != 0 {
|
||||
plan.DeleteIDs = append(plan.DeleteIDs, id)
|
||||
}
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
// UploadToFolderReplacing upserts localPath into folderID by logical name.
|
||||
// Matching is stem + extension with OnlyOffice's server-side conversion
|
||||
// accounted for: a local .xls is stored as .xlsx, so a repeated upload
|
||||
// overwrites the saved document instead of appending a duplicate. When a
|
||||
// counterpart exists it is updated in place (stable id, no window without the
|
||||
// file) and any extra duplicates are removed; portals that reject an in-place
|
||||
// update fall back to delete + fresh upload, which still leaves one file.
|
||||
func (c *Client) UploadToFolderReplacing(ctx context.Context, folderID, localPath string) (*FileEntry, []int, error) {
|
||||
stem := UploadStemFromLocal(localPath)
|
||||
ext := UploadExtFromLocal(localPath)
|
||||
deleted, err := c.DeleteFilesByDedupKey(ctx, folderID, stem, ext)
|
||||
files, err := c.FolderFiles(ctx, folderID)
|
||||
if err != nil {
|
||||
return nil, deleted, err
|
||||
return nil, nil, err
|
||||
}
|
||||
ent, err := c.UploadToFolder(ctx, folderID, localPath)
|
||||
return ent, deleted, err
|
||||
plan := planUploadReplacement(files, stem, ext)
|
||||
if plan.UpdateID == "" {
|
||||
ent, err := c.UploadToFolder(ctx, folderID, localPath)
|
||||
return ent, nil, err
|
||||
}
|
||||
ent, err := c.UpdateFile(ctx, plan.UpdateID, localPath)
|
||||
if err != nil {
|
||||
deleted, derr := c.DeleteFilesByStemExt(ctx, folderID, stem, ext)
|
||||
if derr != nil {
|
||||
return nil, deleted, derr
|
||||
}
|
||||
ent, uerr := c.UploadToFolder(ctx, folderID, localPath)
|
||||
return ent, deleted, uerr
|
||||
}
|
||||
deleted, derr := c.deleteReplacementExtras(ctx, plan.DeleteIDs)
|
||||
if derr != nil {
|
||||
return ent, deleted, derr
|
||||
}
|
||||
return ent, deleted, nil
|
||||
}
|
||||
|
||||
// deleteReplacementExtras removes redundant duplicate ids collected by
|
||||
// planUploadReplacement after the surviving file was updated in place.
|
||||
func (c *Client) deleteReplacementExtras(ctx context.Context, ids []int) ([]int, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if err := c.DeleteFiles(ctx, ids); err != nil {
|
||||
return ids, err
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user