package upstream import ( "context" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "reflect" "time" ) func CacheDir(root string) string { return filepath.Join(root, "build", "upstream") } func ReadLock(root string) (Lock, error) { var lock Lock b, err := os.ReadFile(filepath.Join(root, "gitea.lock.json")) if err != nil { return lock, err } if err = json.Unmarshal(b, &lock); err != nil { return lock, err } return lock, lock.validateIdentity() } func LoadPinned(root string) (*Snapshot, error) { lock, err := ReadLock(root) if err != nil { return nil, err } s, err := Load(CacheDir(root)) if err != nil { return nil, err } if !reflect.DeepEqual(lock, s.Lock) { return nil, fmt.Errorf("upstream cache differs from gitea.lock.json; run upstream prepare after reviewing the lock") } return s, nil } // Prepare fetches immutable commit files only if the cache is absent. A corrupt // cache is an error, not permission to silently replace local evidence. func Prepare(ctx context.Context, root string) (*Snapshot, error) { return prepareFrom(ctx, root, "https://raw.githubusercontent.com/go-gitea/gitea/") } func prepareFrom(ctx context.Context, root, rawBase string) (*Snapshot, error) { lock, err := ReadLock(root) if err != nil { return nil, err } if _, err = os.Stat(filepath.Join(CacheDir(root), "lock.json")); err == nil { return LoadPinned(root) } else if !os.IsNotExist(err) { return nil, err } client := &http.Client{Timeout: 45 * time.Second} s := &Snapshot{Lock: lock, Files: map[string][]byte{}} for name, hash := range lock.Files { if !SafePath(name) { return nil, fmt.Errorf("unsafe locked path %q", name) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawBase+lock.Commit+"/"+name, nil) if err != nil { return nil, err } req.Header.Set("User-Agent", "GiteaMailTemplates-locked-cache") resp, err := client.Do(req) if err != nil { return nil, err } b, readErr := io.ReadAll(io.LimitReader(resp.Body, 16<<20)) resp.Body.Close() if readErr != nil { return nil, readErr } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("%s: HTTP %d", name, resp.StatusCode) } if Digest(b) != hash { return nil, fmt.Errorf("download checksum mismatch: %s", name) } s.Files[name] = b } if err = s.validate(); err != nil { return nil, err } dir := CacheDir(root) if err = os.MkdirAll(filepath.Dir(dir), 0755); err != nil { return nil, err } stage, err := os.MkdirTemp(filepath.Dir(dir), ".upstream-cache-") if err != nil { return nil, err } defer os.RemoveAll(stage) for name, b := range s.Files { target := filepath.Join(stage, filepath.FromSlash(name)) if err = os.MkdirAll(filepath.Dir(target), 0755); err != nil { return nil, err } if err = os.WriteFile(target, b, 0644); err != nil { return nil, err } } b, _ := json.MarshalIndent(lock, "", " ") if err = os.WriteFile(filepath.Join(stage, "lock.json"), append(b, '\n'), 0644); err != nil { return nil, err } if _, err = os.Stat(dir); err == nil { return nil, fmt.Errorf("incomplete upstream cache exists at %s; inspect it before removing it", dir) } if err = os.Rename(stage, dir); err != nil { return nil, err } fmt.Printf("[PASS] Downloaded locked Gitea %s (%s) to build cache\n", lock.Tag, lock.Commit) return LoadPinned(root) } func SyncRoot(ctx context.Context, tag, root string) error { if err := os.MkdirAll(filepath.Join(root, "build"), 0755); err != nil { return err } if err := Sync(ctx, tag, CacheDir(root)); err != nil { return err } s, err := Load(CacheDir(root)) if err != nil { return err } b, err := json.MarshalIndent(s.Lock, "", " ") if err != nil { return err } return os.WriteFile(filepath.Join(root, "gitea.lock.json"), append(b, '\n'), 0644) }