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