FEATURE: Add user file move endpoint and support both personal and org workspace moves
This commit is contained in:
@@ -161,8 +161,11 @@ class FileService {
|
|||||||
String sourcePath,
|
String sourcePath,
|
||||||
String targetPath,
|
String targetPath,
|
||||||
) async {
|
) async {
|
||||||
|
final endpoint = orgId.isEmpty
|
||||||
|
? '/user/files/move'
|
||||||
|
: '/orgs/$orgId/files/move';
|
||||||
await _apiClient.post(
|
await _apiClient.post(
|
||||||
'/orgs/$orgId/files/move',
|
endpoint,
|
||||||
data: {'sourcePath': sourcePath, 'targetPath': targetPath},
|
data: {'sourcePath': sourcePath, 'targetPath': targetPath},
|
||||||
fromJson: (d) => null,
|
fromJson: (d) => null,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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) {
|
r.Post("/user/files/delete", func(w http.ResponseWriter, req *http.Request) {
|
||||||
deleteUserFilePostHandler(w, req, db, auditLogger, cfg)
|
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
|
// Org routes
|
||||||
r.Get("/orgs", func(w http.ResponseWriter, req *http.Request) {
|
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)
|
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
|
// 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) {
|
func deleteUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
|
||||||
userIDStr, ok := middleware.GetUserID(r.Context())
|
userIDStr, ok := middleware.GetUserID(r.Context())
|
||||||
|
|||||||
Reference in New Issue
Block a user