hako/internal/server/handlers/archives.go

277 lines
8.3 KiB
Go

package handlers
import (
"encoding/json"
"fmt"
"io"
"net/http"
"path/filepath"
"git.nakama.town/fmartingr/hako/internal/dependencies"
"git.nakama.town/fmartingr/hako/internal/model"
"git.nakama.town/fmartingr/hako/internal/server/webcontext"
)
// ArchiveHandler handles archive-related HTTP requests
type ArchiveHandler struct {
deps model.Dependencies
}
// NewArchiveHandler creates a new ArchiveHandler
func NewArchiveHandler(deps model.Dependencies) *ArchiveHandler {
return &ArchiveHandler{
deps: deps,
}
}
// getDeps returns the concrete dependencies implementation
func (h *ArchiveHandler) getDeps() *dependencies.Dependencies {
return h.deps.(*dependencies.Dependencies)
}
// ArchiveResponse represents an archive in API responses
type ArchiveResponse struct {
ID string `json:"id"`
LinkID string `json:"link_id"`
Status string `json:"status"`
Title string `json:"title,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
CreatedAt string `json:"created_at"`
CompletedAt *string `json:"completed_at,omitempty"`
}
// HandleGetArchive handles GET /api/v1/archives/{archiveId}
func (h *ArchiveHandler) HandleGetArchive(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
c := webcontext.NewWebContext(w, r)
// Extract archive ID from path
archiveID := r.PathValue("archiveId")
if archiveID == "" {
http.Error(w, "Archive ID is required", http.StatusBadRequest)
return
}
// Get archive
archive, err := h.deps.Domains().Archives().GetArchive(c.Context(), archiveID)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to get archive: %v", err), http.StatusInternalServerError)
return
}
// Build response
item := ArchiveResponse{
ID: archive.ID,
LinkID: archive.LinkID,
Status: string(archive.Status),
Title: archive.Title,
ErrorMessage: archive.ErrorMessage,
CreatedAt: archive.CreatedAt.Format("2006-01-02T15:04:05Z"),
}
if archive.CompletedAt != nil {
completedAt := archive.CompletedAt.Format("2006-01-02T15:04:05Z")
item.CompletedAt = &completedAt
}
c.ResponseWriter().Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(c.ResponseWriter()).Encode(item)
}
// HandleGetArchiveHistory handles GET /api/v1/links/{linkId}/archives
func (h *ArchiveHandler) HandleGetArchiveHistory(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
c := webcontext.NewWebContext(w, r)
// Extract link ID from path
linkID := r.PathValue("linkId")
if linkID == "" {
http.Error(w, "Link ID is required", http.StatusBadRequest)
return
}
// Get archive history
archives, err := h.deps.Domains().Archives().GetArchiveHistory(c.Context(), linkID)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to get archive history: %v", err), http.StatusInternalServerError)
return
}
// Build response
items := make([]ArchiveResponse, 0, len(archives))
for _, archive := range archives {
item := ArchiveResponse{
ID: archive.ID,
LinkID: archive.LinkID,
Status: string(archive.Status),
Title: archive.Title,
ErrorMessage: archive.ErrorMessage,
CreatedAt: archive.CreatedAt.Format("2006-01-02T15:04:05Z"),
}
if archive.CompletedAt != nil {
completedAt := archive.CompletedAt.Format("2006-01-02T15:04:05Z")
item.CompletedAt = &completedAt
}
items = append(items, item)
}
c.ResponseWriter().Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(c.ResponseWriter()).Encode(items)
}
// HandleReArchiveLink handles POST /api/v1/links/{linkId}/archives
func (h *ArchiveHandler) HandleReArchiveLink(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
c := webcontext.NewWebContext(w, r)
// Extract link ID from path
linkID := r.PathValue("linkId")
if linkID == "" {
http.Error(w, "Link ID is required", http.StatusBadRequest)
return
}
// Re-archive the link
if err := h.deps.Domains().Archives().ReArchiveLink(c.Context(), linkID); err != nil {
http.Error(w, fmt.Sprintf("Failed to re-archive link: %v", err), http.StatusInternalServerError)
return
}
c.ResponseWriter().WriteHeader(http.StatusAccepted)
_ = json.NewEncoder(c.ResponseWriter()).Encode(map[string]string{
"message": "Archive job created",
})
}
// HandleGetArchiveFiles handles GET /api/v1/archives/{archiveId}/files
func (h *ArchiveHandler) HandleGetArchiveFiles(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
c := webcontext.NewWebContext(w, r)
// Extract archive ID from path
archiveID := r.PathValue("archiveId")
if archiveID == "" {
http.Error(w, "Archive ID is required", http.StatusBadRequest)
return
}
// Get archive files
files, err := h.deps.Domains().Archives().GetArchiveFiles(c.Context(), archiveID)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to get archive files: %v", err), http.StatusInternalServerError)
return
}
// Build response
items := make([]model.ArchiveFileResponse, 0, len(files))
for _, file := range files {
item := model.ArchiveFileResponse{
ID: file.ID,
ArchiveID: file.ArchiveID,
ArchiverKey: file.ArchiverKey,
Filename: file.Filename,
MimeType: file.MimeType,
FileSize: file.FileSize,
HashSha256: file.HashSha256,
CreatedAt: file.CreatedAt.Format("2006-01-02T15:04:05Z"),
DownloadURL: fmt.Sprintf("/api/v1/archives/%s/files/%s/download", archiveID, file.ID),
Content: file.Content,
ContentMimeType: file.ContentMimeType,
}
items = append(items, item)
}
c.ResponseWriter().Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(c.ResponseWriter()).Encode(items)
}
// HandleDownloadFile handles GET /api/v1/archives/{archiveId}/files/{fileId}/download
// Defaults to inline display. Use ?download=true query parameter to force download.
func (h *ArchiveHandler) HandleDownloadFile(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
c := webcontext.NewWebContext(w, r)
// Extract file ID from path
fileID := r.PathValue("fileId")
if fileID == "" {
http.Error(w, "File ID is required", http.StatusBadRequest)
return
}
// Get archive file
file, err := h.deps.Domains().Archives().GetArchiveFile(c.Context(), fileID)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
// Check if download is requested (default is inline)
forceDownload := r.URL.Query().Get("download") == "true"
// Get file from storage
deps := h.getDeps()
reader, err := deps.Storage.Get(c.Context(), file.StoragePath)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to read file: %v", err), http.StatusInternalServerError)
return
}
defer func() { _ = reader.Close() }()
// Set headers
c.ResponseWriter().Header().Set("Content-Type", file.MimeType)
// Set Content-Disposition: default to inline, use attachment if download=true
if forceDownload {
c.ResponseWriter().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filepath.Base(file.Filename)))
} else {
c.ResponseWriter().Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", filepath.Base(file.Filename)))
}
// Stream file
if _, err := io.Copy(c.ResponseWriter(), reader); err != nil {
// Can't send error response after starting to write body
return
}
}
// HandleDeleteArchive handles DELETE /api/v1/archives/{archiveId}
func (h *ArchiveHandler) HandleDeleteArchive(w http.ResponseWriter, r *http.Request) {
c := webcontext.NewWebContext(w, r)
// Extract archive ID from path
archiveID := r.PathValue("archiveId")
if archiveID == "" {
http.Error(w, "Archive ID is required", http.StatusBadRequest)
return
}
// Delete the archive
if err := h.deps.Domains().Archives().DeleteArchive(c.Context(), archiveID); err != nil {
http.Error(w, fmt.Sprintf("Failed to delete archive: %v", err), http.StatusInternalServerError)
return
}
c.ResponseWriter().WriteHeader(http.StatusNoContent)
}