2024-11-17 16:00:48 +01:00
|
|
|
package fsys
|
|
|
|
|
|
|
|
import (
|
2024-11-18 22:34:07 +01:00
|
|
|
"bytes"
|
|
|
|
"context"
|
2024-11-17 16:00:48 +01:00
|
|
|
"errors"
|
|
|
|
"github.com/minio/minio-go/v7"
|
|
|
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
2024-11-18 22:39:35 +01:00
|
|
|
"io"
|
2024-11-18 22:48:49 +01:00
|
|
|
"io/fs"
|
2024-11-18 22:34:07 +01:00
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
2024-11-19 02:54:41 +01:00
|
|
|
"time"
|
2024-11-17 16:00:48 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
ErrInvalidStorageType = errors.New("invalid storage type")
|
|
|
|
ErrInvalidPath = errors.New("invalid path")
|
|
|
|
ErrFileNotFound = errors.New("file not found")
|
|
|
|
)
|
|
|
|
|
|
|
|
type Storage struct {
|
2024-11-18 23:00:58 +01:00
|
|
|
config Config
|
2024-11-17 16:00:48 +01:00
|
|
|
s3Client *minio.Client
|
2024-11-18 22:34:07 +01:00
|
|
|
ctx context.Context
|
2024-11-17 16:00:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type Config struct {
|
2024-11-18 23:00:58 +01:00
|
|
|
// Type of storage, can be "local" or "s3"
|
2024-11-17 16:00:48 +01:00
|
|
|
Type string
|
|
|
|
|
|
|
|
Path string
|
|
|
|
|
|
|
|
S3Endpoint string
|
|
|
|
S3Location string
|
|
|
|
S3Secure bool
|
|
|
|
|
|
|
|
S3AccessID string
|
|
|
|
S3AccessKey string
|
|
|
|
|
|
|
|
S3BucketName string
|
|
|
|
}
|
|
|
|
|
2024-11-18 23:00:58 +01:00
|
|
|
// New creates a new storage interface
|
|
|
|
// It takes Config as its parameter.
|
|
|
|
// The function returns the Storage interface, and an error, if any.
|
|
|
|
func New(config Config) (*Storage, error) {
|
2024-11-17 16:00:48 +01:00
|
|
|
newStorage := new(Storage)
|
|
|
|
newStorage.config = config
|
2024-11-18 22:34:07 +01:00
|
|
|
newStorage.ctx = context.Background()
|
2024-11-17 16:00:48 +01:00
|
|
|
|
|
|
|
switch config.Type {
|
|
|
|
case "minio", "s3":
|
|
|
|
s3Client, err := minio.New(config.S3Endpoint, &minio.Options{
|
|
|
|
Creds: credentials.NewStaticV4(config.S3AccessID, config.S3AccessKey, ""),
|
|
|
|
Secure: config.S3Secure,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2024-11-18 22:34:07 +01:00
|
|
|
|
|
|
|
bucketExists, err := s3Client.BucketExists(newStorage.ctx, config.S3BucketName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if !bucketExists {
|
|
|
|
if err := s3Client.MakeBucket(newStorage.ctx, config.S3BucketName, minio.MakeBucketOptions{}); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-17 16:00:48 +01:00
|
|
|
newStorage.s3Client = s3Client
|
|
|
|
case "local":
|
2024-11-18 22:48:49 +01:00
|
|
|
if statInfo, err := os.Stat(config.Path); err != nil && errors.Is(err, fs.ErrNotExist) {
|
|
|
|
if err := os.MkdirAll(config.Path, 0740); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
} else if err != nil {
|
2024-11-18 22:34:07 +01:00
|
|
|
return nil, err
|
2024-11-18 22:48:49 +01:00
|
|
|
} else if statInfo.IsDir() {
|
|
|
|
return nil, ErrInvalidPath
|
2024-11-18 22:34:07 +01:00
|
|
|
}
|
2024-11-17 16:00:48 +01:00
|
|
|
default:
|
|
|
|
return nil, ErrInvalidStorageType
|
|
|
|
}
|
|
|
|
|
|
|
|
return newStorage, nil
|
|
|
|
}
|
2024-11-18 22:34:07 +01:00
|
|
|
|
2024-11-18 23:00:58 +01:00
|
|
|
// Open opens a file within the Storage interface.
|
|
|
|
// It takes a file name as its parameter.
|
|
|
|
// The function returns a File and an error, if any.
|
2024-11-18 22:34:07 +01:00
|
|
|
func (s *Storage) Open(name string) (*File, error) {
|
|
|
|
returnFile := new(File)
|
|
|
|
returnFile.storage = s
|
|
|
|
|
|
|
|
if s.s3Client != nil {
|
|
|
|
object, err := s.s3Client.GetObject(s.ctx, s.config.S3BucketName, name, minio.GetObjectOptions{})
|
|
|
|
if err != nil {
|
|
|
|
errResp := minio.ToErrorResponse(err)
|
|
|
|
if errResp.StatusCode == http.StatusNotFound {
|
|
|
|
return nil, ErrFileNotFound
|
|
|
|
} else {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
objectStat, err := object.Stat()
|
|
|
|
if err != nil {
|
|
|
|
errResp := minio.ToErrorResponse(err)
|
|
|
|
if errResp.StatusCode == http.StatusNotFound {
|
|
|
|
return nil, ErrFileNotFound
|
|
|
|
} else {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
returnFile.Name = objectStat.Key
|
|
|
|
returnFile.file = object
|
|
|
|
} else {
|
2024-11-19 02:54:41 +01:00
|
|
|
if _, err := os.Stat(filepath.Join(s.config.Path, name)); err != nil && errors.Is(err, fs.ErrNotExist) {
|
2024-11-18 22:34:07 +01:00
|
|
|
return nil, ErrFileNotFound
|
|
|
|
} else if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
file, err := os.Open(filepath.Join(s.config.Path, name))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
returnFile.file = file
|
|
|
|
|
|
|
|
cutStr, _ := strings.CutPrefix(file.Name(), filepath.Clean(s.config.Path)+"/")
|
|
|
|
|
|
|
|
returnFile.Name = cutStr
|
|
|
|
}
|
|
|
|
|
|
|
|
return returnFile, nil
|
|
|
|
}
|
|
|
|
|
2024-11-18 23:00:58 +01:00
|
|
|
// Read opens a file within the Storage interfaces and reads the contents.
|
|
|
|
// It takes a file name as its parameter.
|
|
|
|
// The function returns a []byte, or an error, if any.
|
2024-11-18 22:34:07 +01:00
|
|
|
func (s *Storage) Read(name string) ([]byte, error) {
|
|
|
|
if s.s3Client != nil {
|
|
|
|
object, err := s.s3Client.GetObject(s.ctx, s.config.S3BucketName, name, minio.GetObjectOptions{})
|
|
|
|
if err != nil {
|
|
|
|
errResp := minio.ToErrorResponse(err)
|
|
|
|
if errResp.StatusCode == http.StatusNotFound {
|
|
|
|
return nil, ErrFileNotFound
|
|
|
|
} else {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-18 22:39:35 +01:00
|
|
|
objectBytes, err := io.ReadAll(object)
|
|
|
|
if err != nil {
|
2024-11-18 22:34:07 +01:00
|
|
|
return nil, err
|
|
|
|
}
|
2024-11-18 22:39:35 +01:00
|
|
|
|
2024-11-18 22:34:07 +01:00
|
|
|
return objectBytes, nil
|
|
|
|
} else {
|
|
|
|
return os.ReadFile(filepath.Join(s.config.Path, name))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type WriteOptions struct {
|
|
|
|
S3Tags map[string]string
|
|
|
|
}
|
|
|
|
|
2024-11-18 23:00:58 +01:00
|
|
|
// Write writes to a file within the Storage interface.
|
|
|
|
// It takes a name, a []byte, and WriteOptions as its parameters.
|
|
|
|
// The function returns an error, if any.
|
2024-11-18 22:34:07 +01:00
|
|
|
func (s *Storage) Write(name string, data []byte, opts WriteOptions) error {
|
|
|
|
if s.s3Client != nil {
|
|
|
|
if _, err := s.s3Client.PutObject(s.ctx, s.config.S3BucketName, name, bytes.NewReader(data), int64(len(data)), minio.PutObjectOptions{
|
|
|
|
UserTags: opts.S3Tags,
|
|
|
|
}); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
} else {
|
2024-11-18 23:04:50 +01:00
|
|
|
return os.WriteFile(filepath.Join(s.config.Path, name), data, 0600)
|
2024-11-18 22:34:07 +01:00
|
|
|
}
|
|
|
|
}
|
2024-11-18 22:48:49 +01:00
|
|
|
|
2024-11-18 23:00:58 +01:00
|
|
|
// Delete deleted a file within the storage interface.
|
|
|
|
// It takes a file name as its parameter.
|
|
|
|
// The function returns an error, if any.
|
2024-11-18 22:48:49 +01:00
|
|
|
func (s *Storage) Delete(name string) error {
|
|
|
|
if s.s3Client != nil {
|
|
|
|
if err := s.s3Client.RemoveObject(s.ctx, s.config.S3BucketName, name, minio.RemoveObjectOptions{}); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
} else {
|
|
|
|
return os.Remove(filepath.Join(s.config.Path, name))
|
|
|
|
}
|
|
|
|
}
|
2024-11-19 02:54:41 +01:00
|
|
|
|
|
|
|
type FileInfo struct {
|
|
|
|
Name string
|
|
|
|
Size int64
|
|
|
|
ModTime time.Time
|
|
|
|
IsDir bool
|
|
|
|
Mode os.FileMode
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stat grabs the file info from the Storage interface.
|
|
|
|
// It takes a file name as its parameter.
|
|
|
|
// The function returns FileInfo, and an error, if any.
|
|
|
|
func (s *Storage) Stat(name string) (*FileInfo, error) {
|
|
|
|
fileInfo := new(FileInfo)
|
|
|
|
|
|
|
|
if s.s3Client != nil {
|
|
|
|
objInfo, err := s.s3Client.StatObject(s.ctx, s.config.S3BucketName, name, minio.StatObjectOptions{})
|
|
|
|
if err != nil {
|
|
|
|
errResp := minio.ToErrorResponse(err)
|
|
|
|
if errResp.StatusCode == http.StatusNotFound {
|
|
|
|
return nil, ErrFileNotFound
|
|
|
|
} else {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fileInfo.Name = objInfo.Key
|
|
|
|
fileInfo.Size = objInfo.Size
|
|
|
|
fileInfo.ModTime = objInfo.LastModified
|
|
|
|
fileInfo.Mode = 0777 // Used as a fake perm. Doesn't actually do anything.
|
|
|
|
} else {
|
|
|
|
fInfo, err := os.Stat(filepath.Join(s.config.Path, name))
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
|
|
return nil, ErrFileNotFound
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
fileInfo.Name = fInfo.Name()
|
|
|
|
fileInfo.Size = fInfo.Size()
|
|
|
|
fileInfo.ModTime = fInfo.ModTime()
|
|
|
|
fileInfo.Mode = fInfo.Mode()
|
2024-11-19 03:10:54 +01:00
|
|
|
fileInfo.IsDir = fInfo.IsDir()
|
2024-11-19 02:54:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return fileInfo, nil
|
|
|
|
}
|
2024-11-19 14:56:43 +01:00
|
|
|
|
|
|
|
// Copy copies a file from one place to another.
|
|
|
|
// It takes the file name, and the destination name as its parameter.
|
|
|
|
// The function returns an error, if any.
|
|
|
|
func (s *Storage) Copy(name string, dest string) error {
|
|
|
|
if s.s3Client != nil {
|
|
|
|
if _, err := s.s3Client.CopyObject(s.ctx, minio.CopyDestOptions{
|
|
|
|
Bucket: s.config.S3BucketName,
|
|
|
|
Object: dest,
|
|
|
|
}, minio.CopySrcOptions{
|
|
|
|
Bucket: s.config.S3BucketName,
|
|
|
|
Object: name,
|
|
|
|
}); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
} else {
|
|
|
|
fileContent, err := os.ReadFile(filepath.Join(s.config.Path, name))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := os.WriteFile(filepath.Join(s.config.Path, dest), fileContent, 0600); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
2024-11-19 15:20:53 +01:00
|
|
|
|
2024-11-19 15:24:58 +01:00
|
|
|
// Rename is a wrapper over the Move function.
|
|
|
|
func (s *Storage) Rename(name, dest string) error {
|
|
|
|
return s.Move(name, dest)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Move moves a file from one place to another.
|
|
|
|
// It takes a file name, and the destination name as its parameter.
|
|
|
|
// The function returns an error, if any.
|
2024-11-19 15:20:53 +01:00
|
|
|
func (s *Storage) Move(name string, dest string) error {
|
|
|
|
if s.s3Client != nil {
|
|
|
|
if _, err := s.s3Client.CopyObject(s.ctx, minio.CopyDestOptions{
|
|
|
|
Bucket: s.config.S3BucketName,
|
|
|
|
Object: dest,
|
|
|
|
}, minio.CopySrcOptions{
|
|
|
|
Bucket: s.config.S3BucketName,
|
|
|
|
Object: name,
|
|
|
|
}); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := s.s3Client.RemoveObject(s.ctx, s.config.S3BucketName, name, minio.RemoveObjectOptions{}); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
} else {
|
|
|
|
return os.Rename(filepath.Join(s.config.Path, name), filepath.Join(s.config.Path, dest))
|
|
|
|
}
|
|
|
|
}
|