Numerous Bug Fixes
Some checks failed
Automated Container Build / build-and-push (push) Failing after 8s
Some checks failed
Automated Container Build / build-and-push (push) Failing after 8s
This commit is contained in:
parent
02eefdac0e
commit
267d429122
959 changed files with 145571 additions and 221 deletions
|
|
@ -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"})
|
||||
|
|
|
|||
|
|
@ -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
28
backend/handlers/utils.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(¤tMax)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Reference in a new issue