indexer/scraper/forgejo_licenses.go
2026-09-28 15:36:42 +02:00

96 lines
2.8 KiB
Go

package scraper
import (
"fmt"
forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3"
"github.com/go-enry/go-license-detector/v4/licensedb"
"github.com/go-enry/go-license-detector/v4/licensedb/filer"
)
// forgejoFiler adapts the Forgejo content API to the filer.Filer interface so
// that go-license-detector can inspect a repository's files without a local
// checkout. ReadDir serves a cached root listing when available (to avoid an
// extra ListContents call) and falls back to the API otherwise. ReadFile
// downloads a file's bytes at the given ref (default branch).
type forgejoFiler struct {
client *forgejo.Client
owner string
repo string
ref string
// cachedRoot, when non-nil, is served for ReadDir("") to avoid a second
// root listing that was already fetched by the caller.
cachedRoot []filer.File
}
var _ filer.Filer = (*forgejoFiler)(nil)
func (f *forgejoFiler) ReadDir(dirPath string) ([]filer.File, error) {
if dirPath == "" && f.cachedRoot != nil {
return f.cachedRoot, nil
}
entries, _, err := f.client.ListContents(f.owner, f.repo, f.ref, dirPath)
if err != nil {
return nil, fmt.Errorf("list contents %q: %w", dirPath, err)
}
files := make([]filer.File, 0, len(entries))
for _, e := range entries {
if e == nil {
continue
}
files = append(files, filer.File{
Name: e.Name,
IsDir: e.Type == "dir",
})
}
return files, nil
}
func (f *forgejoFiler) ReadFile(path string) ([]byte, error) {
// GetFile returns raw file bytes (binary-safe, base64-decoded).
data, _, err := f.client.GetFile(f.owner, f.repo, f.ref, path)
if err != nil {
return nil, fmt.Errorf("read file %q: %w", path, err)
}
return data, nil
}
func (f *forgejoFiler) Close() {}
func (f *forgejoFiler) PathsAreAlwaysSlash() bool { return true }
// licensesFor returns the SPDX identifiers detected for the repository. It
// uses go-license-detector over the repository's files on its default branch.
// The given root listing is served to the detector to avoid a duplicate,
// already-available API call. An empty slice is returned when no license can
// be determined (the repo is still kept in results -- license failures are
// non-fatal).
func (s *ForgejoScraper) licensesFor(owner, repo, ref string, root []filer.File) []string {
detector := &forgejoFiler{
client: s.Client,
owner: owner,
repo: repo,
ref: ref,
cachedRoot: root,
}
matches, err := licensedb.Detect(detector)
if err != nil {
return nil
}
// Keep only the highest-confidence match, sorted deterministically.
ids := make([]string, 0, len(matches))
bestID, bestConf := "", float32(-1)
for id, m := range matches {
if m.Confidence > bestConf {
bestConf = m.Confidence
bestID = id
}
}
if bestID != "" {
ids = append(ids, bestID)
}
return ids
}