package main import ( "fmt" "os" "path/filepath" "strconv" "time" onlyoffice "github.com/eslider/go-onlyoffice" "github.com/spf13/cobra" ) func init() { projectsCmd.AddCommand(projectFilesCmd()) } func projectFilesCmd() *cobra.Command { cmd := &cobra.Command{ Use: "files", Short: "Project Documents folder: list, upload, download, rename, delete", } cmd.AddCommand(prjFilesListCmd()) cmd.AddCommand(prjFilesUploadCmd()) cmd.AddCommand(prjFilesReplaceInCmd()) cmd.AddCommand(prjFilesUpdateCmd()) cmd.AddCommand(prjFilesDownloadCmd()) cmd.AddCommand(prjFilesRenameCmd()) cmd.AddCommand(prjFilesDeleteCmd()) cmd.AddCommand(prjFilesDedupeCmd()) // Convenience aliases into oo docs (md↔docx / OCR pipeline). cmd.AddCommand(aliasDocsAsMD()) cmd.AddCommand(aliasDocsPutMD()) cmd.AddCommand(aliasDocsPutTxt()) cmd.AddCommand(aliasDocsPutXlsx()) return cmd } func aliasDocsAsMD() *cobra.Command { c := docsAsMDCmd() c.Use = "as-md FILE_ID" c.Short = "Alias of `oo docs as-md` — download OO file as Markdown (OCR if needed)" return c } func aliasDocsPutMD() *cobra.Command { c := docsPutMDCmd() c.Use = "put-md PROJECT_ID MARKDOWN_PATH" c.Short = "Alias of `oo docs put-md` — Markdown→DOCX upload into project" return c } func aliasDocsPutTxt() *cobra.Command { c := docsPutTxtCmd() c.Use = "put-txt PROJECT_ID TEXT_PATH" c.Short = "Alias of `oo docs put-txt` — plain text→DOCX upload into project" return c } func aliasDocsPutXlsx() *cobra.Command { c := docsPutXlsxCmd() c.Use = "put-xlsx PROJECT_ID [LOCAL_XLSX]" c.Short = "Alias of `oo docs put-xlsx` — generate/upload XLSX with formulas" return c } func prjFilesListCmd() *cobra.Command { var showFolders bool cmd := &cobra.Command{ Use: "list PROJECT_ID", Short: "List files (and optionally folders) attached to the project", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { c, err := newOO(cmd) if err != nil { return err } pf, err := c.GetProjectFiles(cmd.Context(), args[0]) if err != nil { return err } if showFolders && len(pf.Folders) > 0 { frows := make([]map[string]any, 0, len(pf.Folders)) for _, f := range pf.Folders { if f == nil { continue } frows = append(frows, map[string]any{ "id": folderIDStr(f), "title": derefString(f.Title), "filesCount": derefInt(f.FilesCount), "foldersCount": derefInt(f.FoldersCount), }) } if outputFormat == "table" { fmt.Println("folders:") } printTable([]string{"id", "title", "filesCount", "foldersCount"}, frows) } rows := fileEntryRows(pf.Files) if outputFormat == "table" { fmt.Println("files:") } printTable([]string{"id", "title", "fileExst", "contentLength", "updated"}, rows) return nil }, } cmd.Flags().BoolVar(&showFolders, "folders", false, "also print project subfolders") return cmd } func prjFilesUploadCmd() *cobra.Command { var replace, allowDuplicate bool cmd := &cobra.Command{ Use: "upload PROJECT_ID LOCAL_PATH [LOCAL_PATH...]", Short: "Upload file(s) into the project's Documents folder (upsert by stem, legacy↔OOXML ext)", Long: `Default: replace an existing file with the same logical name (stem, treating legacy .xls/.doc/.ppt and their OOXML .xlsx/.docx/.pptx as one file), 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 { c, err := newOO(cmd) if err != nil { return err } pid := args[0] for _, p := range args[1:] { 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 { return err } obj := fileEntryToMap(entry) if len(deleted) > 0 { obj["replaced_file_ids"] = deleted } printObject(obj) } 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 prjFilesReplaceInCmd() *cobra.Command { return &cobra.Command{ Use: "replace-in FOLDER_ID LOCAL_PATH [LOCAL_PATH...]", Short: "Replace same-named file(s) in a folder: hard delete + fresh upload (no version history)", Long: `Deletes any file in FOLDER_ID with the same stem|ext (hard delete — the CLI delete is permanent) and uploads the local file fresh. Unlike 'update' this leaves a single clean version. Why it exists: repeated 'update' of a shared document accumulated a visible version history and left a stale id. replace-in yields one clean revision; then point links at the returned id (or keep an nginx alias for the legacy fileid). Note: file ids are server-assigned; a fresh upload gets a new id.`, Args: cobra.MinimumNArgs(2), RunE: func(cmd *cobra.Command, args []string) error { c, err := newOO(cmd) if err != nil { return err } folderID := args[0] for _, p := range args[1:] { stem := onlyoffice.UploadStemFromLocal(p) ext := onlyoffice.UploadExtFromLocal(p) deleted, derr := c.DeleteFilesByDedupKey(cmd.Context(), folderID, stem, ext) if derr != nil { return derr } ent, uerr := c.UploadToFolder(cmd.Context(), folderID, p) if uerr != nil { return uerr } obj := fileEntryToMap(ent) if len(deleted) > 0 { obj["replaced_file_ids"] = deleted } printObject(obj) } return nil }, } } func prjFilesUpdateCmd() *cobra.Command { return &cobra.Command{ Use: "update FILE_ID LOCAL_PATH", Short: "Overwrite an existing Documents file with new content (new version)", Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { c, err := newOO(cmd) if err != nil { return err } entry, err := c.UpdateFile(cmd.Context(), args[0], args[1]) if err != nil { return err } printObject(fileEntryToMap(entry)) return nil }, } } func prjFilesDownloadCmd() *cobra.Command { var to string cmd := &cobra.Command{ Use: "download FILE_ID", Short: "Download file bytes via viewUrl (default path: ./