diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc79d4a..f787509 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: go-version-file: go.mod cache: false - - uses: goreleaser/goreleaser-action@e24998b8b67b290c2fa8b7c14fcfa7de2c5c9b8c # v7.1.0 + - uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0 with: version: "~> v2" args: release --clean diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 4acd19b..03ea882 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -26,4 +26,4 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index e5881db..b6256de 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -36,10 +36,11 @@ checksum: signs: - cmd: cosign - signature: "${artifact}.cosign.bundle" + certificate: "${artifact}.pem" args: - sign-blob - - "--bundle=${signature}" + - "--output-certificate=${certificate}" + - "--output-signature=${signature}" - "${artifact}" - "--yes" artifacts: checksum diff --git a/config.example.yaml b/config.example.yaml index cb4881e..e349b5b 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -29,23 +29,6 @@ storage: # Empty or "0" means unlimited max_size: "" - # Redirect cached artifact downloads to presigned storage URLs (HTTP 302) - # instead of streaming through the proxy. Only effective for S3 and Azure. - # Leave disabled if clients reach the proxy through an authenticating gateway, - # since presigned URLs bypass it. - direct_serve: false - - # How long presigned URLs remain valid (e.g. "5m", "1h"). Default: "15m". - direct_serve_ttl: "15m" - - # Public base URL to substitute into presigned URLs. Set this when the - # proxy reaches storage at an internal address (127.0.0.1, a Docker - # service name) but clients must use a public hostname. Only scheme and - # host are used; the signed path and query are preserved. For S3/MinIO - # the reverse proxy at this address must forward requests with the - # internal Host header or the SigV4 signature will not validate. - # direct_serve_base_url: "https://minio.example.com" - # Database configuration database: # Database driver: "sqlite" (default) or "postgres" diff --git a/go.mod b/go.mod index 2619970..7ae8518 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/git-pkgs/archives v0.2.2 github.com/git-pkgs/enrichment v0.2.2 github.com/git-pkgs/purl v0.1.10 - github.com/git-pkgs/registries v0.4.1 + github.com/git-pkgs/registries v0.4.0 github.com/git-pkgs/spdx v0.1.2 github.com/git-pkgs/vers v0.2.4 github.com/git-pkgs/vulns v0.1.4 @@ -22,14 +22,12 @@ require ( golang.org/x/sync v0.20.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.49.1 + modernc.org/sqlite v1.48.2 ) require ( 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect 4d63.com/gochecknoglobals v0.2.2 // indirect - cloud.google.com/go/auth v0.18.2 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect codeberg.org/chavacava/garif v0.2.0 // indirect codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect @@ -42,13 +40,6 @@ require ( github.com/Antonboom/errname v1.1.1 // indirect github.com/Antonboom/nilnil v1.1.1 // indirect github.com/Antonboom/testifylint v1.6.4 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect - github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 // indirect - github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/Djarvur/go-err113 v0.1.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect @@ -147,7 +138,6 @@ require ( github.com/gobwas/glob v0.2.3 // indirect github.com/godoc-lint/godoc-lint v0.11.2 // indirect github.com/gofrs/flock v0.13.0 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golangci/asciicheck v0.5.0 // indirect github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect github.com/golangci/go-printf-func-name v0.1.1 // indirect @@ -160,10 +150,8 @@ require ( github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/google/wire v0.7.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gordonklaus/ineffassign v0.2.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect @@ -186,7 +174,6 @@ require ( github.com/kkHAIKE/contextcheck v1.1.6 // indirect github.com/kulti/thelper v0.7.1 // indirect github.com/kunwardeep/paralleltest v1.0.15 // indirect - github.com/kylelemons/godebug v1.1.0 // indirect github.com/lasiar/canonicalheader v1.1.2 // indirect github.com/ldez/exptostd v0.4.5 // indirect github.com/ldez/gomoddirectives v0.8.0 // indirect @@ -222,7 +209,6 @@ require ( github.com/pandatix/go-cvss v0.6.2 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect @@ -291,12 +277,10 @@ require ( go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.51.0 // indirect - golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/tools v0.42.0 // indirect @@ -307,7 +291,7 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect honnef.co/go/tools v0.7.0 // indirect - modernc.org/libc v1.72.0 // indirect + modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect mvdan.cc/gofumpt v0.9.2 // indirect diff --git a/go.sum b/go.sum index f8028ee..9bdf6f9 100644 --- a/go.sum +++ b/go.sum @@ -43,26 +43,6 @@ github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksuf github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II= github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ= github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 h1:ZJJNFaQ86GVKQ9ehwqyAFE6pIfyicpuJ8IkVaPBc6/4= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3/go.mod h1:URuDvhmATVKqHBH9/0nOiNKk0+YcwfQ3WkK5PqHKxc8= -github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+WNGCVv7OmS5+lTc= -github.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M= -github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= -github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= -github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= @@ -258,8 +238,8 @@ github.com/git-pkgs/packageurl-go v0.3.1 h1:WM3RBABQZLaRBxgKyYughc3cVBE8KyQxbSC6 github.com/git-pkgs/packageurl-go v0.3.1/go.mod h1:rcIxiG37BlQLB6FZfgdj9Fm7yjhRQd3l+5o7J0QPAk4= github.com/git-pkgs/purl v0.1.10 h1:NMjeF10nzFn3tdQlz6rbmHB+i+YkyrFQxho3e33ePTQ= github.com/git-pkgs/purl v0.1.10/go.mod h1:C5Vp/kyZ/wGckCLexx4wPVfUxEiToRkdsOPh5Z7ig/I= -github.com/git-pkgs/registries v0.4.1 h1:4qlKVNhC/6x6Bt87t3wrGJtF3EFrUpHQt9/zKsa5IvU= -github.com/git-pkgs/registries v0.4.1/go.mod h1:49UCPFWQmwNV7rBEr9TrTDWKR7vYxFcxp3VfdkeFbdE= +github.com/git-pkgs/registries v0.4.0 h1:GO7fQ8/jot0ulSQHBdxLSNSX/p8eB3gEXWO+98fmoEo= +github.com/git-pkgs/registries v0.4.0/go.mod h1:49UCPFWQmwNV7rBEr9TrTDWKR7vYxFcxp3VfdkeFbdE= github.com/git-pkgs/spdx v0.1.2 h1:wHSK+CqFsO5N7yDTPvxDmer5LgNEa7vAsiZhi5Aci0A= github.com/git-pkgs/spdx v0.1.2/go.mod h1:V98MgZapNgYw54/pdGR82d7RU93qzJoybahbpZqTfw8= github.com/git-pkgs/vers v0.2.4 h1:Zr3jR/Xf1i/6cvBaJKPxhCwjzqz7uvYHE0Fhid/GPBk= @@ -325,8 +305,6 @@ github.com/godoc-lint/godoc-lint v0.11.2 h1:Bp0FkJWoSdNsBikdNgIcgtaoo+xz6I/Y9s5W github.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx/I/cxV+91L41jo= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= @@ -418,8 +396,6 @@ github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0= github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY= -github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= -github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M= github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= @@ -533,8 +509,6 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterbourgon/g2s v0.0.0-20170223122336-d4e7ad98afea h1:sKwxy1H95npauwu8vtF95vG/syrL0p8fSZo/XlDg5gk= github.com/peterbourgon/g2s v0.0.0-20170223122336-d4e7ad98afea/go.mod h1:1VcHEd3ro4QMoHfiNl/j7Jkln9+KQuorp0PItHMJYNg= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -797,7 +771,6 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -878,10 +851,10 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= -modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U= -modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8= -modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU= -modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= +modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= @@ -890,8 +863,8 @@ modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c= -modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ= +modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= +modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -900,8 +873,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= -modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= +modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c= +modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/internal/config/config.go b/internal/config/config.go index 3a47347..f5aebdf 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -51,7 +51,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "net/url" "os" "path/filepath" "strconv" @@ -139,21 +138,6 @@ type StorageConfig struct { // When exceeded, least recently used artifacts are evicted. // Empty or "0" means unlimited. MaxSize string `json:"max_size" yaml:"max_size"` - - // DirectServe enables redirecting cached artifact downloads to presigned - // storage URLs (HTTP 302) instead of streaming bytes through the proxy. - // Only effective for backends that support URL signing (S3, Azure). - DirectServe bool `json:"direct_serve" yaml:"direct_serve"` - - // DirectServeTTL is how long presigned URLs remain valid. - // Uses Go duration syntax (e.g. "5m", "1h"). Default: "15m". - DirectServeTTL string `json:"direct_serve_ttl" yaml:"direct_serve_ttl"` - - // DirectServeBaseURL overrides the scheme and host of presigned URLs - // before returning them to clients. Useful when the proxy reaches - // storage at an internal address (e.g. 127.0.0.1 or a Docker hostname) - // but clients must use a public one. - DirectServeBaseURL string `json:"direct_serve_base_url" yaml:"direct_serve_base_url"` } // DatabaseConfig configures the cache database. @@ -332,15 +316,6 @@ func (c *Config) LoadFromEnv() { if v := os.Getenv("PROXY_STORAGE_MAX_SIZE"); v != "" { c.Storage.MaxSize = v } - if v := os.Getenv("PROXY_STORAGE_DIRECT_SERVE"); v != "" { - c.Storage.DirectServe = envBool(v) - } - if v := os.Getenv("PROXY_STORAGE_DIRECT_SERVE_TTL"); v != "" { - c.Storage.DirectServeTTL = v - } - if v := os.Getenv("PROXY_STORAGE_DIRECT_SERVE_BASE_URL"); v != "" { - c.Storage.DirectServeBaseURL = v - } if v := os.Getenv("PROXY_DATABASE_DRIVER"); v != "" { c.Database.Driver = v } @@ -360,10 +335,10 @@ func (c *Config) LoadFromEnv() { c.Cooldown.Default = v } if v := os.Getenv("PROXY_CACHE_METADATA"); v != "" { - c.CacheMetadata = envBool(v) + c.CacheMetadata = v == "true" || v == "1" } if v := os.Getenv("PROXY_MIRROR_API"); v != "" { - c.MirrorAPI = envBool(v) + c.MirrorAPI = v == "true" || v == "1" } if v := os.Getenv("PROXY_METADATA_TTL"); v != "" { c.MetadataTTL = v @@ -425,21 +400,6 @@ func (c *Config) Validate() error { } } - // Validate direct serve TTL if specified - if c.Storage.DirectServeTTL != "" { - if _, err := time.ParseDuration(c.Storage.DirectServeTTL); err != nil { - return fmt.Errorf("invalid storage.direct_serve_ttl %q: %w", c.Storage.DirectServeTTL, err) - } - } - - // Validate direct serve base URL if specified - if c.Storage.DirectServeBaseURL != "" { - u, err := url.Parse(c.Storage.DirectServeBaseURL) - if err != nil || u.Scheme == "" || u.Host == "" { - return fmt.Errorf("invalid storage.direct_serve_base_url %q: must be an absolute URL", c.Storage.DirectServeBaseURL) - } - } - // Validate metadata TTL if specified if c.MetadataTTL != "" && c.MetadataTTL != "0" { if _, err := time.ParseDuration(c.MetadataTTL); err != nil { @@ -457,23 +417,7 @@ func (c *Config) Validate() error { return nil } -const ( - defaultMetadataTTL = 5 * time.Minute //nolint:mnd // sensible default - defaultDirectServeTTL = 15 * time.Minute //nolint:mnd // sensible default -) - -// ParseMaxSize returns the maximum cache size in bytes. -// Returns 0 if unset or explicitly disabled (meaning unlimited). -func (c *Config) ParseMaxSize() int64 { - if c.Storage.MaxSize == "" || c.Storage.MaxSize == "0" { - return 0 - } - size, err := ParseSize(c.Storage.MaxSize) - if err != nil { - return 0 - } - return size -} +const defaultMetadataTTL = 5 * time.Minute //nolint:mnd // sensible default // ParseMetadataTTL returns the metadata TTL duration. // Returns 5 minutes if unset, 0 if explicitly disabled. @@ -491,19 +435,6 @@ func (c *Config) ParseMetadataTTL() time.Duration { return d } -// ParseDirectServeTTL returns the presigned URL expiry duration. -// Returns 15 minutes if unset. -func (c *Config) ParseDirectServeTTL() time.Duration { - if c.Storage.DirectServeTTL == "" { - return defaultDirectServeTTL - } - d, err := time.ParseDuration(c.Storage.DirectServeTTL) - if err != nil { - return defaultDirectServeTTL - } - return d -} - // ParseSize parses a human-readable size string (e.g., "10GB", "500MB"). // Returns the size in bytes. func ParseSize(s string) (int64, error) { @@ -584,7 +515,3 @@ func (a *AuthConfig) Header() (name, value string) { func expandEnv(s string) string { return os.Expand(s, os.Getenv) } - -func envBool(v string) bool { - return v == "true" || v == "1" -} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index de10191..6e8c3a0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -303,31 +303,6 @@ func TestLoadFileNotFound(t *testing.T) { } } -func TestParseMaxSize(t *testing.T) { - tests := []struct { - name string - maxSize string - want int64 - }{ - {"empty means unlimited", "", 0}, - {"zero means unlimited", "0", 0}, - {"10GB", "10GB", 10 * 1024 * 1024 * 1024}, - {"500MB", "500MB", 500 * 1024 * 1024}, - {"invalid returns 0", "invalid", 0}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cfg := Default() - cfg.Storage.MaxSize = tt.maxSize - got := cfg.ParseMaxSize() - if got != tt.want { - t.Errorf("ParseMaxSize() = %d, want %d", got, tt.want) - } - }) - } -} - func TestParseMetadataTTL(t *testing.T) { tests := []struct { name string @@ -380,77 +355,3 @@ func TestLoadMetadataTTLFromEnv(t *testing.T) { t.Errorf("MetadataTTL = %q, want %q", cfg.MetadataTTL, "10m") } } - -func TestParseDirectServeTTL(t *testing.T) { - tests := []struct { - name string - ttl string - want time.Duration - }{ - {"empty defaults to 15m", "", 15 * time.Minute}, - {"5 minutes", "5m", 5 * time.Minute}, - {"1 hour", "1h", 1 * time.Hour}, - {"invalid defaults to 15m", "not-a-duration", 15 * time.Minute}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cfg := Default() - cfg.Storage.DirectServeTTL = tt.ttl - got := cfg.ParseDirectServeTTL() - if got != tt.want { - t.Errorf("ParseDirectServeTTL() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestValidateDirectServeTTL(t *testing.T) { - cfg := Default() - cfg.Storage.DirectServeTTL = "invalid" - if err := cfg.Validate(); err == nil { - t.Error("expected validation error for invalid storage.direct_serve_ttl") - } - - cfg.Storage.DirectServeTTL = "5m" - if err := cfg.Validate(); err != nil { - t.Errorf("unexpected error for valid storage.direct_serve_ttl: %v", err) - } -} - -func TestLoadDirectServeFromEnv(t *testing.T) { - cfg := Default() - t.Setenv("PROXY_STORAGE_DIRECT_SERVE", "true") - t.Setenv("PROXY_STORAGE_DIRECT_SERVE_TTL", "30m") - t.Setenv("PROXY_STORAGE_DIRECT_SERVE_BASE_URL", "https://cdn.example.com") - cfg.LoadFromEnv() - - if !cfg.Storage.DirectServe { - t.Error("Storage.DirectServe should be true") - } - if cfg.Storage.DirectServeTTL != "30m" { - t.Errorf("Storage.DirectServeTTL = %q, want %q", cfg.Storage.DirectServeTTL, "30m") - } - if cfg.Storage.DirectServeBaseURL != "https://cdn.example.com" { - t.Errorf("Storage.DirectServeBaseURL = %q, want %q", cfg.Storage.DirectServeBaseURL, "https://cdn.example.com") - } -} - -func TestValidateDirectServeBaseURL(t *testing.T) { - cfg := Default() - - cfg.Storage.DirectServeBaseURL = "not a url" - if err := cfg.Validate(); err == nil { - t.Error("expected validation error for relative direct_serve_base_url") - } - - cfg.Storage.DirectServeBaseURL = "://bad" - if err := cfg.Validate(); err == nil { - t.Error("expected validation error for unparseable direct_serve_base_url") - } - - cfg.Storage.DirectServeBaseURL = "https://cdn.example.com" - if err := cfg.Validate(); err != nil { - t.Errorf("unexpected error for valid direct_serve_base_url: %v", err) - } -} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 746b9e8..d7d79c9 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -10,7 +10,6 @@ import ( "io" "log/slog" "net/http" - "net/url" "strconv" "strings" "time" @@ -62,21 +61,15 @@ func ReadMetadata(r io.Reader) ([]byte, error) { // Proxy provides shared functionality for protocol handlers. type Proxy struct { - DB *database.DB - Storage storage.Storage - Fetcher fetch.FetcherInterface - Resolver *fetch.Resolver - Logger *slog.Logger - Cooldown *cooldown.Config - CacheMetadata bool - MetadataTTL time.Duration - DirectServe bool - DirectServeTTL time.Duration - // DirectServeBaseURL, if set, replaces the scheme and host of presigned - // URLs so clients receive a public address even when the proxy reaches - // storage at an internal one. - DirectServeBaseURL string - HTTPClient *http.Client + DB *database.DB + Storage storage.Storage + Fetcher fetch.FetcherInterface + Resolver *fetch.Resolver + Logger *slog.Logger + Cooldown *cooldown.Config + CacheMetadata bool + MetadataTTL time.Duration + HTTPClient *http.Client } // NewProxy creates a new Proxy with the given dependencies. @@ -99,7 +92,6 @@ func NewProxy(db *database.DB, store storage.Storage, fetcher fetch.FetcherInter // CacheResult contains information about a cached or fetched artifact. type CacheResult struct { Reader io.ReadCloser - RedirectURL string Size int64 ContentType string Hash string @@ -146,26 +138,6 @@ func (p *Proxy) checkCache(ctx context.Context, pkgPURL, versionPURL, filename s return nil, nil } - result := &CacheResult{ - Size: artifact.Size.Int64, - ContentType: artifact.ContentType.String, - Hash: artifact.ContentHash.String, - Cached: true, - } - - if p.DirectServe { - signed, err := p.Storage.SignedURL(ctx, artifact.StoragePath.String, p.DirectServeTTL) - if err == nil { - result.RedirectURL = rewriteSignedURLHost(signed, p.DirectServeBaseURL) - p.recordCacheHit(pkgPURL, versionPURL, filename) - return result, nil - } - if !errors.Is(err, storage.ErrSignedURLUnsupported) { - p.Logger.Warn("failed to sign storage URL, falling back to streaming", - "path", artifact.StoragePath.String, "error", err) - } - } - start := time.Now() reader, err := p.Storage.Open(ctx, artifact.StoragePath.String) metrics.RecordStorageOperation("read", time.Since(start)) @@ -176,36 +148,20 @@ func (p *Proxy) checkCache(ctx context.Context, pkgPURL, versionPURL, filename s return nil, nil } - result.Reader = reader - p.recordCacheHit(pkgPURL, versionPURL, filename) - return result, nil -} - -// rewriteSignedURLHost replaces the scheme and host of a signed URL with those -// from baseURL, preserving the path and query (which carry the signature). -// Returns signed unchanged if baseURL is empty or either URL fails to parse. -func rewriteSignedURLHost(signed, baseURL string) string { - if baseURL == "" { - return signed - } - s, err := url.Parse(signed) - if err != nil { - return signed - } - b, err := url.Parse(baseURL) - if err != nil || b.Scheme == "" || b.Host == "" { - return signed - } - s.Scheme = b.Scheme - s.Host = b.Host - return s.String() -} - -func (p *Proxy) recordCacheHit(pkgPURL, versionPURL, filename string) { _ = p.DB.RecordArtifactHit(versionPURL, filename) - if parsed, err := purl.Parse(pkgPURL); err == nil { - metrics.RecordCacheHit(purl.PURLTypeToEcosystem(parsed.Type)) + + // Extract ecosystem from pkgPURL for metrics + if p, err := purl.Parse(pkgPURL); err == nil { + metrics.RecordCacheHit(purl.PURLTypeToEcosystem(p.Type)) } + + return &CacheResult{ + Reader: reader, + Size: artifact.Size.Int64, + ContentType: artifact.ContentType.String, + Hash: artifact.ContentHash.String, + Cached: true, + }, nil } func (p *Proxy) fetchAndCache(ctx context.Context, ecosystem, name, version, filename, pkgPURL, versionPURL string) (*CacheResult, error) { @@ -320,15 +276,6 @@ func (p *Proxy) updateCacheDB(ecosystem, name, filename, pkgPURL, versionPURL, u // ServeArtifact writes a CacheResult to an HTTP response. func ServeArtifact(w http.ResponseWriter, result *CacheResult) { - if result.RedirectURL != "" { - if result.Hash != "" { - w.Header().Set("ETag", fmt.Sprintf(`"%s"`, result.Hash)) - } - w.Header().Set("Location", result.RedirectURL) - w.WriteHeader(http.StatusFound) - return - } - defer func() { _ = result.Reader.Close() }() if result.ContentType != "" { diff --git a/internal/handler/handler_test.go b/internal/handler/handler_test.go index 3a1d2ab..78ed415 100644 --- a/internal/handler/handler_test.go +++ b/internal/handler/handler_test.go @@ -21,11 +21,9 @@ import ( // mockStorage implements storage.Storage for testing. type mockStorage struct { - files map[string][]byte - storeErr error - openErr error - signedURL string - signErr error + files map[string][]byte + storeErr error + openErr error } func newMockStorage() *mockStorage { @@ -81,16 +79,6 @@ func (s *mockStorage) UsedSpace(_ context.Context) (int64, error) { return total, nil } -func (s *mockStorage) SignedURL(_ context.Context, _ string, _ time.Duration) (string, error) { - if s.signErr != nil { - return "", s.signErr - } - if s.signedURL == "" { - return "", storage.ErrSignedURLUnsupported - } - return s.signedURL, nil -} - func (s *mockStorage) URL() string { return "mem://" } func (s *mockStorage) Close() error { return nil } @@ -323,213 +311,6 @@ func TestGetOrFetchArtifactFromURL_CacheMiss_StorageMissing(t *testing.T) { } } -func TestGetOrFetchArtifact_DirectServe_Redirect(t *testing.T) { - proxy, db, store, fetcher := setupTestProxy(t) - seedPackage(t, db, store, "npm", "lodash", "4.17.21", "lodash-4.17.21.tgz", "cached content") - - proxy.DirectServe = true - proxy.DirectServeTTL = 15 * time.Minute - store.signedURL = "https://bucket.s3.amazonaws.com/npm/lodash?X-Amz-Signature=abc" - - result, err := proxy.GetOrFetchArtifact(context.Background(), "npm", "lodash", "4.17.21", "lodash-4.17.21.tgz") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if !result.Cached { - t.Error("expected result to be cached") - } - if result.RedirectURL != store.signedURL { - t.Errorf("RedirectURL = %q, want %q", result.RedirectURL, store.signedURL) - } - if result.Reader != nil { - t.Error("Reader should be nil when redirecting") - } - if fetcher.fetchCalled { - t.Error("fetcher should not be called on cache hit") - } - - // Hit count should still be recorded on the redirect path. - art, _ := db.GetArtifact("pkg:npm/lodash@4.17.21", "lodash-4.17.21.tgz") - if art == nil || art.HitCount != 1 { - t.Errorf("artifact hit count not recorded: %+v", art) - } -} - -func TestGetOrFetchArtifact_DirectServe_BaseURLRewrite(t *testing.T) { - proxy, db, store, _ := setupTestProxy(t) - seedPackage(t, db, store, "npm", "lodash", "4.17.21", "lodash-4.17.21.tgz", "cached content") - - proxy.DirectServe = true - proxy.DirectServeBaseURL = "https://cdn.example.com" - store.signedURL = "http://127.0.0.1:9000/bucket/npm/lodash?X-Amz-Signature=abc&X-Amz-Expires=900" - - result, err := proxy.GetOrFetchArtifact(context.Background(), "npm", "lodash", "4.17.21", "lodash-4.17.21.tgz") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - want := "https://cdn.example.com/bucket/npm/lodash?X-Amz-Signature=abc&X-Amz-Expires=900" - if result.RedirectURL != want { - t.Errorf("RedirectURL = %q, want %q", result.RedirectURL, want) - } -} - -func TestRewriteSignedURLHost(t *testing.T) { - tests := []struct { - name string - signed string - baseURL string - want string - }{ - { - "empty base url is no-op", - "http://127.0.0.1:9000/bucket/key?sig=abc", - "", - "http://127.0.0.1:9000/bucket/key?sig=abc", - }, - { - "replaces scheme and host", - "http://127.0.0.1:9000/bucket/key?sig=abc", - "https://cdn.example.com", - "https://cdn.example.com/bucket/key?sig=abc", - }, - { - "preserves path and query", - "http://minio:9000/bucket/npm/lodash/4.17.21/lodash.tgz?X-Amz-Signature=abc&X-Amz-Date=20260101", - "https://files.example.com", - "https://files.example.com/bucket/npm/lodash/4.17.21/lodash.tgz?X-Amz-Signature=abc&X-Amz-Date=20260101", - }, - { - "ignores base url path", - "http://127.0.0.1:9000/bucket/key?sig=abc", - "https://cdn.example.com/ignored", - "https://cdn.example.com/bucket/key?sig=abc", - }, - { - "invalid base url is no-op", - "http://127.0.0.1:9000/bucket/key?sig=abc", - "://bad", - "http://127.0.0.1:9000/bucket/key?sig=abc", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := rewriteSignedURLHost(tt.signed, tt.baseURL) - if got != tt.want { - t.Errorf("rewriteSignedURLHost(%q, %q) = %q, want %q", tt.signed, tt.baseURL, got, tt.want) - } - }) - } -} - -func TestGetOrFetchArtifact_DirectServe_FallbackOnUnsupported(t *testing.T) { - proxy, db, store, _ := setupTestProxy(t) - seedPackage(t, db, store, "npm", "lodash", "4.17.21", "lodash-4.17.21.tgz", "cached content") - - proxy.DirectServe = true - // store.signedURL is empty so SignedURL returns ErrSignedURLUnsupported. - - result, err := proxy.GetOrFetchArtifact(context.Background(), "npm", "lodash", "4.17.21", "lodash-4.17.21.tgz") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - defer func() { _ = result.Reader.Close() }() - - if result.RedirectURL != "" { - t.Errorf("RedirectURL should be empty, got %q", result.RedirectURL) - } - if result.Reader == nil { - t.Fatal("Reader should be set when signing is unsupported") - } - body, _ := io.ReadAll(result.Reader) - if string(body) != "cached content" { - t.Errorf("got body %q, want %q", body, "cached content") - } -} - -func TestGetOrFetchArtifact_DirectServe_FallbackOnError(t *testing.T) { - proxy, db, store, _ := setupTestProxy(t) - seedPackage(t, db, store, "npm", "lodash", "4.17.21", "lodash-4.17.21.tgz", "cached content") - - proxy.DirectServe = true - store.signErr = errors.New("signing failed") - - result, err := proxy.GetOrFetchArtifact(context.Background(), "npm", "lodash", "4.17.21", "lodash-4.17.21.tgz") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - defer func() { _ = result.Reader.Close() }() - - if result.RedirectURL != "" { - t.Errorf("RedirectURL should be empty on signing error, got %q", result.RedirectURL) - } - if result.Reader == nil { - t.Fatal("Reader should be set when signing fails") - } -} - -func TestGetOrFetchArtifact_DirectServe_DisabledIgnoresSigning(t *testing.T) { - proxy, db, store, _ := setupTestProxy(t) - seedPackage(t, db, store, "npm", "lodash", "4.17.21", "lodash-4.17.21.tgz", "cached content") - - proxy.DirectServe = false - store.signedURL = "https://bucket.example/should-not-be-used" - - result, err := proxy.GetOrFetchArtifact(context.Background(), "npm", "lodash", "4.17.21", "lodash-4.17.21.tgz") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - defer func() { _ = result.Reader.Close() }() - - if result.RedirectURL != "" { - t.Errorf("RedirectURL should be empty when DirectServe is off, got %q", result.RedirectURL) - } -} - -func TestServeArtifact_Redirect(t *testing.T) { - w := httptest.NewRecorder() - ServeArtifact(w, &CacheResult{ - RedirectURL: "https://bucket.s3.amazonaws.com/file?sig=abc", - Hash: "abc123", - Cached: true, - }) - - if w.Code != http.StatusFound { - t.Errorf("status = %d, want %d", w.Code, http.StatusFound) - } - if loc := w.Header().Get("Location"); loc != "https://bucket.s3.amazonaws.com/file?sig=abc" { - t.Errorf("Location = %q", loc) - } - if etag := w.Header().Get("ETag"); etag != `"abc123"` { - t.Errorf("ETag = %q, want %q", etag, `"abc123"`) - } - if cl := w.Header().Get("Content-Length"); cl != "" { - t.Errorf("Content-Length should not be set on redirect, got %q", cl) - } -} - -func TestServeArtifact_Stream(t *testing.T) { - w := httptest.NewRecorder() - ServeArtifact(w, &CacheResult{ - Reader: io.NopCloser(strings.NewReader("payload")), - Size: 7, - ContentType: "application/octet-stream", - Hash: "abc123", - }) - - if w.Code != http.StatusOK { - t.Errorf("status = %d, want %d", w.Code, http.StatusOK) - } - if w.Body.String() != "payload" { - t.Errorf("body = %q, want %q", w.Body.String(), "payload") - } - if ct := w.Header().Get("Content-Type"); ct != "application/octet-stream" { - t.Errorf("Content-Type = %q", ct) - } -} - func TestGetOrFetchArtifactFromURL_CacheHit(t *testing.T) { proxy, db, store, fetcher := setupTestProxy(t) seedPackage(t, db, store, "pypi", "requests", "2.28.0", "requests-2.28.0.tar.gz", "pypi content") diff --git a/internal/server/eviction.go b/internal/server/eviction.go deleted file mode 100644 index 4173bd5..0000000 --- a/internal/server/eviction.go +++ /dev/null @@ -1,105 +0,0 @@ -package server - -import ( - "context" - "log/slog" - "time" - - "github.com/git-pkgs/proxy/internal/database" - "github.com/git-pkgs/proxy/internal/storage" -) - -const ( - evictionInterval = 1 * time.Minute - evictionBatch = 50 -) - -func (s *Server) startEvictionLoop(ctx context.Context) { - maxSize := s.cfg.ParseMaxSize() - if maxSize <= 0 { - return - } - - s.logger.Info("cache eviction enabled", "max_size", s.cfg.Storage.MaxSize) - - ticker := time.NewTicker(evictionInterval) - defer ticker.Stop() - - s.runEviction(ctx, maxSize) - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - s.runEviction(ctx, maxSize) - } - } -} - -func (s *Server) runEviction(ctx context.Context, maxSize int64) { - evictLRU(ctx, s.db, s.storage, s.logger, maxSize) -} - -func evictLRU(ctx context.Context, db *database.DB, store storage.Storage, logger *slog.Logger, maxSize int64) { - totalSize, err := db.GetTotalCacheSize() - if err != nil { - logger.Warn("eviction: failed to get cache size", "error", err) - return - } - - if totalSize <= maxSize { - return - } - - logger.Info("eviction: cache size exceeds limit, evicting", - "current_size", totalSize, "max_size", maxSize) - - evicted := 0 - freedBytes := int64(0) - - for totalSize-freedBytes > maxSize { - artifacts, err := db.GetLeastRecentlyUsedArtifacts(evictionBatch) - if err != nil { - logger.Warn("eviction: failed to get LRU artifacts", "error", err) - return - } - if len(artifacts) == 0 { - break - } - - for _, art := range artifacts { - if totalSize-freedBytes <= maxSize { - break - } - - if !art.StoragePath.Valid { - continue - } - - if err := store.Delete(ctx, art.StoragePath.String); err != nil { - logger.Warn("eviction: failed to delete from storage", - "path", art.StoragePath.String, "error", err) - continue - } - - if err := db.ClearArtifactCache(art.VersionPURL, art.Filename); err != nil { - logger.Warn("eviction: failed to clear artifact record", - "version_purl", art.VersionPURL, "filename", art.Filename, "error", err) - continue - } - - size := int64(0) - if art.Size.Valid { - size = art.Size.Int64 - } - freedBytes += size - evicted++ - } - } - - if evicted > 0 { - logger.Info("eviction: completed", - "evicted", evicted, "freed_bytes", freedBytes) - } -} diff --git a/internal/server/eviction_test.go b/internal/server/eviction_test.go deleted file mode 100644 index 9fa9e6b..0000000 --- a/internal/server/eviction_test.go +++ /dev/null @@ -1,290 +0,0 @@ -package server - -import ( - "context" - "database/sql" - "io" - "log/slog" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/git-pkgs/proxy/internal/config" - "github.com/git-pkgs/proxy/internal/database" - "github.com/git-pkgs/proxy/internal/storage" -) - -func setupEvictionTest(t *testing.T) (*database.DB, *storage.Filesystem) { - t.Helper() - - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test.db") - storagePath := filepath.Join(tempDir, "artifacts") - - db, err := database.Create(dbPath) - if err != nil { - t.Fatalf("failed to create database: %v", err) - } - - store, err := storage.NewFilesystem(storagePath) - if err != nil { - _ = db.Close() - t.Fatalf("failed to create storage: %v", err) - } - - t.Cleanup(func() { - _ = db.Close() - }) - - return db, store -} - -func seedArtifact(t *testing.T, ctx context.Context, db *database.DB, store storage.Storage, name string, dataSize int, accessedAt time.Time) { - t.Helper() - - pkgPURL := "pkg:npm/" + name - versionPURL := pkgPURL + "@1.0.0" - filename := name + "-1.0.0.tgz" - - if err := db.UpsertPackage(&database.Package{ - PURL: pkgPURL, - Ecosystem: "npm", - Name: name, - }); err != nil { - t.Fatalf("failed to upsert package: %v", err) - } - - if err := db.UpsertVersion(&database.Version{ - PURL: versionPURL, - PackagePURL: pkgPURL, - }); err != nil { - t.Fatalf("failed to upsert version: %v", err) - } - - storagePath := storage.ArtifactPath("npm", "", name, "1.0.0", filename) - data := strings.NewReader(strings.Repeat("x", dataSize)) - size, hash, err := store.Store(ctx, storagePath, data) - if err != nil { - t.Fatalf("failed to store artifact: %v", err) - } - - if err := db.UpsertArtifact(&database.Artifact{ - VersionPURL: versionPURL, - Filename: filename, - UpstreamURL: "https://example.com/" + filename, - StoragePath: sql.NullString{String: storagePath, Valid: true}, - ContentHash: sql.NullString{String: hash, Valid: true}, - Size: sql.NullInt64{Int64: size, Valid: true}, - ContentType: sql.NullString{String: "application/gzip", Valid: true}, - FetchedAt: sql.NullTime{Time: time.Now(), Valid: true}, - LastAccessedAt: sql.NullTime{Time: accessedAt, Valid: true}, - }); err != nil { - t.Fatalf("failed to upsert artifact: %v", err) - } -} - -func TestEvictLRU_NoEvictionWhenUnderLimit(t *testing.T) { - db, store := setupEvictionTest(t) - ctx := context.Background() - logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - - seedArtifact(t, ctx, db, store, "pkg-a", 100, time.Now()) - - evictLRU(ctx, db, store, logger, 1024) - - count, err := db.GetCachedArtifactCount() - if err != nil { - t.Fatalf("failed to get count: %v", err) - } - if count != 1 { - t.Errorf("expected 1 cached artifact, got %d", count) - } -} - -func TestEvictLRU_EvictsOldestFirst(t *testing.T) { - db, store := setupEvictionTest(t) - ctx := context.Background() - logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - - now := time.Now() - seedArtifact(t, ctx, db, store, "old-pkg", 500, now.Add(-3*time.Hour)) - seedArtifact(t, ctx, db, store, "mid-pkg", 500, now.Add(-1*time.Hour)) - seedArtifact(t, ctx, db, store, "new-pkg", 500, now) - - // Total is 1500 bytes, limit to 1100 so only the oldest gets evicted - evictLRU(ctx, db, store, logger, 1100) - - // old-pkg should be evicted - art, err := db.GetArtifact("pkg:npm/old-pkg@1.0.0", "old-pkg-1.0.0.tgz") - if err != nil { - t.Fatalf("failed to get artifact: %v", err) - } - if art.StoragePath.Valid { - t.Error("expected old-pkg to be evicted (storage_path should be NULL)") - } - - // mid-pkg and new-pkg should remain - art, err = db.GetArtifact("pkg:npm/mid-pkg@1.0.0", "mid-pkg-1.0.0.tgz") - if err != nil { - t.Fatalf("failed to get artifact: %v", err) - } - if !art.StoragePath.Valid { - t.Error("expected mid-pkg to remain cached") - } - - art, err = db.GetArtifact("pkg:npm/new-pkg@1.0.0", "new-pkg-1.0.0.tgz") - if err != nil { - t.Fatalf("failed to get artifact: %v", err) - } - if !art.StoragePath.Valid { - t.Error("expected new-pkg to remain cached") - } - - // Storage file should be removed for old-pkg - storagePath := storage.ArtifactPath("npm", "", "old-pkg", "1.0.0", "old-pkg-1.0.0.tgz") - exists, _ := store.Exists(ctx, storagePath) - if exists { - t.Error("expected old-pkg file to be deleted from storage") - } -} - -func TestEvictLRU_EvictsMultipleToGetUnderLimit(t *testing.T) { - db, store := setupEvictionTest(t) - ctx := context.Background() - logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - - now := time.Now() - seedArtifact(t, ctx, db, store, "pkg-1", 400, now.Add(-4*time.Hour)) - seedArtifact(t, ctx, db, store, "pkg-2", 400, now.Add(-3*time.Hour)) - seedArtifact(t, ctx, db, store, "pkg-3", 400, now.Add(-2*time.Hour)) - seedArtifact(t, ctx, db, store, "pkg-4", 400, now) - - // Total is 1600 bytes, limit to 900 so pkg-1 and pkg-2 get evicted - evictLRU(ctx, db, store, logger, 900) - - count, err := db.GetCachedArtifactCount() - if err != nil { - t.Fatalf("failed to get count: %v", err) - } - if count != 2 { - t.Errorf("expected 2 cached artifacts remaining, got %d", count) - } - - // Verify the right ones remain - for _, name := range []string{"pkg-3", "pkg-4"} { - art, err := db.GetArtifact("pkg:npm/"+name+"@1.0.0", name+"-1.0.0.tgz") - if err != nil { - t.Fatalf("failed to get artifact %s: %v", name, err) - } - if !art.StoragePath.Valid { - t.Errorf("expected %s to remain cached", name) - } - } -} - -func TestEvictLRU_NothingToEvictWhenEmpty(t *testing.T) { - db, store := setupEvictionTest(t) - ctx := context.Background() - logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - - // Should not panic or error with no artifacts - evictLRU(ctx, db, store, logger, 1024) - - count, err := db.GetCachedArtifactCount() - if err != nil { - t.Fatalf("failed to get count: %v", err) - } - if count != 0 { - t.Errorf("expected 0 cached artifacts, got %d", count) - } -} - -func TestEvictLRU_StorageFileDeleted(t *testing.T) { - db, store := setupEvictionTest(t) - ctx := context.Background() - logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - - seedArtifact(t, ctx, db, store, "delete-me", 1000, time.Now().Add(-1*time.Hour)) - - storagePath := storage.ArtifactPath("npm", "", "delete-me", "1.0.0", "delete-me-1.0.0.tgz") - exists, _ := store.Exists(ctx, storagePath) - if !exists { - t.Fatal("expected artifact file to exist before eviction") - } - - evictLRU(ctx, db, store, logger, 500) - - exists, _ = store.Exists(ctx, storagePath) - if exists { - t.Error("expected artifact file to be deleted after eviction") - } - - art, err := db.GetArtifact("pkg:npm/delete-me@1.0.0", "delete-me-1.0.0.tgz") - if err != nil { - t.Fatalf("failed to get artifact: %v", err) - } - if art.StoragePath.Valid { - t.Error("expected storage_path to be NULL after eviction") - } - if art.Size.Valid { - t.Error("expected size to be NULL after eviction") - } -} - -func TestStartEvictionLoop_UnlimitedSkips(t *testing.T) { - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test.db") - storagePath := filepath.Join(tempDir, "artifacts") - - db, err := database.Create(dbPath) - if err != nil { - t.Fatalf("failed to create database: %v", err) - } - defer func() { _ = db.Close() }() - - store, err := storage.NewFilesystem(storagePath) - if err != nil { - t.Fatalf("failed to create storage: %v", err) - } - - cfg := defaultTestConfig(storagePath, dbPath) - - s := &Server{ - cfg: cfg, - db: db, - storage: store, - logger: slog.New(slog.NewTextHandler(io.Discard, nil)), - } - - ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel() - - // Should return immediately since max_size is empty (unlimited) - done := make(chan struct{}) - go func() { - s.startEvictionLoop(ctx) - close(done) - }() - - select { - case <-done: - // Good, returned immediately - case <-time.After(1 * time.Second): - t.Error("startEvictionLoop should return immediately when max_size is unlimited") - cancel() - } -} - -func defaultTestConfig(storagePath, dbPath string) *config.Config { - return &config.Config{ - Listen: ":8080", - BaseURL: "http://localhost:8080", - Storage: config.StorageConfig{Path: storagePath, MaxSize: ""}, - Database: config.DatabaseConfig{ - Driver: "sqlite", - Path: dbPath, - }, - Log: config.LogConfig{Level: "info", Format: "text"}, - } -} diff --git a/internal/server/server.go b/internal/server/server.go index e3b13af..9196b19 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -148,9 +148,6 @@ func (s *Server) Start() error { proxy.Cooldown = cd proxy.CacheMetadata = s.cfg.CacheMetadata proxy.MetadataTTL = s.cfg.ParseMetadataTTL() - proxy.DirectServe = s.cfg.Storage.DirectServe - proxy.DirectServeTTL = s.cfg.ParseDirectServeTTL() - proxy.DirectServeBaseURL = s.cfg.Storage.DirectServeBaseURL // Create router with Chi r := chi.NewRouter() @@ -272,7 +269,6 @@ func (s *Server) Start() error { "storage", s.storage.URL(), "database", s.cfg.Database.Path) go s.updateCacheStatsMetrics() - go s.startEvictionLoop(bgCtx) return s.http.ListenAndServe() } diff --git a/internal/storage/blob.go b/internal/storage/blob.go index dc41668..2d6af46 100644 --- a/internal/storage/blob.go +++ b/internal/storage/blob.go @@ -6,15 +6,12 @@ import ( "encoding/hex" "fmt" "io" - "net/http" "os" "path/filepath" "runtime" "strings" - "time" "gocloud.dev/blob" - _ "gocloud.dev/blob/azureblob" _ "gocloud.dev/blob/fileblob" _ "gocloud.dev/blob/s3blob" "gocloud.dev/gcerrors" @@ -141,20 +138,6 @@ func (b *Blob) Delete(ctx context.Context, path string) error { return nil } -func (b *Blob) SignedURL(ctx context.Context, path string, expiry time.Duration) (string, error) { - url, err := b.bucket.SignedURL(ctx, path, &blob.SignedURLOptions{ - Method: http.MethodGet, - Expiry: expiry, - }) - if err != nil { - if gcerrors.Code(err) == gcerrors.Unimplemented { - return "", ErrSignedURLUnsupported - } - return "", fmt.Errorf("signing URL: %w", err) - } - return url, nil -} - func (b *Blob) Size(ctx context.Context, path string) (int64, error) { attrs, err := b.bucket.Attributes(ctx, path) if err != nil { diff --git a/internal/storage/blob_test.go b/internal/storage/blob_test.go index d80290b..bb2d089 100644 --- a/internal/storage/blob_test.go +++ b/internal/storage/blob_test.go @@ -10,7 +10,6 @@ import ( "runtime" "strings" "testing" - "time" ) func TestOpenBucket(t *testing.T) { @@ -189,18 +188,6 @@ func TestBlobLargeFile(t *testing.T) { assertLargeFileRoundTrip(t, createTestBlob(t)) } -func TestBlobSignedURLUnsupported(t *testing.T) { - b := createTestBlob(t) - ctx := context.Background() - - // fileblob has no URL signer configured, so this must surface as - // ErrSignedURLUnsupported rather than a generic error. - _, err := b.SignedURL(ctx, "test/file.txt", time.Minute) - if !errors.Is(err, ErrSignedURLUnsupported) { - t.Errorf("SignedURL on fileblob = %v, want ErrSignedURLUnsupported", err) - } -} - func TestBlobOverwrite(t *testing.T) { b := createTestBlob(t) ctx := context.Background() diff --git a/internal/storage/filesystem.go b/internal/storage/filesystem.go index 7a4147a..cf6a1fe 100644 --- a/internal/storage/filesystem.go +++ b/internal/storage/filesystem.go @@ -8,7 +8,6 @@ import ( "io" "os" "path/filepath" - "time" ) // Filesystem implements Storage using the local filesystem. @@ -130,10 +129,6 @@ func (fs *Filesystem) Delete(ctx context.Context, path string) error { return nil } -func (fs *Filesystem) SignedURL(_ context.Context, _ string, _ time.Duration) (string, error) { - return "", ErrSignedURLUnsupported -} - func (fs *Filesystem) Size(ctx context.Context, path string) (int64, error) { fullPath := fs.fullPath(path) diff --git a/internal/storage/filesystem_test.go b/internal/storage/filesystem_test.go index cdd7f6b..7b7828d 100644 --- a/internal/storage/filesystem_test.go +++ b/internal/storage/filesystem_test.go @@ -10,7 +10,6 @@ import ( "path/filepath" "strings" "testing" - "time" ) func TestNewFilesystem(t *testing.T) { @@ -237,15 +236,6 @@ func TestFilesystemLargeFile(t *testing.T) { assertLargeFileRoundTrip(t, createTestFilesystem(t)) } -func TestFilesystemSignedURLUnsupported(t *testing.T) { - fs := createTestFilesystem(t) - - _, err := fs.SignedURL(context.Background(), "test/file.txt", time.Minute) - if !errors.Is(err, ErrSignedURLUnsupported) { - t.Errorf("SignedURL = %v, want ErrSignedURLUnsupported", err) - } -} - func createTestFilesystem(t *testing.T) *Filesystem { t.Helper() dir := t.TempDir() diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 0dba46a..8a9026c 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -15,17 +15,12 @@ import ( "encoding/hex" "errors" "io" - "time" ) const dirPermissions = 0755 var ( ErrNotFound = errors.New("artifact not found") - - // ErrSignedURLUnsupported is returned by SignedURL when the backend - // cannot generate presigned URLs (e.g. local filesystem). - ErrSignedURLUnsupported = errors.New("signed URLs not supported by storage backend") ) // Storage defines the interface for artifact storage backends. @@ -50,10 +45,6 @@ type Storage interface { // Returns ErrNotFound if the path does not exist. Size(ctx context.Context, path string) (int64, error) - // SignedURL returns a presigned URL granting time-limited GET access to path. - // Returns ErrSignedURLUnsupported if the backend cannot generate presigned URLs. - SignedURL(ctx context.Context, path string, expiry time.Duration) (string, error) - // UsedSpace returns the total bytes used by all stored content. UsedSpace(ctx context.Context) (int64, error)