storage.go (6585B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package storage 19 20 import ( 21 "context" 22 "fmt" 23 "io" 24 "mime" 25 "net/url" 26 "path" 27 "time" 28 29 "codeberg.org/gruf/go-bytesize" 30 "codeberg.org/gruf/go-cache/v3/ttl" 31 "codeberg.org/gruf/go-store/v2/storage" 32 "github.com/minio/minio-go/v7" 33 "github.com/minio/minio-go/v7/pkg/credentials" 34 "github.com/superseriousbusiness/gotosocial/internal/config" 35 ) 36 37 const ( 38 urlCacheTTL = time.Hour * 24 39 urlCacheExpiryFrequency = time.Minute * 5 40 ) 41 42 // PresignedURL represents a pre signed S3 URL with 43 // an expiry time. 44 type PresignedURL struct { 45 *url.URL 46 Expiry time.Time // link expires at this time 47 } 48 49 // ErrAlreadyExists is a ptr to underlying storage.ErrAlreadyExists, 50 // to put the related errors in the same package as our storage wrapper. 51 var ErrAlreadyExists = storage.ErrAlreadyExists 52 53 // Driver wraps a kv.KVStore to also provide S3 presigned GET URLs. 54 type Driver struct { 55 // Underlying storage 56 Storage storage.Storage 57 58 // S3-only parameters 59 Proxy bool 60 Bucket string 61 PresignedCache *ttl.Cache[string, PresignedURL] 62 } 63 64 // Get returns the byte value for key in storage. 65 func (d *Driver) Get(ctx context.Context, key string) ([]byte, error) { 66 return d.Storage.ReadBytes(ctx, key) 67 } 68 69 // GetStream returns an io.ReadCloser for the value bytes at key in the storage. 70 func (d *Driver) GetStream(ctx context.Context, key string) (io.ReadCloser, error) { 71 return d.Storage.ReadStream(ctx, key) 72 } 73 74 // Put writes the supplied value bytes at key in the storage 75 func (d *Driver) Put(ctx context.Context, key string, value []byte) (int, error) { 76 return d.Storage.WriteBytes(ctx, key, value) 77 } 78 79 // PutStream writes the bytes from supplied reader at key in the storage 80 func (d *Driver) PutStream(ctx context.Context, key string, r io.Reader) (int64, error) { 81 return d.Storage.WriteStream(ctx, key, r) 82 } 83 84 // Remove attempts to remove the supplied key (and corresponding value) from storage. 85 func (d *Driver) Delete(ctx context.Context, key string) error { 86 return d.Storage.Remove(ctx, key) 87 } 88 89 // Has checks if the supplied key is in the storage. 90 func (d *Driver) Has(ctx context.Context, key string) (bool, error) { 91 return d.Storage.Stat(ctx, key) 92 } 93 94 // WalkKeys walks the keys in the storage. 95 func (d *Driver) WalkKeys(ctx context.Context, walk func(context.Context, string) error) error { 96 return d.Storage.WalkKeys(ctx, storage.WalkKeysOptions{ 97 WalkFn: func(ctx context.Context, entry storage.Entry) error { 98 return walk(ctx, entry.Key) 99 }, 100 }) 101 } 102 103 // Close will close the storage, releasing any file locks. 104 func (d *Driver) Close() error { 105 return d.Storage.Close() 106 } 107 108 // URL will return a presigned GET object URL, but only if running on S3 storage with proxying disabled. 109 func (d *Driver) URL(ctx context.Context, key string) *PresignedURL { 110 // Check whether S3 *without* proxying is enabled 111 s3, ok := d.Storage.(*storage.S3Storage) 112 if !ok || d.Proxy { 113 return nil 114 } 115 116 // Check cache underlying cache map directly to 117 // avoid extending the TTL (which cache.Get() does). 118 d.PresignedCache.Lock() 119 e, ok := d.PresignedCache.Cache.Get(key) 120 d.PresignedCache.Unlock() 121 122 if ok { 123 return &e.Value 124 } 125 126 u, err := s3.Client().PresignedGetObject(ctx, d.Bucket, key, urlCacheTTL, url.Values{ 127 "response-content-type": []string{mime.TypeByExtension(path.Ext(key))}, 128 }) 129 if err != nil { 130 // If URL request fails, fallback is to fetch the file. So ignore the error here 131 return nil 132 } 133 134 psu := PresignedURL{ 135 URL: u, 136 Expiry: time.Now().Add(urlCacheTTL), // link expires in 24h time 137 } 138 139 d.PresignedCache.Set(key, psu) 140 return &psu 141 } 142 143 func AutoConfig() (*Driver, error) { 144 switch backend := config.GetStorageBackend(); backend { 145 case "s3": 146 return NewS3Storage() 147 case "local": 148 return NewFileStorage() 149 default: 150 return nil, fmt.Errorf("invalid storage backend: %s", backend) 151 } 152 } 153 154 func NewFileStorage() (*Driver, error) { 155 // Load runtime configuration 156 basePath := config.GetStorageLocalBasePath() 157 158 // Open the disk storage implementation 159 disk, err := storage.OpenDisk(basePath, &storage.DiskConfig{ 160 // Put the store lockfile in the storage dir itself. 161 // Normally this would not be safe, since we could end up 162 // overwriting the lockfile if we store a file called 'store.lock'. 163 // However, in this case it's OK because the keys are set by 164 // GtS and not the user, so we know we're never going to overwrite it. 165 LockFile: path.Join(basePath, "store.lock"), 166 WriteBufSize: int(16 * bytesize.KiB), 167 }) 168 if err != nil { 169 return nil, fmt.Errorf("error opening disk storage: %w", err) 170 } 171 172 return &Driver{ 173 Storage: disk, 174 }, nil 175 } 176 177 func NewS3Storage() (*Driver, error) { 178 // Load runtime configuration 179 endpoint := config.GetStorageS3Endpoint() 180 access := config.GetStorageS3AccessKey() 181 secret := config.GetStorageS3SecretKey() 182 secure := config.GetStorageS3UseSSL() 183 bucket := config.GetStorageS3BucketName() 184 185 // Open the s3 storage implementation 186 s3, err := storage.OpenS3(endpoint, bucket, &storage.S3Config{ 187 CoreOpts: minio.Options{ 188 Creds: credentials.NewStaticV4(access, secret, ""), 189 Secure: secure, 190 }, 191 GetOpts: minio.GetObjectOptions{}, 192 PutOpts: minio.PutObjectOptions{}, 193 PutChunkSize: 5 * 1024 * 1024, // 5MiB 194 StatOpts: minio.StatObjectOptions{}, 195 RemoveOpts: minio.RemoveObjectOptions{}, 196 ListSize: 200, 197 }) 198 if err != nil { 199 return nil, fmt.Errorf("error opening s3 storage: %w", err) 200 } 201 202 // ttl should be lower than the expiry used by S3 to avoid serving invalid URLs 203 presignedCache := ttl.New[string, PresignedURL](0, 1000, urlCacheTTL-urlCacheExpiryFrequency) 204 presignedCache.Start(urlCacheExpiryFrequency) 205 206 return &Driver{ 207 Proxy: config.GetStorageS3Proxy(), 208 Bucket: config.GetStorageS3BucketName(), 209 Storage: s3, 210 PresignedCache: presignedCache, 211 }, nil 212 }