feat(webdav): create by extension and update by id

Add CreateDavFile/CreateDavTextFile/CreateDavHtmlFile to create an empty file
(the upload endpoint rejects empty bodies) and UpdateDavFile to replace an
existing file in place via PUT /files/{id}/update.

The /upload endpoint creates a second file when the title already exists, so a
WebDAV overwrite must go through /update to keep a single file. uploadReader now
takes the HTTP method.
This commit is contained in:
eSlider
2026-10-06 13:53:18 +00:00
parent 13a4b9f846
commit 0034f3a38f
+86 -3
View File
@@ -133,6 +133,67 @@ func (c *Client) CreateDavFolder(ctx context.Context, parentID, title string) (*
return env.Response, nil return env.Response, nil
} }
// CreateDavFile creates an empty file titled title inside parentID via
// POST /api/2.0/files/{folderId}/file. When enableExternalExt is true the
// extension is kept as-is instead of being coerced to .docx/.xlsx/.pptx,
// which is what non-office WebDAV PUTs need. Mirrors ASC.WebDav createFile.
func (c *Client) CreateDavFile(ctx context.Context, parentID, title string, enableExternalExt bool) (*DavFile, error) {
body := map[string]any{"title": title, "EnableExternalExt": enableExternalExt}
path := "/api/2.0/files/" + url.PathEscape(parentID) + "/file"
out, err := c.postJSONObject(ctx, path, body)
if err != nil {
out, err = c.postJSONObject(ctx, path+".json", body)
if err != nil {
return nil, err
}
}
return davFileFromMap(out), nil
}
// CreateDavTextFile creates a new text file (POST /api/2.0/files/{folderId}/text).
// The reference ASC.WebDav implementation sends a single space as content
// because the portal rejects fully empty bodies; the same applies here.
func (c *Client) CreateDavTextFile(ctx context.Context, parentID, title string) (*DavFile, error) {
return c.createDavContentFile(ctx, parentID, title, "/text")
}
// CreateDavHtmlFile creates a new HTML file (POST .../html).
func (c *Client) CreateDavHtmlFile(ctx context.Context, parentID, title string) (*DavFile, error) {
return c.createDavContentFile(ctx, parentID, title, "/html")
}
func (c *Client) createDavContentFile(ctx context.Context, parentID, title, suffix string) (*DavFile, error) {
body := map[string]any{"title": title, "content": " "}
path := "/api/2.0/files/" + url.PathEscape(parentID) + suffix
out, err := c.postJSONObject(ctx, path, body)
if err != nil {
out, err = c.postJSONObject(ctx, path+".json", body)
if err != nil {
return nil, err
}
}
return davFileFromMap(out), nil
}
// davFileFromMap renders a create-file response object as a DavFile. Only the
// fields the portal reliably returns are mapped.
func davFileFromMap(m map[string]any) *DavFile {
if m == nil {
return &DavFile{}
}
f := &DavFile{
ID: stringField(m, "id"),
Title: stringField(m, "title"),
Size: flexInt(m["pureContentLength"]),
Updated: stringField(m, "updated"),
ViewURL: stringField(m, "viewUrl"),
}
if f.Size == 0 {
f.Size = flexInt(m["contentLength"])
}
return f
}
// RenameDavFolder renames a folder. // RenameDavFolder renames a folder.
// //
// Deprecated: use FileStore.Rename via Client.Files()/Client.FileStore. // Deprecated: use FileStore.Rename via Client.Files()/Client.FileStore.
@@ -268,7 +329,7 @@ func (c *Client) deleteDavItem(ctx context.Context, path string, body any) error
// //
// Deprecated: use FileStore.Upload via Client.Files()/Client.FileStore. // Deprecated: use FileStore.Upload via Client.Files()/Client.FileStore.
func (c *Client) UploadDavFile(ctx context.Context, folderID, fileName string, src io.Reader) (*DavFile, error) { func (c *Client) UploadDavFile(ctx context.Context, folderID, fileName string, src io.Reader) (*DavFile, error) {
raw, err := c.uploadReader(ctx, "/api/2.0/files/"+url.PathEscape(folderID)+"/upload", "file", fileName, src) raw, err := c.uploadReader(ctx, http.MethodPost, "/api/2.0/files/"+url.PathEscape(folderID)+"/upload", "file", fileName, src)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -331,7 +392,7 @@ func (c *Client) deleteJSON(ctx context.Context, path string, body any) (json.Ra
} }
// uploadReader uploads a stream to path under the given form field name. // uploadReader uploads a stream to path under the given form field name.
func (c *Client) uploadReader(ctx context.Context, path, fieldName, fileName string, src io.Reader) (json.RawMessage, error) { func (c *Client) uploadReader(ctx context.Context, method, path, fieldName, fileName string, src io.Reader) (json.RawMessage, error) {
var buf bytes.Buffer var buf bytes.Buffer
mw := multipart.NewWriter(&buf) mw := multipart.NewWriter(&buf)
part, err := mw.CreateFormFile(fieldName, fileName) part, err := mw.CreateFormFile(fieldName, fileName)
@@ -348,7 +409,7 @@ func (c *Client) uploadReader(ctx context.Context, path, fieldName, fileName str
if err != nil { if err != nil {
return nil, err return nil, err
} }
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL()+path, &buf) req, err := http.NewRequestWithContext(ctx, method, c.baseURL()+path, &buf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -370,6 +431,28 @@ func (c *Client) uploadReader(ctx context.Context, path, fieldName, fileName str
return raw, nil return raw, nil
} }
// UpdateDavFile uploads src as a new version of the existing file fileID
// (PUT /api/2.0/files/{id}/update). Unlike UploadDavFile, it replaces the
// existing file instead of creating a second file with the same title — the
// ONLYOFFICE /upload endpoint duplicates on a title clash.
func (c *Client) UpdateDavFile(ctx context.Context, fileID, fileName string, src io.Reader) (*DavFile, error) {
path := "/api/2.0/files/" + url.PathEscape(fileID) + "/update"
raw, err := c.uploadReader(ctx, http.MethodPut, path, "file", fileName, src)
if err != nil {
return nil, err
}
var env struct {
Response *DavFile `json:"response"`
}
if err := json.Unmarshal(raw, &env); err != nil {
return nil, err
}
if env.Response == nil {
return nil, fmt.Errorf("onlyoffice: empty update response")
}
return env.Response, nil
}
func nums(ids []string) []json.Number { func nums(ids []string) []json.Number {
out := make([]json.Number, 0, len(ids)) out := make([]json.Number, 0, len(ids))
for _, id := range ids { for _, id := range ids {