| 
									
										
										
										
											2022-07-03 12:08:30 +02:00
										 |  |  | /* | 
					
						
							|  |  |  |    GoToSocial | 
					
						
							| 
									
										
										
										
											2023-01-05 12:43:00 +01:00
										 |  |  |    Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org | 
					
						
							| 
									
										
										
										
											2022-07-03 12:08:30 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |    This program is free software: you can redistribute it and/or modify | 
					
						
							|  |  |  |    it under the terms of the GNU Affero General Public License as published by | 
					
						
							|  |  |  |    the Free Software Foundation, either version 3 of the License, or | 
					
						
							|  |  |  |    (at your option) any later version. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |    This program is distributed in the hope that it will be useful, | 
					
						
							|  |  |  |    but WITHOUT ANY WARRANTY; without even the implied warranty of | 
					
						
							|  |  |  |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
					
						
							|  |  |  |    GNU Affero General Public License for more details. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |    You should have received a copy of the GNU Affero General Public License | 
					
						
							|  |  |  |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | 
					
						
							|  |  |  | */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | package storage | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							| 
									
										
										
										
											2022-11-24 08:35:46 +00:00
										 |  |  | 	"mime" | 
					
						
							| 
									
										
										
										
											2022-07-03 12:08:30 +02:00
										 |  |  | 	"net/url" | 
					
						
							|  |  |  | 	"path" | 
					
						
							| 
									
										
										
										
											2022-11-24 08:35:46 +00:00
										 |  |  | 	"time" | 
					
						
							| 
									
										
										
										
											2022-07-03 12:08:30 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-11 11:13:13 +00:00
										 |  |  | 	"codeberg.org/gruf/go-bytesize" | 
					
						
							| 
									
										
										
										
											2022-12-05 11:09:22 +01:00
										 |  |  | 	"codeberg.org/gruf/go-cache/v3/ttl" | 
					
						
							| 
									
										
										
										
											2022-11-05 12:10:19 +01:00
										 |  |  | 	"codeberg.org/gruf/go-store/v2/kv" | 
					
						
							|  |  |  | 	"codeberg.org/gruf/go-store/v2/storage" | 
					
						
							| 
									
										
										
										
											2022-07-03 12:08:30 +02:00
										 |  |  | 	"github.com/minio/minio-go/v7" | 
					
						
							|  |  |  | 	"github.com/minio/minio-go/v7/pkg/credentials" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/config" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-02 19:40:49 +01:00
										 |  |  | const ( | 
					
						
							|  |  |  | 	urlCacheTTL             = time.Hour * 24 | 
					
						
							|  |  |  | 	urlCacheExpiryFrequency = time.Minute * 5 | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-16 14:18:53 +01:00
										 |  |  | // PresignedURL represents a pre signed S3 URL with | 
					
						
							|  |  |  | // an expiry time. | 
					
						
							|  |  |  | type PresignedURL struct { | 
					
						
							|  |  |  | 	*url.URL | 
					
						
							|  |  |  | 	Expiry time.Time // link expires at this time | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-24 08:35:46 +00:00
										 |  |  | // ErrAlreadyExists is a ptr to underlying storage.ErrAlreadyExists, | 
					
						
							|  |  |  | // to put the related errors in the same package as our storage wrapper. | 
					
						
							|  |  |  | var ErrAlreadyExists = storage.ErrAlreadyExists | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Driver wraps a kv.KVStore to also provide S3 presigned GET URLs. | 
					
						
							|  |  |  | type Driver struct { | 
					
						
							|  |  |  | 	// Underlying storage | 
					
						
							|  |  |  | 	*kv.KVStore | 
					
						
							|  |  |  | 	Storage storage.Storage | 
					
						
							| 
									
										
										
										
											2022-07-03 12:08:30 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-24 08:35:46 +00:00
										 |  |  | 	// S3-only parameters | 
					
						
							| 
									
										
										
										
											2022-12-02 19:40:49 +01:00
										 |  |  | 	Proxy          bool | 
					
						
							|  |  |  | 	Bucket         string | 
					
						
							| 
									
										
										
										
											2023-02-16 14:18:53 +01:00
										 |  |  | 	PresignedCache *ttl.Cache[string, PresignedURL] | 
					
						
							| 
									
										
										
										
											2022-07-03 12:08:30 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-24 08:35:46 +00:00
										 |  |  | // URL will return a presigned GET object URL, but only if running on S3 storage with proxying disabled. | 
					
						
							| 
									
										
										
										
											2023-02-16 14:18:53 +01:00
										 |  |  | func (d *Driver) URL(ctx context.Context, key string) *PresignedURL { | 
					
						
							| 
									
										
										
										
											2022-11-24 08:35:46 +00:00
										 |  |  | 	// Check whether S3 *without* proxying is enabled | 
					
						
							|  |  |  | 	s3, ok := d.Storage.(*storage.S3Storage) | 
					
						
							|  |  |  | 	if !ok || d.Proxy { | 
					
						
							|  |  |  | 		return nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-11 11:13:13 +00:00
										 |  |  | 	// Check cache underlying cache map directly to | 
					
						
							|  |  |  | 	// avoid extending the TTL (which cache.Get() does). | 
					
						
							|  |  |  | 	d.PresignedCache.Lock() | 
					
						
							|  |  |  | 	e, ok := d.PresignedCache.Cache.Get(key) | 
					
						
							|  |  |  | 	d.PresignedCache.Unlock() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if ok { | 
					
						
							| 
									
										
										
										
											2023-02-16 14:18:53 +01:00
										 |  |  | 		return &e.Value | 
					
						
							| 
									
										
										
										
											2022-12-02 19:40:49 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	u, err := s3.Client().PresignedGetObject(ctx, d.Bucket, key, urlCacheTTL, url.Values{ | 
					
						
							| 
									
										
										
										
											2022-11-24 08:35:46 +00:00
										 |  |  | 		"response-content-type": []string{mime.TypeByExtension(path.Ext(key))}, | 
					
						
							|  |  |  | 	}) | 
					
						
							| 
									
										
										
										
											2022-12-02 19:40:49 +01:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		// If URL request fails, fallback is to fetch the file. So ignore the error here | 
					
						
							|  |  |  | 		return nil | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-02-16 14:18:53 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	psu := PresignedURL{ | 
					
						
							|  |  |  | 		URL:    u, | 
					
						
							|  |  |  | 		Expiry: time.Now().Add(urlCacheTTL), // link expires in 24h time | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	d.PresignedCache.Set(key, psu) | 
					
						
							|  |  |  | 	return &psu | 
					
						
							| 
									
										
										
										
											2022-11-24 08:35:46 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func AutoConfig() (*Driver, error) { | 
					
						
							|  |  |  | 	switch backend := config.GetStorageBackend(); backend { | 
					
						
							| 
									
										
										
										
											2022-07-03 12:08:30 +02:00
										 |  |  | 	case "s3": | 
					
						
							| 
									
										
										
										
											2022-12-02 19:40:49 +01:00
										 |  |  | 		return NewS3Storage() | 
					
						
							| 
									
										
										
										
											2022-07-03 12:08:30 +02:00
										 |  |  | 	case "local": | 
					
						
							| 
									
										
										
										
											2022-12-02 19:40:49 +01:00
										 |  |  | 		return NewFileStorage() | 
					
						
							| 
									
										
										
										
											2022-11-24 08:35:46 +00:00
										 |  |  | 	default: | 
					
						
							|  |  |  | 		return nil, fmt.Errorf("invalid storage backend: %s", backend) | 
					
						
							| 
									
										
										
										
											2022-07-03 12:08:30 +02:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-12-02 19:40:49 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func NewFileStorage() (*Driver, error) { | 
					
						
							|  |  |  | 	// Load runtime configuration | 
					
						
							|  |  |  | 	basePath := config.GetStorageLocalBasePath() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Open the disk storage implementation | 
					
						
							|  |  |  | 	disk, err := storage.OpenDisk(basePath, &storage.DiskConfig{ | 
					
						
							|  |  |  | 		// Put the store lockfile in the storage dir itself. | 
					
						
							|  |  |  | 		// Normally this would not be safe, since we could end up | 
					
						
							|  |  |  | 		// overwriting the lockfile if we store a file called 'store.lock'. | 
					
						
							|  |  |  | 		// However, in this case it's OK because the keys are set by | 
					
						
							|  |  |  | 		// GtS and not the user, so we know we're never going to overwrite it. | 
					
						
							| 
									
										
										
										
											2023-01-11 11:13:13 +00:00
										 |  |  | 		LockFile:     path.Join(basePath, "store.lock"), | 
					
						
							|  |  |  | 		WriteBufSize: int(16 * bytesize.KiB), | 
					
						
							| 
									
										
										
										
											2022-12-02 19:40:49 +01:00
										 |  |  | 	}) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, fmt.Errorf("error opening disk storage: %w", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return &Driver{ | 
					
						
							|  |  |  | 		KVStore: kv.New(disk), | 
					
						
							|  |  |  | 		Storage: disk, | 
					
						
							|  |  |  | 	}, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func NewS3Storage() (*Driver, error) { | 
					
						
							|  |  |  | 	// Load runtime configuration | 
					
						
							|  |  |  | 	endpoint := config.GetStorageS3Endpoint() | 
					
						
							|  |  |  | 	access := config.GetStorageS3AccessKey() | 
					
						
							|  |  |  | 	secret := config.GetStorageS3SecretKey() | 
					
						
							|  |  |  | 	secure := config.GetStorageS3UseSSL() | 
					
						
							|  |  |  | 	bucket := config.GetStorageS3BucketName() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Open the s3 storage implementation | 
					
						
							|  |  |  | 	s3, err := storage.OpenS3(endpoint, bucket, &storage.S3Config{ | 
					
						
							|  |  |  | 		CoreOpts: minio.Options{ | 
					
						
							|  |  |  | 			Creds:  credentials.NewStaticV4(access, secret, ""), | 
					
						
							|  |  |  | 			Secure: secure, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		GetOpts:      minio.GetObjectOptions{}, | 
					
						
							|  |  |  | 		PutOpts:      minio.PutObjectOptions{}, | 
					
						
							|  |  |  | 		PutChunkSize: 5 * 1024 * 1024, // 5MiB | 
					
						
							|  |  |  | 		StatOpts:     minio.StatObjectOptions{}, | 
					
						
							|  |  |  | 		RemoveOpts:   minio.RemoveObjectOptions{}, | 
					
						
							|  |  |  | 		ListSize:     200, | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, fmt.Errorf("error opening s3 storage: %w", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// ttl should be lower than the expiry used by S3 to avoid serving invalid URLs | 
					
						
							| 
									
										
										
										
											2023-02-16 14:18:53 +01:00
										 |  |  | 	presignedCache := ttl.New[string, PresignedURL](0, 1000, urlCacheTTL-urlCacheExpiryFrequency) | 
					
						
							| 
									
										
										
										
											2022-12-02 19:40:49 +01:00
										 |  |  | 	presignedCache.Start(urlCacheExpiryFrequency) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-24 08:35:46 +00:00
										 |  |  | 	return &Driver{ | 
					
						
							| 
									
										
										
										
											2022-12-02 19:40:49 +01:00
										 |  |  | 		KVStore:        kv.New(s3), | 
					
						
							|  |  |  | 		Proxy:          config.GetStorageS3Proxy(), | 
					
						
							|  |  |  | 		Bucket:         config.GetStorageS3BucketName(), | 
					
						
							|  |  |  | 		Storage:        s3, | 
					
						
							|  |  |  | 		PresignedCache: presignedCache, | 
					
						
							| 
									
										
										
										
											2022-11-24 08:35:46 +00:00
										 |  |  | 	}, nil | 
					
						
							| 
									
										
										
										
											2022-07-03 12:08:30 +02:00
										 |  |  | } |