Delete the kagane-specific cover path (#63)
The web proxy for kagane covers is dead: since #62 every Site's cover bytes land in the content-addressed store at creation or on the poll, and the one public route serves them all. Remove the second way to reach a Cover: - Bookmark.CoverURL() and the templates' use of it: templates render the wire value (.Cover) and nothing else. - GET /img/kagane/{id}, web.CoverFetcher, coverIDRe: the route and its identifier validation are gone, and with them web/cover.go. - store.KaganeImageID, GetKaganeCover, PutKaganeCover, kaganeCoverSourceURL: the proxy's persistence. - Bookmark.CoverSource: dead once CoverURL is gone. Acquisition keeps the browser where kagane genuinely needs it, but the Site name leaves the routing: kaganeImageURLRe lives in browser.go with the rest of the per-Site knowledge, browserCoverURL claims the URLs the sidecar alone can fetch, and fetchCoverBytes routes by URL shape with no Site argument. No plain-TLS fallback for a claimed URL — that would only retrieve a challenge page. Cover tests: kagane route tests removed, the general-route guarantees they pinned kept and re-pinned — unstored and traversal-shaped addresses serve nothing (TestPublicCoverRejectsUnknownAddress), non-image content types are never echoed back (TestPublicCoverNeverEchoesNonImage + TestCoverStoreAcceptsAnySourceURL). Store content-addressing and reopen-persistence tests rewritten against PutCover/GetCover. No Site name remains in a cover code path outside the acquisition module; go test ./... green.
This commit is contained in:
@@ -40,10 +40,6 @@ type Bookmark struct {
|
||||
// address and never an address that 404s (ADR-0007). A client may still
|
||||
// send this field and it is discarded on the way in; see Upsert.
|
||||
Cover string `json:"cover"`
|
||||
// CoverSource is the third-party address the bytes were fetched from. It
|
||||
// stays off the wire: it is the acquisition path's dedupe key, and no
|
||||
// client is ever asked to render one.
|
||||
CoverSource string `json:"-"`
|
||||
LastChapter string `json:"last_chapter"`
|
||||
LastChapterNum float64 `json:"last_chapter_num"`
|
||||
LastChapterURL string `json:"last_chapter_url"`
|
||||
@@ -155,20 +151,6 @@ func (b Bookmark) Initial() string {
|
||||
return "?"
|
||||
}
|
||||
|
||||
// kaganeCoverRe matches the cover URL kagane's og:image carries, which is what
|
||||
// the userscript stores for that site.
|
||||
var kaganeCoverRe = regexp.MustCompile(`^https://kagane\.to/api/v2/image/([0-9a-f-]{36})/compressed$`)
|
||||
|
||||
// KaganeImageID extracts the validated image id from the cover URL recorded by
|
||||
// the userscript.
|
||||
func KaganeImageID(cover string) (string, bool) {
|
||||
m := kaganeCoverRe.FindStringSubmatch(cover)
|
||||
if m == nil {
|
||||
return "", false
|
||||
}
|
||||
return m[1], true
|
||||
}
|
||||
|
||||
// CoverContentType canonicalises a fetched response's media type and reports
|
||||
// whether the bytes are safe to store and serve. comix answers "image/jpg",
|
||||
// which no standard lists but browsers accept; it is stored as the real name
|
||||
@@ -184,17 +166,6 @@ func CoverContentType(contentType string) (string, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// CoverURL is the src the web UI puts in an <img>. Cover already is an address
|
||||
// on this origin, so for every site but kagane it is used as-is. kagane's
|
||||
// bytes still arrive through the browser-backed proxy, which is keyed by image
|
||||
// id rather than by content address until #62 moves it onto the same path.
|
||||
func (b Bookmark) CoverURL() string {
|
||||
if imageID, ok := KaganeImageID(b.CoverSource); ok {
|
||||
return "/img/kagane/" + imageID
|
||||
}
|
||||
return b.Cover
|
||||
}
|
||||
|
||||
// Library buckets. A bookmark is in exactly one. This cannot be derived from
|
||||
// Site: asurascans serves manga and novels from the same /comics/ path, so the
|
||||
// userscript that recorded the page is the only party that knows which.
|
||||
@@ -217,7 +188,7 @@ var migrations embed.FS
|
||||
// compile-time constant; every request value is bound as a parameter. The
|
||||
// series-owned fields are joined in from the series table, in scanBookmark
|
||||
// order, so the flat Bookmark reads back whole despite the split (ADR-0004).
|
||||
const bookmarkColumns = `b.site, b.series_id, s.title, s.series_url, s.cover, s.cover_address,
|
||||
const bookmarkColumns = `b.site, b.series_id, s.title, s.series_url, s.cover_address,
|
||||
b.last_chapter, b.last_chapter_num, b.last_chapter_url,
|
||||
b.favorite, s.latest_chapter, s.latest_chapter_num, b.updated_at, b.status, s.kind`
|
||||
|
||||
@@ -562,7 +533,7 @@ func (s *Store) scanBookmark(scan func(...any) error) (Bookmark, error) {
|
||||
latestChapterNum sql.NullFloat64
|
||||
)
|
||||
if err := scan(
|
||||
&b.Site, &b.SeriesID, &b.Title, &b.SeriesURL, &b.CoverSource, &coverAddress,
|
||||
&b.Site, &b.SeriesID, &b.Title, &b.SeriesURL, &coverAddress,
|
||||
&b.LastChapter, &b.LastChapterNum, &b.LastChapterURL,
|
||||
&b.Favorite, &b.LatestChapter, &latestChapterNum, &b.UpdatedAt, &b.Status, &b.Kind,
|
||||
); err != nil {
|
||||
@@ -617,10 +588,6 @@ func coverRelativePath(address string) string {
|
||||
return address[:2] + "/" + address[2:4] + "/" + address
|
||||
}
|
||||
|
||||
func kaganeCoverSourceURL(imageID string) string {
|
||||
return "https://kagane.to/api/v2/image/" + imageID + "/compressed"
|
||||
}
|
||||
|
||||
func (s *Store) getCover(sourceURL string) ([]byte, string, bool, error) {
|
||||
return s.getCoverByAddress(coverSourceAddress(sourceURL))
|
||||
}
|
||||
@@ -703,18 +670,6 @@ func (s *Store) PutCover(sourceURL string, body []byte, contentType string) erro
|
||||
return s.putCover(sourceURL, body, contentType)
|
||||
}
|
||||
|
||||
// GetKaganeCover returns one persisted cover. Missing covers are reported with
|
||||
// ok=false rather than as an error so the web handler can fetch them once.
|
||||
func (s *Store) GetKaganeCover(imageID string) ([]byte, string, bool, error) {
|
||||
return s.getCover(kaganeCoverSourceURL(imageID))
|
||||
}
|
||||
|
||||
// PutKaganeCover persists one fetched cover. The source URL's content address
|
||||
// makes each stored object immutable, so later writes for that URL are ignored.
|
||||
func (s *Store) PutKaganeCover(imageID string, body []byte, contentType string) error {
|
||||
return s.putCover(kaganeCoverSourceURL(imageID), body, contentType)
|
||||
}
|
||||
|
||||
// CoverAddress is the content address bytes fetched from sourceURL are stored
|
||||
// under. It is a pure function of the URL, so the acquisition path can name a
|
||||
// Cover before it has the bytes.
|
||||
|
||||
@@ -550,45 +550,6 @@ func TestDisplayChapter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// CoverURL reads the source address for the kagane branch and the wire value
|
||||
// otherwise, so both are set the way scanBookmark sets them.
|
||||
func TestCoverURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
coverSource string
|
||||
cover string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"kagane routes through the proxy",
|
||||
"https://kagane.to/api/v2/image/019fe11a-84c3-7fc3-a84b-88787374b617/compressed",
|
||||
"https://bookmarks.test/covers/" + CoverAddress("kagane"),
|
||||
"/img/kagane/019fe11a-84c3-7fc3-a84b-88787374b617",
|
||||
},
|
||||
{
|
||||
"another site is served from our own origin",
|
||||
"https://gg.asuracomic.net/storage/media/1/conversions/cover.webp",
|
||||
"https://bookmarks.test/covers/" + CoverAddress("asura"),
|
||||
"https://bookmarks.test/covers/" + CoverAddress("asura"),
|
||||
},
|
||||
{
|
||||
"a lookalike host is not rewritten",
|
||||
"https://evil.example/api/v2/image/019fe11a-84c3-7fc3-a84b-88787374b617/compressed",
|
||||
"https://bookmarks.test/covers/" + CoverAddress("evil"),
|
||||
"https://bookmarks.test/covers/" + CoverAddress("evil"),
|
||||
},
|
||||
{"no cover stays empty", "", "", ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
b := Bookmark{CoverSource: tc.coverSource, Cover: tc.cover}
|
||||
if got := b.CoverURL(); got != tc.want {
|
||||
t.Errorf("CoverURL() = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertKindDefaultsToManga(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
got, err := store.Upsert(store.OwnerID(), Bookmark{
|
||||
@@ -1397,7 +1358,7 @@ func TestTwoReadersShareOneSeriesWithIndependentProgress(t *testing.T) {
|
||||
t.Fatalf("due after one Reader left = %+v, want the series still polled", due)
|
||||
}
|
||||
}
|
||||
func TestKaganeCoverPersistsAcrossReopen(t *testing.T) {
|
||||
func TestCoverPersistsAcrossReopen(t *testing.T) {
|
||||
url := pgtest.URL(t)
|
||||
coverDir := t.TempDir()
|
||||
first, err := Open(url, testOwner, coverDir, testCoverBaseURL)
|
||||
@@ -1405,8 +1366,9 @@ func TestKaganeCoverPersistsAcrossReopen(t *testing.T) {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
body := []byte("stored-cover")
|
||||
if err := first.PutKaganeCover("019fe11a-84c3-7fc3-a84b-88787374b617", body, "image/webp"); err != nil {
|
||||
t.Fatalf("PutKaganeCover: %v", err)
|
||||
const sourceURL = "https://cdn.example/covers/series.jpg"
|
||||
if err := first.PutCover(sourceURL, body, "image/webp"); err != nil {
|
||||
t.Fatalf("PutCover: %v", err)
|
||||
}
|
||||
if err := first.Close(); err != nil {
|
||||
t.Fatalf("close first store: %v", err)
|
||||
@@ -1417,9 +1379,9 @@ func TestKaganeCoverPersistsAcrossReopen(t *testing.T) {
|
||||
t.Fatalf("reopen: %v", err)
|
||||
}
|
||||
defer second.Close()
|
||||
got, contentType, ok, err := second.GetKaganeCover("019fe11a-84c3-7fc3-a84b-88787374b617")
|
||||
got, contentType, ok, err := second.GetCover(sourceURL)
|
||||
if err != nil {
|
||||
t.Fatalf("GetKaganeCover: %v", err)
|
||||
t.Fatalf("GetCover: %v", err)
|
||||
}
|
||||
if !ok || !bytes.Equal(got, body) || contentType != "image/webp" {
|
||||
t.Fatalf("stored cover = (%q, %q, %v), want (%q, image/webp, true)", got, contentType, ok, body)
|
||||
@@ -1443,7 +1405,7 @@ func TestOpenRequiresAbsoluteCoverBaseURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestKaganeCoverIsContentAddressedOnFilesystem(t *testing.T) {
|
||||
func TestCoverIsContentAddressedOnFilesystem(t *testing.T) {
|
||||
url := pgtest.URL(t)
|
||||
coverDir := t.TempDir()
|
||||
first, err := Open(url, testOwner, coverDir, testCoverBaseURL)
|
||||
@@ -1451,14 +1413,13 @@ func TestKaganeCoverIsContentAddressedOnFilesystem(t *testing.T) {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
body := []byte("stored-cover")
|
||||
const imageID = "019fe11a-84c3-7fc3-a84b-88787374b617"
|
||||
if err := first.PutKaganeCover(imageID, body, "image/webp"); err != nil {
|
||||
const sourceURL = "https://cdn.example/covers/series.jpg"
|
||||
if err := first.PutCover(sourceURL, body, "image/webp"); err != nil {
|
||||
first.Close()
|
||||
t.Fatalf("PutKaganeCover: %v", err)
|
||||
t.Fatalf("PutCover: %v", err)
|
||||
}
|
||||
defer first.Close()
|
||||
|
||||
sourceURL := "https://kagane.to/api/v2/image/" + imageID + "/compressed"
|
||||
addressBytes := sha256.Sum256([]byte(sourceURL))
|
||||
address := hex.EncodeToString(addressBytes[:])
|
||||
wantPath := filepath.Join(address[:2], address[2:4], address)
|
||||
|
||||
Reference in New Issue
Block a user