Skip to content

Commit 941ed51

Browse files
committed
Auto-detect and strip single top-level directory prefix when browsing archives
GitHub zipballs wrap all files in a repo-hash/ directory. Instead of hardcoding prefixes per ecosystem, open the archive once to check if all files share a single root directory and strip it automatically. The npm package/ prefix is still handled as a special case.
1 parent b68184c commit 941ed51

1 file changed

Lines changed: 64 additions & 10 deletions

File tree

internal/server/browse.go

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package server
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"fmt"
67
"io"
@@ -28,6 +29,63 @@ func archiveFilename(filename string) string {
2829
return filename
2930
}
3031

32+
// detectSingleRootDir returns the single top-level directory name if all files
33+
// in the archive live under one common directory (e.g. GitHub zipballs use
34+
// "repo-hash/"). Returns "" if there's no single root or the archive is flat.
35+
func detectSingleRootDir(reader archives.Reader) string {
36+
files, err := reader.List()
37+
if err != nil || len(files) == 0 {
38+
return ""
39+
}
40+
41+
var root string
42+
for _, f := range files {
43+
parts := strings.SplitN(f.Path, "/", 2)
44+
if len(parts) == 0 {
45+
continue
46+
}
47+
dir := parts[0]
48+
if root == "" {
49+
root = dir
50+
} else if dir != root {
51+
return ""
52+
}
53+
}
54+
55+
if root == "" {
56+
return ""
57+
}
58+
return root + "/"
59+
}
60+
61+
// openArchive opens a cached artifact as an archive reader, auto-detecting
62+
// and stripping a single top-level directory prefix (like GitHub zipballs).
63+
// For npm, the hardcoded "package/" prefix takes precedence.
64+
func openArchive(filename string, content io.Reader, ecosystem string) (archives.Reader, error) {
65+
fname := archiveFilename(filename)
66+
67+
// npm always uses package/ prefix
68+
if ecosystem == "npm" {
69+
return archives.OpenWithPrefix(fname, content, "package/")
70+
}
71+
72+
// Read content into memory so we can scan then wrap with prefix
73+
data, err := io.ReadAll(content)
74+
if err != nil {
75+
return nil, fmt.Errorf("reading artifact: %w", err)
76+
}
77+
78+
// Open once to detect root prefix
79+
probe, err := archives.Open(fname, bytes.NewReader(data))
80+
if err != nil {
81+
return nil, err
82+
}
83+
prefix := detectSingleRootDir(probe)
84+
_ = probe.Close()
85+
86+
return archives.OpenWithPrefix(fname, bytes.NewReader(data), prefix)
87+
}
88+
3189
// getStripPrefix returns the path prefix to strip for a given ecosystem.
3290
// npm packages wrap content in a "package/" directory.
3391
func getStripPrefix(ecosystem string) string {
@@ -185,9 +243,8 @@ func (s *Server) browseList(w http.ResponseWriter, r *http.Request, ecosystem, n
185243
}
186244
defer func() { _ = artifactReader.Close() }()
187245

188-
// Open archive with appropriate prefix stripping
189-
stripPrefix := getStripPrefix(ecosystem)
190-
archiveReader, err := archives.OpenWithPrefix(archiveFilename(cachedArtifact.Filename), artifactReader, stripPrefix)
246+
// Open archive with auto-detected prefix stripping
247+
archiveReader, err := openArchive(cachedArtifact.Filename, artifactReader, ecosystem)
191248
if err != nil {
192249
s.logger.Error("failed to open archive", "error", err, "filename", cachedArtifact.Filename)
193250
http.Error(w, "failed to open archive", http.StatusInternalServerError)
@@ -280,9 +337,8 @@ func (s *Server) browseFile(w http.ResponseWriter, r *http.Request, ecosystem, n
280337
}
281338
defer func() { _ = artifactReader.Close() }()
282339

283-
// Open archive with appropriate prefix stripping
284-
stripPrefix := getStripPrefix(ecosystem)
285-
archiveReader, err := archives.OpenWithPrefix(archiveFilename(cachedArtifact.Filename), artifactReader, stripPrefix)
340+
// Open archive with auto-detected prefix stripping
341+
archiveReader, err := openArchive(cachedArtifact.Filename, artifactReader, ecosystem)
286342
if err != nil {
287343
s.logger.Error("failed to open archive", "error", err, "filename", cachedArtifact.Filename)
288344
http.Error(w, "failed to open archive", http.StatusInternalServerError)
@@ -495,17 +551,15 @@ func (s *Server) compareDiff(w http.ResponseWriter, r *http.Request, ecosystem,
495551
}
496552
defer func() { _ = toReader.Close() }()
497553

498-
stripPrefix := getStripPrefix(ecosystem)
499-
500-
fromArchive, err := archives.OpenWithPrefix(archiveFilename(fromArtifact.Filename), fromReader, stripPrefix)
554+
fromArchive, err := openArchive(fromArtifact.Filename, fromReader, ecosystem)
501555
if err != nil {
502556
s.logger.Error("failed to open from archive", "error", err)
503557
http.Error(w, "failed to open from archive", http.StatusInternalServerError)
504558
return
505559
}
506560
defer func() { _ = fromArchive.Close() }()
507561

508-
toArchive, err := archives.OpenWithPrefix(archiveFilename(toArtifact.Filename), toReader, stripPrefix)
562+
toArchive, err := openArchive(toArtifact.Filename, toReader, ecosystem)
509563
if err != nil {
510564
s.logger.Error("failed to open to archive", "error", err)
511565
http.Error(w, "failed to open to archive", http.StatusInternalServerError)

0 commit comments

Comments
 (0)