hehehehe
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

This commit is contained in:
R3D347HR4Y 2026-06-09 17:06:06 +02:00
parent ea709cc3db
commit d02ec4afd9
4 changed files with 232 additions and 9 deletions

View File

@ -80,6 +80,8 @@ func (h *Handler) Routes() chi.Router {
r.With(read).Get("/shares/recipients/lookup", h.LookupShareRecipient)
r.With(read).Get("/download/*", h.Download)
r.With(read).Get("/preview/*", h.Preview)
r.With(read).Get("/files/id/{fileId}", h.GetFileByID)
r.With(read).Get("/files/info/*", h.GetFileInfo)
r.With(read).Get("/files/*", h.ListFiles)
r.With(write).Post("/files/*", h.Upload)
r.With(write).Post("/files/new", h.CreateNewFile)
@ -123,6 +125,44 @@ func (h *Handler) ListFiles(w http.ResponseWriter, r *http.Request) {
apiresponse.WriteJSON(w, http.StatusOK, result)
}
func (h *Handler) GetFileByID(w http.ResponseWriter, r *http.Request) {
claims := middleware.ClaimsFromContext(r.Context())
ncUser, ok := h.nextcloudUser(w, r, claims)
if !ok {
return
}
fileID, err := nextcloud.ParseDriveFileID(chi.URLParam(r, "fileId"))
if err != nil {
apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(
apivalidate.FieldDetail{Field: "fileId", Message: "invalid"},
))
return
}
file, err := h.svc.GetFileByID(r.Context(), ncUser, fileID)
if err != nil {
writeDriveError(w, r, err)
return
}
h.svc.EnrichSources(r.Context(), claims.Sub, []nextcloud.FileInfo{file})
apiresponse.WriteJSON(w, http.StatusOK, file)
}
func (h *Handler) GetFileInfo(w http.ResponseWriter, r *http.Request) {
claims := middleware.ClaimsFromContext(r.Context())
ncUser, ok := h.nextcloudUser(w, r, claims)
if !ok {
return
}
path := nextcloud.NormalizeClientPath(chi.URLParam(r, "*"))
file, err := h.svc.StatFile(r.Context(), ncUser, path)
if err != nil {
writeDriveError(w, r, err)
return
}
h.svc.EnrichSources(r.Context(), claims.Sub, []nextcloud.FileInfo{file})
apiresponse.WriteJSON(w, http.StatusOK, file)
}
func (h *Handler) ListFilterCorpus(w http.ResponseWriter, r *http.Request) {
claims := middleware.ClaimsFromContext(r.Context())
ncUser, ok := h.nextcloudUser(w, r, claims)
@ -723,13 +763,16 @@ func (h *Handler) CreateNewFile(w http.ResponseWriter, r *http.Request) {
return
}
kind := NewFileKind(strings.TrimSpace(strings.ToLower(req.Kind)))
target, err := h.svc.CreateNewFile(r.Context(), ncUser, req.ParentPath, req.Name, kind)
target, fileID, err := h.svc.CreateNewFile(r.Context(), ncUser, req.ParentPath, req.Name, kind)
if err != nil {
writeDriveError(w, r, err)
return
}
h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileCreated, target, false)
apiresponse.WriteJSON(w, http.StatusCreated, map[string]string{"path": target})
apiresponse.WriteJSON(w, http.StatusCreated, map[string]any{
"path": target,
"file_id": fileID,
})
}
func writeDriveError(w http.ResponseWriter, r *http.Request, err error) {

View File

@ -266,6 +266,26 @@ func (s *Service) Preview(ctx context.Context, userID, filePath string, width, h
return body, contentType, nil
}
func (s *Service) StatFile(ctx context.Context, userID, filePath string) (nextcloud.FileInfo, error) {
filePath = nextcloud.NormalizeClientFilePath(userID, nextcloud.NormalizeClientPath(filePath))
file, err := s.nc.StatFile(ctx, userID, filePath)
if err != nil {
return nextcloud.FileInfo{}, mapDriveError(err)
}
return file, nil
}
func (s *Service) GetFileByID(ctx context.Context, userID string, fileID int64) (nextcloud.FileInfo, error) {
file, err := s.nc.FindFileByID(ctx, userID, fileID)
if err != nil {
if strings.Contains(err.Error(), "file not found") {
return nextcloud.FileInfo{}, ErrNotFound
}
return nextcloud.FileInfo{}, mapDriveError(err)
}
return file, nil
}
func (s *Service) Delete(ctx context.Context, userID, path string) error {
return mapDriveError(s.nc.Delete(ctx, userID, path))
}
@ -550,19 +570,23 @@ const (
NewFilePresentation NewFileKind = "presentation"
)
func (s *Service) CreateNewFile(ctx context.Context, userID, parentPath, name string, kind NewFileKind) (string, error) {
func (s *Service) CreateNewFile(ctx context.Context, userID, parentPath, name string, kind NewFileKind) (string, int64, error) {
if strings.TrimSpace(name) == "" {
return "", ErrInvalid
return "", 0, ErrInvalid
}
content, contentType := blankOfficeFile(kind)
if content == nil {
return "", ErrInvalid
return "", 0, ErrInvalid
}
target := path.Join(strings.TrimSuffix(parentPath, "/"), name)
if err := mapDriveError(s.nc.Upload(ctx, userID, target, bytes.NewReader(content), contentType)); err != nil {
return "", err
return "", 0, err
}
return target, nil
fileID, err := s.nc.FileID(ctx, userID, target)
if err != nil {
return target, 0, nil
}
return target, fileID, nil
}
func filterFiles(files []nextcloud.FileInfo, q string) []nextcloud.FileInfo {

View File

@ -126,12 +126,15 @@ func (h *Handler) CreateDocument(w http.ResponseWriter, r *http.Request) {
return
}
kind := drive.NewFileKind(strings.TrimSpace(strings.ToLower(req.Kind)))
target, err := h.drive.CreateNewFile(r.Context(), ncUser, req.ParentPath, req.Name, kind)
target, fileID, err := h.drive.CreateNewFile(r.Context(), ncUser, req.ParentPath, req.Name, kind)
if err != nil {
apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, err.Error(), nil)
return
}
apiresponse.WriteJSON(w, http.StatusCreated, map[string]string{"path": target})
apiresponse.WriteJSON(w, http.StatusCreated, map[string]any{
"path": target,
"file_id": fileID,
})
}
func (h *Handler) ServeDocument(w http.ResponseWriter, r *http.Request) {

View File

@ -0,0 +1,153 @@
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
}