feat(cbz): adapt reader to real archive structure — PageIndex skips macOS junk (__MACOSX/ and ._ AppleDouble were polluting pages with 163-byte blacks), pages endpoint derives chapters[{title,start}] from folder layout, imageless zip now lands state=error instead of an empty reader, CbzReader gains 目录 sidebar + prev/next chapter (TextReader pattern, rd-* classes); pagesidx cache key bumped for one-time invalidation; e2e on 311MB 調教開關:第二季.zip: 15048→7524 real pages, 52 chapters, all JPEGs

This commit is contained in:
2026-09-08 00:22:13 +08:00
parent 7d650107d2
commit b8da9fd92e
9 changed files with 216 additions and 3 deletions
+11
View File
@@ -26,6 +26,14 @@ func isImage(name string) bool {
return false
}
// junkEntry: macOS 打包混入的资源叉垃圾(__MACOSX/ 目录与 ._* AppleDouble),不是页
func junkEntry(name string) bool {
if strings.HasPrefix(strings.ToLower(name), "__macosx/") {
return true
}
return strings.HasPrefix(path.Base(name), "._")
}
func PageIndex(f io.ReaderAt, size int64) ([]string, error) {
zr, err := zip.NewReader(f, size)
if err != nil {
@@ -36,6 +44,9 @@ func PageIndex(f io.ReaderAt, size int64) ([]string, error) {
if unsafeEntry(zf.Name) {
return nil, fmt.Errorf("%w: %s", ErrUnsafeZip, zf.Name)
}
if junkEntry(zf.Name) {
continue
}
if isImage(zf.Name) {
names = append(names, zf.Name)
}
+17
View File
@@ -38,6 +38,23 @@ func TestPageIndexSortAndFilter(t *testing.T) {
}
}
func TestPageIndexSkipsAppleDouble(t *testing.T) {
r := zipOf(t, "第2季/第2話/0001.jpg", "第2季/第1話/._0001.jpg", "__MACOSX/第2季/._0001.jpg", "第2季/第1話/0001.jpg", "._top.jpg")
idx, err := PageIndex(r, int64(r.Len()))
if err != nil {
t.Fatal(err)
}
want := []string{"第2季/第1話/0001.jpg", "第2季/第2話/0001.jpg"}
if len(idx) != len(want) {
t.Fatalf("got %v want %v", idx, want)
}
for i := range want {
if idx[i] != want[i] {
t.Fatalf("got %v want %v", idx, want)
}
}
}
func TestPageIndexRejectsSlip(t *testing.T) {
for _, bad := range []string{"../evil.jpg", "/etc/passwd.jpg", "a\\..\\b.jpg", "pag\ne.jpg", "pag\re.jpg"} {
r := zipOf(t, bad)
+7
View File
@@ -2,6 +2,7 @@ package scanner
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
@@ -146,6 +147,9 @@ func (s *Scanner) add(ctx context.Context, libID int64, root, rel string, ds dis
idx, err := s.zipIndex(root, rel)
pageCount = len(idx)
idxErr = err
if idxErr == nil && pageCount == 0 { // 视频/文档 zip 不是漫画,空白 reader 没有意义
idxErr = errors.New("no images in archive")
}
}
id, err := s.st.InsertBook(ctx, libID, rel, titleOf(rel), format, ds.size, ds.modTS, pageCount)
if err != nil {
@@ -167,6 +171,9 @@ func (s *Scanner) update(ctx context.Context, libID, bookID int64, root, rel str
idx, err := s.zipIndex(root, rel)
pageCount = len(idx)
idxErr = err
if idxErr == nil && pageCount == 0 { // 视频/文档 zip 不是漫画,空白 reader 没有意义
idxErr = errors.New("no images in archive")
}
}
if err := s.st.UpdateBookFile(ctx, bookID, ds.size, ds.modTS, pageCount); err != nil {
log.Printf("scan: update %s: %v", rel, err)