The proxy keeps the person's library in a cache folder (hub-products in its data folder), one pack per product, served like any other pack: the hub says which revision to load and which catalogs host it, each file comes from a catalog and is checked against the hub's sha256 (files are kept by hash, so a new revision only downloads what changed), and adult files are fetched with the hub's access token. Products someone wears in a room that aren't here yet are fetched too, and put on them when they arrive. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
93fa2f5d90
commit
c281fb572c
6 files changed
+579
-3
No files matched your search
@@ -0,0 +1,143 @@
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"git.t5de.dev/t5de/t5de-api/pack"
|
||||
"git.t5de.dev/t5de/t5de-proxy/internal/hub"
|
||||
)
|
||||
|
||||
const id = "abcdefgh23"
|
||||
|
||||
func indexXML(rating string) []byte {
|
||||
return []byte(`<?xml version="1.0" encoding="UTF-8" ?><Template><ProductRating>` + rating +
|
||||
`</ProductRating><__DATAIMPORT>product://500/index.xml</__DATAIMPORT><ProductName>x</ProductName></Template>`)
|
||||
}
|
||||
|
||||
func entry(name string, data []byte) hub.FileEntry {
|
||||
sum := sha256.Sum256(data)
|
||||
return hub.FileEntry{Name: name, SHA256: hex.EncodeToString(sum[:]), Size: int64(len(data))}
|
||||
}
|
||||
|
||||
// fakeHub answers for one product.
|
||||
type fakeHub struct {
|
||||
mu sync.Mutex
|
||||
files hub.Files
|
||||
}
|
||||
|
||||
func (h *fakeHub) ProductFiles(ctx context.Context, pid string) (*hub.Files, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
f := h.files
|
||||
return &f, nil
|
||||
}
|
||||
func (h *fakeHub) AccessToken(ctx context.Context) (string, error) { return "adult-token", nil }
|
||||
func (h *fakeHub) Library(ctx context.Context) ([]hub.LibraryEntry, error) {
|
||||
var e hub.LibraryEntry
|
||||
e.Product.ID = id
|
||||
return []hub.LibraryEntry{e}, nil
|
||||
}
|
||||
|
||||
// catalog serves blobs by sha256; adult ones need the token.
|
||||
func catalog(t *testing.T, blobs map[string][]byte, adult map[string]bool, served *int) *httptest.Server {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sum := strings.TrimPrefix(r.URL.Path, "/api/v1/files/")
|
||||
b, ok := blobs[sum]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if adult[sum] && r.Header.Get("Authorization") != "Bearer adult-token" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
*served++
|
||||
w.Write(b)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
return srv
|
||||
}
|
||||
|
||||
func TestEnsureWritesAPack(t *testing.T) {
|
||||
index, tex := indexXML("GA"), []byte("texture one")
|
||||
tex2 := []byte("texture two")
|
||||
blobs := map[string][]byte{}
|
||||
for _, b := range [][]byte{index, tex, tex2} {
|
||||
blobs[entry("", b).SHA256] = b
|
||||
}
|
||||
served := 0
|
||||
cat := catalog(t, blobs, nil, &served)
|
||||
h := &fakeHub{files: hub.Files{Product: id, Revision: 1, Rating: "general", Name: "Blue Coat", Creator: "maker",
|
||||
Files: []hub.FileEntry{entry("index.xml", index), entry("tex.png", tex)}, Catalogs: []string{"http://127.0.0.1:1", cat.URL}}}
|
||||
dir := t.TempDir()
|
||||
f := New(h, dir, t.Logf)
|
||||
var ready []string
|
||||
f.OnReady = func(id string) { ready = append(ready, id) }
|
||||
|
||||
f.SyncLibrary(context.Background())
|
||||
p, err := pack.Load(filepath.Join(dir, id))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.Manifest.Name != "Blue Coat" || p.Manifest.Revision != 1 || len(p.Files) != 2 || len(ready) != 1 {
|
||||
t.Fatalf("pack %+v, ready %v", p.Manifest, ready)
|
||||
}
|
||||
// Already there: nothing fetched again.
|
||||
if err := f.Ensure(context.Background(), id); err != nil || served != 2 || len(ready) != 1 {
|
||||
t.Fatalf("again: %v, served %d", err, served)
|
||||
}
|
||||
|
||||
// A new revision: only the changed file is downloaded; the pack is replaced.
|
||||
h.files.Revision, h.files.Files = 2, []hub.FileEntry{entry("index.xml", index), entry("tex.png", tex2)}
|
||||
if err := f.Ensure(context.Background(), id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b, _ := os.ReadFile(filepath.Join(dir, id, "files", "tex.png")); string(b) != "texture two" || served != 3 {
|
||||
t.Fatalf("tex %q, served %d", b, served)
|
||||
}
|
||||
entries, _ := os.ReadDir(dir)
|
||||
for _, e := range entries {
|
||||
if e.Name() != id && e.Name() != ".blobs" {
|
||||
t.Fatalf("left behind %s", e.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureChecksFilesAndAdultTokens(t *testing.T) {
|
||||
index := indexXML("AP")
|
||||
blobs := map[string][]byte{entry("", index).SHA256: index}
|
||||
adult := map[string]bool{entry("", index).SHA256: true}
|
||||
served := 0
|
||||
cat := catalog(t, blobs, adult, &served)
|
||||
h := &fakeHub{files: hub.Files{Product: id, Revision: 1, Rating: "adult", Name: "Lace",
|
||||
Files: []hub.FileEntry{entry("index.xml", index)}, Catalogs: []string{cat.URL}}}
|
||||
f := New(h, t.TempDir(), t.Logf)
|
||||
if err := f.Ensure(context.Background(), id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// A catalog sending something else than the hub listed is refused.
|
||||
h.files.Revision = 2
|
||||
h.files.Files = []hub.FileEntry{{Name: "index.xml", SHA256: strings.Repeat("0", 64), Size: int64(len(index))}}
|
||||
blobs[strings.Repeat("0", 64)] = []byte(strings.Repeat("x", len(index)))
|
||||
if err := f.Ensure(context.Background(), id); err == nil || !strings.Contains(err.Error(), "different file") {
|
||||
t.Fatalf("got %v", err)
|
||||
}
|
||||
// File names that would leave the pack's folder are refused.
|
||||
h.files.Files = []hub.FileEntry{{Name: `..\evil`, SHA256: strings.Repeat("0", 64), Size: 1}}
|
||||
if err := f.Ensure(context.Background(), id); err == nil || !strings.Contains(err.Error(), "bad file name") {
|
||||
t.Fatalf("got %v", err)
|
||||
}
|
||||
if err := f.Ensure(context.Background(), "../etc"); err == nil {
|
||||
t.Fatal("bad ID accepted")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user