Skip to content

Commit dcf90cf

Browse files
authored
Close the Composer download gaps left after #430 (#433)
* GitLab Composer archives are now cached per commit and fetched for the commit the lock file pins, and branch names with a slash are escaped in dist URLs so the download route matches them. * Composer branch archives that an older proxy cached from another commit's URL are now discarded and fetched again, and a refetched artifact records the URL it actually came from. * When a GitLab commit is fetched under its own name, the archive older proxies cached under the shared archive.zip name is dropped. A test now moves the branch from A to B and checks that A keeps coming from the cache.
1 parent 16ebeaa commit dcf90cf

3 files changed

Lines changed: 312 additions & 16 deletions

File tree

‎internal/database/queries.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ func (db *DB) upsertArtifactFrom(a *Artifact, previous sql.NullString) (bool, er
359359
created_at, updated_at)
360360
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
361361
ON CONFLICT(version_purl, filename) DO UPDATE SET
362+
upstream_url = EXCLUDED.upstream_url,
362363
storage_path = EXCLUDED.storage_path,
363364
content_hash = EXCLUDED.content_hash,
364365
size = EXCLUDED.size,
@@ -374,6 +375,7 @@ func (db *DB) upsertArtifactFrom(a *Artifact, previous sql.NullString) (bool, er
374375
created_at, updated_at)
375376
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
376377
ON CONFLICT(version_purl, filename) DO UPDATE SET
378+
upstream_url = excluded.upstream_url,
377379
storage_path = excluded.storage_path,
378380
content_hash = excluded.content_hash,
379381
size = excluded.size,

‎internal/handler/composer.go‎

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"io"
1010
"net/http"
11+
"net/url"
1112
"path"
1213
"regexp"
1314
"strings"
@@ -372,7 +373,7 @@ func (h *ComposerHandler) proxyDistURL(packageName, version, upstreamURL, distTy
372373
return "", false
373374
}
374375
return fmt.Sprintf("%s/composer/files/%s/%s/%s/%s",
375-
h.proxyURL, parts[0], parts[1], version, distFilename(upstreamURL, distType)), true
376+
h.proxyURL, parts[0], parts[1], url.PathEscape(version), distFilename(upstreamURL, distType)), true
376377
}
377378

