Some checks failed
Automated Container Build / build-and-push (push) Failing after 12s
151 lines
4.3 KiB
Go
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
|
|
}
|