844 lines
25 KiB
Go
844 lines
25 KiB
Go
package drive
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/api/apiresponse"
|
|
"github.com/ultisuite/ulti-backend/internal/api/apivalidate"
|
|
"github.com/ultisuite/ulti-backend/internal/api/middleware"
|
|
"github.com/ultisuite/ulti-backend/internal/api/query"
|
|
"github.com/ultisuite/ulti-backend/internal/auth"
|
|
"github.com/ultisuite/ulti-backend/internal/mail/rules"
|
|
"github.com/ultisuite/ulti-backend/internal/nextcloud"
|
|
"github.com/ultisuite/ulti-backend/internal/permission"
|
|
"github.com/ultisuite/ulti-backend/internal/realtime"
|
|
)
|
|
|
|
type Handler struct {
|
|
svc *Service
|
|
publicOffice PublicOfficeAPI
|
|
publicRichText PublicRichTextAPI
|
|
logger *slog.Logger
|
|
}
|
|
|
|
type PublicRichTextAPI interface {
|
|
RegisterPublicShareRoutes(r chi.Router)
|
|
}
|
|
|
|
func NewHandler(nc *nextcloud.Client, hub *realtime.Hub, db *pgxpool.Pool) *Handler {
|
|
return NewHandlerWithService(NewService(nc, hub, db))
|
|
}
|
|
|
|
func NewHandlerWithService(svc *Service) *Handler {
|
|
return &Handler{
|
|
svc: svc,
|
|
logger: slog.Default().With("component", "drive-api"),
|
|
}
|
|
}
|
|
|
|
func (h *Handler) SetPublicOffice(api PublicOfficeAPI) {
|
|
h.publicOffice = api
|
|
}
|
|
|
|
func (h *Handler) SetPublicRichText(api PublicRichTextAPI) {
|
|
h.publicRichText = api
|
|
}
|
|
|
|
func (h *Handler) nextcloudUser(w http.ResponseWriter, r *http.Request, claims *auth.Claims) (string, bool) {
|
|
userID, err := h.svc.EnsureNextcloudUser(r.Context(), claims)
|
|
if err != nil {
|
|
h.logger.Error("ensure nextcloud user", "error", err, "sub", claims.Sub, "email", claims.Email)
|
|
apivalidate.WriteInternal(w, r)
|
|
return "", false
|
|
}
|
|
return userID, true
|
|
}
|
|
|
|
func (h *Handler) Routes() chi.Router {
|
|
r := chi.NewRouter()
|
|
read := middleware.RequirePermission(permission.ResourceDrive, permission.LevelRead)
|
|
write := middleware.RequirePermission(permission.ResourceDrive, permission.LevelWrite)
|
|
|
|
r.With(read).Get("/quota", h.GetQuota)
|
|
r.With(read).Get("/trash", h.ListTrash)
|
|
r.With(read).Get("/recent", h.ListRecent)
|
|
r.With(read).Get("/starred", h.ListStarred)
|
|
r.With(read).Get("/starred/*", h.ListStarred)
|
|
r.With(read).Get("/shared", h.ListSharedWithMe)
|
|
r.With(read).Get("/search", h.Search)
|
|
r.With(read).Get("/filter-corpus", h.ListFilterCorpus)
|
|
r.With(read).Get("/filter-corpus/*", h.ListFilterCorpus)
|
|
r.With(read).Get("/shares", h.ListShares)
|
|
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)
|
|
r.With(write).Delete("/files/*", h.DeleteFile)
|
|
r.With(write).Post("/folders/*", h.CreateFolder)
|
|
r.With(write).Post("/move", h.Move)
|
|
r.With(write).Post("/copy", h.Copy)
|
|
r.With(write).Post("/rename", h.Rename)
|
|
r.With(write).Post("/trash/restore", h.RestoreTrash)
|
|
r.With(write).Post("/trash/delete", h.DeleteTrash)
|
|
r.With(write).Delete("/trash", h.EmptyTrash)
|
|
r.With(write).Post("/favorite", h.SetFavorite)
|
|
r.With(write).Post("/shares", h.CreateShare)
|
|
r.With(write).Post("/shares/{shareID}/send-email", h.SendShareEmail)
|
|
r.With(write).Put("/shares/{shareID}", h.UpdateShare)
|
|
r.With(write).Delete("/shares/{shareID}", h.DeleteShare)
|
|
|
|
return r
|
|
}
|
|
|
|
func (h *Handler) ListFiles(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
params, err := query.ParseListRequest(r)
|
|
if err != nil {
|
|
apivalidate.WriteQueryError(w, r, err)
|
|
return
|
|
}
|
|
|
|
path := nextcloud.NormalizeClientPath(chi.URLParam(r, "*"))
|
|
result, err := h.svc.ListFiles(r.Context(), ncUser, path, params)
|
|
if err != nil {
|
|
h.logger.Error("list files", "error", err)
|
|
apivalidate.WriteInternal(w, r)
|
|
return
|
|
}
|
|
h.svc.EnrichSources(r.Context(), claims.Sub, result.Files)
|
|
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)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
path := nextcloud.NormalizeClientPath(chi.URLParam(r, "*"))
|
|
result, err := h.svc.ListFilterCorpus(r.Context(), ncUser, path)
|
|
if err != nil {
|
|
h.logger.Error("list filter corpus", "error", err, "path", path)
|
|
apivalidate.WriteInternal(w, r)
|
|
return
|
|
}
|
|
h.svc.EnrichSources(r.Context(), claims.Sub, result.Files)
|
|
apiresponse.WriteJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
func (h *Handler) Upload(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
path := chi.URLParam(r, "*")
|
|
if verr := validatePath(path); verr != nil {
|
|
apivalidate.WriteValidationError(w, r, verr)
|
|
return
|
|
}
|
|
|
|
if chunk, ok, verr := chunkUploadFromHeaders(r); verr != nil {
|
|
apivalidate.WriteValidationError(w, r, verr)
|
|
return
|
|
} else if ok {
|
|
if err := h.svc.UploadChunk(r.Context(), ncUser, chunk.UploadID, path, chunk.ChunkUpload, r.Body, r.Header.Get("Content-Type")); err != nil {
|
|
h.logger.Error("upload chunk", "error", err)
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
status := http.StatusAccepted
|
|
message := "chunk_uploaded"
|
|
if chunk.Complete {
|
|
status = http.StatusCreated
|
|
message = "uploaded"
|
|
}
|
|
apiresponse.WriteJSON(w, status, map[string]any{
|
|
"status": message,
|
|
"path": path,
|
|
"upload_id": chunk.UploadID,
|
|
"index": chunk.Index,
|
|
"total": chunk.Total,
|
|
"complete": chunk.Complete,
|
|
})
|
|
return
|
|
}
|
|
|
|
if err := h.svc.Upload(r.Context(), ncUser, path, r.Body, r.Header.Get("Content-Type"), r.ContentLength); err != nil {
|
|
h.logger.Error("upload", "error", err)
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileCreated, path, false)
|
|
apiresponse.WriteJSON(w, http.StatusCreated, map[string]string{"status": "uploaded", "path": path})
|
|
}
|
|
|
|
func (h *Handler) Download(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
path := chi.URLParam(r, "*")
|
|
if verr := validatePath(path); verr != nil {
|
|
apivalidate.WriteValidationError(w, r, verr)
|
|
return
|
|
}
|
|
|
|
body, contentType, err := h.svc.Download(r.Context(), ncUser, path)
|
|
if err != nil {
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
defer body.Close()
|
|
|
|
w.Header().Set("Content-Type", contentType)
|
|
io.Copy(w, body)
|
|
}
|
|
|
|
func (h *Handler) Preview(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
path := chi.URLParam(r, "*")
|
|
if verr := validatePath(path); verr != nil {
|
|
apivalidate.WriteValidationError(w, r, verr)
|
|
return
|
|
}
|
|
|
|
width, _ := strconv.Atoi(r.URL.Query().Get("w"))
|
|
height, _ := strconv.Atoi(r.URL.Query().Get("h"))
|
|
|
|
body, contentType, err := h.svc.Preview(r.Context(), ncUser, path, width, height)
|
|
if err != nil {
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
defer body.Close()
|
|
|
|
w.Header().Set("Content-Type", contentType)
|
|
w.Header().Set("Cache-Control", "private, max-age=300")
|
|
io.Copy(w, body)
|
|
}
|
|
|
|
func (h *Handler) DeleteFile(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
path := chi.URLParam(r, "*")
|
|
if verr := validatePath(path); verr != nil {
|
|
apivalidate.WriteValidationError(w, r, verr)
|
|
return
|
|
}
|
|
|
|
if err := h.svc.Delete(r.Context(), ncUser, path); err != nil {
|
|
h.logger.Error("delete file", "error", err)
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileDeleted, path, false)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) CreateFolder(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
path := chi.URLParam(r, "*")
|
|
if verr := validatePath(path); verr != nil {
|
|
apivalidate.WriteValidationError(w, r, verr)
|
|
return
|
|
}
|
|
|
|
if err := h.svc.CreateFolder(r.Context(), ncUser, path); err != nil {
|
|
h.logger.Error("create folder", "error", err)
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileCreated, path, true)
|
|
w.WriteHeader(http.StatusCreated)
|
|
}
|
|
|
|
func (h *Handler) Move(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var req moveRequest
|
|
if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
|
|
return
|
|
}
|
|
if verr := validateMoveRequest(&req); verr != nil {
|
|
apivalidate.WriteValidationError(w, r, verr)
|
|
return
|
|
}
|
|
if middleware.DenyIfDrivePathOutOfScope(w, r, req.Source, req.Destination) {
|
|
return
|
|
}
|
|
|
|
if err := h.svc.Move(r.Context(), ncUser, req.Source, req.Destination); err != nil {
|
|
h.logger.Error("move", "error", err)
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileMoved, req.Destination, false)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) Copy(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var req copyRequest
|
|
if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
|
|
return
|
|
}
|
|
if verr := validateCopyRequest(&req); verr != nil {
|
|
apivalidate.WriteValidationError(w, r, verr)
|
|
return
|
|
}
|
|
if middleware.DenyIfDrivePathOutOfScope(w, r, req.Source, req.Destination) {
|
|
return
|
|
}
|
|
|
|
if err := h.svc.Copy(r.Context(), ncUser, req.Source, req.Destination); err != nil {
|
|
h.logger.Error("copy", "error", err)
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileCreated, req.Destination, false)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) Rename(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var req renameRequest
|
|
if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
|
|
return
|
|
}
|
|
if verr := validateRenameRequest(&req); verr != nil {
|
|
apivalidate.WriteValidationError(w, r, verr)
|
|
return
|
|
}
|
|
if middleware.DenyIfDrivePathOutOfScope(w, r, req.Path) {
|
|
return
|
|
}
|
|
|
|
if err := h.svc.Rename(r.Context(), ncUser, req.Path, req.NewName); err != nil {
|
|
h.logger.Error("rename", "error", err)
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileUpdated, renamedPath(req.Path, req.NewName), false)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) ListTrash(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
params, err := query.ParseListRequest(r)
|
|
if err != nil {
|
|
apivalidate.WriteQueryError(w, r, err)
|
|
return
|
|
}
|
|
result, err := h.svc.ListTrash(r.Context(), ncUser, params)
|
|
if err != nil {
|
|
h.logger.Error("list trash", "error", err)
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.EnrichSources(r.Context(), claims.Sub, result.Files)
|
|
apiresponse.WriteJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
func (h *Handler) ListRecent(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
params, err := query.ParseListRequest(r)
|
|
if err != nil {
|
|
apivalidate.WriteQueryError(w, r, err)
|
|
return
|
|
}
|
|
result, err := h.svc.ListRecent(r.Context(), ncUser, params)
|
|
if err != nil {
|
|
h.logger.Error("list recent", "error", err)
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.EnrichSources(r.Context(), claims.Sub, result.Files)
|
|
apiresponse.WriteJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
func (h *Handler) ListStarred(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
params, err := query.ParseListRequest(r)
|
|
if err != nil {
|
|
apivalidate.WriteQueryError(w, r, err)
|
|
return
|
|
}
|
|
basePath := strings.TrimPrefix(chi.URLParam(r, "*"), "/")
|
|
result, err := h.svc.ListStarred(r.Context(), ncUser, basePath, params)
|
|
if err != nil {
|
|
h.logger.Error("list starred", "error", err)
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.EnrichSources(r.Context(), claims.Sub, result.Files)
|
|
apiresponse.WriteJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
func (h *Handler) ListSharedWithMe(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
params, err := query.ParseListRequest(r)
|
|
if err != nil {
|
|
apivalidate.WriteQueryError(w, r, err)
|
|
return
|
|
}
|
|
result, err := h.svc.ListSharedWithMe(r.Context(), ncUser, params)
|
|
if err != nil {
|
|
h.logger.Error("list shared with me", "error", err)
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.EnrichSources(r.Context(), claims.Sub, result.Files)
|
|
apiresponse.WriteJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
func (h *Handler) CreateShare(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var req createShareRequest
|
|
if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
|
|
return
|
|
}
|
|
if verr := validateCreateShareRequest(&req); verr != nil {
|
|
apivalidate.WriteValidationError(w, r, verr)
|
|
return
|
|
}
|
|
|
|
permissions := req.Permissions
|
|
if permissions == 0 && strings.TrimSpace(req.Role) != "" {
|
|
if mapped, ok := sharePermissionsForRole(req.Role); ok {
|
|
permissions = mapped
|
|
}
|
|
}
|
|
|
|
share, err := h.svc.CreateShare(r.Context(), ncUser, req.Path, req, permissions)
|
|
if err != nil {
|
|
h.logger.Error("create share", "error", err)
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.afterDriveShareEvent(r.Context(), claims.Sub, req.Path)
|
|
apiresponse.WriteJSON(w, http.StatusCreated, share)
|
|
}
|
|
|
|
func (h *Handler) GetQuota(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
quota, err := h.svc.GetQuota(r.Context(), ncUser)
|
|
if err != nil {
|
|
h.logger.Error("get quota", "error", err, "nc_user", ncUser)
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
apiresponse.WriteJSON(w, http.StatusOK, quota)
|
|
}
|
|
|
|
func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
params, err := query.ParseListRequest(r)
|
|
if err != nil {
|
|
apivalidate.WriteQueryError(w, r, err)
|
|
return
|
|
}
|
|
result, err := h.svc.Search(r.Context(), ncUser, SearchOptions{
|
|
Scope: r.URL.Query().Get("scope"),
|
|
BasePath: r.URL.Query().Get("path"),
|
|
Suggest: r.URL.Query().Get("suggest") == "1",
|
|
}, params)
|
|
if err != nil {
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.EnrichSources(r.Context(), claims.Sub, result.Files)
|
|
apiresponse.WriteJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
func (h *Handler) ListShares(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
filePath := r.URL.Query().Get("path")
|
|
if filePath == "" {
|
|
apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(
|
|
apivalidate.FieldDetail{Field: "path", Message: "required"},
|
|
))
|
|
return
|
|
}
|
|
shares, err := h.svc.ListShares(r.Context(), ncUser, filePath)
|
|
if err != nil {
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"shares": shares})
|
|
}
|
|
|
|
func (h *Handler) LookupShareRecipient(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
_ = ncUser
|
|
email := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("email")))
|
|
if email == "" {
|
|
apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(
|
|
apivalidate.FieldDetail{Field: "email", Message: "required"},
|
|
))
|
|
return
|
|
}
|
|
registered, err := h.svc.UserExists(r.Context(), email)
|
|
if err != nil {
|
|
h.logger.Error("lookup share recipient", "error", err, "email", email)
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
apiresponse.WriteJSON(w, http.StatusOK, map[string]any{
|
|
"email": email,
|
|
"registered": registered,
|
|
})
|
|
}
|
|
|
|
func (h *Handler) SendShareEmail(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
shareID := chi.URLParam(r, "shareID")
|
|
var req struct {
|
|
Password string `json:"password"`
|
|
}
|
|
if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
|
|
return
|
|
}
|
|
if err := h.svc.SendShareEmail(r.Context(), ncUser, shareID, req.Password); err != nil {
|
|
h.logger.Error("send share email", "error", err, "share_id", shareID)
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) UpdateShare(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
shareID := chi.URLParam(r, "shareID")
|
|
var req updateShareRequest
|
|
if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
|
|
return
|
|
}
|
|
permissions := req.Permissions
|
|
if permissions == 0 && strings.TrimSpace(req.Role) != "" {
|
|
if mapped, ok := sharePermissionsForRole(req.Role); ok {
|
|
permissions = mapped
|
|
}
|
|
}
|
|
share, err := h.svc.UpdateShare(r.Context(), ncUser, shareID, permissions, req.ExpireDate, req.Password)
|
|
if err != nil {
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.afterDriveShareEvent(r.Context(), claims.Sub, share.Path)
|
|
apiresponse.WriteJSON(w, http.StatusOK, share)
|
|
}
|
|
|
|
func (h *Handler) DeleteShare(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
shareID := chi.URLParam(r, "shareID")
|
|
if err := h.svc.DeleteShare(r.Context(), ncUser, shareID); err != nil {
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.afterDriveShareEvent(r.Context(), claims.Sub, "")
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) RestoreTrash(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req restoreTrashRequest
|
|
if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
|
|
return
|
|
}
|
|
if verr := validateRestoreTrashRequest(&req); verr != nil {
|
|
apivalidate.WriteValidationError(w, r, verr)
|
|
return
|
|
}
|
|
if err := h.svc.RestoreTrash(r.Context(), ncUser, req.Name); err != nil {
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileCreated, req.Name, false)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) DeleteTrash(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req deleteTrashRequest
|
|
if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
|
|
return
|
|
}
|
|
if verr := validateDeleteTrashRequest(&req); verr != nil {
|
|
apivalidate.WriteValidationError(w, r, verr)
|
|
return
|
|
}
|
|
if err := h.svc.DeleteTrash(r.Context(), ncUser, req.Name); err != nil {
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) EmptyTrash(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.svc.EmptyTrash(r.Context(), ncUser); err != nil {
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) SetFavorite(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req favoriteRequest
|
|
if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
|
|
return
|
|
}
|
|
if verr := validateFavoriteRequest(&req); verr != nil {
|
|
apivalidate.WriteValidationError(w, r, verr)
|
|
return
|
|
}
|
|
if err := h.svc.SetFavorite(r.Context(), ncUser, req.Path, req.Favorite); err != nil {
|
|
writeDriveError(w, r, err)
|
|
return
|
|
}
|
|
h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileUpdated, req.Path, false)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) CreateNewFile(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
ncUser, ok := h.nextcloudUser(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req newFileRequest
|
|
if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
|
|
return
|
|
}
|
|
if verr := validateNewFileRequest(&req); verr != nil {
|
|
apivalidate.WriteValidationError(w, r, verr)
|
|
return
|
|
}
|
|
kind := NewFileKind(strings.TrimSpace(strings.ToLower(req.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]any{
|
|
"path": target,
|
|
"file_id": fileID,
|
|
})
|
|
}
|
|
|
|
func writeDriveError(w http.ResponseWriter, r *http.Request, err error) {
|
|
switch {
|
|
case errors.Is(err, ErrNotFound):
|
|
apivalidate.WriteNotFound(w, r, "not found")
|
|
case errors.Is(err, ErrConflict):
|
|
apiresponse.WriteError(w, r, http.StatusConflict, "drive.conflict", "resource conflict", nil)
|
|
case errors.Is(err, ErrForbidden):
|
|
apiresponse.WriteError(w, r, http.StatusForbidden, apiresponse.CodeAuthForbidden, "forbidden", nil)
|
|
case errors.Is(err, ErrQuotaExceeded):
|
|
apiresponse.WriteError(w, r, http.StatusInsufficientStorage, "drive.quota_exceeded", "quota exceeded", nil)
|
|
case errors.Is(err, ErrMalware):
|
|
apiresponse.WriteError(w, r, http.StatusUnprocessableEntity, "drive.malware_detected", "malware detected in file", nil)
|
|
case errors.Is(err, ErrInvalid):
|
|
apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, "invalid request body", nil)
|
|
default:
|
|
apivalidate.WriteInternal(w, r)
|
|
}
|
|
}
|
|
|
|
type chunkRequest struct {
|
|
UploadID string
|
|
ChunkUpload
|
|
}
|
|
|
|
func chunkUploadFromHeaders(r *http.Request) (chunkRequest, bool, *apivalidate.ValidationError) {
|
|
uploadID := strings.TrimSpace(r.Header.Get("X-Upload-ID"))
|
|
if uploadID == "" {
|
|
return chunkRequest{}, false, nil
|
|
}
|
|
var details []apivalidate.FieldDetail
|
|
|
|
index, err := strconv.Atoi(strings.TrimSpace(r.Header.Get("X-Chunk-Index")))
|
|
if err != nil || index < 0 {
|
|
details = append(details, apivalidate.FieldDetail{Field: "X-Chunk-Index", Message: "required positive integer"})
|
|
}
|
|
total, err := strconv.Atoi(strings.TrimSpace(r.Header.Get("X-Chunk-Total")))
|
|
if err != nil || total <= 0 {
|
|
details = append(details, apivalidate.FieldDetail{Field: "X-Chunk-Total", Message: "required positive integer"})
|
|
}
|
|
totalSize := int64(-1)
|
|
if raw := strings.TrimSpace(r.Header.Get("X-Upload-Total-Size")); raw != "" {
|
|
parsed, parseErr := strconv.ParseInt(raw, 10, 64)
|
|
if parseErr != nil || parsed < 0 {
|
|
details = append(details, apivalidate.FieldDetail{Field: "X-Upload-Total-Size", Message: "invalid"})
|
|
} else {
|
|
totalSize = parsed
|
|
}
|
|
}
|
|
complete := strings.EqualFold(strings.TrimSpace(r.Header.Get("X-Upload-Complete")), "true")
|
|
if total > 0 && index == total-1 {
|
|
complete = true
|
|
}
|
|
|
|
if len(details) > 0 {
|
|
return chunkRequest{}, true, apivalidate.NewValidationError(details...)
|
|
}
|
|
return chunkRequest{
|
|
UploadID: uploadID,
|
|
ChunkUpload: ChunkUpload{
|
|
Index: index,
|
|
Total: total,
|
|
TotalSize: totalSize,
|
|
Complete: complete,
|
|
},
|
|
}, true, nil
|
|
}
|