package web import ( "context" "errors" "net/http" "path/filepath" "strings" "github.com/a-h/templ" "github.com/arnef/ebooks/internal/library" "github.com/arnef/ebooks/views" ) type Handler struct { lib *library.Service } func NewHandler(lib *library.Service) *Handler { return &Handler{lib: lib} } func (h *Handler) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /", h.listBooks) mux.HandleFunc("GET /book/{id}", h.bookDetails) mux.HandleFunc("GET /download/{id}", h.downloadBook) mux.HandleFunc("GET /cover/{id}", h.bookCover) mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) } func (h *Handler) listBooks(w http.ResponseWriter, r *http.Request) { books, err := h.lib.ListBooks() if err != nil { http.Error(w, "failed to load books", http.StatusInternalServerError) return } render(w, r.Context(), views.IndexPage(books)) } func (h *Handler) bookDetails(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if id == "" { http.NotFound(w, r) return } book, err := h.lib.FindBook(id) if err != nil { http.Error(w, "failed to load book", http.StatusInternalServerError) return } if book == nil { http.NotFound(w, r) return } render(w, r.Context(), views.BookPage(*book)) } func (h *Handler) downloadBook(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if id == "" { http.NotFound(w, r) return } book, err := h.lib.FindBook(id) if err != nil { http.Error(w, "failed to load book", http.StatusInternalServerError) return } if book == nil { http.NotFound(w, r) return } cleanBase := filepath.Clean(h.lib.BooksDir()) cleanPath := filepath.Clean(book.Path) if !strings.HasPrefix(cleanPath, cleanBase+string(filepath.Separator)) && cleanPath != cleanBase { http.Error(w, "invalid file path", http.StatusBadRequest) return } w.Header().Set("Content-Disposition", "attachment; filename=\""+book.Filename+"\"") http.ServeFile(w, r, cleanPath) } func (h *Handler) bookCover(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if id == "" { http.NotFound(w, r) return } data, contentType, err := h.lib.CoverBytes(id) if err != nil { http.Error(w, "failed to load cover", http.StatusInternalServerError) return } if len(data) == 0 { http.NotFound(w, r) return } if contentType == "" { contentType = "application/octet-stream" } w.Header().Set("Content-Type", contentType) w.Header().Set("Cache-Control", "public, max-age=3600") _, _ = w.Write(data) } func render(w http.ResponseWriter, ctx context.Context, c templ.Component) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := c.Render(ctx, w); err != nil && !errors.Is(err, context.Canceled) { http.Error(w, "render failed", http.StatusInternalServerError) } }