junoberryferry 2025-12-11 13:25:08 +07:00 committed by GitHub
commit f54906f9f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 706 additions and 255 deletions

File diff suppressed because one or more lines are too long

@ -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

@ -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)
}
}

@ -4,8 +4,10 @@
package storage
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
@ -15,12 +17,19 @@ import (
"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/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"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 (
@ -29,50 +38,128 @@ var (
quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
)
type minioObject struct {
*minio.Object
// 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 (m *minioObject) Stat() (os.FileInfo, error) {
oi, err := m.Object.Stat()
if err != nil {
return nil, convertMinioErr(err)
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
}
return &minioFileInfo{oi}, nil
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 *minio.Client
client *s3.Client
bucket string
basePath string
}
func convertMinioErr(err error) error {
func convertS3Err(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":
// Check for specific S3 error types
var notFound *types.NotFound
if errors.As(err, &notFound) {
return os.ErrNotExist
case "AccessDenied":
return os.ErrPermission
}
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, minioClient *minio.Client, bucket string) error {
_, err := minioClient.GetBucketVersioning(ctx, bucket)
var getBucketVersioning = func(ctx context.Context, client *s3.Client, bucket string) error {
_, err := client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{
Bucket: aws.String(bucket),
})
return err
}
@ -85,64 +172,97 @@ func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage,
log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath)
var lookup minio.BucketLookupType
// 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", "":
lookup = minio.BucketLookupAuto
// For Minio compatibility, default to path style
usePathStyle = true
case "dns":
lookup = minio.BucketLookupDNS
usePathStyle = false
case "path":
lookup = minio.BucketLookupPath
usePathStyle = true
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,
// Create S3 client
s3Client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String(endpointURL)
o.UsePathStyle = usePathStyle
})
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)
// 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 {
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)
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
exists, err := minioClient.BucketExists(ctx, config.Bucket)
_, err = s3Client.HeadBucket(ctx, &s3.HeadBucketInput{
Bucket: aws.String(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)
var notFound *types.NotFound
var noSuchBucket *types.NoSuchBucket
if errors.As(err, &notFound) || 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: minioClient,
client: s3Client,
bucket: config.Bucket,
basePath: config.BasePath,
}, nil
@ -165,128 +285,345 @@ func (m *MinioStorage) buildMinioDirPrefix(p string) string {
return p
}
func buildMinioCredentials(config setting.MinioStorageConfig) *credentials.Credentials {
// 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.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.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 credentials.NewChainCredentials(chain)
return 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)
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, convertMinioErr(err)
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 &minioObject{object}, nil
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) {
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",
},
)
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, convertMinioErr(err)
return 0, convertS3Err(err)
}
return uploadInfo.Size, nil
return size, nil
}
type minioFileInfo struct {
minio.ObjectInfo
type s3FileInfo struct {
key string
size int64
lastMod time.Time
}
func (m minioFileInfo) Name() string {
return path.Base(m.ObjectInfo.Key)
func (m s3FileInfo) Name() string {
return path.Base(m.key)
}
func (m minioFileInfo) Size() int64 {
return m.ObjectInfo.Size
func (m s3FileInfo) Size() int64 {
return m.size
}
func (m minioFileInfo) ModTime() time.Time {
return m.LastModified
func (m s3FileInfo) ModTime() time.Time {
return m.lastMod
}
func (m minioFileInfo) IsDir() bool {
return strings.HasSuffix(m.ObjectInfo.Key, "/")
func (m s3FileInfo) IsDir() bool {
return strings.HasSuffix(m.key, "/")
}
func (m minioFileInfo) Mode() os.FileMode {
func (m s3FileInfo) Mode() os.FileMode {
return os.ModePerm
}
func (m minioFileInfo) Sys() any {
func (m s3FileInfo) 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{},
)
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, convertMinioErr(err)
return nil, convertS3Err(err)
}
return &minioFileInfo{info}, nil
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.RemoveObject(m.ctx, m.bucket, m.buildMinioPath(path), minio.RemoveObjectOptions{})
return convertMinioErr(err)
_, 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, 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.
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
@ -304,40 +641,94 @@ func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams
// TODO: refactor with "modules/public/mime_types.go", for example: "DetectWellKnownSafeInlineMimeType"
}
var contentType, contentDisposition string
if mimeType, ok := inlineExtMimeTypes[ext]; ok {
reqParams.Set("response-content-type", mimeType)
reqParams.Set("response-content-disposition", "inline")
contentType = mimeType
contentDisposition = "inline"
} else {
reqParams.Set("response-content-disposition", fmt.Sprintf(`attachment; filename="%s"`, quoteEscaper.Replace(name)))
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 {
u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams)
return u, convertMinioErr(err)
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)
}
u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams)
return u, convertMinioErr(err)
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 {
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)
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 convertMinioErr(err)
return convertS3Err(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)
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
}

@ -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)
})
})
}