This commit is contained in:
Leon Bösche
2026-01-09 18:58:09 +01:00
parent 2a62e13fc7
commit f18e779375
8 changed files with 759 additions and 23 deletions

View File

@@ -78,16 +78,20 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut
r.Get("/user/files", func(w http.ResponseWriter, req *http.Request) {
userFilesHandler(w, req, db)
})
// Download user file
r.Get("/user/files/download", func(w http.ResponseWriter, req *http.Request) {
downloadUserFileHandler(w, req, db, storageClient)
})
// Create / delete in user workspace
r.Post("/user/files", func(w http.ResponseWriter, req *http.Request) {
createUserFileHandler(w, req, db, auditLogger, storageClient)
})
r.Delete("/user/files", func(w http.ResponseWriter, req *http.Request) {
deleteUserFileHandler(w, req, db, auditLogger)
deleteUserFileHandler(w, req, db, auditLogger, storageClient)
})
// POST wrapper for delete
r.Post("/user/files/delete", func(w http.ResponseWriter, req *http.Request) {
deleteUserFilePostHandler(w, req, db, auditLogger)
deleteUserFilePostHandler(w, req, db, auditLogger, storageClient)
})
// Org routes
@@ -106,6 +110,10 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut
r.With(middleware.Permission(db, auditLogger, permission.FileRead)).Get("/files", func(w http.ResponseWriter, req *http.Request) {
listFilesHandler(w, req, db)
})
// Download org file
r.With(middleware.Permission(db, auditLogger, permission.FileRead)).Get("/files/download", func(w http.ResponseWriter, req *http.Request) {
downloadOrgFileHandler(w, req, db, storageClient)
})
// Create file/folder in org workspace
r.With(middleware.Permission(db, auditLogger, permission.FileWrite)).Post("/files", func(w http.ResponseWriter, req *http.Request) {
@@ -113,12 +121,12 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut
})
// Also accept POST delete for clients that cannot send DELETE with body
r.With(middleware.Permission(db, auditLogger, permission.FileWrite)).Post("/files/delete", func(w http.ResponseWriter, req *http.Request) {
deleteOrgFilePostHandler(w, req, db, auditLogger)
deleteOrgFilePostHandler(w, req, db, auditLogger, storageClient)
})
// Delete file/folder in org workspace (body: {"path":"/path"})
r.With(middleware.Permission(db, auditLogger, permission.FileWrite)).Delete("/files", func(w http.ResponseWriter, req *http.Request) {
deleteOrgFileHandler(w, req, db, auditLogger)
deleteOrgFileHandler(w, req, db, auditLogger, storageClient)
})
r.Route("/files/{fileId}", func(r chi.Router) {
r.With(middleware.Permission(db, auditLogger, permission.DocumentView)).Get("/view", func(w http.ResponseWriter, req *http.Request) {
@@ -991,7 +999,7 @@ func createOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.D
}
// deleteOrgFileHandler deletes a file/folder in org workspace by path
func deleteOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
func deleteOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, storageClient *storage.WebDAVClient) {
orgID := r.Context().Value("org").(uuid.UUID)
userIDStr, _ := r.Context().Value("user").(string)
userID, _ := uuid.Parse(userIDStr)
@@ -1004,6 +1012,16 @@ func deleteOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.D
return
}
// Delete from Nextcloud if configured
if storageClient != nil {
rel := strings.TrimPrefix(req.Path, "/")
remotePath := path.Join("/orgs", orgID.String(), rel)
if err := storageClient.Delete(r.Context(), remotePath); err != nil {
errors.LogError(r, err, "Failed to delete from Nextcloud (continuing anyway)")
}
}
// Delete from database
if err := db.DeleteFileByPath(r.Context(), &orgID, nil, req.Path); err != nil {
errors.LogError(r, err, "Failed to delete org file")
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
@@ -1022,8 +1040,8 @@ func deleteOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.D
}
// Also accept POST /orgs/{orgId}/files/delete for clients that cannot send DELETE with body
func deleteOrgFilePostHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
deleteOrgFileHandler(w, r, db, auditLogger)
func deleteOrgFilePostHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, storageClient *storage.WebDAVClient) {
deleteOrgFileHandler(w, r, db, auditLogger, storageClient)
}
// createUserFileHandler creates a file or folder record for the authenticated user's personal workspace.
@@ -1154,12 +1172,12 @@ func createUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.
}
// Also accept POST /user/files/delete
func deleteUserFilePostHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
deleteUserFileHandler(w, r, db, auditLogger)
func deleteUserFilePostHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, storageClient *storage.WebDAVClient) {
deleteUserFileHandler(w, r, db, auditLogger, storageClient)
}
// deleteUserFileHandler deletes a file/folder in user's personal workspace by path
func deleteUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
func deleteUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, storageClient *storage.WebDAVClient) {
userIDStr, ok := r.Context().Value("user").(string)
if !ok || userIDStr == "" {
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
@@ -1175,6 +1193,16 @@ func deleteUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.
return
}
// Delete from Nextcloud if configured
if storageClient != nil {
rel := strings.TrimPrefix(req.Path, "/")
remotePath := path.Join("/user", userID.String(), rel)
if err := storageClient.Delete(r.Context(), remotePath); err != nil {
errors.LogError(r, err, "Failed to delete from Nextcloud (continuing anyway)")
}
}
// Delete from database
if err := db.DeleteFileByPath(r.Context(), nil, &userID, req.Path); err != nil {
errors.LogError(r, err, "Failed to delete user file")
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
@@ -1190,3 +1218,86 @@ func deleteUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
}
// downloadOrgFileHandler downloads a file from org workspace
func downloadOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, storageClient *storage.WebDAVClient) {
orgID := r.Context().Value("org").(uuid.UUID)
filePath := r.URL.Query().Get("path")
if filePath == "" {
errors.WriteError(w, errors.CodeInvalidArgument, "Missing path parameter", http.StatusBadRequest)
return
}
// Try to download from Nextcloud first
if storageClient != nil {
rel := strings.TrimPrefix(filePath, "/")
remotePath := path.Join("/orgs", orgID.String(), rel)
reader, size, err := storageClient.Download(r.Context(), remotePath)
if err == nil {
defer reader.Close()
// Set appropriate headers
fileName := path.Base(filePath)
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
w.Header().Set("Content-Type", "application/octet-stream")
if size > 0 {
w.Header().Set("Content-Length", fmt.Sprintf("%d", size))
}
// Stream the file
io.Copy(w, reader)
return
}
errors.LogError(r, err, "Failed to download from Nextcloud, trying local storage")
}
// Fallback to local storage (if implemented)
errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
}
// downloadUserFileHandler downloads a file from user's personal workspace
func downloadUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, storageClient *storage.WebDAVClient) {
userIDStr, ok := r.Context().Value("user").(string)
if !ok || userIDStr == "" {
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
return
}
userID, _ := uuid.Parse(userIDStr)
filePath := r.URL.Query().Get("path")
if filePath == "" {
errors.WriteError(w, errors.CodeInvalidArgument, "Missing path parameter", http.StatusBadRequest)
return
}
// Try to download from Nextcloud first
if storageClient != nil {
rel := strings.TrimPrefix(filePath, "/")
remotePath := path.Join("/user", userID.String(), rel)
reader, size, err := storageClient.Download(r.Context(), remotePath)
if err == nil {
defer reader.Close()
// Set appropriate headers
fileName := path.Base(filePath)
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
w.Header().Set("Content-Type", "application/octet-stream")
if size > 0 {
w.Header().Set("Content-Length", fmt.Sprintf("%d", size))
}
// Stream the file
io.Copy(w, reader)
return
}
errors.LogError(r, err, "Failed to download from Nextcloud, trying local storage")
}
// Fallback to local storage (if implemented)
errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
}