Numerous Bug Fixes
Some checks failed
Automated Container Build / build-and-push (push) Failing after 8s

This commit is contained in:
Elijah 2026-05-22 15:50:45 -07:00
parent 02eefdac0e
commit 267d429122
959 changed files with 145571 additions and 221 deletions

View file

@ -1,6 +1,8 @@
package handlers
import (
"archive/zip"
"bufio"
"database/sql"
"fmt"
"io"
@ -89,13 +91,17 @@ func (h *FSHandler) ListDirectory(c *fiber.Ctx) error {
// Enrich with DB metadata (pinned status, etc.)
var isPinned, isTrashed int
var checksum sql.NullString
err = h.DB.QueryRow(
"SELECT is_pinned, is_trashed FROM files WHERE path = ?",
"SELECT is_pinned, is_trashed, checksum FROM files WHERE path = ?",
filepath.ToSlash(entryPath),
).Scan(&isPinned, &isTrashed)
).Scan(&isPinned, &isTrashed, &checksum)
if err == nil {
fi.IsPinned = isPinned == 1
fi.IsTrashed = isTrashed == 1
if checksum.Valid {
fi.Checksum = checksum.String
}
}
// Don't show trashed items in normal listings
@ -139,14 +145,16 @@ func (h *FSHandler) getFileInfo(c *fiber.Ctx, absPath string) error {
}
var isPinned int
var checksum string
var checksum sql.NullString
err = h.DB.QueryRow(
"SELECT is_pinned, checksum FROM files WHERE path = ?",
filepath.ToSlash(relativePath),
).Scan(&isPinned, &checksum)
if err == nil {
fi.IsPinned = isPinned == 1
fi.Checksum = checksum
if checksum.Valid {
fi.Checksum = checksum.String
}
}
return c.JSON(fi)
@ -166,21 +174,32 @@ func (h *FSHandler) CreateFolder(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "path is required"})
}
// Validate path within jail
fullPath, err := h.resolveSafe(body.Path)
parentDir := filepath.Dir(body.Path)
folderName := filepath.Base(body.Path)
parentFullPath, err := h.resolveSafe(parentDir)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
}
uniqueName := GetUniquePath(parentFullPath, folderName)
fullPath := filepath.Join(parentFullPath, uniqueName)
cleanPath := filepath.ToSlash(filepath.Join(parentDir, uniqueName))
// Remove any prefix like "." if cleanPath was "." and it got joined.
// filepath.ToSlash cleans the path.
if cleanPath == "." {
// Just for safety, but base shouldn't be empty
}
if err := os.MkdirAll(fullPath, 0755); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to create folder"})
}
// Record in DB
cleanPath := filepath.ToSlash(body.Path)
h.DB.Exec(
"INSERT OR IGNORE INTO files (path, name, is_dir) VALUES (?, ?, 1)",
cleanPath, filepath.Base(cleanPath),
cleanPath, uniqueName,
)
h.DB.AddAuditLog("folder_created", cleanPath, c.IP())
@ -207,7 +226,10 @@ func (h *FSHandler) Rename(c *fiber.Ctx) error {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
}
newPath := filepath.Join(filepath.Dir(body.OldPath), body.NewName)
parentDir := filepath.Dir(oldFull)
uniqueName := GetUniquePath(parentDir, body.NewName)
newPath := filepath.Join(filepath.Dir(body.OldPath), uniqueName)
newFull, err := h.resolveSafe(newPath)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
@ -216,6 +238,9 @@ func (h *FSHandler) Rename(c *fiber.Ctx) error {
if err := os.Rename(oldFull, newFull); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to rename"})
}
// Update body.NewName to reflect the actual unique name used
body.NewName = uniqueName
// Update DB
h.DB.Exec(
@ -251,8 +276,17 @@ func (h *FSHandler) Move(c *fiber.Ctx) error {
// If destination is a directory, move into it
if info, err := os.Stat(dstFull); err == nil && info.IsDir() {
dstFull = filepath.Join(dstFull, filepath.Base(srcFull))
body.DestPath = filepath.Join(body.DestPath, filepath.Base(body.SourcePath))
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)
}
if err := os.Rename(srcFull, dstFull); err != nil {
@ -324,11 +358,16 @@ func (h *FSHandler) Delete(c *fiber.Ctx) error {
// Soft-delete: move to .trash directory
trashPath := filepath.Join(h.Config.TrashDir, relativePath)
if err := os.MkdirAll(filepath.Dir(trashPath), 0755); err != nil {
fmt.Printf("MkdirAll error: %v\n", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to prepare trash"})
}
if err := os.Rename(resolvedPath, trashPath); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to move to trash"})
fmt.Printf("Rename error %s -> %s: %v. Attempting copy & delete fallback...\n", resolvedPath, trashPath, err)
if err := copyFile(resolvedPath, trashPath); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to move to trash"})
}
os.RemoveAll(resolvedPath)
}
// Update DB
@ -438,12 +477,75 @@ func (h *FSHandler) EmptyTrash(c *fiber.Ctx) error {
os.MkdirAll(h.Config.TrashDir, 0755)
h.DB.Exec("DELETE FROM files WHERE is_trashed = 1")
h.DB.AddAuditLog("trash_emptied", "All trashed items permanently deleted", c.IP())
h.DB.AddAuditLog("emptied_trash", "User emptied trash", c.IP())
return c.JSON(fiber.Map{"message": "trash emptied"})
}
// DownloadFolder creates and streams a ZIP archive of a folder.
// GET /api/files/download-folder?path=...
func (h *FSHandler) DownloadFolder(c *fiber.Ctx) error {
folderPath := c.Query("path")
if folderPath == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "path is required"})
}
fullPath, err := h.resolveSafe(folderPath)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
}
info, err := os.Stat(fullPath)
if err != nil || !info.IsDir() {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "folder not found"})
}
c.Set("Content-Type", "application/zip")
c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", filepath.Base(fullPath)))
c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
zipWriter := zip.NewWriter(w)
defer zipWriter.Close()
filepath.Walk(fullPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if path == fullPath {
return nil
}
relPath, err := filepath.Rel(fullPath, path)
if err != nil {
return err
}
if info.IsDir() {
// add directory entry
relPath += "/"
_, err = zipWriter.Create(relPath)
return err
}
// add file entry
zipFile, err := zipWriter.Create(relPath)
if err != nil {
return err
}
fsFile, err := os.Open(path)
if err != nil {
return err
}
defer fsFile.Close()
_, err = io.Copy(zipFile, fsFile)
return err
})
})
return nil
}
// TogglePin pins or unpins a file/folder.
// POST /api/files/pin
func (h *FSHandler) TogglePin(c *fiber.Ctx) error {
@ -585,8 +687,9 @@ func (h *FSHandler) Upload(c *fiber.Ctx) error {
// If destination is a directory, save into it
if info, err := os.Stat(fullDest); err == nil && info.IsDir() {
fullDest = filepath.Join(fullDest, file.Filename)
destPath = filepath.Join(destPath, file.Filename)
uniqueName := GetUniquePath(fullDest, file.Filename)
fullDest = filepath.Join(fullDest, uniqueName)
destPath = filepath.Join(destPath, uniqueName)
}
// Ensure parent directory exists
@ -594,9 +697,6 @@ func (h *FSHandler) Upload(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to create directory"})
}
// Version existing file if it exists
CreateVersion(h.DB, h.Config, fullDest, filepath.ToSlash(destPath))
// Save the file
if err := c.SaveFile(file, fullDest); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save file"})

View file

@ -83,6 +83,10 @@ func (h *TusHandler) Create(c *fiber.Ctx) error {
destPath = "."
}
// Resolve destination directory and ensure unique filename
destFull := filepath.Join(h.Config.StorageDir, filepath.FromSlash(destPath))
uniqueFileName := GetUniquePath(destFull, fileName)
// Generate unique upload ID
uploadID, err := generateUploadID()
if err != nil {
@ -103,12 +107,12 @@ func (h *TusHandler) Create(c *fiber.Ctx) error {
upload := &tusUpload{
ID: uploadID,
FilePath: filepath.ToSlash(filepath.Join(destPath, fileName)),
FileName: fileName,
FilePath: filepath.ToSlash(filepath.Join(destPath, uniqueFileName)),
FileName: uniqueFileName,
FileSize: fileSize,
Offset: 0,
TempPath: tempPath,
MimeType: mime.TypeByExtension(filepath.Ext(fileName)),
MimeType: mime.TypeByExtension(filepath.Ext(uniqueFileName)),
}
h.mu.Lock()
@ -231,9 +235,6 @@ func (h *TusHandler) finalizeUpload(upload *tusUpload, ip string) {
// Ensure parent dir exists
os.MkdirAll(filepath.Dir(destFull), 0755)
// Version existing file if it exists
CreateVersion(h.DB, h.Config, destFull, filepath.ToSlash(upload.FilePath))
// Move temp file to final location
if err := os.Rename(upload.TempPath, destFull); err != nil {
fmt.Printf("Error finalizing upload %s: %v\n", upload.ID, err)

28
backend/handlers/utils.go Normal file
View file

@ -0,0 +1,28 @@
package handlers
import (
"fmt"
"os"
"path/filepath"
)
// GetUniquePath checks if a file or folder exists at the given baseDir with the given name.
// If it does, it systematically appends " (1)", " (2)", etc., until it finds an available name.
// It returns the unique filename.
func GetUniquePath(baseDir, name string) string {
ext := filepath.Ext(name)
base := name[:len(name)-len(ext)]
finalPath := filepath.Join(baseDir, name)
if _, err := os.Stat(finalPath); os.IsNotExist(err) {
return name
}
for i := 1; ; i++ {
newName := fmt.Sprintf("%s (%d)%s", base, i, ext)
finalPath = filepath.Join(baseDir, newName)
if _, err := os.Stat(finalPath); os.IsNotExist(err) {
return newName
}
}
}

View file

@ -1,118 +0,0 @@
package handlers
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"git.elijahkuntz.com/Elijah/drive/config"
"git.elijahkuntz.com/Elijah/drive/database"
)
// CreateVersion checks if a file exists and if so, moves it to the .versions directory
// and records it in the database before it gets overwritten.
// Call this BEFORE writing the new file to disk.
func CreateVersion(db *database.DB, cfg *config.Config, fullPath string, relPath string) error {
info, err := os.Stat(fullPath)
if err != nil {
if os.IsNotExist(err) {
return nil // No existing file, nothing to version
}
return err
}
if info.IsDir() {
return nil // Don't version directories
}
// Check if versioning is enabled/has a max limit
val, err := db.GetSetting("max_versions")
maxVersions := 5
if err == nil {
if v, e := strconv.Atoi(val); e == nil {
maxVersions = v
}
}
if maxVersions <= 0 {
return nil // Versioning disabled
}
// Get current file details from DB (for checksum)
var checksum string
err = db.QueryRow("SELECT checksum FROM files WHERE path = ?", filepath.ToSlash(relPath)).Scan(&checksum)
if err != nil {
checksum = "" // fallback
}
// Generate version path
timestamp := time.Now().Format("20060102150405")
versionFileName := fmt.Sprintf("%s_%s%s", strings.TrimSuffix(filepath.Base(relPath), filepath.Ext(relPath)), timestamp, filepath.Ext(relPath))
versionRelPath := filepath.Join(filepath.Dir(relPath), versionFileName)
versionFullPath := filepath.Join(cfg.VersionsDir, versionRelPath)
if err := os.MkdirAll(filepath.Dir(versionFullPath), 0755); err != nil {
return err
}
// Move current file to versions directory
if err := os.Rename(fullPath, versionFullPath); err != nil {
return err
}
// Determine version number
var currentMax int
err = db.QueryRow("SELECT COALESCE(MAX(version_number), 0) FROM file_versions WHERE file_path = ?", filepath.ToSlash(relPath)).Scan(&currentMax)
if err != nil {
currentMax = 0
}
versionNumber := currentMax + 1
// Insert version record
_, err = db.Exec(`
INSERT INTO file_versions (file_path, version_number, version_path, size, checksum)
VALUES (?, ?, ?, ?, ?)
`, filepath.ToSlash(relPath), versionNumber, filepath.ToSlash(versionRelPath), info.Size(), checksum)
if err != nil {
return err
}
// Prune old versions
pruneVersions(db, cfg, relPath, maxVersions)
return nil
}
func pruneVersions(db *database.DB, cfg *config.Config, relPath string, maxVersions int) {
rows, err := db.Query(`
SELECT id, version_path FROM file_versions
WHERE file_path = ?
ORDER BY version_number DESC
LIMIT -1 OFFSET ?
`, filepath.ToSlash(relPath), maxVersions)
if err != nil {
return
}
defer rows.Close()
var idsToDelete []int
for rows.Next() {
var id int
var vPath string
if err := rows.Scan(&id, &vPath); err == nil {
idsToDelete = append(idsToDelete, id)
os.Remove(filepath.Join(cfg.VersionsDir, filepath.FromSlash(vPath)))
}
}
if len(idsToDelete) > 0 {
for _, id := range idsToDelete {
db.Exec("DELETE FROM file_versions WHERE id = ?", id)
}
}
}

View file

@ -84,13 +84,7 @@ func (h *WebDAVHandler) Handle(c *fiber.Ctx) error {
// Adapt Fiber's FastHTTP to standard net/http for the WebDAV handler
fasthttpadaptor.NewFastHTTPHandlerFunc(func(w http.ResponseWriter, r *http.Request) {
relPath := strings.TrimPrefix(r.URL.Path, "/webdav")
fullPath := filepath.Join(h.Config.StorageDir, filepath.FromSlash(relPath))
// If it's a PUT request, version the existing file before it gets overwritten
if r.Method == "PUT" {
CreateVersion(h.DB, h.Config, fullPath, filepath.ToSlash(relPath))
}
// Native WebDAV handles overwriting automatically
h.Handler.ServeHTTP(w, r)
// If it was a PUT request, we should sync it with our DB (checksum, size, etc.)

View file

@ -158,6 +158,7 @@ func main() {
protected.Post("/files/rename", fsHandler.Rename)
protected.Post("/files/move", fsHandler.Move)
protected.Post("/files/copy", fsHandler.Copy)
protected.Get("/files/download-folder", fsHandler.DownloadFolder)
protected.Post("/files/upload", fsHandler.Upload)
// File listing and download (with path jail)