mirror of
https://github.com/Dvorinka/Productier.git
synced 2026-06-04 20:43:02 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package filestorage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type LocalStorage struct {
|
||||
root string
|
||||
}
|
||||
|
||||
func NewLocal(root string) *LocalStorage {
|
||||
return &LocalStorage{root: root}
|
||||
}
|
||||
|
||||
func (l *LocalStorage) Provider() string {
|
||||
return "local"
|
||||
}
|
||||
|
||||
func (l *LocalStorage) Probe(_ context.Context) error {
|
||||
if err := os.MkdirAll(l.root, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
probePath := filepath.Join(l.root, ".healthcheck")
|
||||
file, err := os.OpenFile(probePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Remove(probePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LocalStorage) Put(_ context.Context, key string, reader io.Reader, _ string, _ int64) error {
|
||||
path := filepath.Join(l.root, filepath.Clean(key))
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(file, reader); err != nil {
|
||||
_ = file.Close()
|
||||
_ = os.Remove(path)
|
||||
return err
|
||||
}
|
||||
return file.Close()
|
||||
}
|
||||
|
||||
func (l *LocalStorage) Get(_ context.Context, key string) (Object, error) {
|
||||
path := filepath.Join(l.root, filepath.Clean(key))
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return Object{}, ErrNotFound
|
||||
}
|
||||
return Object{}, err
|
||||
}
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return Object{}, err
|
||||
}
|
||||
if info.IsDir() {
|
||||
_ = file.Close()
|
||||
return Object{}, ErrNotFound
|
||||
}
|
||||
return Object{
|
||||
Body: file,
|
||||
Size: info.Size(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *LocalStorage) Delete(_ context.Context, key string) error {
|
||||
path := filepath.Join(l.root, filepath.Clean(key))
|
||||
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package filestorage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/aws/smithy-go"
|
||||
)
|
||||
|
||||
type S3Storage struct {
|
||||
client *s3.Client
|
||||
bucket string
|
||||
}
|
||||
|
||||
func NewS3FromEnv() (*S3Storage, error) {
|
||||
bucket := strings.TrimSpace(os.Getenv("S3_BUCKET"))
|
||||
accessKey := strings.TrimSpace(os.Getenv("S3_ACCESS_KEY"))
|
||||
secretKey := strings.TrimSpace(os.Getenv("S3_SECRET_KEY"))
|
||||
if bucket == "" || accessKey == "" || secretKey == "" {
|
||||
return nil, errors.New("S3_BUCKET, S3_ACCESS_KEY, and S3_SECRET_KEY are required for FILE_STORAGE_PROVIDER=s3")
|
||||
}
|
||||
|
||||
region := strings.TrimSpace(os.Getenv("S3_REGION"))
|
||||
if region == "" {
|
||||
region = "us-east-1"
|
||||
}
|
||||
endpoint := strings.TrimSpace(os.Getenv("S3_ENDPOINT"))
|
||||
usePathStyle, _ := strconv.ParseBool(strings.TrimSpace(os.Getenv("S3_USE_PATH_STYLE")))
|
||||
|
||||
cfg := aws.Config{
|
||||
Region: region,
|
||||
Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")),
|
||||
}
|
||||
client := s3.NewFromConfig(cfg, func(options *s3.Options) {
|
||||
options.UsePathStyle = usePathStyle
|
||||
if endpoint != "" {
|
||||
options.BaseEndpoint = aws.String(endpoint)
|
||||
}
|
||||
})
|
||||
return &S3Storage{client: client, bucket: bucket}, nil
|
||||
}
|
||||
|
||||
func (s *S3Storage) Provider() string {
|
||||
return "s3"
|
||||
}
|
||||
|
||||
func (s *S3Storage) Probe(ctx context.Context) error {
|
||||
_, err := s.client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: aws.String(s.bucket)})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *S3Storage) Put(ctx context.Context, key string, reader io.Reader, contentType string, size int64) error {
|
||||
_, err := s.client.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(s.bucket),
|
||||
Key: aws.String(key),
|
||||
Body: reader,
|
||||
ContentType: aws.String(contentType),
|
||||
ContentLength: aws.Int64(size),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *S3Storage) Get(ctx context.Context, key string) (Object, error) {
|
||||
response, err := s.client.GetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: aws.String(s.bucket),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
if err != nil {
|
||||
if isS3NotFoundError(err) {
|
||||
return Object{}, ErrNotFound
|
||||
}
|
||||
return Object{}, err
|
||||
}
|
||||
|
||||
contentType := ""
|
||||
if response.ContentType != nil {
|
||||
contentType = *response.ContentType
|
||||
}
|
||||
size := int64(0)
|
||||
if response.ContentLength != nil {
|
||||
size = *response.ContentLength
|
||||
}
|
||||
return Object{
|
||||
Body: response.Body,
|
||||
ContentType: contentType,
|
||||
Size: size,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *S3Storage) Delete(ctx context.Context, key string) error {
|
||||
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(s.bucket),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
if err != nil {
|
||||
if isS3NotFoundError(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("delete s3 object: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isS3NotFoundError(err error) bool {
|
||||
var noSuchKey *types.NoSuchKey
|
||||
if errors.As(err, &noSuchKey) {
|
||||
return true
|
||||
}
|
||||
var apiErr smithy.APIError
|
||||
if errors.As(err, &apiErr) {
|
||||
switch apiErr.ErrorCode() {
|
||||
case "NoSuchKey", "NotFound", "NoSuchBucket":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package filestorage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("file storage object not found")
|
||||
|
||||
type Object struct {
|
||||
Body io.ReadCloser
|
||||
ContentType string
|
||||
Size int64
|
||||
}
|
||||
|
||||
type Storage interface {
|
||||
Provider() string
|
||||
Probe(ctx context.Context) error
|
||||
Put(ctx context.Context, key string, reader io.Reader, contentType string, size int64) error
|
||||
Get(ctx context.Context, key string) (Object, error)
|
||||
Delete(ctx context.Context, key string) error
|
||||
}
|
||||
|
||||
func NewFromEnv() (Storage, error) {
|
||||
provider := strings.ToLower(strings.TrimSpace(os.Getenv("FILE_STORAGE_PROVIDER")))
|
||||
if provider == "" || provider == "local" {
|
||||
root := strings.TrimSpace(os.Getenv("FILE_STORAGE_DIR"))
|
||||
if root == "" {
|
||||
root = "./data/uploads"
|
||||
}
|
||||
return NewLocal(root), nil
|
||||
}
|
||||
if provider == "s3" {
|
||||
return NewS3FromEnv()
|
||||
}
|
||||
return nil, errors.New("unsupported file storage provider")
|
||||
}
|
||||
Reference in New Issue
Block a user