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

This commit is contained in:
Elijah 2026-05-23 13:54:17 -07:00
parent 3e34a37c95
commit 10bab4ea5b
7 changed files with 625 additions and 58 deletions

View file

@ -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})
}

View file

@ -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()

View file

@ -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)