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