package main
import (
"fmt"
"os"
"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(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())
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 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 {
return &cobra.Command{
Use: "upload PROJECT_ID LOCAL_PATH [LOCAL_PATH...]",
Short: "Upload file(s) into the project's Documents folder",
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:] {
entry, err := c.UploadProjectFile(cmd.Context(), pid, p)
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: ./
)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newOO(cmd)
if err != nil {
return err
}
f, err := c.GetFile(cmd.Context(), args[0])
if err != nil {
return err
}
path := to
if path == "" {
path = onlyoffice.SafeLocalFileName(onlyoffice.FileEntryTitle(f))
}
out, err := os.Create(path)
if err != nil {
return err
}
defer out.Close()
n, err := c.DownloadFile(cmd.Context(), args[0], out)
if err != nil {
_ = os.Remove(path)
return err
}
if outputFormat == "json" {
printObject(map[string]any{"path": path, "bytes": n})
return nil
}
fmt.Printf("downloaded: %s (%d bytes)\n", path, n)
return nil
},
}
cmd.Flags().StringVar(&to, "to", "", "output path (default: ./)")
return cmd
}
func prjFilesRenameCmd() *cobra.Command {
return &cobra.Command{
Use: "rename FILE_ID NEW_TITLE",
Short: "Rename a file (include extension in NEW_TITLE)",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newOO(cmd)
if err != nil {
return err
}
entry, err := c.RenameFile(cmd.Context(), args[0], args[1])
if err != nil {
return err
}
printObject(fileEntryToMap(entry))
return nil
},
}
}
func prjFilesDeleteCmd() *cobra.Command {
return &cobra.Command{
Use: "delete FILE_ID [FILE_ID...]",
Aliases: []string{"rm"},
Short: "Permanently delete file(s) from Documents",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newOO(cmd)
if err != nil {
return err
}
ids := make([]int, 0, len(args))
for _, s := range args {
id, err := strconv.Atoi(s)
if err != nil {
return fmt.Errorf("file id %q: %w", s, err)
}
ids = append(ids, id)
}
if err := c.DeleteFiles(cmd.Context(), ids); err != nil {
return err
}
printObject(map[string]any{"deleted": ids})
return nil
},
}
}
func prjFilesDedupeCmd() *cobra.Command {
var apply, cross bool
cmd := &cobra.Command{
Use: "dedupe PROJECT_ID",
Short: "Find (and optionally remove) duplicate files in project Documents folders",
Long: `Duplicates share the same logical name: stem|ext (OO title+fileExst).
Default: dry-run report. Pass --apply to delete older copies (keeps newest; --cross prefers non-trash folders).`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newOO(cmd)
if err != nil {
return err
}
groups, deleted, err := c.DedupeProject(cmd.Context(), args[0], onlyoffice.DedupOptions{
CrossFolder: cross,
}, apply)
if err != nil {
return err
}
rows := make([]map[string]any, 0, len(groups))
for _, g := range groups {
row := map[string]any{
"key": g.Key,
"folder_id": g.FolderID,
"folder_title": g.FolderTitle,
"keep_id": fileIDStr(g.Keep),
"keep_title": onlyoffice.FileEntryTitle(g.Keep),
"remove_count": len(g.Remove),
}
removeIDs := make([]string, 0, len(g.Remove))
for _, f := range g.Remove {
removeIDs = append(removeIDs, fileIDStr(f))
}
row["remove_ids"] = removeIDs
rows = append(rows, row)
}
out := map[string]any{
"project_id": args[0],
"dry_run": !apply,
"cross": cross,
"groups": len(groups),
"duplicates": rows,
}
if apply {
out["deleted_ids"] = deleted
}
printObject(out)
return nil
},
}
cmd.Flags().BoolVar(&apply, "apply", false, "delete duplicate files (default: report only)")
cmd.Flags().BoolVar(&cross, "cross", false, "also dedupe same stem|ext across folders (prefers non-_trash)")
return cmd
}
func fileEntryRows(files []*onlyoffice.FileEntry) []map[string]any {
rows := make([]map[string]any, 0, len(files))
for _, f := range files {
if f == nil {
continue
}
rows = append(rows, fileEntryToMap(f))
}
return rows
}
func fileEntryToMap(f *onlyoffice.FileEntry) map[string]any {
m := map[string]any{
"id": fileIDStr(f),
"title": onlyoffice.FileEntryTitle(f),
"fileExst": derefString(f.FileExst),
"contentLength": derefString(f.ContentLength),
}
if f.Updated != nil {
m["updated"] = f.Updated.Format(time.RFC3339)
}
return m
}
func fileIDStr(f *onlyoffice.FileEntry) string {
if f == nil || f.ID == nil {
return ""
}
return f.ID.String()
}
func folderIDStr(f *onlyoffice.FolderEntry) string {
if f == nil || f.ID == nil {
return ""
}
return f.ID.String()
}