Merge pull request 'fix(files): upsert uploads by default + dedupe project root (#45)' (#15) from fix/docs-upsert-dedupe into main
This commit was merged in pull request #15.
This commit is contained in:
@@ -28,7 +28,7 @@ Canonical Go client for OnlyOffice Workspace (Projects + Calendar + CRM) and the
|
|||||||
- Prefer `ResponseObject` / `postFormObject` / `putFormObject` / `deleteObject` over hand-rolled `json.Unmarshal(responseField(...))` blocks — they exist for DRY, use them.
|
- Prefer `ResponseObject` / `postFormObject` / `putFormObject` / `deleteObject` over hand-rolled `json.Unmarshal(responseField(...))` blocks — they exist for DRY, use them.
|
||||||
- Domain split is by file, **not** by subpackage. Don't introduce `internal/` or `pkg/*` subpackages inside the library — it flattens the `*Client` call surface for a reason.
|
- Domain split is by file, **not** by subpackage. Don't introduce `internal/` or `pkg/*` subpackages inside the library — it flattens the `*Client` call surface for a reason.
|
||||||
- CLI commands follow **subject → verb** structure (`oo <subject> <verb>`), never `oo <verb>-<subject>`. Add new commands to the existing subject file if one fits; create a new `cmd/oo/<subject>.go` for a genuinely new domain.
|
- CLI commands follow **subject → verb** structure (`oo <subject> <verb>`), never `oo <verb>-<subject>`. Add new commands to the existing subject file if one fits; create a new `cmd/oo/<subject>.go` for a genuinely new domain.
|
||||||
- **Documents for agents:** prefer Markdown in git; OnlyOffice UI is weak for `.md`/`.txt`. Use `oo docs put-md` (md→docx) and `oo docs put-txt` (txt→docx, preserves line breaks). `oo projects files dedupe PROJECT_ID` reports/removes duplicate stem|ext copies (`--apply`, `--cross`).
|
- **Documents for agents:** prefer Markdown in git; OnlyOffice UI is weak for `.md`/`.txt`. Use `oo docs put-md` (md→docx) and `oo docs put-txt` (txt→docx, preserves line breaks). All upload paths default to **upsert** by `stem|ext` (`--replace`, default true); `--no-replace` fails on conflict; `--allow-duplicate` opts into raw OO append. `oo projects files dedupe PROJECT_ID` reports/removes duplicate stem|ext copies (`--apply`, `--cross`; includes project root folder).
|
||||||
- Every table output goes through `printTable(headers, rows)`; every single-object through `printObject(v)`. Do not `fmt.Println` rows ad-hoc or the `--output json` flag breaks for that command.
|
- Every table output goes through `printTable(headers, rows)`; every single-object through `printObject(v)`. Do not `fmt.Println` rows ad-hoc or the `--output json` flag breaks for that command.
|
||||||
- No secrets in the repo; use `.env` (gitignored). Commit `.env.example` only.
|
- No secrets in the repo; use `.env` (gitignored). Commit `.env.example` only.
|
||||||
- Follow SemVer on tags; this repo is tagged at GitHub under `git@github.com:eSlider/go-onlyoffice.git`.
|
- Follow SemVer on tags; this repo is tagged at GitHub under `git@github.com:eSlider/go-onlyoffice.git`.
|
||||||
|
|||||||
+44
-70
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -423,44 +424,28 @@ func docsPutMDCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx := cmd.Context()
|
ctx := cmd.Context()
|
||||||
var ent *onlyoffice.FileEntry
|
ent, deleted, err := uploadProjectDoc(ctx, c, pid, docxPath, folderID, replace)
|
||||||
var deleted []int
|
|
||||||
if folderID != "" {
|
|
||||||
if replace {
|
|
||||||
ent, deleted, err = c.UploadToFolderReplacing(ctx, folderID, docxPath)
|
|
||||||
} else {
|
|
||||||
ent, err = c.UploadToFolder(ctx, folderID, docxPath)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
obj := map[string]any{
|
|
||||||
"project_id": pid,
|
|
||||||
"folder_id": folderID,
|
|
||||||
"md": mdPath,
|
|
||||||
"uploaded": fileEntryToMap(ent),
|
|
||||||
}
|
|
||||||
if len(deleted) > 0 {
|
|
||||||
obj["replaced_file_ids"] = deleted
|
|
||||||
}
|
|
||||||
printObject(obj)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ent, err = c.UploadProjectFile(ctx, pid, docxPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
printObject(map[string]any{
|
obj := map[string]any{
|
||||||
"project_id": pid,
|
"project_id": pid,
|
||||||
"md": mdPath,
|
"md": mdPath,
|
||||||
"uploaded": fileEntryToMap(ent),
|
"uploaded": fileEntryToMap(ent),
|
||||||
})
|
}
|
||||||
|
if folderID != "" {
|
||||||
|
obj["folder_id"] = folderID
|
||||||
|
}
|
||||||
|
if len(deleted) > 0 {
|
||||||
|
obj["replaced_file_ids"] = deleted
|
||||||
|
}
|
||||||
|
printObject(obj)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVar(&folderID, "folder", "", "Documents folder id (default: project root)")
|
cmd.Flags().StringVar(&folderID, "folder", "", "Documents folder id (default: project root)")
|
||||||
cmd.Flags().StringVar(&keepLocalDOCX, "keep-docx", "", "also write the generated DOCX to this local path")
|
cmd.Flags().StringVar(&keepLocalDOCX, "keep-docx", "", "also write the generated DOCX to this local path")
|
||||||
cmd.Flags().BoolVar(&replace, "replace", true, "delete same-stem files in folder before upload (put-md upsert)")
|
cmd.Flags().BoolVar(&replace, "replace", true, "replace same stem|ext before upload (default); false = fail if name taken")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,44 +488,28 @@ func docsPutTxtCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx := cmd.Context()
|
ctx := cmd.Context()
|
||||||
var ent *onlyoffice.FileEntry
|
ent, deleted, err := uploadProjectDoc(ctx, c, pid, docxPath, folderID, replace)
|
||||||
var deleted []int
|
|
||||||
if folderID != "" {
|
|
||||||
if replace {
|
|
||||||
ent, deleted, err = c.UploadToFolderReplacing(ctx, folderID, docxPath)
|
|
||||||
} else {
|
|
||||||
ent, err = c.UploadToFolder(ctx, folderID, docxPath)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
obj := map[string]any{
|
|
||||||
"project_id": pid,
|
|
||||||
"txt": txtPath,
|
|
||||||
"folder_id": folderID,
|
|
||||||
"uploaded": fileEntryToMap(ent),
|
|
||||||
}
|
|
||||||
if len(deleted) > 0 {
|
|
||||||
obj["replaced_file_ids"] = deleted
|
|
||||||
}
|
|
||||||
printObject(obj)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ent, err = c.UploadProjectFile(ctx, pid, docxPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
printObject(map[string]any{
|
obj := map[string]any{
|
||||||
"project_id": pid,
|
"project_id": pid,
|
||||||
"txt": txtPath,
|
"txt": txtPath,
|
||||||
"uploaded": fileEntryToMap(ent),
|
"uploaded": fileEntryToMap(ent),
|
||||||
})
|
}
|
||||||
|
if folderID != "" {
|
||||||
|
obj["folder_id"] = folderID
|
||||||
|
}
|
||||||
|
if len(deleted) > 0 {
|
||||||
|
obj["replaced_file_ids"] = deleted
|
||||||
|
}
|
||||||
|
printObject(obj)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVar(&folderID, "folder", "", "Documents folder id (default: project root)")
|
cmd.Flags().StringVar(&folderID, "folder", "", "Documents folder id (default: project root)")
|
||||||
cmd.Flags().StringVar(&keepLocalDOCX, "keep-docx", "", "also write the generated DOCX to this local path")
|
cmd.Flags().StringVar(&keepLocalDOCX, "keep-docx", "", "also write the generated DOCX to this local path")
|
||||||
cmd.Flags().BoolVar(&replace, "replace", true, "delete same-stem files in folder before upload (put-txt upsert)")
|
cmd.Flags().BoolVar(&replace, "replace", true, "replace same stem|ext before upload (default); false = fail if name taken")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,21 +587,7 @@ formulas (SUM/AVG, cross-sheet refs, named inputs) via --template, or upload an
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx := cmd.Context()
|
ctx := cmd.Context()
|
||||||
var ent *onlyoffice.FileEntry
|
ent, deleted, err := uploadProjectDoc(ctx, c, pid, xlsxPath, folderID, replace)
|
||||||
var deleted []int
|
|
||||||
if folderID != "" {
|
|
||||||
if replace {
|
|
||||||
ent, deleted, err = c.UploadToFolderReplacing(ctx, folderID, xlsxPath)
|
|
||||||
} else {
|
|
||||||
ent, err = c.UploadToFolder(ctx, folderID, xlsxPath)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if replace {
|
|
||||||
ent, deleted, err = c.UploadProjectFileReplacing(ctx, pid, xlsxPath)
|
|
||||||
} else {
|
|
||||||
ent, err = c.UploadProjectFile(ctx, pid, xlsxPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -655,6 +610,25 @@ formulas (SUM/AVG, cross-sheet refs, named inputs) via --template, or upload an
|
|||||||
cmd.Flags().StringVar(&template, "template", "", "built-in workbook template (cutover-portugal)")
|
cmd.Flags().StringVar(&template, "template", "", "built-in workbook template (cutover-portugal)")
|
||||||
cmd.Flags().StringVar(&title, "title", "", "upload file name when using --template")
|
cmd.Flags().StringVar(&title, "title", "", "upload file name when using --template")
|
||||||
cmd.Flags().StringVar(&keepLocal, "keep-xlsx", "", "also write generated/uploaded bytes to this local path")
|
cmd.Flags().StringVar(&keepLocal, "keep-xlsx", "", "also write generated/uploaded bytes to this local path")
|
||||||
cmd.Flags().BoolVar(&replace, "replace", true, "delete same-stem files before upload")
|
cmd.Flags().BoolVar(&replace, "replace", true, "replace same stem|ext before upload (default); false = fail if name taken")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uploadProjectDoc upserts (--replace, default) or no-clobbers into project/folder Documents.
|
||||||
|
func uploadProjectDoc(ctx context.Context, c *onlyoffice.Client, pid, localPath, folderID string, replace bool) (*onlyoffice.FileEntry, []int, error) {
|
||||||
|
if folderID != "" {
|
||||||
|
if replace {
|
||||||
|
return c.UploadToFolderReplacing(ctx, folderID, localPath)
|
||||||
|
}
|
||||||
|
if err := c.AssertNoFileConflict(ctx, folderID, localPath); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
ent, err := c.UploadToFolder(ctx, folderID, localPath)
|
||||||
|
return ent, nil, err
|
||||||
|
}
|
||||||
|
if replace {
|
||||||
|
return c.UploadProjectFileReplacing(ctx, pid, localPath)
|
||||||
|
}
|
||||||
|
ent, err := c.UploadProjectFileNoClobber(ctx, pid, localPath)
|
||||||
|
return ent, nil, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -107,10 +107,13 @@ func prjFilesListCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func prjFilesUploadCmd() *cobra.Command {
|
func prjFilesUploadCmd() *cobra.Command {
|
||||||
return &cobra.Command{
|
var replace, allowDuplicate bool
|
||||||
|
cmd := &cobra.Command{
|
||||||
Use: "upload PROJECT_ID LOCAL_PATH [LOCAL_PATH...]",
|
Use: "upload PROJECT_ID LOCAL_PATH [LOCAL_PATH...]",
|
||||||
Short: "Upload file(s) into the project's Documents folder",
|
Short: "Upload file(s) into the project's Documents folder (upsert by stem|ext)",
|
||||||
Args: cobra.MinimumNArgs(2),
|
Long: `Default: replace an existing file with the same logical name (stem|ext), like cp overwrite.
|
||||||
|
Pass --no-replace to fail when the name is taken; --allow-duplicate to always create a new file id.`,
|
||||||
|
Args: cobra.MinimumNArgs(2),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
c, err := newOO(cmd)
|
c, err := newOO(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -118,15 +121,31 @@ func prjFilesUploadCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
pid := args[0]
|
pid := args[0]
|
||||||
for _, p := range args[1:] {
|
for _, p := range args[1:] {
|
||||||
entry, err := c.UploadProjectFile(cmd.Context(), pid, p)
|
var entry *onlyoffice.FileEntry
|
||||||
|
var deleted []int
|
||||||
|
switch {
|
||||||
|
case allowDuplicate:
|
||||||
|
entry, err = c.UploadProjectFile(cmd.Context(), pid, p)
|
||||||
|
case replace:
|
||||||
|
entry, deleted, err = c.UploadProjectFileReplacing(cmd.Context(), pid, p)
|
||||||
|
default:
|
||||||
|
entry, err = c.UploadProjectFileNoClobber(cmd.Context(), pid, p)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
printObject(fileEntryToMap(entry))
|
obj := fileEntryToMap(entry)
|
||||||
|
if len(deleted) > 0 {
|
||||||
|
obj["replaced_file_ids"] = deleted
|
||||||
|
}
|
||||||
|
printObject(obj)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
cmd.Flags().BoolVar(&replace, "replace", true, "replace same stem|ext in project folder (default)")
|
||||||
|
cmd.Flags().BoolVar(&allowDuplicate, "allow-duplicate", false, "always create a new file even when the name exists")
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func prjFilesDownloadCmd() *cobra.Command {
|
func prjFilesDownloadCmd() *cobra.Command {
|
||||||
|
|||||||
+52
-6
@@ -2,6 +2,7 @@ package onlyoffice
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -310,25 +311,70 @@ func (c *Client) DeleteFilesByDedupKey(ctx context.Context, folderID, stem, ext
|
|||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mergeProjectRootForDedupe includes projectFolder files in dedupe scans. OO often lists
|
||||||
|
// root documents only in pf.Files while pf.Folders is empty.
|
||||||
|
func mergeProjectRootForDedupe(rootID string, folders []*FolderEntry, filesByFolder map[string][]*FileEntry, rootFiles []*FileEntry) ([]*FolderEntry, map[string][]*FileEntry) {
|
||||||
|
if rootID == "" {
|
||||||
|
return folders, filesByFolder
|
||||||
|
}
|
||||||
|
if filesByFolder == nil {
|
||||||
|
filesByFolder = map[string][]*FileEntry{}
|
||||||
|
}
|
||||||
|
for _, folder := range folders {
|
||||||
|
if folder != nil && folder.ID != nil && folder.ID.String() == rootID {
|
||||||
|
if len(rootFiles) > 0 {
|
||||||
|
filesByFolder[rootID] = rootFiles
|
||||||
|
}
|
||||||
|
return folders, filesByFolder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rootFiles) == 0 {
|
||||||
|
return folders, filesByFolder
|
||||||
|
}
|
||||||
|
id := json.Number(rootID)
|
||||||
|
title := "(project root)"
|
||||||
|
folders = append(folders, &FolderEntry{ID: &id, Title: &title})
|
||||||
|
filesByFolder[rootID] = rootFiles
|
||||||
|
return folders, filesByFolder
|
||||||
|
}
|
||||||
|
|
||||||
// DedupeProject scans project folders and optionally deletes duplicates.
|
// DedupeProject scans project folders and optionally deletes duplicates.
|
||||||
func (c *Client) DedupeProject(ctx context.Context, projectID string, opts DedupOptions, apply bool) ([]DedupGroup, []int, error) {
|
func (c *Client) DedupeProject(ctx context.Context, projectID string, opts DedupOptions, apply bool) ([]DedupGroup, []int, error) {
|
||||||
pf, err := c.GetProjectFiles(ctx, projectID)
|
pf, err := c.GetProjectFiles(ctx, projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
filesByFolder := make(map[string][]*FileEntry, len(pf.Folders))
|
rootID, err := c.projectFolderID(ctx, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
var rootFiles []*FileEntry
|
||||||
|
if rootID != "" {
|
||||||
|
rootFiles, err = c.FolderFiles(ctx, rootID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filesByFolder := make(map[string][]*FileEntry, len(pf.Folders)+1)
|
||||||
|
folders := make([]*FolderEntry, 0, len(pf.Folders)+1)
|
||||||
for _, folder := range pf.Folders {
|
for _, folder := range pf.Folders {
|
||||||
if folder == nil || folder.ID == nil {
|
if folder == nil || folder.ID == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fid := folder.ID.String()
|
fid := folder.ID.String()
|
||||||
files, err := c.FolderFiles(ctx, fid)
|
if fid == rootID {
|
||||||
if err != nil {
|
filesByFolder[fid] = rootFiles
|
||||||
return nil, nil, err
|
} else {
|
||||||
|
files, err := c.FolderFiles(ctx, fid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
filesByFolder[fid] = files
|
||||||
}
|
}
|
||||||
filesByFolder[fid] = files
|
folders = append(folders, folder)
|
||||||
}
|
}
|
||||||
groups := FindProjectDuplicates(pf.Folders, filesByFolder, opts)
|
folders, filesByFolder = mergeProjectRootForDedupe(rootID, folders, filesByFolder, rootFiles)
|
||||||
|
groups := FindProjectDuplicates(folders, filesByFolder, opts)
|
||||||
if !apply || len(groups) == 0 {
|
if !apply || len(groups) == 0 {
|
||||||
return groups, nil, nil
|
return groups, nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,26 @@ func TestCrossFolderPrefersNonTrash(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMergeProjectRootForDedupe(t *testing.T) {
|
||||||
|
old := &FileEntry{ID: jsonNum("1"), Title: strPtr("a.docx"), FileExst: strPtr(".docx")}
|
||||||
|
newer := &FileEntry{ID: jsonNum("2"), Title: strPtr("a.docx"), FileExst: strPtr(".docx")}
|
||||||
|
rootFiles := []*FileEntry{old, newer}
|
||||||
|
folders, byFolder := mergeProjectRootForDedupe("489", nil, nil, rootFiles)
|
||||||
|
if len(folders) != 1 || folders[0].ID.String() != "489" {
|
||||||
|
t.Fatalf("folders=%+v", folders)
|
||||||
|
}
|
||||||
|
if len(byFolder["489"]) != 2 {
|
||||||
|
t.Fatalf("root files=%d", len(byFolder["489"]))
|
||||||
|
}
|
||||||
|
groups := findWithinFolderDuplicates([]ProjectFolderFile{
|
||||||
|
{FolderID: "489", FolderTitle: "(project root)", File: old},
|
||||||
|
{FolderID: "489", FolderTitle: "(project root)", File: newer},
|
||||||
|
})
|
||||||
|
if len(groups) != 1 {
|
||||||
|
t.Fatalf("groups=%d", len(groups))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestIsTrashFolderTitle(t *testing.T) {
|
func TestIsTrashFolderTitle(t *testing.T) {
|
||||||
if !IsTrashFolderTitle("_trash-md") {
|
if !IsTrashFolderTitle("_trash-md") {
|
||||||
t.Fatal("expected trash")
|
t.Fatal("expected trash")
|
||||||
|
|||||||
@@ -3,10 +3,15 @@ package onlyoffice
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrFileExists is returned when --no-replace / no-clobber upload hits an existing stem|ext.
|
||||||
|
var ErrFileExists = errors.New("onlyoffice: file already exists in folder (use replace or delete first)")
|
||||||
|
|
||||||
// FileEntryStem returns the logical basename without duplicated extensions.
|
// FileEntryStem returns the logical basename without duplicated extensions.
|
||||||
// OO often stores title="foo.docx" and fileExst=".docx" (UI shows foo.docx.docx).
|
// OO often stores title="foo.docx" and fileExst=".docx" (UI shows foo.docx.docx).
|
||||||
func FileEntryStem(f *FileEntry) string {
|
func FileEntryStem(f *FileEntry) string {
|
||||||
@@ -116,6 +121,38 @@ func (c *Client) DeleteFilesByStem(ctx context.Context, folderID, stem string) (
|
|||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssertNoFileConflict reports ErrFileExists when localPath stem|ext is already in folderID.
|
||||||
|
func (c *Client) AssertNoFileConflict(ctx context.Context, folderID, localPath string) error {
|
||||||
|
files, err := c.FolderFiles(ctx, folderID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stem := UploadStemFromLocal(localPath)
|
||||||
|
ext := UploadExtFromLocal(localPath)
|
||||||
|
matches := FindFilesByDedupKey(files, stem, ext)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ids := make([]string, 0, len(matches))
|
||||||
|
for _, f := range matches {
|
||||||
|
ids = append(ids, fmt.Sprintf("%d", FileEntryNumericID(f)))
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%w: %s%s in folder %s (existing file ids: %s)",
|
||||||
|
ErrFileExists, stem, ext, folderID, strings.Join(ids, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadProjectFileNoClobber uploads only when stem|ext is not already in the project folder.
|
||||||
|
func (c *Client) UploadProjectFileNoClobber(ctx context.Context, projectID, localPath string) (*FileEntry, error) {
|
||||||
|
folderID, err := c.projectFolderID(ctx, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := c.AssertNoFileConflict(ctx, folderID, localPath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.UploadProjectFile(ctx, projectID, localPath)
|
||||||
|
}
|
||||||
|
|
||||||
// UploadToFolderReplacing deletes same stem+ext 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) {
|
func (c *Client) UploadToFolderReplacing(ctx context.Context, folderID, localPath string) (*FileEntry, []int, error) {
|
||||||
stem := UploadStemFromLocal(localPath)
|
stem := UploadStemFromLocal(localPath)
|
||||||
|
|||||||
Reference in New Issue
Block a user