This commit is contained in:
parent
fa2be029a2
commit
724d70e58b
3339 changed files with 1075535 additions and 0 deletions
151
backend/handlers/archives.go
Normal file
151
backend/handlers/archives.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.elijahkuntz.com/Elijah/drive/config"
|
||||
"git.elijahkuntz.com/Elijah/drive/database"
|
||||
"git.elijahkuntz.com/Elijah/drive/workers"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type ArchiveHandler struct {
|
||||
DB *database.DB
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
// Zip creates a zip archive of a folder.
|
||||
// POST /api/files/zip
|
||||
func (h *ArchiveHandler) Zip(c *fiber.Ctx) error {
|
||||
var body struct {
|
||||
SourcePath string `json:"source_path"`
|
||||
DestPath string `json:"dest_path"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
|
||||
srcFull, err := resolveSafe(h.Config.StorageDir, body.SourcePath)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
info, err := os.Stat(srcFull)
|
||||
if err != nil || !info.IsDir() {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "source must be a valid directory"})
|
||||
}
|
||||
|
||||
// Default dest path if not provided
|
||||
if body.DestPath == "" {
|
||||
body.DestPath = filepath.ToSlash(filepath.Clean(body.SourcePath)) + ".zip"
|
||||
}
|
||||
|
||||
dstFull, err := resolveSafe(h.Config.StorageDir, body.DestPath)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
task := workers.Tasks.CreateTask("zip", "Starting zip process...")
|
||||
|
||||
// Start async worker
|
||||
go func() {
|
||||
workers.ZipFolderAsync(task.ID, srcFull, dstFull)
|
||||
// Sync to DB when complete
|
||||
h.syncArchiveToDB(body.DestPath)
|
||||
}()
|
||||
|
||||
h.DB.AddAuditLog("zip_started", fmt.Sprintf("%s -> %s", body.SourcePath, body.DestPath), c.IP())
|
||||
|
||||
return c.JSON(fiber.Map{"message": "Zip process started", "task_id": task.ID})
|
||||
}
|
||||
|
||||
// Unzip extracts a zip archive to a folder.
|
||||
// POST /api/files/unzip
|
||||
func (h *ArchiveHandler) Unzip(c *fiber.Ctx) error {
|
||||
var body struct {
|
||||
SourcePath string `json:"source_path"`
|
||||
DestPath string `json:"dest_path"` // Folder to extract into
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
|
||||
srcFull, err := resolveSafe(h.Config.StorageDir, body.SourcePath)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
// Default dest folder
|
||||
if body.DestPath == "" {
|
||||
body.DestPath = strings.TrimSuffix(body.SourcePath, filepath.Ext(body.SourcePath))
|
||||
}
|
||||
|
||||
dstFull, err := resolveSafe(h.Config.StorageDir, body.DestPath)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
task := workers.Tasks.CreateTask("unzip", "Starting unzip process...")
|
||||
|
||||
// Start async worker
|
||||
go func() {
|
||||
workers.UnzipAsync(task.ID, srcFull, dstFull)
|
||||
// Trigger a DB resync for the extracted folder (lightweight for now)
|
||||
h.syncArchiveToDB(body.DestPath)
|
||||
}()
|
||||
|
||||
h.DB.AddAuditLog("unzip_started", fmt.Sprintf("%s -> %s", body.SourcePath, body.DestPath), c.IP())
|
||||
|
||||
return c.JSON(fiber.Map{"message": "Unzip process started", "task_id": task.ID})
|
||||
}
|
||||
|
||||
// Tasks returns the list of active background tasks.
|
||||
// GET /api/tasks
|
||||
func (h *ArchiveHandler) Tasks(c *fiber.Ctx) error {
|
||||
tasks := workers.Tasks.GetTasks()
|
||||
return c.JSON(fiber.Map{"tasks": tasks})
|
||||
}
|
||||
|
||||
// syncArchiveToDB records the output of the zip/unzip operation.
|
||||
func (h *ArchiveHandler) syncArchiveToDB(relPath string) {
|
||||
fullPath := filepath.Join(h.Config.StorageDir, filepath.FromSlash(relPath))
|
||||
info, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cleanPath := filepath.ToSlash(relPath)
|
||||
|
||||
// Just insert the top-level record, deeper sync isn't strictly needed
|
||||
// unless FTS is required immediately for all extracted contents.
|
||||
h.DB.Exec(`
|
||||
INSERT INTO files (path, name, size, is_dir)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(path) DO UPDATE SET
|
||||
size = excluded.size,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`, cleanPath, filepath.Base(cleanPath), info.Size(), info.IsDir())
|
||||
}
|
||||
|
||||
func resolveSafe(root, rel string) (string, error) {
|
||||
cleaned := filepath.Clean(rel)
|
||||
if strings.Contains(cleaned, "..") {
|
||||
return "", fmt.Errorf("path traversal detected")
|
||||
}
|
||||
|
||||
full := filepath.Join(root, cleaned)
|
||||
abs, err := filepath.Abs(full)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid path")
|
||||
}
|
||||
|
||||
absRoot, _ := filepath.Abs(root)
|
||||
if abs != absRoot && !strings.HasPrefix(abs, absRoot+string(filepath.Separator)) {
|
||||
return "", fmt.Errorf("access denied")
|
||||
}
|
||||
|
||||
return abs, nil
|
||||
}
|
||||
Reference in a new issue