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 6b7d0de208
Some checks failed
Automated Container Build / build-and-push (push) Failing after 32s
Add archive preview and new storage categories
2026-06-01 08:57:01 -07:00

197 lines
5.5 KiB
Go

package handlers
import (
"archive/zip"
"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/bodgit/sevenzip"
"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())
}
type ArchiveContentItem struct {
Name string `json:"name"`
Size uint64 `json:"size"`
Path string `json:"path"`
}
// GetArchiveContents lists files within a zip or 7z archive without extracting.
// GET /api/files/archive-contents
func (h *ArchiveHandler) GetArchiveContents(c *fiber.Ctx) error {
relPath := c.Query("path")
if relPath == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "path is required"})
}
fullPath, err := resolveSafe(h.Config.StorageDir, relPath)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
}
ext := strings.ToLower(filepath.Ext(fullPath))
var items []ArchiveContentItem
if ext == ".zip" {
r, err := zip.OpenReader(fullPath)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to open zip file"})
}
defer r.Close()
for _, f := range r.File {
if f.FileInfo().IsDir() {
continue
}
items = append(items, ArchiveContentItem{
Name: f.FileInfo().Name(),
Size: f.UncompressedSize64,
Path: f.Name,
})
}
} else if ext == ".7z" {
r, err := sevenzip.OpenReader(fullPath)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to open 7z file"})
}
defer r.Close()
for _, f := range r.File {
if f.FileInfo().IsDir() {
continue
}
items = append(items, ArchiveContentItem{
Name: f.FileInfo().Name(),
Size: f.UncompressedSize,
Path: f.Name,
})
}
} else {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "unsupported archive type"})
}
return c.JSON(fiber.Map{"items": items})
}