diff --git a/b0esche_cloud/lib/services/file_service.dart b/b0esche_cloud/lib/services/file_service.dart index 6780cb3..92b4645 100644 --- a/b0esche_cloud/lib/services/file_service.dart +++ b/b0esche_cloud/lib/services/file_service.dart @@ -161,8 +161,11 @@ class FileService { String sourcePath, String targetPath, ) async { + final endpoint = orgId.isEmpty + ? '/user/files/move' + : '/orgs/$orgId/files/move'; await _apiClient.post( - '/orgs/$orgId/files/move', + endpoint, data: {'sourcePath': sourcePath, 'targetPath': targetPath}, fromJson: (d) => null, ); diff --git a/go_cloud/internal/http/routes.go b/go_cloud/internal/http/routes.go index e61dcc7..d714df3 100644 --- a/go_cloud/internal/http/routes.go +++ b/go_cloud/internal/http/routes.go @@ -155,6 +155,10 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut r.Post("/user/files/delete", func(w http.ResponseWriter, req *http.Request) { deleteUserFilePostHandler(w, req, db, auditLogger, cfg) }) + // Move file/folder in user workspace + r.Post("/user/files/move", func(w http.ResponseWriter, req *http.Request) { + moveUserFileHandler(w, req, db, auditLogger, cfg) + }) // Org routes r.Get("/orgs", func(w http.ResponseWriter, req *http.Request) { @@ -1471,6 +1475,90 @@ func deleteUserFilePostHandler(w http.ResponseWriter, r *http.Request, db *datab deleteUserFileHandler(w, r, db, auditLogger, cfg) } +// moveUserFileHandler moves/renames a file in user's personal workspace +func moveUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) { + userIDStr, ok := middleware.GetUserID(r.Context()) + if !ok || userIDStr == "" { + errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized) + return + } + userID, _ := uuid.Parse(userIDStr) + + var req struct { + SourcePath string `json:"sourcePath"` + TargetPath string `json:"targetPath"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest) + return + } + + // Get source file details before moving + sourceFiles, err := db.GetUserFiles(r.Context(), userID, "/", "", 0, 1000) + if err != nil { + errors.LogError(r, err, "Failed to get user files") + errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError) + return + } + + var sourceFile *database.File + for i := range sourceFiles { + if sourceFiles[i].Path == req.SourcePath { + sourceFile = &sourceFiles[i] + break + } + } + + if sourceFile == nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Source file not found", http.StatusNotFound) + return + } + + // Determine new file name + var newPath string + if strings.HasSuffix(req.TargetPath, "/") { + // Moving into a folder + newPath = path.Join(req.TargetPath, sourceFile.Name) + } else { + // Moving/renaming to a specific path + newPath = req.TargetPath + } + + // Get or create user's WebDAV client and move in Nextcloud + storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass) + if err != nil { + errors.LogError(r, err, "Failed to get user WebDAV client (continuing with database operation)") + } else { + sourceRel := strings.TrimPrefix(req.SourcePath, "/") + sourcePath := path.Join("/users", userID.String(), sourceRel) + targetRel := strings.TrimPrefix(newPath, "/") + targetPath := path.Join("/users", userID.String(), targetRel) + if err := storageClient.Move(r.Context(), sourcePath, targetPath); err != nil { + errors.LogError(r, err, "Failed to move in Nextcloud (continuing with database operation)") + } + } + + // Delete old file record + if err := db.DeleteFileByPath(r.Context(), nil, &userID, req.SourcePath); err != nil { + errors.LogError(r, err, "Failed to delete old file record") + } + + // Create new file record at the new location + if _, err := db.CreateFile(r.Context(), nil, &userID, sourceFile.Name, newPath, sourceFile.Type, sourceFile.Size); err != nil { + errors.LogError(r, err, "Failed to create new file record at destination") + } + + auditLogger.Log(r.Context(), audit.Entry{ + UserID: &userID, + Action: "move_file", + Success: true, + }) + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"ok"}`)) +} + + // 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, cfg *config.Config) { userIDStr, ok := middleware.GetUserID(r.Context())