feat(files): project/task Documents API + oo projects|tasks files
Library (files.go):
- Types: FileEntry, FolderEntry, ProjectFilesResponse.
- GetProjectFiles, GetTaskFiles, GetFile, UploadProjectFile (POST .../upload),
AttachFilesToTask (POST form files=), UploadTaskFile (upload+attach),
DetachTaskFile (DELETE ?fileid=), RenameFile, DeleteFiles, DownloadFile.
- Helpers: FileEntryNumericID, FileEntryTitle, SafeLocalFileName, projectIDFromTaskMap.
http.go: putJSON for JSON PUT bodies.
CLI: oo projects files {list,upload,download,rename,delete}, oo tasks files
{list,upload,detach}; projects_files.go + tasks_files.go.
Tests: files_integration_test.go (live), files_test.go + testdata (decode).
Docs: CHANGELOG 0.6.0, README, AGENTS.md, cmd/oo/main.go tree.
Made-with: Cursor
This commit is contained in:
+2
-2
@@ -3,8 +3,8 @@
|
||||
// Command tree is subject-based (mirrors the library split and the `tea` CLI):
|
||||
//
|
||||
// oo calendar list | events | add | delete
|
||||
// oo projects list | get | milestones | create | update | delete
|
||||
// oo tasks list | get | create | update | delete | subtask add
|
||||
// oo projects list | get | milestones | create | update | delete | files (list|upload|download|rename|delete)
|
||||
// oo tasks list | get | create | update | delete | subtask add | files (list|upload|detach)
|
||||
// oo users list | self (alias: oo whoami)
|
||||
// oo contacts list | get | delete | info-add
|
||||
// oo persons list | create | delete
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
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())
|
||||
return cmd
|
||||
}
|
||||
|
||||
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: ./<title>)",
|
||||
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: ./<server title>)")
|
||||
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 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()
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
tasksCmd.AddCommand(taskFilesCmd())
|
||||
}
|
||||
|
||||
func taskFilesCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "files",
|
||||
Short: "Task attachments: list, upload (project folder + attach), detach",
|
||||
}
|
||||
cmd.AddCommand(taskFilesListCmd())
|
||||
cmd.AddCommand(taskFilesUploadCmd())
|
||||
cmd.AddCommand(taskFilesDetachCmd())
|
||||
return cmd
|
||||
}
|
||||
|
||||
func taskFilesListCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list TASK_ID",
|
||||
Short: "List files attached to a task",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list, err := c.GetTaskFiles(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printTable([]string{"id", "title", "fileExst", "contentLength", "updated"}, fileEntryRows(list))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func taskFilesUploadCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "upload TASK_ID LOCAL_PATH [LOCAL_PATH...]",
|
||||
Short: "Upload into the task's project folder and attach each file to the task",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tid := args[0]
|
||||
for _, p := range args[1:] {
|
||||
entry, err := c.UploadTaskFile(cmd.Context(), tid, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(fileEntryToMap(entry))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func taskFilesDetachCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "detach TASK_ID FILE_ID [FILE_ID...]",
|
||||
Short: "Detach file(s) from the task (files remain in Documents)",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tid := args[0]
|
||||
for _, fid := range args[1:] {
|
||||
if _, err := strconv.Atoi(fid); err != nil {
|
||||
return fmt.Errorf("file id %q: %w", fid, err)
|
||||
}
|
||||
if err := c.DetachTaskFile(cmd.Context(), tid, fid); err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(map[string]any{"taskId": tid, "detachedFileId": fid})
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user