fix(web): add comic delete UI and fix container Cloudflare bypass for #6

- Add delete button (SVG X, hover-reveal) and confirmation modal to comic cards
- Add DELETE /api/comics/delete endpoint with path traversal protection
- Fix container downloads: delegate Cloudflare-blocked requests to FlareSolverr
  (headless Chrome sidecar) instead of retrying with Go HTTP client, whose Linux
  TCP fingerprint is flagged by Cloudflare even with network_mode: host
- Add FlareSolverr service to docker-compose; inject FLARESOLVERR_URL env var
- Add diagnostic logging to BatcaveBizMarkup request flow
- Trim URL whitespace before storing in download job
- Guard Archive() against empty filelist; fix runJob error-check ordering
This commit is contained in:
2026-03-12 09:41:03 -04:00
parent d2c715e973
commit 89a5013fb2
8 changed files with 248 additions and 17 deletions

View File

@@ -76,6 +76,7 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("/api/download", s.handleDownload)
mux.HandleFunc("/api/upload", s.handleUpload)
mux.HandleFunc("/api/comics", s.handleComics)
mux.HandleFunc("/api/comics/delete", s.handleDeleteComic)
mux.HandleFunc("/api/jobs", s.handleJobs)
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
@@ -117,6 +118,7 @@ func (s *Server) handleDownload(w http.ResponseWriter, r *http.Request) {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
req.URL = strings.TrimSpace(req.URL)
job := &Job{
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
@@ -149,16 +151,18 @@ func (s *Server) runJob(job *Job) {
job.Title = c.Title
s.mu.Unlock()
errs := c.Download(len(c.Filelist))
if len(errs) > 0 {
if len(c.Filelist) == 0 {
s.mu.Lock()
job.Status = StatusError
job.Error = errs[0].Error()
job.Error = "no images found"
s.mu.Unlock()
return
}
errs := c.Download(len(c.Filelist))
if err := c.Archive(); err != nil {
c.Cleanup()
s.mu.Lock()
job.Status = StatusError
job.Error = err.Error()
@@ -168,6 +172,14 @@ func (s *Server) runJob(job *Job) {
c.Cleanup()
if len(errs) > 0 {
s.mu.Lock()
job.Status = StatusError
job.Error = errs[0].Error()
s.mu.Unlock()
return
}
s.mu.Lock()
job.Status = StatusComplete
s.mu.Unlock()
@@ -351,6 +363,38 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"title": title, "status": "complete"})
}
func (s *Server) handleDeleteComic(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Title string `json:"title"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.Title) == "" {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
// Sanitize: prevent path traversal
title := filepath.Base(strings.TrimSpace(req.Title))
comicDir := filepath.Join(s.libraryPath, title)
// Ensure the resolved path is still under the library
if !strings.HasPrefix(comicDir, filepath.Clean(s.libraryPath)+string(filepath.Separator)) {
http.Error(w, "invalid title", http.StatusBadRequest)
return
}
if err := os.RemoveAll(comicDir); err != nil {
http.Error(w, "failed to delete comic", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
func Listen(addr string, libraryPath string) error {
srv := NewServer(libraryPath)
fmt.Printf("Yoink web server listening on %s\n", addr)