fix: resolve IDE errors, minor UI tweaks, and implement file conflict resolution UI/API
Some checks failed
Automated Container Build / build-and-push (push) Failing after 1m13s
Some checks failed
Automated Container Build / build-and-push (push) Failing after 1m13s
This commit is contained in:
parent
3e34a37c95
commit
10bab4ea5b
7 changed files with 625 additions and 58 deletions
|
|
@ -433,14 +433,18 @@ func (h *FSHandler) Move(c *fiber.Ctx) error {
|
|||
uniqueName := GetUniquePath(dstFull, filepath.Base(srcFull))
|
||||
dstFull = filepath.Join(dstFull, uniqueName)
|
||||
body.DestPath = filepath.Join(body.DestPath, uniqueName)
|
||||
} else {
|
||||
// If destination is not a directory but the file exists (rename), we don't overwrite if they want `(1)`
|
||||
// Wait, if they intentionally rename to an existing file, should it append (1)?
|
||||
// Yes, to prevent accidental overwrites.
|
||||
parentDir := filepath.Dir(dstFull)
|
||||
uniqueName := GetUniquePath(parentDir, filepath.Base(dstFull))
|
||||
dstFull = filepath.Join(parentDir, uniqueName)
|
||||
body.DestPath = filepath.Join(filepath.Dir(body.DestPath), uniqueName)
|
||||
} else if err == nil {
|
||||
if body.Resolution == "skip" {
|
||||
return c.JSON(fiber.Map{"message": "skipped"})
|
||||
}
|
||||
if body.Resolution != "overwrite" {
|
||||
parentDir := filepath.Dir(dstFull)
|
||||
uniqueName := GetUniquePath(parentDir, filepath.Base(dstFull))
|
||||
dstFull = filepath.Join(parentDir, uniqueName)
|
||||
body.DestPath = filepath.Join(filepath.Dir(body.DestPath), uniqueName)
|
||||
} else {
|
||||
os.RemoveAll(dstFull)
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Rename(srcFull, dstFull); err != nil {
|
||||
|
|
@ -457,13 +461,16 @@ func (h *FSHandler) Move(c *fiber.Ctx) error {
|
|||
return c.JSON(fiber.Map{"message": "moved successfully"})
|
||||
}
|
||||
|
||||
type CopyRequest struct {
|
||||
SourcePath string `json:"source_path"`
|
||||
DestPath string `json:"dest_path"`
|
||||
Resolution string `json:"resolution"` // "overwrite", "rename", "skip"
|
||||
}
|
||||
|
||||
// Copy copies a file or folder.
|
||||
// POST /api/files/copy
|
||||
func (h *FSHandler) Copy(c *fiber.Ctx) error {
|
||||
var body struct {
|
||||
SourcePath string `json:"source_path"`
|
||||
DestPath string `json:"dest_path"`
|
||||
}
|
||||
var body CopyRequest
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
|
|
@ -483,12 +490,27 @@ func (h *FSHandler) Copy(c *fiber.Ctx) error {
|
|||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "source not found"})
|
||||
}
|
||||
|
||||
// Handle conflict
|
||||
finalDst := dstFull
|
||||
if _, err := os.Stat(dstFull); err == nil {
|
||||
if body.Resolution == "skip" {
|
||||
return c.JSON(fiber.Map{"message": "skipped"})
|
||||
}
|
||||
if body.Resolution != "overwrite" {
|
||||
parentDir := filepath.Dir(dstFull)
|
||||
uniqueName := GetUniquePath(parentDir, filepath.Base(dstFull))
|
||||
finalDst = filepath.Join(parentDir, uniqueName)
|
||||
} else {
|
||||
os.RemoveAll(dstFull)
|
||||
}
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
if err := copyDir(srcFull, dstFull); err != nil {
|
||||
if err := copyDir(srcFull, finalDst); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to copy directory"})
|
||||
}
|
||||
} else {
|
||||
if err := copyFile(srcFull, dstFull); err != nil {
|
||||
if err := copyFile(srcFull, finalDst); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to copy file"})
|
||||
}
|
||||
}
|
||||
|
|
@ -516,6 +538,16 @@ func (h *FSHandler) Delete(c *fiber.Ctx) error {
|
|||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to prepare trash"})
|
||||
}
|
||||
|
||||
info, err := os.Stat(resolvedPath)
|
||||
isDir := 0
|
||||
size := int64(0)
|
||||
if err == nil {
|
||||
if info.IsDir() {
|
||||
isDir = 1
|
||||
}
|
||||
size = info.Size()
|
||||
}
|
||||
|
||||
if err := os.Rename(resolvedPath, trashPath); err != nil {
|
||||
fmt.Printf("Rename error %s -> %s: %v. Attempting copy & delete fallback...\n", resolvedPath, trashPath, err)
|
||||
if err := copyFile(resolvedPath, trashPath); err != nil {
|
||||
|
|
@ -527,14 +559,14 @@ func (h *FSHandler) Delete(c *fiber.Ctx) error {
|
|||
// Update DB
|
||||
cleanPath := filepath.ToSlash(relativePath)
|
||||
h.DB.Exec(
|
||||
`UPDATE files SET is_trashed = 1, trashed_at = CURRENT_TIMESTAMP, original_path = ? WHERE path = ?`,
|
||||
cleanPath, cleanPath,
|
||||
`UPDATE files SET is_trashed = 1, trashed_at = CURRENT_TIMESTAMP, original_path = ?, is_dir = ?, size = ? WHERE path = ?`,
|
||||
cleanPath, isDir, size, cleanPath,
|
||||
)
|
||||
|
||||
// Also insert if not tracked
|
||||
h.DB.Exec(
|
||||
`INSERT OR IGNORE INTO files (path, name, is_trashed, trashed_at, original_path) VALUES (?, ?, 1, CURRENT_TIMESTAMP, ?)`,
|
||||
cleanPath, filepath.Base(cleanPath), cleanPath,
|
||||
`INSERT OR IGNORE INTO files (path, name, is_trashed, trashed_at, original_path, is_dir, size) VALUES (?, ?, 1, CURRENT_TIMESTAMP, ?, ?, ?)`,
|
||||
cleanPath, filepath.Base(cleanPath), cleanPath, isDir, size,
|
||||
)
|
||||
|
||||
h.DB.AddAuditLog("deleted", cleanPath, c.IP())
|
||||
|
|
@ -1153,3 +1185,32 @@ func (h *FSHandler) ServeThumbnail(c *fiber.Ctx) error {
|
|||
|
||||
return c.SendFile(thumbPath)
|
||||
}
|
||||
|
||||
type CheckConflictsRequest struct {
|
||||
DestPath string `json:"dest_path"`
|
||||
Files []string `json:"files"`
|
||||
}
|
||||
|
||||
// CheckConflicts checks if files exist in the destination.
|
||||
// POST /api/files/check-conflicts
|
||||
func (h *FSHandler) CheckConflicts(c *fiber.Ctx) error {
|
||||
var req CheckConflictsRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid body"})
|
||||
}
|
||||
|
||||
destFull, err := resolveSafe(h.Config.StorageDir, req.DestPath)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid path"})
|
||||
}
|
||||
|
||||
conflicts := []string{}
|
||||
for _, file := range req.Files {
|
||||
filePath := filepath.Join(destFull, file)
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
conflicts = append(conflicts, file)
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"conflicts": conflicts})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ func (h *TusHandler) Create(c *fiber.Ctx) error {
|
|||
metadata := parseTusMetadata(c.Get("Upload-Metadata"))
|
||||
fileName := metadata["filename"]
|
||||
destPath := metadata["path"]
|
||||
overwrite := metadata["overwrite"] == "true"
|
||||
|
||||
if fileName == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "filename metadata is required"})
|
||||
|
|
@ -115,13 +116,15 @@ func (h *TusHandler) Create(c *fiber.Ctx) error {
|
|||
destPath = "."
|
||||
}
|
||||
|
||||
// Resolve destination directory and ensure unique filename
|
||||
destFull, err := resolveSafe(h.Config.StorageDir, destPath)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "invalid destination path"})
|
||||
}
|
||||
|
||||
uniqueFileName := GetUniquePath(destFull, fileName)
|
||||
|
||||
uniqueFileName := fileName
|
||||
if !overwrite {
|
||||
uniqueFileName = GetUniquePath(destFull, fileName)
|
||||
}
|
||||
|
||||
// Generate unique upload ID
|
||||
uploadID, err := generateUploadID()
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ func main() {
|
|||
protected.Post("/files/move", fsHandler.Move)
|
||||
protected.Post("/files/copy", fsHandler.Copy)
|
||||
protected.Post("/files/upload", fsHandler.Upload)
|
||||
protected.Post("/files/check-conflicts", fsHandler.CheckConflicts)
|
||||
|
||||
protected.Get("/files/folders-tree", fsHandler.GetFolderTree)
|
||||
|
||||
|
|
|
|||
Reference in a new issue