378379
// distFilename returns the file name this proxy serves an archive under: the
@@ -410,6 +411,16 @@ func upstreamDistURL(listedURL, distType, filename string, branch bool) (string,
410411
if filename == listed {
411412
return listedURL, true
412413
}
414+
if commit, ok := shaQueryCommit(listed); ok {
415+
requested, ok := shaQueryCommit(filename)
416+
listedSha, requestedSha := "sha="+commit, "sha="+requested
417+
idx := strings.LastIndex(listed, listedSha)
418+
if !branch || !ok || idx < 0 || listed[:idx]+requestedSha+listed[idx+len(listedSha):] != filename {
419+
return "", false
420+
}
421+
idx = strings.LastIndex(listedURL, listedSha)
422+
return listedURL[:idx] + requestedSha + listedURL[idx+len(listedSha):], true
423+
}
413424
ext := path.Ext(listed)
414425
commit := strings.TrimSuffix(listed, ext)
415426
requested, ok := strings.CutSuffix(filename, ext)
@@ -420,6 +431,22 @@ func upstreamDistURL(listedURL, distType, filename string, branch bool) (string,
420431
return listedURL[:idx] + requested + listedURL[idx+len(commit):], true
421432
}
422433

434+
// shaQueryCommit returns the commit an archive name passes in a sha query
435+
// parameter. GitLab names the archive of every commit archive.zip and picks
436+
// the commit that way.
437+
func shaQueryCommit(filename string) (string, bool) {
438+
_, query, ok := strings.Cut(filename, "?")
439+
if !ok {
440+
return "", false
441+
}
442+
values, err := url.ParseQuery(query)
443+
if err != nil {
444+
return "", false
445+
}
446+
commit := values.Get("sha")
447+
return commit, composerCommitPattern.MatchString(commit)
448+
}
449+
423450
// composerFields holds one version's fields as Composer sees them after
424451
// expanding the minified format: raw values under their decoded names, in the
425452
// order the names first appeared. A removed field keeps its slot with a nil
@@ -544,8 +571,7 @@ func (h *ComposerHandler) handleDownload(w http.ResponseWriter, r *http.Request)
544571

545572
// The archive is cached under the file name from the path, so fetch the
546573
// one that name stands for. Compare it as sent, the way rewriteDist handed
547-
// it out. A query such as GitLab's ?sha= is compared but is not part of
548-
// the cache key.
574+
// it out, query included.
549575
requested := path.Base(r.URL.EscapedPath())
550576
if r.URL.RawQuery != "" {
551577
requested += "?" + r.URL.RawQuery
@@ -564,16 +590,60 @@ func (h *ComposerHandler) handleDownload(w http.ResponseWriter, r *http.Request)
564590
"package", packageName, "version", version,
565591
"download_url", downloadURL)
566592

593+
// Archives picked by ?sha= share one file name, so the commit goes into
594+
// the name they are cached under. Proxy versions before that cached every
595+
// commit under the shared name, which is dropped once a commit is fetched.
596+
var sharedFilename string
597+
if commit, ok := shaQueryCommit(requested); ok {
598+
sharedFilename = h.routes.cacheFilename(packageName, filename)
599+
ext := path.Ext(filename)
600+
filename = strings.TrimSuffix(filename, ext) + "-" + commit + ext
601+
}
567602
cacheFilename := h.routes.cacheFilename(packageName, filename)
603+
if commit := strings.TrimSuffix(filename, path.Ext(filename)); isDevVersion(version) && composerCommitPattern.MatchString(commit) {
604+
h.discardOtherCommit(packageName, version, cacheFilename, commit)
605+
}
568606
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "composer", packageName, version, cacheFilename, downloadURL)
569607
if err != nil {
570608
h.proxy.serveArtifactError(w, err, "failed to fetch package")
571609
return
572610
}
611+
if sharedFilename != "" && !result.Cached {
612+
if err := h.proxy.ClearCachedArtifact("composer", packageName, version, sharedFilename); err != nil {
613+
h.proxy.Logger.Warn("failed to discard composer archive cached under a shared name",
614+
"package", packageName, "version", version, "filename", sharedFilename, "error", err)
615+
}
616+
}
573617

574618
ServeArtifactRequest(w, r, result)
575619
}
576620

621+
// discardOtherCommit clears a cached branch archive named for commit that was
622+
// fetched from another commit's URL. Proxy versions before the pinned-commit
623+
// fix cached the branch head under whatever commit a lock file asked for. The
624+
// record is only cleared while it still points at the object checked here.
625+
func (h *ComposerHandler) discardOtherCommit(packageName, version, cacheFilename, commit string) {
626+
_, versionPURL, err := packagePURLStrings("composer", packageName, version)
627+
if err != nil {
628+
return
629+
}
630+
record, err := h.proxy.DB.GetArtifact(versionPURL, cacheFilename)
631+
if err != nil {
632+
h.proxy.Logger.Warn("failed to read composer cache record",
633+
"purl", versionPURL, "filename", cacheFilename, "error", err)
634+
return
635+
}
636+
if record == nil || !record.StoragePath.Valid || strings.Contains(record.UpstreamURL, commit) {
637+
return
638+
}
639+
h.proxy.Logger.Warn("cached composer archive is from another commit, discarding",
640+
"purl", versionPURL, "filename", cacheFilename, "upstream_url", record.UpstreamURL)
641+
if err := h.proxy.DB.DiscardArtifact(versionPURL, cacheFilename, record.StoragePath.String); err != nil {
642+
h.proxy.Logger.Warn("failed to discard composer cache record",
643+
"purl", versionPURL, "filename", cacheFilename, "error", err)
644+
}
645+
}
646+
577647
// isDevVersion reports whether a Composer version string refers to a
578648
// development (unstable, branch) version rather than a tagged release.
579649
// Composer formats these as either "dev-<branch>" (e.g. "dev-master") or

0 commit comments

Comments
 (0)