fix(api): lazy cover self-heal + tmp-file skip
This commit is contained in:
@@ -36,8 +36,42 @@ func (a *api) serveCover(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
a.immutable(c)
|
a.immutable(c)
|
||||||
dir := bookfile.CoverDir(a.cfg.CacheDir, bookfile.DirKey(b.ID, bookfile.Hash(b.FileSize, b.ModTS)))
|
dir := bookfile.CoverDir(a.cfg.CacheDir, bookfile.DirKey(b.ID, bookfile.Hash(b.FileSize, b.ModTS)))
|
||||||
if entries, e := os.ReadDir(dir); e == nil && len(entries) > 0 {
|
if entries, e := os.ReadDir(dir); e == nil {
|
||||||
http.ServeFile(c.Writer, c.Request, filepath.Join(dir, entries[0].Name()))
|
for _, en := range entries { // 跳过写一半的 .tmp 落盘中间态
|
||||||
|
if !strings.Contains(en.Name(), ".tmp") {
|
||||||
|
http.ServeFile(c.Writer, c.Request, filepath.Join(dir, en.Name()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if b.Format == "cbz" || b.Format == "epub" { // 自愈:缓存丢了就地抽封面(重启/卷漂移/扫描器还没跑到)
|
||||||
|
if root, ok := a.bookRoot(c, b); ok {
|
||||||
|
if f, size, ok := a.openBook(c, b, root); ok {
|
||||||
|
defer f.Close()
|
||||||
|
var img []byte
|
||||||
|
var ext string
|
||||||
|
var e error
|
||||||
|
if b.Format == "cbz" {
|
||||||
|
img, ext, e = bookfile.CBZCover(f, size)
|
||||||
|
} else {
|
||||||
|
img, ext, e = bookfile.EPUBCover(f, size)
|
||||||
|
}
|
||||||
|
if e == nil {
|
||||||
|
dst := filepath.Join(dir, "cover"+ext)
|
||||||
|
if e := os.MkdirAll(dir, 0o755); e == nil {
|
||||||
|
tmp := fmt.Sprintf("%s.tmp-%d", dst, time.Now().UnixNano()) // 并发幂等:唯一 tmp + rename 原子
|
||||||
|
if e := os.WriteFile(tmp, img, 0o644); e == nil {
|
||||||
|
if e := os.Rename(tmp, dst); e == nil {
|
||||||
|
http.ServeFile(c.Writer, c.Request, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os.Remove(tmp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.Writer.Written() { // openBook/bookRoot 已写 403/404/500,不再叠加占位图
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Data(http.StatusOK, "image/svg+xml", []byte(defaultCover))
|
c.Data(http.StatusOK, "image/svg+xml", []byte(defaultCover))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -47,6 +48,29 @@ func TestCoverCBZAndPlaceholder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 不经 scanner 直接入库(模拟缓存丢失/卷漂移)→ GET cover 就地抽图自愈并落盘(tmp+rename)
|
||||||
|
func TestCoverSelfHeal(t *testing.T) {
|
||||||
|
st, _, h, booksDir := setupAPI(t)
|
||||||
|
atok := adminToken(t, h)
|
||||||
|
lib, root := newLibrary(t, st, h, atok, booksDir, "comics")
|
||||||
|
p := filepath.Join(root, "heal.cbz")
|
||||||
|
writeCBZ(t, p, 2)
|
||||||
|
fi, err := os.Stat(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
id, err := st.InsertBook(context.Background(), lib.ID, "heal.cbz", "heal", "cbz", fi.Size(), fi.ModTime().Unix(), 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for i := 1; i <= 2; i++ { // 第二次走已缓存目录,均应 200 image/*
|
||||||
|
w := do(h, "GET", "/api/books/"+itoa(id)+"/cover", atok, nil)
|
||||||
|
if w.Code != 200 || !strings.Contains(w.Header().Get("Content-Type"), "image/") {
|
||||||
|
t.Fatalf("self-heal cover pass %d: %d %q", i, w.Code, w.Header().Get("Content-Type"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPages(t *testing.T) {
|
func TestPages(t *testing.T) {
|
||||||
h, cbzID, txtID := serveFixture(t)
|
h, cbzID, txtID := serveFixture(t)
|
||||||
tok := adminToken(t, h)
|
tok := adminToken(t, h)
|
||||||
|
|||||||
Reference in New Issue
Block a user