gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }