Skip to content

Commit 7d971b1

Browse files
authored
feat(storage): add named attachment storage drivers (#6184)
1 parent bba1d6d commit 7d971b1

40 files changed

Lines changed: 3956 additions & 444 deletions

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require (
1515
github.com/google/uuid v1.6.0
1616
github.com/gorilla/feeds v1.2.0
1717
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0
18+
github.com/johannesboyne/gofakes3 v1.2.0
1819
github.com/joho/godotenv v1.5.1
1920
github.com/labstack/echo/v5 v5.1.0
2021
github.com/lib/pq v1.12.3
@@ -28,6 +29,7 @@ require (
2829
github.com/spf13/viper v1.21.0
2930
github.com/stretchr/testify v1.11.1
3031
github.com/testcontainers/testcontainers-go v0.42.0
32+
github.com/testcontainers/testcontainers-go/modules/minio v0.42.0
3133
github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0
3234
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0
3335
github.com/yuin/goldmark v1.8.2
@@ -95,6 +97,7 @@ require (
9597
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
9698
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
9799
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
100+
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
98101
github.com/sagikazarmark/locafero v0.12.0 // indirect
99102
github.com/segmentio/asm v1.1.3 // indirect
100103
github.com/segmentio/encoding v0.5.4 // indirect
@@ -117,9 +120,11 @@ require (
117120
go.opentelemetry.io/otel v1.43.0 // indirect
118121
go.opentelemetry.io/otel/metric v1.43.0 // indirect
119122
go.opentelemetry.io/otel/trace v1.43.0 // indirect
123+
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d // indirect
120124
go.yaml.in/yaml/v3 v3.0.4 // indirect
121125
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
122126
golang.org/x/image v0.39.0 // indirect
127+
golang.org/x/tools v0.44.0 // indirect
123128
google.golang.org/api v0.276.0 // indirect
124129
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect
125130
modernc.org/libc v1.72.1 // indirect

go.sum

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ github.com/aws/aws-sdk-go-v2/credentials v1.19.15 h1:fyvgWTszojq8hEnMi8PPBTvZdTt
3232
github.com/aws/aws-sdk-go-v2/credentials v1.19.15/go.mod h1:gJiYyMOjNg8OEdRWOf3CrFQxM2a98qmrtjx1zuiQfB8=
3333
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 h1:IOGsJ1xVWhsi+ZO7/NW8OuZZBtMJLZbk4P5HDjJO0jQ=
3434
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22/go.mod h1:b+hYdbU+jGKfXE8kKM6g1+h+L/Go3vMvzlxBsiuGsxg=
35+
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.75 h1:S61/E3N01oral6B3y9hZ2E1iFDqCZPPOBoBQretCnBI=
36+
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.75/go.mod h1:bDMQbkI1vJbNjnvJYpPTSNYBkI/VIv18ngWb/K84tkk=
3537
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 h1:GmLa5Kw1ESqtFpXsx5MmC84QWa/ZrLZvlJGa2y+4kcQ=
3638
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22/go.mod h1:6sW9iWm9DK9YRpRGga/qzrzNLgKpT2cIxb7Vo2eNOp0=
3739
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 h1:dY4kWZiSaXIzxnKlj17nHnBcXXBfac6UlsAx2qL6XrU=
@@ -62,6 +64,8 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
6264
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
6365
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
6466
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
67+
github.com/cevatbarisyilmaz/ara v0.0.4 h1:SGH10hXpBJhhTlObuZzTuFn1rrdmjQImITXnZVPSodc=
68+
github.com/cevatbarisyilmaz/ara v0.0.4/go.mod h1:BfFOxnUd6Mj6xmcvRxHN3Sr21Z1T3U2MYkYOmoQe4Ts=
6569
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
6670
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
6771
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -145,10 +149,16 @@ github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8=
145149
github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
146150
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
147151
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
152+
github.com/johannesboyne/gofakes3 v1.2.0 h1:I9VEzPWvvAUAGzDlhYFoZjF0AXMlkcEyZlmBwiI6Oms=
153+
github.com/johannesboyne/gofakes3 v1.2.0/go.mod h1:UHhRZRod9rENGFrUWTYnQHZqlNgSmjOq8DaD/ATQYRM=
148154
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
149155
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
156+
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
157+
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
150158
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
151159
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
160+
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
161+
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
152162
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
153163
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
154164
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -167,6 +177,12 @@ github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw
167177
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
168178
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
169179
github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
180+
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
181+
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
182+
github.com/minio/minio-go/v7 v7.0.68 h1:hTqSIfLlpXaKuNy4baAp4Jjy2sqZEN9hRxD0M4aOfrQ=
183+
github.com/minio/minio-go/v7 v7.0.68/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ=
184+
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
185+
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
170186
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
171187
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
172188
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
@@ -187,6 +203,10 @@ github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
187203
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
188204
github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU=
189205
github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
206+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
207+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
208+
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
209+
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
190210
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
191211
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
192212
github.com/openai/openai-go/v3 v3.32.0 h1:aHp/3wkX1W6jB8zTtf9xV0aK0qPFSVDqS7AHmlJ4hXs=
@@ -209,7 +229,11 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
209229
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
210230
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
211231
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
232+
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
233+
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
212234
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
235+
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
236+
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
213237
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
214238
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
215239
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
@@ -239,6 +263,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
239263
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
240264
github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=
241265
github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
266+
github.com/testcontainers/testcontainers-go/modules/minio v0.42.0 h1:8yTWNv8ALG7JQHYvm1n9PegH0uJT7dRtWNHf6eQeTRs=
267+
github.com/testcontainers/testcontainers-go/modules/minio v0.42.0/go.mod h1:bcjonmVMA/aEzxFFIh/FRwSkeZ+fnxwvkGN/Z4EiW28=
242268
github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0 h1:Yhv1k7vDpyzZePntg5R5Oj4ZMCyWpAfpJeRu1ROsgiU=
243269
github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0/go.mod h1:Z7SCTuiZlghAdRjkv3Ir0iXJKC2T2avbtxLR0DRe+ng=
244270
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo=
@@ -264,6 +290,8 @@ github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
264290
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
265291
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
266292
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
293+
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
294+
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
267295
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
268296
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
269297
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
@@ -278,6 +306,8 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC
278306
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
279307
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
280308
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
309+
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d h1:Ns9kd1Rwzw7t0BR8XMphenji4SmIoNZPn8zhYmaVKP8=
310+
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d/go.mod h1:92Uoe3l++MlthCm+koNi0tcUCX3anayogF0Pa/sp24k=
281311
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
282312
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
283313
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
@@ -329,6 +359,10 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
329359
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
330360
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
331361
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
362+
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
363+
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
364+
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
365+
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
332366
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
333367
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
334368
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=

internal/storage/driver.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package storage
2+
3+
import (
4+
"context"
5+
"io"
6+
7+
"github.com/pkg/errors"
8+
9+
"github.com/usememos/memos/internal/storage/s3"
10+
storepb "github.com/usememos/memos/proto/gen/store"
11+
)
12+
13+
// Driver provides object operations for a configured attachment storage.
14+
type Driver interface {
15+
UploadObject(ctx context.Context, key string, fileType string, content io.Reader) (string, error)
16+
PresignGetObject(ctx context.Context, key string) (string, error)
17+
GetObject(ctx context.Context, key string) ([]byte, error)
18+
GetObjectStream(ctx context.Context, key string) (io.ReadCloser, error)
19+
DeleteObject(ctx context.Context, key string) error
20+
}
21+
22+
// NewDriver creates the driver for a configured storage.
23+
func NewDriver(ctx context.Context, configuredStorage *storepb.Storage) (Driver, error) {
24+
if configuredStorage == nil {
25+
return nil, errors.New("storage is required")
26+
}
27+
switch configuredStorage.Type {
28+
case storepb.StorageType_STORAGE_TYPE_S3:
29+
if configuredStorage.GetS3Config() == nil {
30+
return nil, errors.Errorf("S3 config is missing for storage %q", configuredStorage.Id)
31+
}
32+
return s3.NewDriver(ctx, configuredStorage.GetS3Config())
33+
default:
34+
return nil, errors.Errorf("storage %q has unsupported driver type %s", configuredStorage.Id, configuredStorage.Type.String())
35+
}
36+
}

internal/storage/driver_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package storage
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/usememos/memos/internal/testutil/fakes3"
11+
storepb "github.com/usememos/memos/proto/gen/store"
12+
)
13+
14+
func TestNewDriver(t *testing.T) {
15+
ctx := context.Background()
16+
17+
t.Run("creates S3 driver", func(t *testing.T) {
18+
fake := fakes3.New(t, "driver-factory")
19+
driver, err := NewDriver(ctx, &storepb.Storage{
20+
Id: "s3-primary",
21+
Type: storepb.StorageType_STORAGE_TYPE_S3,
22+
Config: &storepb.Storage_S3Config{S3Config: fake.Config("driver-factory")},
23+
})
24+
require.NoError(t, err)
25+
26+
content := []byte("created through the storage driver factory")
27+
key, err := driver.UploadObject(ctx, "factory/object.txt", "text/plain", bytes.NewReader(content))
28+
require.NoError(t, err)
29+
downloaded, err := driver.GetObject(ctx, key)
30+
require.NoError(t, err)
31+
require.Equal(t, content, downloaded)
32+
})
33+
34+
tests := []struct {
35+
name string
36+
storage *storepb.Storage
37+
wantErr string
38+
}{
39+
{name: "missing storage", wantErr: "storage is required"},
40+
{
41+
name: "missing S3 config",
42+
storage: &storepb.Storage{Id: "s3-missing", Type: storepb.StorageType_STORAGE_TYPE_S3},
43+
wantErr: `S3 config is missing for storage "s3-missing"`,
44+
},
45+
{
46+
name: "unsupported storage type",
47+
storage: &storepb.Storage{Id: "local", Type: storepb.StorageType_STORAGE_TYPE_LOCAL},
48+
wantErr: `storage "local" has unsupported driver type STORAGE_TYPE_LOCAL`,
49+
},
50+
}
51+
for _, test := range tests {
52+
t.Run(test.name, func(t *testing.T) {
53+
driver, err := NewDriver(ctx, test.storage)
54+
require.Nil(t, driver)
55+
require.EqualError(t, err, test.wantErr)
56+
})
57+
}
58+
}

internal/storage/s3/s3.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ import (
1717
storepb "github.com/usememos/memos/proto/gen/store"
1818
)
1919

20-
type Client struct {
20+
// Driver stores attachment objects in an S3-compatible object store.
21+
type Driver struct {
2122
Client *s3.Client
2223
Bucket *string
2324
}
2425

25-
func NewClient(ctx context.Context, s3Config *storepb.StorageS3Config) (*Client, error) {
26+
// NewDriver creates an S3 storage driver from the supplied configuration.
27+
func NewDriver(ctx context.Context, s3Config *storepb.StorageS3Config) (*Driver, error) {
2628
loadOptions := []func(*config.LoadOptions) error{
2729
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(s3Config.AccessKeyId, s3Config.AccessKeySecret, "")),
2830
config.WithRegion(s3Config.Region),
@@ -47,28 +49,28 @@ func NewClient(ctx context.Context, s3Config *storepb.StorageS3Config) (*Client,
4749
o.RequestChecksumCalculation = aws.RequestChecksumCalculationWhenRequired
4850
o.ResponseChecksumValidation = aws.ResponseChecksumValidationWhenRequired
4951
})
50-
return &Client{
52+
return &Driver{
5153
Client: client,
5254
Bucket: aws.String(s3Config.Bucket),
5355
}, nil
5456
}
5557

5658
// UploadObject uploads an object to S3.
57-
func (c *Client) UploadObject(ctx context.Context, key string, fileType string, content io.Reader) (string, error) {
59+
func (c *Driver) UploadObject(ctx context.Context, key string, fileType string, content io.Reader) (string, error) {
5860
putInput := s3.PutObjectInput{
5961
Bucket: c.Bucket,
6062
Key: aws.String(key),
6163
ContentType: aws.String(fileType),
6264
Body: content,
6365
}
6466
if _, err := c.Client.PutObject(ctx, &putInput); err != nil {
65-
return "", err
67+
return "", errors.Wrap(err, "failed to upload object")
6668
}
6769
return key, nil
6870
}
6971

7072
// PresignGetObject presigns an object in S3.
71-
func (c *Client) PresignGetObject(ctx context.Context, key string) (string, error) {
73+
func (c *Driver) PresignGetObject(ctx context.Context, key string) (string, error) {
7274
presignClient := s3.NewPresignClient(c.Client)
7375
presignResult, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
7476
Bucket: aws.String(*c.Bucket),
@@ -85,7 +87,7 @@ func (c *Client) PresignGetObject(ctx context.Context, key string) (string, erro
8587
}
8688

8789
// GetObject retrieves an object from S3.
88-
func (c *Client) GetObject(ctx context.Context, key string) ([]byte, error) {
90+
func (c *Driver) GetObject(ctx context.Context, key string) ([]byte, error) {
8991
output, err := c.Client.GetObject(ctx, &s3.GetObjectInput{
9092
Bucket: c.Bucket,
9193
Key: aws.String(key),
@@ -102,7 +104,7 @@ func (c *Client) GetObject(ctx context.Context, key string) ([]byte, error) {
102104
}
103105

104106
// GetObjectStream retrieves an object from S3 as a stream.
105-
func (c *Client) GetObjectStream(ctx context.Context, key string) (io.ReadCloser, error) {
107+
func (c *Driver) GetObjectStream(ctx context.Context, key string) (io.ReadCloser, error) {
106108
output, err := c.Client.GetObject(ctx, &s3.GetObjectInput{
107109
Bucket: c.Bucket,
108110
Key: aws.String(key),
@@ -114,7 +116,7 @@ func (c *Client) GetObjectStream(ctx context.Context, key string) (io.ReadCloser
114116
}
115117

116118
// DeleteObject deletes an object in S3.
117-
func (c *Client) DeleteObject(ctx context.Context, key string) error {
119+
func (c *Driver) DeleteObject(ctx context.Context, key string) error {
118120
_, err := c.Client.DeleteObject(ctx, &s3.DeleteObjectInput{
119121
Bucket: c.Bucket,
120122
Key: aws.String(key),

0 commit comments

Comments
 (0)