280 lines
8.3 KiB
Go
280 lines
8.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"git.nakama.town/fmartingr/hako/internal/dependencies"
|
|
"git.nakama.town/fmartingr/hako/internal/model"
|
|
"git.nakama.town/fmartingr/hako/internal/server/webcontext"
|
|
)
|
|
|
|
// LinkHandler handles link-related HTTP requests
|
|
type LinkHandler struct {
|
|
deps model.Dependencies
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// NewLinkHandler creates a new LinkHandler
|
|
func NewLinkHandler(deps model.Dependencies, logger *slog.Logger) *LinkHandler {
|
|
return &LinkHandler{
|
|
deps: deps,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// getDeps returns the concrete dependencies implementation
|
|
func (h *LinkHandler) getDeps() *dependencies.Dependencies {
|
|
return h.deps.(*dependencies.Dependencies)
|
|
}
|
|
|
|
// CreateLinkRequest represents a request to create a link
|
|
type CreateLinkRequest struct {
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
// ListLinksResponse represents a paginated list of links
|
|
type ListLinksResponse struct {
|
|
Items []model.LinkListItem `json:"items"`
|
|
Total int `json:"total"`
|
|
Limit int `json:"limit"`
|
|
Offset int `json:"offset"`
|
|
}
|
|
|
|
// HandleCreateLink handles POST /api/v1/links
|
|
func (h *LinkHandler) HandleCreateLink(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)
|
|
|
|
// Get user ID from context
|
|
userID := c.GetUserID()
|
|
if userID == "" {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Parse request body
|
|
var req CreateLinkRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate URL
|
|
if req.URL == "" {
|
|
http.Error(w, "URL is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Create link
|
|
link, err := h.deps.Domains().Links().CreateLink(c.Context(), req.URL, userID)
|
|
if err != nil {
|
|
h.logger.Error("Failed to create link", "error", err, "url", req.URL, "user_id", userID)
|
|
http.Error(w, fmt.Sprintf("Failed to create link: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
h.logger.Info("Link created successfully", "link_id", link.ID, "url", req.URL, "user_id", userID)
|
|
|
|
// Build response using LinkListItem
|
|
response := model.LinkListItem{
|
|
Link: *link,
|
|
}
|
|
|
|
c.ResponseWriter().Header().Set("Content-Type", "application/json")
|
|
c.ResponseWriter().WriteHeader(http.StatusCreated)
|
|
_ = json.NewEncoder(c.ResponseWriter()).Encode(response)
|
|
}
|
|
|
|
// HandleGetLink handles GET /api/v1/links/{id}
|
|
func (h *LinkHandler) HandleGetLink(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 ID from path
|
|
id := r.PathValue("id")
|
|
if id == "" {
|
|
http.Error(w, "Link ID is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get link
|
|
link, err := h.deps.Domains().Links().GetLink(c.Context(), id)
|
|
if err != nil {
|
|
http.Error(w, "Link not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Build response using LinkListItem
|
|
response := model.LinkListItem{
|
|
Link: model.Link{
|
|
ID: link.ID,
|
|
URL: link.URL,
|
|
UserID: link.UserID,
|
|
TotalSize: link.TotalSize,
|
|
CreatedAt: link.CreatedAt,
|
|
UpdatedAt: link.UpdatedAt,
|
|
},
|
|
}
|
|
|
|
// Get latest archive for this link
|
|
deps := h.getDeps()
|
|
latestArchive, err := deps.ArchiveStore.GetLatestByLinkID(c.Context(), link.ID)
|
|
if err == nil && latestArchive != nil {
|
|
response.LatestArchiveTitle = latestArchive.Title
|
|
response.LatestArchiveStatus = string(latestArchive.Status)
|
|
|
|
// Get thumbnail using new query method
|
|
thumbnailFile, err := deps.ArchiveFileStore.GetThumbnailByArchiveID(c.Context(), latestArchive.ID)
|
|
if err == nil && thumbnailFile != nil {
|
|
// Convert to ArchiveFileResponse
|
|
response.Thumbnail = &model.ArchiveFileResponse{
|
|
ID: thumbnailFile.ID,
|
|
ArchiveID: thumbnailFile.ArchiveID,
|
|
ArchiverKey: thumbnailFile.ArchiverKey,
|
|
Filename: thumbnailFile.Filename,
|
|
MimeType: thumbnailFile.MimeType,
|
|
FileSize: thumbnailFile.FileSize,
|
|
HashSha256: thumbnailFile.HashSha256,
|
|
CreatedAt: thumbnailFile.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
|
DownloadURL: fmt.Sprintf("/api/v1/archives/%s/files/%s/download", latestArchive.ID, thumbnailFile.ID),
|
|
Content: thumbnailFile.Content,
|
|
ContentMimeType: thumbnailFile.ContentMimeType,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get categories for this link
|
|
categories, err := deps.LinkCategoryStore.GetCategoriesByLinkID(c.Context(), link.ID)
|
|
if err == nil {
|
|
response.Categories = make([]model.CategoryResponse, 0, len(categories))
|
|
for _, cat := range categories {
|
|
response.Categories = append(response.Categories, model.CategoryResponse{
|
|
ID: cat.ID,
|
|
Name: cat.Name,
|
|
Icon: cat.Icon,
|
|
})
|
|
}
|
|
}
|
|
|
|
c.ResponseWriter().Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(c.ResponseWriter()).Encode(response)
|
|
}
|
|
|
|
// HandleListLinks handles GET /api/v1/links
|
|
func (h *LinkHandler) HandleListLinks(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)
|
|
|
|
// Get user ID from context
|
|
userID := c.GetUserID()
|
|
if userID == "" {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Parse request from URL query parameters
|
|
req := model.LinkListRequest{UserID: userID}
|
|
if err := req.FromURLValues(r.URL.Query()); err != nil {
|
|
http.Error(w, fmt.Sprintf("Invalid query parameters: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
req.Defaults()
|
|
if err := req.IsValid(); err != nil {
|
|
http.Error(w, fmt.Sprintf("Invalid request: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Debug: log search query if present
|
|
if req.SearchQuery != "" {
|
|
h.logger.Info("Search query received", "query", req.SearchQuery, "userID", userID)
|
|
}
|
|
|
|
// List links (domain layer handles thumbnail lookup)
|
|
linkItems, total, err := h.deps.Domains().Links().ListLinks(c.Context(), req)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to list links: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Build response from domain structs - populate Thumbnail if available
|
|
deps := h.getDeps()
|
|
for i := range linkItems {
|
|
item := &linkItems[i]
|
|
|
|
// Get thumbnail file if available
|
|
if item.HasThumbnail {
|
|
thumbnailFile, err := deps.ArchiveFileStore.GetThumbnailByLinkID(c.Context(), item.ID)
|
|
if err == nil && thumbnailFile != nil {
|
|
// Get the archive ID for the thumbnail
|
|
latestArchive, err := deps.ArchiveStore.GetLatestByLinkID(c.Context(), item.ID)
|
|
if err == nil && latestArchive != nil {
|
|
// Convert to ArchiveFileResponse
|
|
item.Thumbnail = &model.ArchiveFileResponse{
|
|
ID: thumbnailFile.ID,
|
|
ArchiveID: thumbnailFile.ArchiveID,
|
|
ArchiverKey: thumbnailFile.ArchiverKey,
|
|
Filename: thumbnailFile.Filename,
|
|
MimeType: thumbnailFile.MimeType,
|
|
FileSize: thumbnailFile.FileSize,
|
|
HashSha256: thumbnailFile.HashSha256,
|
|
CreatedAt: thumbnailFile.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
|
DownloadURL: fmt.Sprintf("/api/v1/archives/%s/files/%s/download", latestArchive.ID, thumbnailFile.ID),
|
|
Content: thumbnailFile.Content,
|
|
ContentMimeType: thumbnailFile.ContentMimeType,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
response := ListLinksResponse{
|
|
Items: linkItems,
|
|
Total: total,
|
|
Limit: req.Limit,
|
|
Offset: req.Offset,
|
|
}
|
|
|
|
c.ResponseWriter().Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(c.ResponseWriter()).Encode(response)
|
|
}
|
|
|
|
// HandleDeleteLink handles DELETE /api/v1/links/{id}
|
|
func (h *LinkHandler) HandleDeleteLink(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodDelete {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
c := webcontext.NewWebContext(w, r)
|
|
|
|
// Extract ID from path
|
|
id := r.PathValue("id")
|
|
if id == "" {
|
|
http.Error(w, "Link ID is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Delete link
|
|
if err := h.deps.Domains().Links().DeleteLink(c.Context(), id); err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to delete link: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
c.ResponseWriter().WriteHeader(http.StatusNoContent)
|
|
}
|