diff --git a/backend/internal/backend/handlers_import.go b/backend/internal/backend/handlers_import.go index 2c4e276..624168d 100644 --- a/backend/internal/backend/handlers_import.go +++ b/backend/internal/backend/handlers_import.go @@ -127,16 +127,17 @@ func (s *Server) handleAdminImport(w http.ResponseWriter, r *http.Request) { }) } -// analyzeImportFile does a quick scan of the file to count chapters. -// This is a placeholder - real implementation would parse PDF/EPUB properly. +// analyzeImportFile parses the file to count chapters and extract preview lines. func analyzeImportFile(data []byte, fileType string) *importPreview { - // TODO: Implement actual PDF/EPUB parsing to count chapters - // For now, estimate based on file size - preview := &importPreview{ - Chapters: estimateChapters(data, fileType), - FirstLines: []string{}, + count, firstLines, err := storage.AnalyzeFile(data, fileType) + if err != nil || count == 0 { + // Fall back to rough size estimate so the UI still shows something + count = estimateChapters(data, fileType) + } + return &importPreview{ + Chapters: count, + FirstLines: firstLines, } - return preview } func estimateChapters(data []byte, fileType string) int { diff --git a/backend/internal/storage/import.go b/backend/internal/storage/import.go index cefe1b6..73244ce 100644 --- a/backend/internal/storage/import.go +++ b/backend/internal/storage/import.go @@ -54,7 +54,41 @@ func (i *importer) Import(ctx context.Context, objectKey, fileType string) ([]bo return parseEPUB(data) } -// ── PDF parsing ─────────────────────────────────────────────────────────────── +// AnalyzeFile parses the given PDF or EPUB data and returns the detected +// chapter count and up to 3 preview lines (first non-empty line of each of +// the first 3 chapters). It is used by the analyze-only endpoint so users +// can preview chapter count before committing the import. +func AnalyzeFile(data []byte, fileType string) (chapterCount int, firstLines []string, err error) { + var chapters []bookstore.Chapter + switch fileType { + case "pdf": + chapters, err = parsePDF(data) + case "epub": + chapters, err = parseEPUB(data) + default: + return 0, nil, fmt.Errorf("unsupported file type: %s", fileType) + } + if err != nil { + return 0, nil, err + } + chapterCount = len(chapters) + for i, ch := range chapters { + if i >= 3 { + break + } + line := strings.TrimSpace(ch.Content) + if nl := strings.Index(line, "\n"); nl > 0 { + line = line[:nl] + } + if len(line) > 120 { + line = line[:120] + "…" + } + firstLines = append(firstLines, line) + } + return chapterCount, firstLines, nil +} + + func parsePDF(data []byte) ([]bookstore.Chapter, error) { r, err := pdf.NewReader(bytes.NewReader(data), int64(len(data))) diff --git a/docker-compose.yml b/docker-compose.yml index 96cb47a..59f451c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -307,6 +307,8 @@ services: # OpenTelemetry tracing OTEL_EXPORTER_OTLP_ENDPOINT: "${OTEL_EXPORTER_OTLP_ENDPOINT}" OTEL_SERVICE_NAME: "ui" + # Allow large PDF/EPUB uploads (adapter-node default is 512KB) + BODY_SIZE_LIMIT: "52428800" # OAuth2 providers GOOGLE_CLIENT_ID: "${GOOGLE_CLIENT_ID}" GOOGLE_CLIENT_SECRET: "${GOOGLE_CLIENT_SECRET}"