mirror of
https://github.com/go-gitea/gitea.git
synced 2025-12-14 21:15:18 +08:00
Merge 1b7529d511 into 7190519fb3
This commit is contained in:
commit
4db48fa05e
105
assets/go-licenses.json
generated
105
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
31
go.mod
31
go.mod
@ -29,8 +29,13 @@ require (
|
||||
github.com/PuerkitoBio/goquery v1.10.3
|
||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0
|
||||
github.com/alecthomas/chroma/v2 v2.20.0
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.4
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.4
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
|
||||
github.com/aws/aws-sdk-go-v2/service/codecommit v1.32.2
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.1
|
||||
github.com/aws/smithy-go v1.24.0
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||
github.com/blevesearch/bleve/v2 v2.5.3
|
||||
github.com/bohde/codel v0.2.0
|
||||
@ -88,7 +93,6 @@ require (
|
||||
github.com/mholt/archives v0.0.0-20251009205813-e30ac6010726
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/microsoft/go-mssqldb v1.9.3
|
||||
github.com/minio/minio-go/v7 v7.0.95
|
||||
github.com/msteinert/pam v1.2.0
|
||||
github.com/nektos/act v0.2.63
|
||||
github.com/niklasfasching/go-org v1.9.1
|
||||
@ -148,10 +152,19 @@ require (
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect
|
||||
github.com/aws/smithy-go v1.23.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.4 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.0 // indirect
|
||||
@ -200,7 +213,6 @@ require (
|
||||
github.com/go-enry/go-oniguruma v1.2.1 // indirect
|
||||
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-webauthn/x v0.1.24 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
@ -233,8 +245,6 @@ require (
|
||||
github.com/mholt/acmez/v3 v3.1.2 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minlz v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
@ -248,7 +258,6 @@ require (
|
||||
github.com/olekukonko/ll v0.1.0 // indirect
|
||||
github.com/olekukonko/tablewriter v1.0.9 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pjbgf/sha1cd v0.4.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
@ -257,14 +266,12 @@ require (
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/rhysd/actionlint v1.7.7 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/tinylib/msgp v1.4.0 // indirect
|
||||
github.com/unknwon/com v1.0.1 // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
|
||||
63
go.sum
63
go.sum
@ -114,18 +114,46 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.4 h1:gl+DxVuadpkYoaDcWllZqLkhGEbvwyqgNVRTmlaf5PI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.4/go.mod h1:MBUp9Og/bzMmQHjMwace4aJfyvJeadzXjoTcR/SxLV0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.4 h1:KeIZxHVbGWRLhPvhdPbbi/DtFBHNKm6OsVDuiuFefdQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.4/go.mod h1:Smw5n0nCZE9PeFEguofdXyt8kUC4JNrkDTfBOioPhFA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
|
||||
github.com/aws/aws-sdk-go-v2/service/codecommit v1.32.2 h1:qIySgaSYDLcInLpY0e7HPCi+AVeD/LTsl9EL1b692oA=
|
||||
github.com/aws/aws-sdk-go-v2/service/codecommit v1.32.2/go.mod h1:SobWM1535Mn1WuThoIVLiLa/C1rRbxbbq5PZW2QFCIM=
|
||||
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
||||
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.1 h1:5FhzzN6JmlGQF6c04kDIb5KNGm6KnNdLISNrfivIhHg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.1/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.4 h1:YCu/iAhQer8WZ66lldyKkpvMyv+HkPufMa4dyT6wils=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.4/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
|
||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@ -343,8 +371,6 @@ github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6
|
||||
github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
|
||||
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
|
||||
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
|
||||
@ -528,7 +554,6 @@ github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
@ -582,12 +607,6 @@ github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
|
||||
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
|
||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
|
||||
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
|
||||
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
|
||||
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
@ -642,8 +661,6 @@ github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgr
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
|
||||
@ -689,8 +706,6 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
|
||||
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@ -751,8 +766,6 @@ github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKN
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
|
||||
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
|
||||
github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ=
|
||||
github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
|
||||
@ -136,7 +136,7 @@ func NewAzureBlobStorage(ctx context.Context, cfg *setting.Storage) (ObjectStora
|
||||
if err != nil {
|
||||
// Check to see if we already own this container (which happens if you run this twice)
|
||||
if !bloberror.HasCode(err, bloberror.ContainerAlreadyExists) {
|
||||
return nil, convertMinioErr(err)
|
||||
return nil, convertAzureBlobErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,346 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
var (
|
||||
_ ObjectStorage = &MinioStorage{}
|
||||
|
||||
quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
|
||||
)
|
||||
|
||||
type minioObject struct {
|
||||
*minio.Object
|
||||
}
|
||||
|
||||
func (m *minioObject) Stat() (os.FileInfo, error) {
|
||||
oi, err := m.Object.Stat()
|
||||
if err != nil {
|
||||
return nil, convertMinioErr(err)
|
||||
}
|
||||
|
||||
return &minioFileInfo{oi}, nil
|
||||
}
|
||||
|
||||
// MinioStorage returns a minio bucket storage
|
||||
type MinioStorage struct {
|
||||
cfg *setting.MinioStorageConfig
|
||||
ctx context.Context
|
||||
client *minio.Client
|
||||
bucket string
|
||||
basePath string
|
||||
}
|
||||
|
||||
func convertMinioErr(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
errResp, ok := err.(minio.ErrorResponse)
|
||||
if !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert two responses to standard analogues
|
||||
switch errResp.Code {
|
||||
case "NoSuchKey":
|
||||
return os.ErrNotExist
|
||||
case "AccessDenied":
|
||||
return os.ErrPermission
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var getBucketVersioning = func(ctx context.Context, minioClient *minio.Client, bucket string) error {
|
||||
_, err := minioClient.GetBucketVersioning(ctx, bucket)
|
||||
return err
|
||||
}
|
||||
|
||||
// NewMinioStorage returns a minio storage
|
||||
func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage, error) {
|
||||
config := cfg.MinioConfig
|
||||
if config.ChecksumAlgorithm != "" && config.ChecksumAlgorithm != "default" && config.ChecksumAlgorithm != "md5" {
|
||||
return nil, fmt.Errorf("invalid minio checksum algorithm: %s", config.ChecksumAlgorithm)
|
||||
}
|
||||
|
||||
log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath)
|
||||
|
||||
var lookup minio.BucketLookupType
|
||||
switch config.BucketLookUpType {
|
||||
case "auto", "":
|
||||
lookup = minio.BucketLookupAuto
|
||||
case "dns":
|
||||
lookup = minio.BucketLookupDNS
|
||||
case "path":
|
||||
lookup = minio.BucketLookupPath
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid minio bucket lookup type: %s", config.BucketLookUpType)
|
||||
}
|
||||
|
||||
minioClient, err := minio.New(config.Endpoint, &minio.Options{
|
||||
Creds: buildMinioCredentials(config),
|
||||
Secure: config.UseSSL,
|
||||
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}},
|
||||
Region: config.Location,
|
||||
BucketLookup: lookup,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, convertMinioErr(err)
|
||||
}
|
||||
|
||||
// The GetBucketVersioning is only used for checking whether the Object Storage parameters are generally good. It doesn't need to succeed.
|
||||
// The assumption is that if the API returns the HTTP code 400, then the parameters could be incorrect.
|
||||
// Otherwise even if the request itself fails (403, 404, etc), the code should still continue because the parameters seem "good" enough.
|
||||
// Keep in mind that GetBucketVersioning requires "owner" to really succeed, so it can't be used to check the existence.
|
||||
// Not using "BucketExists (HeadBucket)" because it doesn't include detailed failure reasons.
|
||||
err = getBucketVersioning(ctx, minioClient, config.Bucket)
|
||||
if err != nil {
|
||||
errResp, ok := err.(minio.ErrorResponse)
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
if errResp.StatusCode == http.StatusBadRequest {
|
||||
log.Error("S3 storage connection failure at %s:%s with base path %s and region: %s", config.Endpoint, config.Bucket, config.Location, errResp.Message)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check to see if we already own this bucket
|
||||
exists, err := minioClient.BucketExists(ctx, config.Bucket)
|
||||
if err != nil {
|
||||
return nil, convertMinioErr(err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
if err := minioClient.MakeBucket(ctx, config.Bucket, minio.MakeBucketOptions{
|
||||
Region: config.Location,
|
||||
}); err != nil {
|
||||
return nil, convertMinioErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return &MinioStorage{
|
||||
cfg: &config,
|
||||
ctx: ctx,
|
||||
client: minioClient,
|
||||
bucket: config.Bucket,
|
||||
basePath: config.BasePath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MinioStorage) buildMinioPath(p string) string {
|
||||
p = strings.TrimPrefix(util.PathJoinRelX(m.basePath, p), "/") // object store doesn't use slash for root path
|
||||
if p == "." {
|
||||
p = "" // object store doesn't use dot as relative path
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (m *MinioStorage) buildMinioDirPrefix(p string) string {
|
||||
// ending slash is required for avoiding matching like "foo/" and "foobar/" with prefix "foo"
|
||||
p = m.buildMinioPath(p) + "/"
|
||||
if p == "/" {
|
||||
p = "" // object store doesn't use slash for root path
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func buildMinioCredentials(config setting.MinioStorageConfig) *credentials.Credentials {
|
||||
// If static credentials are provided, use those
|
||||
if config.AccessKeyID != "" {
|
||||
return credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, "")
|
||||
}
|
||||
|
||||
// Otherwise, fallback to a credentials chain for S3 access
|
||||
chain := []credentials.Provider{
|
||||
// configure based upon MINIO_ prefixed environment variables
|
||||
&credentials.EnvMinio{},
|
||||
// configure based upon AWS_ prefixed environment variables
|
||||
&credentials.EnvAWS{},
|
||||
// read credentials from MINIO_SHARED_CREDENTIALS_FILE
|
||||
// environment variable, or default json config files
|
||||
&credentials.FileMinioClient{},
|
||||
// read credentials from AWS_SHARED_CREDENTIALS_FILE
|
||||
// environment variable, or default credentials file
|
||||
&credentials.FileAWSCredentials{},
|
||||
// read IAM role from EC2 metadata endpoint if available
|
||||
&credentials.IAM{
|
||||
// passing in an empty Endpoint lets the IAM Provider
|
||||
// decide which endpoint to resolve internally
|
||||
Endpoint: config.IamEndpoint,
|
||||
Client: &http.Client{
|
||||
Transport: http.DefaultTransport,
|
||||
},
|
||||
},
|
||||
}
|
||||
return credentials.NewChainCredentials(chain)
|
||||
}
|
||||
|
||||
// Open opens a file
|
||||
func (m *MinioStorage) Open(path string) (Object, error) {
|
||||
opts := minio.GetObjectOptions{}
|
||||
object, err := m.client.GetObject(m.ctx, m.bucket, m.buildMinioPath(path), opts)
|
||||
if err != nil {
|
||||
return nil, convertMinioErr(err)
|
||||
}
|
||||
return &minioObject{object}, nil
|
||||
}
|
||||
|
||||
// Save saves a file to minio
|
||||
func (m *MinioStorage) Save(path string, r io.Reader, size int64) (int64, error) {
|
||||
uploadInfo, err := m.client.PutObject(
|
||||
m.ctx,
|
||||
m.bucket,
|
||||
m.buildMinioPath(path),
|
||||
r,
|
||||
size,
|
||||
minio.PutObjectOptions{
|
||||
ContentType: "application/octet-stream",
|
||||
// some storages like:
|
||||
// * https://developers.cloudflare.com/r2/api/s3/api/
|
||||
// * https://www.backblaze.com/b2/docs/s3_compatible_api.html
|
||||
// do not support "x-amz-checksum-algorithm" header, so use legacy MD5 checksum
|
||||
SendContentMd5: m.cfg.ChecksumAlgorithm == "md5",
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return 0, convertMinioErr(err)
|
||||
}
|
||||
return uploadInfo.Size, nil
|
||||
}
|
||||
|
||||
type minioFileInfo struct {
|
||||
minio.ObjectInfo
|
||||
}
|
||||
|
||||
func (m minioFileInfo) Name() string {
|
||||
return path.Base(m.ObjectInfo.Key)
|
||||
}
|
||||
|
||||
func (m minioFileInfo) Size() int64 {
|
||||
return m.ObjectInfo.Size
|
||||
}
|
||||
|
||||
func (m minioFileInfo) ModTime() time.Time {
|
||||
return m.LastModified
|
||||
}
|
||||
|
||||
func (m minioFileInfo) IsDir() bool {
|
||||
return strings.HasSuffix(m.ObjectInfo.Key, "/")
|
||||
}
|
||||
|
||||
func (m minioFileInfo) Mode() os.FileMode {
|
||||
return os.ModePerm
|
||||
}
|
||||
|
||||
func (m minioFileInfo) Sys() any {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stat returns the stat information of the object
|
||||
func (m *MinioStorage) Stat(path string) (os.FileInfo, error) {
|
||||
info, err := m.client.StatObject(
|
||||
m.ctx,
|
||||
m.bucket,
|
||||
m.buildMinioPath(path),
|
||||
minio.StatObjectOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, convertMinioErr(err)
|
||||
}
|
||||
return &minioFileInfo{info}, nil
|
||||
}
|
||||
|
||||
// Delete delete a file
|
||||
func (m *MinioStorage) Delete(path string) error {
|
||||
err := m.client.RemoveObject(m.ctx, m.bucket, m.buildMinioPath(path), minio.RemoveObjectOptions{})
|
||||
|
||||
return convertMinioErr(err)
|
||||
}
|
||||
|
||||
// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
|
||||
func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams url.Values) (*url.URL, error) {
|
||||
// copy serveDirectReqParams
|
||||
reqParams, err := url.ParseQuery(serveDirectReqParams.Encode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Here we might not know the real filename, and it's quite inefficient to detect the mine type by pre-fetching the object head.
|
||||
// So we just do a quick detection by extension name, at least if works for the "View Raw File" for an LFS file on the Web UI.
|
||||
// Detect content type by extension name, only support the well-known safe types for inline rendering.
|
||||
// TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future
|
||||
ext := path.Ext(name)
|
||||
inlineExtMimeTypes := map[string]string{
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".avif": "image/avif",
|
||||
// ATTENTION! Don't support unsafe types like HTML/SVG due to security concerns: they can contain JS code, and maybe they need proper Content-Security-Policy
|
||||
// HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context, it seems fine to render it inline
|
||||
".pdf": "application/pdf",
|
||||
|
||||
// TODO: refactor with "modules/public/mime_types.go", for example: "DetectWellKnownSafeInlineMimeType"
|
||||
}
|
||||
if mimeType, ok := inlineExtMimeTypes[ext]; ok {
|
||||
reqParams.Set("response-content-type", mimeType)
|
||||
reqParams.Set("response-content-disposition", "inline")
|
||||
} else {
|
||||
reqParams.Set("response-content-disposition", fmt.Sprintf(`attachment; filename="%s"`, quoteEscaper.Replace(name)))
|
||||
}
|
||||
|
||||
expires := 5 * time.Minute
|
||||
if method == http.MethodHead {
|
||||
u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams)
|
||||
return u, convertMinioErr(err)
|
||||
}
|
||||
u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams)
|
||||
return u, convertMinioErr(err)
|
||||
}
|
||||
|
||||
// IterateObjects iterates across the objects in the miniostorage
|
||||
func (m *MinioStorage) IterateObjects(dirName string, fn func(path string, obj Object) error) error {
|
||||
opts := minio.GetObjectOptions{}
|
||||
for mObjInfo := range m.client.ListObjects(m.ctx, m.bucket, minio.ListObjectsOptions{
|
||||
Prefix: m.buildMinioDirPrefix(dirName),
|
||||
Recursive: true,
|
||||
}) {
|
||||
object, err := m.client.GetObject(m.ctx, m.bucket, mObjInfo.Key, opts)
|
||||
if err != nil {
|
||||
return convertMinioErr(err)
|
||||
}
|
||||
if err := func(object *minio.Object, fn func(path string, obj Object) error) error {
|
||||
defer object.Close()
|
||||
return fn(strings.TrimPrefix(mObjInfo.Key, m.basePath), &minioObject{object})
|
||||
}(object, fn); err != nil {
|
||||
return convertMinioErr(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterStorageType(setting.MinioStorageType, NewMinioStorage)
|
||||
}
|
||||
737
modules/storage/s3.go
Normal file
737
modules/storage/s3.go
Normal file
@ -0,0 +1,737 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
awshttp "github.com/aws/smithy-go/transport/http"
|
||||
)
|
||||
|
||||
var (
|
||||
_ ObjectStorage = &MinioStorage{}
|
||||
|
||||
quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
|
||||
)
|
||||
|
||||
// s3Object wraps the S3 object to implement the Object interface with seeking support
|
||||
type s3Object struct {
|
||||
s3Client *s3.Client
|
||||
ctx context.Context
|
||||
bucket string
|
||||
key string
|
||||
size int64
|
||||
lastMod time.Time
|
||||
offset int64
|
||||
body io.ReadCloser
|
||||
}
|
||||
|
||||
func (o *s3Object) Read(p []byte) (n int, err error) {
|
||||
if o.offset >= o.size {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
// If we don't have a body or need to re-fetch (after seek), get one
|
||||
if o.body == nil {
|
||||
rangeHeader := fmt.Sprintf("bytes=%d-", o.offset)
|
||||
resp, err := o.s3Client.GetObject(o.ctx, &s3.GetObjectInput{
|
||||
Bucket: aws.String(o.bucket),
|
||||
Key: aws.String(o.key),
|
||||
Range: aws.String(rangeHeader),
|
||||
})
|
||||
if err != nil {
|
||||
return 0, convertS3Err(err)
|
||||
}
|
||||
o.body = resp.Body
|
||||
}
|
||||
|
||||
n, err = o.body.Read(p)
|
||||
o.offset += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (o *s3Object) Seek(offset int64, whence int) (int64, error) {
|
||||
var newOffset int64
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
newOffset = offset
|
||||
case io.SeekCurrent:
|
||||
newOffset = o.offset + offset
|
||||
case io.SeekEnd:
|
||||
newOffset = o.size + offset
|
||||
default:
|
||||
return 0, errors.New("Seek: invalid whence")
|
||||
}
|
||||
|
||||
if newOffset < 0 {
|
||||
return 0, errors.New("Seek: invalid offset")
|
||||
}
|
||||
if newOffset > o.size {
|
||||
return 0, errors.New("Seek: invalid offset")
|
||||
}
|
||||
|
||||
// If seeking to a different position, close current body so Read will re-fetch
|
||||
if newOffset != o.offset && o.body != nil {
|
||||
o.body.Close()
|
||||
o.body = nil
|
||||
}
|
||||
o.offset = newOffset
|
||||
return o.offset, nil
|
||||
}
|
||||
|
||||
func (o *s3Object) Close() error {
|
||||
if o.body != nil {
|
||||
return o.body.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *s3Object) Stat() (os.FileInfo, error) {
|
||||
return &s3FileInfo{
|
||||
key: o.key,
|
||||
size: o.size,
|
||||
lastMod: o.lastMod,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MinioStorage returns a minio bucket storage
|
||||
type MinioStorage struct {
|
||||
cfg *setting.MinioStorageConfig
|
||||
ctx context.Context
|
||||
client *s3.Client
|
||||
bucket string
|
||||
basePath string
|
||||
}
|
||||
|
||||
func convertS3Err(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for specific S3 error types
|
||||
var notFound *types.NotFound
|
||||
if errors.As(err, ¬Found) {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
var noSuchKey *types.NoSuchKey
|
||||
if errors.As(err, &noSuchKey) {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
// Check HTTP response errors
|
||||
var respErr *awshttp.ResponseError
|
||||
if errors.As(err, &respErr) {
|
||||
switch respErr.HTTPStatusCode() {
|
||||
case http.StatusNotFound:
|
||||
return os.ErrNotExist
|
||||
case http.StatusForbidden:
|
||||
return os.ErrPermission
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var getBucketVersioning = func(ctx context.Context, client *s3.Client, bucket string) error {
|
||||
_, err := client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{
|
||||
Bucket: aws.String(bucket),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// NewMinioStorage returns a minio storage
|
||||
func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage, error) {
|
||||
config := cfg.MinioConfig
|
||||
if config.ChecksumAlgorithm != "" && config.ChecksumAlgorithm != "default" && config.ChecksumAlgorithm != "md5" {
|
||||
return nil, fmt.Errorf("invalid minio checksum algorithm: %s", config.ChecksumAlgorithm)
|
||||
}
|
||||
|
||||
log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath)
|
||||
|
||||
// Build the endpoint URL
|
||||
var endpointURL string
|
||||
if config.UseSSL {
|
||||
endpointURL = "https://" + config.Endpoint
|
||||
} else {
|
||||
endpointURL = "http://" + config.Endpoint
|
||||
}
|
||||
|
||||
// Build custom HTTP client with TLS settings and timeout
|
||||
httpClient := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify},
|
||||
},
|
||||
}
|
||||
|
||||
// Build credentials provider chain
|
||||
credProvider := buildS3CredentialsProvider(config)
|
||||
|
||||
// Build AWS config directly without LoadDefaultConfig to avoid
|
||||
// background network calls (e.g., EC2 metadata discovery)
|
||||
awsCfg := aws.Config{
|
||||
Region: config.Location,
|
||||
Credentials: credProvider,
|
||||
HTTPClient: httpClient,
|
||||
}
|
||||
|
||||
// Determine path style based on bucket lookup type
|
||||
usePathStyle := false
|
||||
switch config.BucketLookUpType {
|
||||
case "auto", "":
|
||||
// For Minio compatibility, default to path style
|
||||
usePathStyle = true
|
||||
case "dns":
|
||||
usePathStyle = false
|
||||
case "path":
|
||||
usePathStyle = true
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid minio bucket lookup type: %s", config.BucketLookUpType)
|
||||
}
|
||||
|
||||
// Create S3 client
|
||||
s3Client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
|
||||
o.BaseEndpoint = aws.String(endpointURL)
|
||||
o.UsePathStyle = usePathStyle
|
||||
})
|
||||
|
||||
// The GetBucketVersioning is only used for checking whether the Object Storage parameters are generally good.
|
||||
// It doesn't need to succeed. The assumption is that if the API returns the HTTP code 400, then the parameters
|
||||
// could be incorrect. Otherwise even if the request itself fails (403, 404, etc), the code should still continue
|
||||
// because the parameters seem "good" enough.
|
||||
err := getBucketVersioning(ctx, s3Client, config.Bucket)
|
||||
if err != nil {
|
||||
var respErr *awshttp.ResponseError
|
||||
if errors.As(err, &respErr) && respErr.HTTPStatusCode() == http.StatusBadRequest {
|
||||
log.Error("S3 storage connection failure at %s:%s with base path %s: %v", config.Endpoint, config.Bucket, config.Location, err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check to see if we already own this bucket
|
||||
_, err = s3Client.HeadBucket(ctx, &s3.HeadBucketInput{
|
||||
Bucket: aws.String(config.Bucket),
|
||||
})
|
||||
if err != nil {
|
||||
var notFound *types.NotFound
|
||||
var noSuchBucket *types.NoSuchBucket
|
||||
if errors.As(err, ¬Found) || errors.As(err, &noSuchBucket) {
|
||||
// Bucket doesn't exist, create it
|
||||
createInput := &s3.CreateBucketInput{
|
||||
Bucket: aws.String(config.Bucket),
|
||||
}
|
||||
// Only set LocationConstraint if not us-east-1 (AWS S3 requirement)
|
||||
if config.Location != "" && config.Location != "us-east-1" {
|
||||
createInput.CreateBucketConfiguration = &types.CreateBucketConfiguration{
|
||||
LocationConstraint: types.BucketLocationConstraint(config.Location),
|
||||
}
|
||||
}
|
||||
_, err = s3Client.CreateBucket(ctx, createInput)
|
||||
if err != nil {
|
||||
return nil, convertS3Err(err)
|
||||
}
|
||||
} else {
|
||||
return nil, convertS3Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
return &MinioStorage{
|
||||
cfg: &config,
|
||||
ctx: ctx,
|
||||
client: s3Client,
|
||||
bucket: config.Bucket,
|
||||
basePath: config.BasePath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MinioStorage) buildMinioPath(p string) string {
|
||||
p = strings.TrimPrefix(util.PathJoinRelX(m.basePath, p), "/") // object store doesn't use slash for root path
|
||||
if p == "." {
|
||||
p = "" // object store doesn't use dot as relative path
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (m *MinioStorage) buildMinioDirPrefix(p string) string {
|
||||
// ending slash is required for avoiding matching like "foo/" and "foobar/" with prefix "foo"
|
||||
p = m.buildMinioPath(p) + "/"
|
||||
if p == "/" {
|
||||
p = "" // object store doesn't use slash for root path
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// envCredentialsProvider checks a pair of environment variables for credentials.
|
||||
// This is a generic provider that can be configured for different env var names.
|
||||
type envCredentialsProvider struct {
|
||||
accessKeyEnv string
|
||||
secretKeyEnv string
|
||||
source string
|
||||
}
|
||||
|
||||
func (p envCredentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) {
|
||||
accessKey := os.Getenv(p.accessKeyEnv)
|
||||
secretKey := os.Getenv(p.secretKeyEnv)
|
||||
if accessKey == "" || secretKey == "" {
|
||||
return aws.Credentials{}, fmt.Errorf("%s or %s not set", p.accessKeyEnv, p.secretKeyEnv)
|
||||
}
|
||||
return aws.Credentials{
|
||||
AccessKeyID: accessKey,
|
||||
SecretAccessKey: secretKey,
|
||||
Source: p.source,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// minioFileCredentialsProvider reads credentials from MINIO_SHARED_CREDENTIALS_FILE.
|
||||
// This uses Minio's JSON config format (not AWS INI format), so we need a custom parser.
|
||||
type minioFileCredentialsProvider struct{}
|
||||
|
||||
type minioConfigFile struct {
|
||||
Version string `json:"version"`
|
||||
Aliases map[string]minioAliasConf `json:"aliases"`
|
||||
}
|
||||
|
||||
type minioAliasConf struct {
|
||||
URL string `json:"url"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
API string `json:"api"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func (p minioFileCredentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) {
|
||||
filePath := os.Getenv("MINIO_SHARED_CREDENTIALS_FILE")
|
||||
if filePath == "" {
|
||||
return aws.Credentials{}, errors.New("MINIO_SHARED_CREDENTIALS_FILE not set")
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return aws.Credentials{}, fmt.Errorf("failed to read minio credentials file: %w", err)
|
||||
}
|
||||
|
||||
var config minioConfigFile
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return aws.Credentials{}, fmt.Errorf("failed to parse minio credentials file: %w", err)
|
||||
}
|
||||
|
||||
// Try to find s3 alias first, then use the first available alias
|
||||
var alias minioAliasConf
|
||||
if s3Alias, ok := config.Aliases["s3"]; ok {
|
||||
alias = s3Alias
|
||||
} else {
|
||||
for _, a := range config.Aliases {
|
||||
alias = a
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if alias.AccessKey == "" || alias.SecretKey == "" {
|
||||
return aws.Credentials{}, errors.New("no valid credentials found in minio credentials file")
|
||||
}
|
||||
|
||||
return aws.Credentials{
|
||||
AccessKeyID: alias.AccessKey,
|
||||
SecretAccessKey: alias.SecretKey,
|
||||
Source: "MinioFileCredentials",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// awsFileCredentialsProvider reads credentials from AWS_SHARED_CREDENTIALS_FILE or the default
|
||||
// ~/.aws/credentials file using the AWS SDK's built-in INI parser.
|
||||
type awsFileCredentialsProvider struct{}
|
||||
|
||||
func (p awsFileCredentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) {
|
||||
// Check if AWS_SHARED_CREDENTIALS_FILE is set (matching original Minio SDK behavior)
|
||||
if os.Getenv("AWS_SHARED_CREDENTIALS_FILE") == "" {
|
||||
return aws.Credentials{}, errors.New("AWS_SHARED_CREDENTIALS_FILE not set")
|
||||
}
|
||||
|
||||
// Use SDK's built-in shared credentials loading with a timeout
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cfg, err := awsconfig.LoadDefaultConfig(timeoutCtx,
|
||||
// Disable EC2 IMDS so we only load from the credentials file
|
||||
awsconfig.WithEC2IMDSClientEnableState(imds.ClientDisabled),
|
||||
)
|
||||
if err != nil {
|
||||
return aws.Credentials{}, fmt.Errorf("failed to load AWS config: %w", err)
|
||||
}
|
||||
|
||||
creds, err := cfg.Credentials.Retrieve(timeoutCtx)
|
||||
if err != nil {
|
||||
return aws.Credentials{}, fmt.Errorf("failed to retrieve AWS credentials: %w", err)
|
||||
}
|
||||
|
||||
if creds.AccessKeyID == "" || creds.SecretAccessKey == "" {
|
||||
return aws.Credentials{}, errors.New("no valid credentials found in AWS credentials file")
|
||||
}
|
||||
|
||||
creds.Source = "AWSFileCredentials"
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
// iamCredentialsProvider wraps EC2 role credentials from the AWS SDK.
|
||||
// A thin wrapper is needed to support custom IAM endpoints (MINIO_IAM_ENDPOINT).
|
||||
type iamCredentialsProvider struct {
|
||||
endpoint string
|
||||
}
|
||||
|
||||
func (p iamCredentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) {
|
||||
// Use a short timeout for IMDS - it should respond quickly if available,
|
||||
// and we don't want to hang if not running on EC2/ECS
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var provider *ec2rolecreds.Provider
|
||||
if p.endpoint != "" {
|
||||
// Create IMDS client with custom endpoint
|
||||
imdsClient := imds.New(imds.Options{
|
||||
Endpoint: p.endpoint,
|
||||
})
|
||||
provider = ec2rolecreds.New(func(o *ec2rolecreds.Options) {
|
||||
o.Client = imdsClient
|
||||
})
|
||||
} else {
|
||||
provider = ec2rolecreds.New()
|
||||
}
|
||||
return provider.Retrieve(timeoutCtx)
|
||||
}
|
||||
|
||||
// credentialChainProvider tries multiple providers in order until one succeeds.
|
||||
// AWS SDK v2 doesn't expose a public chain provider, so we implement our own.
|
||||
type credentialChainProvider struct {
|
||||
providers []aws.CredentialsProvider
|
||||
}
|
||||
|
||||
func (c credentialChainProvider) Retrieve(ctx context.Context) (aws.Credentials, error) {
|
||||
var lastErr error
|
||||
for _, provider := range c.providers {
|
||||
creds, err := provider.Retrieve(ctx)
|
||||
if err == nil {
|
||||
return creds, nil
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
if lastErr != nil {
|
||||
return aws.Credentials{}, fmt.Errorf("all credential providers failed: %w", lastErr)
|
||||
}
|
||||
return aws.Credentials{}, errors.New("no credential providers configured")
|
||||
}
|
||||
|
||||
func buildS3CredentialsProvider(config setting.MinioStorageConfig) aws.CredentialsProvider {
|
||||
// If static credentials are provided, use those
|
||||
if config.AccessKeyID != "" {
|
||||
return credentials.NewStaticCredentialsProvider(config.AccessKeyID, config.SecretAccessKey, "")
|
||||
}
|
||||
|
||||
// Otherwise, build a chain of credential providers.
|
||||
// The chain tries each provider in order until one succeeds.
|
||||
chain := credentialChainProvider{
|
||||
providers: []aws.CredentialsProvider{
|
||||
// Check MINIO_ACCESS_KEY/MINIO_SECRET_KEY (Minio-specific env vars)
|
||||
envCredentialsProvider{
|
||||
accessKeyEnv: "MINIO_ACCESS_KEY",
|
||||
secretKeyEnv: "MINIO_SECRET_KEY",
|
||||
source: "MinioEnvCredentials",
|
||||
},
|
||||
// Check AWS_ACCESS_KEY/AWS_SECRET_KEY (Minio SDK style, without _ID suffix)
|
||||
envCredentialsProvider{
|
||||
accessKeyEnv: "AWS_ACCESS_KEY",
|
||||
secretKeyEnv: "AWS_SECRET_KEY",
|
||||
source: "AWSEnvCredentials",
|
||||
},
|
||||
// Check AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (standard AWS style)
|
||||
envCredentialsProvider{
|
||||
accessKeyEnv: "AWS_ACCESS_KEY_ID",
|
||||
secretKeyEnv: "AWS_SECRET_ACCESS_KEY",
|
||||
source: "AWSEnvCredentials",
|
||||
},
|
||||
// Read credentials from MINIO_SHARED_CREDENTIALS_FILE (JSON format)
|
||||
minioFileCredentialsProvider{},
|
||||
// Read credentials from AWS_SHARED_CREDENTIALS_FILE (INI format)
|
||||
awsFileCredentialsProvider{},
|
||||
// Read IAM role from EC2 metadata endpoint if available
|
||||
iamCredentialsProvider{endpoint: config.IamEndpoint},
|
||||
},
|
||||
}
|
||||
|
||||
return chain
|
||||
}
|
||||
|
||||
// Open opens a file
|
||||
func (m *MinioStorage) Open(path string) (Object, error) {
|
||||
key := m.buildMinioPath(path)
|
||||
|
||||
// First get object metadata to know the size
|
||||
headResp, err := m.client.HeadObject(m.ctx, &s3.HeadObjectInput{
|
||||
Bucket: aws.String(m.bucket),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, convertS3Err(err)
|
||||
}
|
||||
|
||||
var size int64
|
||||
if headResp.ContentLength != nil {
|
||||
size = *headResp.ContentLength
|
||||
}
|
||||
var lastMod time.Time
|
||||
if headResp.LastModified != nil {
|
||||
lastMod = *headResp.LastModified
|
||||
}
|
||||
|
||||
return &s3Object{
|
||||
s3Client: m.client,
|
||||
ctx: m.ctx,
|
||||
bucket: m.bucket,
|
||||
key: key,
|
||||
size: size,
|
||||
lastMod: lastMod,
|
||||
offset: 0,
|
||||
body: nil, // Will be fetched on first Read
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Save saves a file to minio
|
||||
func (m *MinioStorage) Save(path string, r io.Reader, size int64) (int64, error) {
|
||||
key := m.buildMinioPath(path)
|
||||
|
||||
// AWS SDK v2 requires either a seekable reader or we must buffer the content
|
||||
// to properly send Content-Length header
|
||||
var body io.ReadSeeker
|
||||
switch v := r.(type) {
|
||||
case io.ReadSeeker:
|
||||
body = v
|
||||
default:
|
||||
// Buffer the content - required for proper Content-Length handling
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to read content: %w", err)
|
||||
}
|
||||
if size < 0 {
|
||||
size = int64(len(data))
|
||||
}
|
||||
body = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
input := &s3.PutObjectInput{
|
||||
Bucket: aws.String(m.bucket),
|
||||
Key: aws.String(key),
|
||||
Body: body,
|
||||
ContentLength: aws.Int64(size),
|
||||
ContentType: aws.String("application/octet-stream"),
|
||||
}
|
||||
|
||||
_, err := m.client.PutObject(m.ctx, input)
|
||||
if err != nil {
|
||||
return 0, convertS3Err(err)
|
||||
}
|
||||
return size, nil
|
||||
}
|
||||
|
||||
type s3FileInfo struct {
|
||||
key string
|
||||
size int64
|
||||
lastMod time.Time
|
||||
}
|
||||
|
||||
func (m s3FileInfo) Name() string {
|
||||
return path.Base(m.key)
|
||||
}
|
||||
|
||||
func (m s3FileInfo) Size() int64 {
|
||||
return m.size
|
||||
}
|
||||
|
||||
func (m s3FileInfo) ModTime() time.Time {
|
||||
return m.lastMod
|
||||
}
|
||||
|
||||
func (m s3FileInfo) IsDir() bool {
|
||||
return strings.HasSuffix(m.key, "/")
|
||||
}
|
||||
|
||||
func (m s3FileInfo) Mode() os.FileMode {
|
||||
return os.ModePerm
|
||||
}
|
||||
|
||||
func (m s3FileInfo) Sys() any {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stat returns the stat information of the object
|
||||
func (m *MinioStorage) Stat(path string) (os.FileInfo, error) {
|
||||
key := m.buildMinioPath(path)
|
||||
resp, err := m.client.HeadObject(m.ctx, &s3.HeadObjectInput{
|
||||
Bucket: aws.String(m.bucket),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, convertS3Err(err)
|
||||
}
|
||||
|
||||
var size int64
|
||||
if resp.ContentLength != nil {
|
||||
size = *resp.ContentLength
|
||||
}
|
||||
var lastMod time.Time
|
||||
if resp.LastModified != nil {
|
||||
lastMod = *resp.LastModified
|
||||
}
|
||||
|
||||
return &s3FileInfo{
|
||||
key: key,
|
||||
size: size,
|
||||
lastMod: lastMod,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Delete delete a file
|
||||
func (m *MinioStorage) Delete(path string) error {
|
||||
_, err := m.client.DeleteObject(m.ctx, &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(m.bucket),
|
||||
Key: aws.String(m.buildMinioPath(path)),
|
||||
})
|
||||
return convertS3Err(err)
|
||||
}
|
||||
|
||||
// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
|
||||
func (m *MinioStorage) URL(storePath, name, method string, _ url.Values) (*url.URL, error) {
|
||||
// Here we might not know the real filename, and it's quite inefficient to detect the mime type by pre-fetching the object head.
|
||||
// So we just do a quick detection by extension name, at least if works for the "View Raw File" for an LFS file on the Web UI.
|
||||
// Detect content type by extension name, only support the well-known safe types for inline rendering.
|
||||
// TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future
|
||||
ext := path.Ext(name)
|
||||
inlineExtMimeTypes := map[string]string{
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".avif": "image/avif",
|
||||
// ATTENTION! Don't support unsafe types like HTML/SVG due to security concerns: they can contain JS code, and maybe they need proper Content-Security-Policy
|
||||
// HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context, it seems fine to render it inline
|
||||
".pdf": "application/pdf",
|
||||
|
||||
// TODO: refactor with "modules/public/mime_types.go", for example: "DetectWellKnownSafeInlineMimeType"
|
||||
}
|
||||
|
||||
var contentType, contentDisposition string
|
||||
if mimeType, ok := inlineExtMimeTypes[ext]; ok {
|
||||
contentType = mimeType
|
||||
contentDisposition = "inline"
|
||||
} else {
|
||||
contentDisposition = fmt.Sprintf(`attachment; filename="%s"`, quoteEscaper.Replace(name))
|
||||
}
|
||||
|
||||
expires := 5 * time.Minute
|
||||
key := m.buildMinioPath(storePath)
|
||||
presignClient := s3.NewPresignClient(m.client)
|
||||
|
||||
if method == http.MethodHead {
|
||||
presignReq, err := presignClient.PresignHeadObject(m.ctx, &s3.HeadObjectInput{
|
||||
Bucket: aws.String(m.bucket),
|
||||
Key: aws.String(key),
|
||||
ResponseContentDisposition: aws.String(contentDisposition),
|
||||
ResponseContentType: aws.String(contentType),
|
||||
}, s3.WithPresignExpires(expires))
|
||||
if err != nil {
|
||||
return nil, convertS3Err(err)
|
||||
}
|
||||
return url.Parse(presignReq.URL)
|
||||
}
|
||||
|
||||
presignReq, err := presignClient.PresignGetObject(m.ctx, &s3.GetObjectInput{
|
||||
Bucket: aws.String(m.bucket),
|
||||
Key: aws.String(key),
|
||||
ResponseContentDisposition: aws.String(contentDisposition),
|
||||
ResponseContentType: aws.String(contentType),
|
||||
}, s3.WithPresignExpires(expires))
|
||||
if err != nil {
|
||||
return nil, convertS3Err(err)
|
||||
}
|
||||
return url.Parse(presignReq.URL)
|
||||
}
|
||||
|
||||
// IterateObjects iterates across the objects in the miniostorage
|
||||
func (m *MinioStorage) IterateObjects(dirName string, fn func(path string, obj Object) error) error {
|
||||
prefix := m.buildMinioDirPrefix(dirName)
|
||||
|
||||
paginator := s3.NewListObjectsV2Paginator(m.client, &s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(m.bucket),
|
||||
Prefix: aws.String(prefix),
|
||||
})
|
||||
|
||||
for paginator.HasMorePages() {
|
||||
page, err := paginator.NextPage(m.ctx)
|
||||
if err != nil {
|
||||
return convertS3Err(err)
|
||||
}
|
||||
|
||||
for _, obj := range page.Contents {
|
||||
if obj.Key == nil {
|
||||
continue
|
||||
}
|
||||
key := *obj.Key
|
||||
|
||||
var size int64
|
||||
if obj.Size != nil {
|
||||
size = *obj.Size
|
||||
}
|
||||
var lastMod time.Time
|
||||
if obj.LastModified != nil {
|
||||
lastMod = *obj.LastModified
|
||||
}
|
||||
|
||||
s3Obj := &s3Object{
|
||||
s3Client: m.client,
|
||||
ctx: m.ctx,
|
||||
bucket: m.bucket,
|
||||
key: key,
|
||||
size: size,
|
||||
lastMod: lastMod,
|
||||
offset: 0,
|
||||
body: nil,
|
||||
}
|
||||
|
||||
if err := func() error {
|
||||
defer s3Obj.Close()
|
||||
return fn(strings.TrimPrefix(key, m.basePath), s3Obj)
|
||||
}(); err != nil {
|
||||
return convertS3Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterStorageType(setting.MinioStorageType, NewMinioStorage)
|
||||
}
|
||||
@ -5,6 +5,7 @@ package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@ -12,7 +13,8 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
awshttp "github.com/aws/smithy-go/transport/http"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -83,11 +85,14 @@ func TestS3StorageBadRequest(t *testing.T) {
|
||||
message := "ERROR"
|
||||
old := getBucketVersioning
|
||||
defer func() { getBucketVersioning = old }()
|
||||
getBucketVersioning = func(ctx context.Context, minioClient *minio.Client, bucket string) error {
|
||||
return minio.ErrorResponse{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Code: "FixtureError",
|
||||
Message: message,
|
||||
getBucketVersioning = func(ctx context.Context, client *s3.Client, bucket string) error {
|
||||
return &awshttp.ResponseError{
|
||||
Response: &awshttp.Response{
|
||||
Response: &http.Response{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
},
|
||||
},
|
||||
Err: errors.New(message),
|
||||
}
|
||||
}
|
||||
_, err := NewStorage(setting.MinioStorageType, cfg)
|
||||
@ -109,12 +114,12 @@ func TestMinioCredentials(t *testing.T) {
|
||||
SecretAccessKey: ExpectedSecretAccessKey,
|
||||
IamEndpoint: FakeEndpoint,
|
||||
}
|
||||
creds := buildMinioCredentials(cfg)
|
||||
v, err := creds.Get()
|
||||
credProvider := buildS3CredentialsProvider(cfg)
|
||||
creds, err := credProvider.Retrieve(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ExpectedAccessKey, v.AccessKeyID)
|
||||
assert.Equal(t, ExpectedSecretAccessKey, v.SecretAccessKey)
|
||||
assert.Equal(t, ExpectedAccessKey, creds.AccessKeyID)
|
||||
assert.Equal(t, ExpectedSecretAccessKey, creds.SecretAccessKey)
|
||||
})
|
||||
|
||||
t.Run("Chain", func(t *testing.T) {
|
||||
@ -126,24 +131,24 @@ func TestMinioCredentials(t *testing.T) {
|
||||
t.Setenv("MINIO_ACCESS_KEY", ExpectedAccessKey+"Minio")
|
||||
t.Setenv("MINIO_SECRET_KEY", ExpectedSecretAccessKey+"Minio")
|
||||
|
||||
creds := buildMinioCredentials(cfg)
|
||||
v, err := creds.Get()
|
||||
credProvider := buildS3CredentialsProvider(cfg)
|
||||
creds, err := credProvider.Retrieve(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ExpectedAccessKey+"Minio", v.AccessKeyID)
|
||||
assert.Equal(t, ExpectedSecretAccessKey+"Minio", v.SecretAccessKey)
|
||||
assert.Equal(t, ExpectedAccessKey+"Minio", creds.AccessKeyID)
|
||||
assert.Equal(t, ExpectedSecretAccessKey+"Minio", creds.SecretAccessKey)
|
||||
})
|
||||
|
||||
t.Run("EnvAWS", func(t *testing.T) {
|
||||
t.Setenv("AWS_ACCESS_KEY", ExpectedAccessKey+"AWS")
|
||||
t.Setenv("AWS_SECRET_KEY", ExpectedSecretAccessKey+"AWS")
|
||||
|
||||
creds := buildMinioCredentials(cfg)
|
||||
v, err := creds.Get()
|
||||
credProvider := buildS3CredentialsProvider(cfg)
|
||||
creds, err := credProvider.Retrieve(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ExpectedAccessKey+"AWS", v.AccessKeyID)
|
||||
assert.Equal(t, ExpectedSecretAccessKey+"AWS", v.SecretAccessKey)
|
||||
assert.Equal(t, ExpectedAccessKey+"AWS", creds.AccessKeyID)
|
||||
assert.Equal(t, ExpectedSecretAccessKey+"AWS", creds.SecretAccessKey)
|
||||
})
|
||||
|
||||
t.Run("FileMinio", func(t *testing.T) {
|
||||
@ -151,12 +156,12 @@ func TestMinioCredentials(t *testing.T) {
|
||||
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/minio.json")
|
||||
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/fake")
|
||||
|
||||
creds := buildMinioCredentials(cfg)
|
||||
v, err := creds.Get()
|
||||
credProvider := buildS3CredentialsProvider(cfg)
|
||||
creds, err := credProvider.Retrieve(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ExpectedAccessKey+"MinioFile", v.AccessKeyID)
|
||||
assert.Equal(t, ExpectedSecretAccessKey+"MinioFile", v.SecretAccessKey)
|
||||
assert.Equal(t, ExpectedAccessKey+"MinioFile", creds.AccessKeyID)
|
||||
assert.Equal(t, ExpectedSecretAccessKey+"MinioFile", creds.SecretAccessKey)
|
||||
})
|
||||
|
||||
t.Run("FileAWS", func(t *testing.T) {
|
||||
@ -164,12 +169,12 @@ func TestMinioCredentials(t *testing.T) {
|
||||
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/fake.json")
|
||||
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/aws_credentials")
|
||||
|
||||
creds := buildMinioCredentials(cfg)
|
||||
v, err := creds.Get()
|
||||
credProvider := buildS3CredentialsProvider(cfg)
|
||||
creds, err := credProvider.Retrieve(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ExpectedAccessKey+"AWSFile", v.AccessKeyID)
|
||||
assert.Equal(t, ExpectedSecretAccessKey+"AWSFile", v.SecretAccessKey)
|
||||
assert.Equal(t, ExpectedAccessKey+"AWSFile", creds.AccessKeyID)
|
||||
assert.Equal(t, ExpectedSecretAccessKey+"AWSFile", creds.SecretAccessKey)
|
||||
})
|
||||
|
||||
t.Run("IAM", func(t *testing.T) {
|
||||
@ -190,14 +195,14 @@ func TestMinioCredentials(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
// Use the provided EC2 Instance Metadata server
|
||||
creds := buildMinioCredentials(setting.MinioStorageConfig{
|
||||
credProvider := buildS3CredentialsProvider(setting.MinioStorageConfig{
|
||||
IamEndpoint: server.URL,
|
||||
})
|
||||
v, err := creds.Get()
|
||||
creds, err := credProvider.Retrieve(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ExpectedAccessKey+"IAM", v.AccessKeyID)
|
||||
assert.Equal(t, ExpectedSecretAccessKey+"IAM", v.SecretAccessKey)
|
||||
assert.Equal(t, ExpectedAccessKey+"IAM", creds.AccessKeyID)
|
||||
assert.Equal(t, ExpectedSecretAccessKey+"IAM", creds.SecretAccessKey)
|
||||
})
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user