package ebooks import ( "context" "database/sql" "fmt" "io" "os" "path" "path/filepath" "strings" "github.com/mithral/vellum/internal/media" ) // Kind is the media kind this module owns. const Kind = "ebooks" func init() { media.RegisterCoverExtractor(Kind, ExtractCover) } // ExtractCover writes the EPUB's cover image into the cache and returns its // path. A missing cover is not an error: it returns ("", nil) and the caller // serves a placeholder. func ExtractCover(ctx context.Context, db *sql.DB, item media.Item, absPath, cacheDir string) (string, error) { md, err := Parse(absPath) if err != nil { return "", err } if md.CoverHref == "" { return "", nil } dir, err := media.CoverCacheDir(cacheDir, Kind) if err != nil { return "", err } ext := strings.ToLower(path.Ext(md.CoverHref)) if ext == "" { ext = ".jpg" } outPath := filepath.Join(dir, fmt.Sprintf("%d%s", item.ID, ext)) rc, _, err := OpenEntry(absPath, md.CoverHref) if err != nil { // Cover declared but not present in the archive: not fatal. return "", nil } defer rc.Close() tmp := outPath + ".tmp" f, err := os.Create(tmp) if err != nil { return "", fmt.Errorf("ebooks: create cover: %w", err) } if _, err := io.Copy(f, io.LimitReader(rc, 32<<20)); err != nil { f.Close() _ = os.Remove(tmp) return "", fmt.Errorf("ebooks: write cover: %w", err) } if err := f.Close(); err != nil { _ = os.Remove(tmp) return "", err } // Atomic replace so a concurrent reader never sees a half-written image. if err := os.Rename(tmp, outPath); err != nil { _ = os.Remove(tmp) return "", err } return outPath, nil }