ultisuite-backend/internal/nextcloud/drive.go

250 lines
6.4 KiB
Go

package nextcloud
import (
"context"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"strings"
)
type FileInfo struct {
Path string `json:"path"`
Name string `json:"name"`
Type string `json:"type"` // "file" or "directory"
Size int64 `json:"size"`
MimeType string `json:"mime_type"`
LastModified string `json:"last_modified"`
ETag string `json:"etag"`
}
type ShareInfo struct {
ID string `json:"id"`
Path string `json:"path"`
ShareType int `json:"share_type"`
Permissions int `json:"permissions"`
URL string `json:"url"`
ExpiresAt string `json:"expires_at,omitempty"`
}
func (c *Client) ListFiles(ctx context.Context, userID, path string) ([]FileInfo, error) {
davPath := c.WebDAVPath(userID, path)
body := `<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:prop>
<d:getlastmodified/>
<d:getetag/>
<d:getcontenttype/>
<d:getcontentlength/>
<d:resourcetype/>
<oc:fileid/>
<oc:size/>
</d:prop>
</d:propfind>`
resp, err := c.DoAsUser(ctx, "PROPFIND", davPath, strings.NewReader(body), userID, map[string]string{
"Depth": "1",
"Content-Type": "application/xml",
})
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 207 {
return nil, fmt.Errorf("propfind failed: %d", resp.StatusCode)
}
return parsePropfindResponse(resp.Body, davPath)
}
func (c *Client) Upload(ctx context.Context, userID, path string, content io.Reader, contentType string) error {
davPath := c.WebDAVPath(userID, path)
headers := map[string]string{}
if contentType != "" {
headers["Content-Type"] = contentType
}
resp, err := c.DoAsUser(ctx, "PUT", davPath, content, userID, headers)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 201 && resp.StatusCode != 204 {
return fmt.Errorf("upload failed: %d", resp.StatusCode)
}
return nil
}
func (c *Client) Download(ctx context.Context, userID, path string) (io.ReadCloser, string, error) {
davPath := c.WebDAVPath(userID, path)
resp, err := c.DoAsUser(ctx, "GET", davPath, nil, userID, nil)
if err != nil {
return nil, "", err
}
if resp.StatusCode != 200 {
resp.Body.Close()
return nil, "", fmt.Errorf("download failed: %d", resp.StatusCode)
}
return resp.Body, resp.Header.Get("Content-Type"), nil
}
func (c *Client) CreateFolder(ctx context.Context, userID, path string) error {
davPath := c.WebDAVPath(userID, path)
resp, err := c.DoAsUser(ctx, "MKCOL", davPath, nil, userID, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 201 {
return fmt.Errorf("mkcol failed: %d", resp.StatusCode)
}
return nil
}
func (c *Client) Delete(ctx context.Context, userID, path string) error {
davPath := c.WebDAVPath(userID, path)
resp, err := c.DoAsUser(ctx, "DELETE", davPath, nil, userID, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 204 {
return fmt.Errorf("delete failed: %d", resp.StatusCode)
}
return nil
}
func (c *Client) Move(ctx context.Context, userID, srcPath, destPath string) error {
davSrc := c.WebDAVPath(userID, srcPath)
destURL := c.baseURL + c.WebDAVPath(userID, destPath)
resp, err := c.DoAsUser(ctx, "MOVE", davSrc, nil, userID, map[string]string{
"Destination": destURL,
"Overwrite": "F",
})
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 201 && resp.StatusCode != 204 {
return fmt.Errorf("move failed: %d", resp.StatusCode)
}
return nil
}
func (c *Client) CreateShare(ctx context.Context, userID, path string, shareType int, permissions int) (*ShareInfo, error) {
formData := fmt.Sprintf("path=%s&shareType=%d&permissions=%d", path, shareType, permissions)
resp, err := c.DoAsUser(ctx, "POST", "/ocs/v2.php/apps/files_sharing/api/v1/shares",
strings.NewReader(formData), userID, map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
})
if err != nil {
return nil, err
}
defer resp.Body.Close()
var ocsResp struct {
OCS struct {
Data struct {
ID int `json:"id"`
URL string `json:"url"`
Permissions int `json:"permissions"`
Expiration string `json:"expiration"`
} `json:"data"`
} `json:"ocs"`
}
if err := json.NewDecoder(resp.Body).Decode(&ocsResp); err != nil {
return nil, err
}
return &ShareInfo{
ID: fmt.Sprintf("%d", ocsResp.OCS.Data.ID),
Path: path,
ShareType: shareType,
Permissions: ocsResp.OCS.Data.Permissions,
URL: ocsResp.OCS.Data.URL,
ExpiresAt: ocsResp.OCS.Data.Expiration,
}, nil
}
// PROPFIND XML response parsing
type multistatus struct {
XMLName xml.Name `xml:"multistatus"`
Responses []response `xml:"response"`
}
type response struct {
Href string `xml:"href"`
Propstat propstat `xml:"propstat"`
}
type propstat struct {
Prop prop `xml:"prop"`
Status string `xml:"status"`
}
type prop struct {
LastModified string `xml:"getlastmodified"`
ETag string `xml:"getetag"`
ContentType string `xml:"getcontenttype"`
ContentLength int64 `xml:"getcontentlength"`
ResourceType resourceType `xml:"resourcetype"`
Size int64 `xml:"size"`
DisplayName string `xml:"displayname"`
CalendarColor string `xml:"calendar-color"`
}
type resourceType struct {
Collection *struct{} `xml:"collection"`
}
func parsePropfindResponse(body io.Reader, basePath string) ([]FileInfo, error) {
var ms multistatus
if err := xml.NewDecoder(body).Decode(&ms); err != nil {
return nil, err
}
files := make([]FileInfo, 0, len(ms.Responses))
for i, r := range ms.Responses {
if i == 0 {
continue // skip the folder itself
}
name := r.Href
if idx := strings.LastIndex(strings.TrimSuffix(name, "/"), "/"); idx >= 0 {
name = name[idx+1:]
}
name = strings.TrimSuffix(name, "/")
fileType := "file"
if r.Propstat.Prop.ResourceType.Collection != nil {
fileType = "directory"
}
size := r.Propstat.Prop.ContentLength
if r.Propstat.Prop.Size > 0 {
size = r.Propstat.Prop.Size
}
files = append(files, FileInfo{
Path: r.Href,
Name: name,
Type: fileType,
Size: size,
MimeType: r.Propstat.Prop.ContentType,
LastModified: r.Propstat.Prop.LastModified,
ETag: strings.Trim(r.Propstat.Prop.ETag, "\""),
})
}
return files, nil
}