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 }