feat(dav): oo dav upload (local file) + ensure-path (mkdir -p) in Documents (#80)
Release / GoReleaser (push) Skipped
Tests / Secret scan (gitleaks) (push) Skipped
Tests / Test (Go 1.25) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Tests / Secret scan (gitleaks) (pull_request) Successful in 5s
Tests / Test (Go stable) (pull_request) Successful in 1m9s
Tests / Test (Go 1.25) (pull_request) Successful in 1m11s
Release / GoReleaser (push) Skipped
Tests / Secret scan (gitleaks) (push) Skipped
Tests / Test (Go 1.25) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Tests / Secret scan (gitleaks) (pull_request) Successful in 5s
Tests / Test (Go stable) (pull_request) Successful in 1m9s
Tests / Test (Go 1.25) (pull_request) Successful in 1m11s
This commit is contained in:
+189
@@ -1,8 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
onlyoffice "github.com/eslider/go-onlyoffice"
|
||||
@@ -26,6 +28,8 @@ func davCmd() *cobra.Command {
|
||||
cmd.AddCommand(davMoveCmd())
|
||||
cmd.AddCommand(davCopyCmd())
|
||||
cmd.AddCommand(davMkdirCmd())
|
||||
cmd.AddCommand(davEnsurePathCmd())
|
||||
cmd.AddCommand(davUploadCmd())
|
||||
cmd.AddCommand(davRemoveCmd())
|
||||
cmd.AddCommand(davRenameFileCmd())
|
||||
cmd.AddCommand(davRenameFolderCmd())
|
||||
@@ -187,6 +191,191 @@ func davMkdirCmd() *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func davEnsurePathCmd() *cobra.Command {
|
||||
var under string
|
||||
cmd := &cobra.Command{
|
||||
Use: "ensure-path PATH",
|
||||
Short: "Resolve or create nested Documents folders (mkdir -p), print the final folder id",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root := under
|
||||
if root == "" {
|
||||
root, err = myDocumentsID(cmd.Context(), c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
f, err := ensurePath(cmd.Context(), c.FileStore(onlyoffice.ProviderDAV), root, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(map[string]any{"id": f.ID, "title": f.Title, "under": root})
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&under, "under", "", "parent folder id (default: My documents)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func davUploadCmd() *cobra.Command {
|
||||
var replace bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "upload DEST_FOLDER_ID LOCAL_FILE [LOCAL_FILE...]",
|
||||
Short: "Upload local file(s) into a Documents folder (multipart, upsert by name)",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
results, err := uploadLocal(cmd.Context(), c, args[0], args[1:], replace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows := make([]map[string]any, 0, len(results))
|
||||
for _, r := range results {
|
||||
replaced := make([]string, 0, len(r.Replaced))
|
||||
for _, id := range r.Replaced {
|
||||
replaced = append(replaced, fmt.Sprint(id))
|
||||
}
|
||||
rows = append(rows, map[string]any{
|
||||
"id": r.Entry.ID,
|
||||
"title": r.Entry.Title,
|
||||
"size": r.Entry.Size,
|
||||
"replaced": strings.Join(replaced, ", "),
|
||||
})
|
||||
}
|
||||
printTable([]string{"id", "title", "size", "replaced"}, rows)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&replace, "replace", true, "replace same stem|ext before upload (default); false = fail if name taken")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// uploadedFile pairs an upload result with the ids deleted to make room for it.
|
||||
type uploadedFile struct {
|
||||
Entry onlyoffice.Entry
|
||||
Replaced []int
|
||||
}
|
||||
|
||||
// uploader is the slice of *onlyoffice.Client that dav upload needs, so the
|
||||
// command logic is unit-testable with a fake.
|
||||
type uploader interface {
|
||||
UploadToFolderReplacing(ctx context.Context, folderID, localPath string) (*onlyoffice.FileEntry, []int, error)
|
||||
AssertNoFileConflict(ctx context.Context, folderID, localPath string) error
|
||||
UploadToFolder(ctx context.Context, folderID, localPath string) (*onlyoffice.FileEntry, error)
|
||||
}
|
||||
|
||||
// uploadLocal uploads each local file into folderID. With replace a same
|
||||
// stem|ext file is removed first (the UploadToFolderReplacing upsert); without
|
||||
// replace a name clash fails with ErrFileExists before anything is sent.
|
||||
func uploadLocal(ctx context.Context, up uploader, folderID string, paths []string, replace bool) ([]uploadedFile, error) {
|
||||
results := make([]uploadedFile, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
var (
|
||||
fe *onlyoffice.FileEntry
|
||||
deleted []int
|
||||
err error
|
||||
)
|
||||
if replace {
|
||||
fe, deleted, err = up.UploadToFolderReplacing(ctx, folderID, path)
|
||||
} else {
|
||||
if err := up.AssertNoFileConflict(ctx, folderID, path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fe, err = up.UploadToFolder(ctx, folderID, path)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dav upload: %s: %w", path, err)
|
||||
}
|
||||
results = append(results, uploadedFile{
|
||||
Entry: onlyoffice.FileEntryToEntry(fe, onlyoffice.ProviderDAV),
|
||||
Replaced: deleted,
|
||||
})
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// ensurePath resolves-or-creates every "/"-separated segment under rootID
|
||||
// through store. Existing folders are reused by title (case-insensitive), so
|
||||
// repeated calls return the same id without creating duplicates.
|
||||
func ensurePath(ctx context.Context, store onlyoffice.FileStore, rootID, path string) (onlyoffice.Entry, error) {
|
||||
segments, err := splitDavPath(path)
|
||||
if err != nil {
|
||||
return onlyoffice.Entry{}, err
|
||||
}
|
||||
parent := rootID
|
||||
var current onlyoffice.Entry
|
||||
for _, name := range segments {
|
||||
entries, err := store.List(ctx, parent)
|
||||
if err != nil {
|
||||
return onlyoffice.Entry{}, fmt.Errorf("dav ensure-path: list %s: %w", parent, err)
|
||||
}
|
||||
found := false
|
||||
for _, e := range entries {
|
||||
if e.Kind == onlyoffice.Folder && strings.EqualFold(strings.TrimSpace(e.Title), name) {
|
||||
current, found = e, true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
current, err = store.CreateFolder(ctx, parent, name)
|
||||
if err != nil {
|
||||
return onlyoffice.Entry{}, fmt.Errorf("dav ensure-path: mkdir %s/%s: %w", parent, name, err)
|
||||
}
|
||||
}
|
||||
parent = current.ID
|
||||
}
|
||||
return current, nil
|
||||
}
|
||||
|
||||
// splitDavPath normalizes a Documents path into non-empty segments. "." is
|
||||
// ignored; ".." is rejected rather than creating a literal folder named "..".
|
||||
func splitDavPath(path string) ([]string, error) {
|
||||
var segments []string
|
||||
for _, s := range strings.Split(path, "/") {
|
||||
s = strings.TrimSpace(s)
|
||||
switch s {
|
||||
case "", ".":
|
||||
continue
|
||||
case "..":
|
||||
return nil, fmt.Errorf("dav ensure-path: %q not allowed", s)
|
||||
}
|
||||
segments = append(segments, s)
|
||||
}
|
||||
if len(segments) == 0 {
|
||||
return nil, fmt.Errorf("dav ensure-path: empty path")
|
||||
}
|
||||
return segments, nil
|
||||
}
|
||||
|
||||
// myDocumentsID resolves the concrete id of the "My documents" section so
|
||||
// ensure-path can create folders under it (the symbolic "@my" is list-only on
|
||||
// some portals). rootFolderType 5 = My; the title match covers portals that
|
||||
// omit rootFolderType.
|
||||
func myDocumentsID(ctx context.Context, c *onlyoffice.Client) (string, error) {
|
||||
sections, err := c.ListDavSections(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, s := range sections {
|
||||
if s.RootType == 5 && s.ID != "" {
|
||||
return s.ID, nil
|
||||
}
|
||||
}
|
||||
for _, s := range sections {
|
||||
if strings.EqualFold(strings.TrimSpace(s.Title), "My documents") && s.ID != "" {
|
||||
return s.ID, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("dav: no My documents section in @root; pass --under FOLDER_ID")
|
||||
}
|
||||
|
||||
func davRemoveCmd() *cobra.Command {
|
||||
var folderIDs []string
|
||||
cmd := &cobra.Command{
|
||||
|
||||
Reference in New Issue
Block a user