feat(files): dedupe by stem|ext + oo projects files dedupe

- FileDedupKey groups true duplicates (same name+extension)
- put-md --replace deletes matching stem|ext only (not other formats)
- oo projects files dedupe PROJECT_ID [--apply] [--cross]
- Cross-folder mode prefers non-_trash folders, keeps newest file

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-27 23:40:07 +01:00
co-authored by Cursor
parent a8cb4e9780
commit ac06d86363
6 changed files with 484 additions and 4 deletions
+5 -3
View File
@@ -89,7 +89,8 @@ func FindFilesByStem(files []*FileEntry, stem string) []*FileEntry {
return out
}
// DeleteFilesByStem removes all files in folderID matching stem (for put-md upsert).
// DeleteFilesByStem removes all files in folderID matching stem (any extension).
// Prefer DeleteFilesByDedupKey when the upload extension is known.
func (c *Client) DeleteFilesByStem(ctx context.Context, folderID, stem string) ([]int, error) {
files, err := c.FolderFiles(ctx, folderID)
if err != nil {
@@ -115,10 +116,11 @@ func (c *Client) DeleteFilesByStem(ctx context.Context, folderID, stem string) (
return ids, nil
}
// UploadToFolderReplacing deletes same-stem files then uploads localPath.
// UploadToFolderReplacing deletes same stem+ext files then uploads localPath.
func (c *Client) UploadToFolderReplacing(ctx context.Context, folderID, localPath string) (*FileEntry, []int, error) {
stem := UploadStemFromLocal(localPath)
deleted, err := c.DeleteFilesByStem(ctx, folderID, stem)
ext := UploadExtFromLocal(localPath)
deleted, err := c.DeleteFilesByDedupKey(ctx, folderID, stem, ext)
if err != nil {
return nil, deleted, err
}