diff --git a/internal/api/drive/handlers.go b/internal/api/drive/handlers.go
index 5fd5a43..f6c8775 100644
--- a/internal/api/drive/handlers.go
+++ b/internal/api/drive/handlers.go
@@ -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) {
diff --git a/internal/api/drive/service.go b/internal/api/drive/service.go
index 9b7fe8a..904f741 100644
--- a/internal/api/drive/service.go
+++ b/internal/api/drive/service.go
@@ -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 {
diff --git a/internal/api/office/handlers.go b/internal/api/office/handlers.go
index bcd5533..ca581e2 100644
--- a/internal/api/office/handlers.go
+++ b/internal/api/office/handlers.go
@@ -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) {
diff --git a/internal/nextcloud/file_stat.go b/internal/nextcloud/file_stat.go
new file mode 100644
index 0000000..b6b6eca
--- /dev/null
+++ b/internal/nextcloud/file_stat.go
@@ -0,0 +1,153 @@
+package nextcloud
+
+import (
+ "context"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net/http"
+ "strconv"
+ "strings"
+)
+
+const propfindFileProps = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
+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(`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /files/%s
+ infinity
+
+
+
+
+
+ %d
+
+
+
+ 1
+
+`, 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
+}