This repository has been archived on 2026-07-15. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
drive/backend/handlers/archives.go
Elijah 724d70e58b
Some checks failed
Automated Container Build / build-and-push (push) Failing after 12s
Inital build
2026-05-22 12:29:43 -07:00

151 lines
4.3 KiB
Go

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
}