ultisuite-backend/internal/nextcloud/file_stat.go
R3D347HR4Y d02ec4afd9
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
hehehehe
2026-06-09 17:06:06 +02:00

154 lines
4.1 KiB
Go

package nextcloud
import (
"context"
"encoding/xml"
"fmt"
"io"
"net/http"
"strconv"
"strings"
)
const propfindFileProps = `<?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/>
<oc:favorite/>
<oc:share-types/>
<d:displayname/>
</d:prop>
</d:propfind>`
func (c *Client) StatFile(ctx context.Context, userID, filePath string) (FileInfo, error) {
filePath = NormalizeClientFilePath(userID, filePath)
davPath := c.WebDAVPath(userID, filePath)
resp, err := c.DoAsUser(ctx, "PROPFIND", davPath, strings.NewReader(propfindFileProps), userID, map[string]string{
"Depth": "0",
"Content-Type": "application/xml",
})
if err != nil {
return FileInfo{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusMultiStatus && resp.StatusCode != http.StatusOK {
return FileInfo{}, &HTTPStatusError{Operation: "propfind stat file", StatusCode: resp.StatusCode}
}
return parseSinglePropfindResponse(resp.Body)
}
func (c *Client) FindFileByID(ctx context.Context, userID string, fileID int64) (FileInfo, error) {
if fileID <= 0 {
return FileInfo{}, fmt.Errorf("invalid file id")
}
userSeg := strings.TrimSpace(userID)
searchBody := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<d:searchrequest xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:basicsearch>
<d:select>
<d:prop>
<oc:fileid/>
<d:displayname/>
<d:getcontenttype/>
<d:getcontentlength/>
<d:getlastmodified/>
<d:getetag/>
<d:resourcetype/>
<oc:size/>
<oc:favorite/>
<oc:share-types/>
</d:prop>
</d:select>
<d:from>
<d:scope>
<d:href>/files/%s</d:href>
<d:depth>infinity</d:depth>
</d:scope>
</d:from>
<d:where>
<d:eq>
<d:prop><oc:fileid/></d:prop>
<d:literal>%d</d:literal>
</d:eq>
</d:where>
<d:orderby/>
<d:limit><d:nresults>1</d:nresults></d:limit>
</d:basicsearch>
</d:searchrequest>`, userSeg, fileID)
resp, err := c.DoAsUser(ctx, "SEARCH", "/remote.php/dav/", strings.NewReader(searchBody), userID, map[string]string{
"Content-Type": "text/xml",
})
if err != nil {
return FileInfo{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusMultiStatus && resp.StatusCode != http.StatusOK {
return FileInfo{}, &HTTPStatusError{Operation: "search file by id", StatusCode: resp.StatusCode}
}
return parseSinglePropfindResponse(resp.Body)
}
func parseSinglePropfindResponse(body io.Reader) (FileInfo, error) {
var ms multistatus
if err := xml.NewDecoder(body).Decode(&ms); err != nil {
return FileInfo{}, err
}
if len(ms.Responses) == 0 {
return FileInfo{}, fmt.Errorf("file not found")
}
return fileInfoFromDAVResponse(ms.Responses[0]), nil
}
func fileInfoFromDAVResponse(r response) FileInfo {
name := fileNameFromDAVProp(r.Propstat.Prop.DisplayName, r.Href)
clientPath := clientPathFromDAVHref(r.Href)
name = SyncFileDisplayName(clientPath, 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
}
return FileInfo{
Path: clientPath,
Name: name,
Type: fileType,
Size: size,
MimeType: r.Propstat.Prop.ContentType,
LastModified: r.Propstat.Prop.LastModified,
ETag: strings.Trim(r.Propstat.Prop.ETag, "\""),
FileID: parseFileID(r.Propstat.Prop.FileID),
IsFavorite: r.Propstat.Prop.Favorite == 1,
IsShared: len(r.Propstat.Prop.ShareTypes.ShareType) > 0,
}
}
func ParseDriveFileID(raw string) (int64, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return 0, fmt.Errorf("empty file id")
}
id, err := strconv.ParseInt(raw, 10, 64)
if err != nil || id <= 0 {
return 0, fmt.Errorf("invalid file id %q", raw)
}
return id, nil
}