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}) }