mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-06-04 04:23:00 +00:00
160 lines
4.1 KiB
Go
160 lines
4.1 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type PutResult struct {
|
|
Path string
|
|
SizeBytes int64
|
|
SHA256Digest string
|
|
}
|
|
|
|
type LocalStore struct {
|
|
root string
|
|
}
|
|
|
|
func NewLocalStore(root string) (*LocalStore, error) {
|
|
if err := os.MkdirAll(root, 0o755); err != nil {
|
|
return nil, fmt.Errorf("create storage root: %w", err)
|
|
}
|
|
return &LocalStore{root: root}, nil
|
|
}
|
|
|
|
func (s *LocalStore) Put(ctx context.Context, bucketID, objectKey string, reader io.Reader) (PutResult, error) {
|
|
cleanKey, err := sanitizeObjectKey(objectKey)
|
|
if err != nil {
|
|
return PutResult{}, err
|
|
}
|
|
dir := filepath.Join(s.root, bucketID)
|
|
if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, cleanKey)), 0o755); err != nil {
|
|
return PutResult{}, fmt.Errorf("create object directory: %w", err)
|
|
}
|
|
path := filepath.Join(dir, cleanKey)
|
|
tmpPath := path + ".tmp"
|
|
file, err := os.Create(tmpPath)
|
|
if err != nil {
|
|
return PutResult{}, fmt.Errorf("create temp object: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
hasher := sha256.New()
|
|
writer := io.MultiWriter(file, hasher)
|
|
written, err := copyWithContext(ctx, writer, reader)
|
|
if err != nil {
|
|
_ = os.Remove(tmpPath)
|
|
return PutResult{}, err
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
return PutResult{}, fmt.Errorf("close temp object: %w", err)
|
|
}
|
|
if err := os.Rename(tmpPath, path); err != nil {
|
|
return PutResult{}, fmt.Errorf("rename object: %w", err)
|
|
}
|
|
return PutResult{
|
|
Path: path,
|
|
SizeBytes: written,
|
|
SHA256Digest: hex.EncodeToString(hasher.Sum(nil)),
|
|
}, nil
|
|
}
|
|
|
|
func (s *LocalStore) Open(bucketID, objectKey string) (*os.File, string, error) {
|
|
cleanKey, err := sanitizeObjectKey(objectKey)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
path := filepath.Join(s.root, bucketID, cleanKey)
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("open object: %w", err)
|
|
}
|
|
return file, path, nil
|
|
}
|
|
|
|
func (s *LocalStore) Delete(bucketID, objectKey string) error {
|
|
cleanKey, err := sanitizeObjectKey(objectKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.Remove(filepath.Join(s.root, bucketID, cleanKey)); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("delete object: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *LocalStore) Move(bucketID, fromKey, toKey string) (string, error) {
|
|
return s.MoveBetweenBuckets(bucketID, bucketID, fromKey, toKey)
|
|
}
|
|
|
|
func (s *LocalStore) MoveBetweenBuckets(sourceBucketID, destinationBucketID, fromKey, toKey string) (string, error) {
|
|
cleanFrom, err := sanitizeObjectKey(fromKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
cleanTo, err := sanitizeObjectKey(toKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
fromPath := filepath.Join(s.root, strings.TrimSpace(sourceBucketID), cleanFrom)
|
|
toPath := filepath.Join(s.root, strings.TrimSpace(destinationBucketID), cleanTo)
|
|
if err := os.MkdirAll(filepath.Dir(toPath), 0o755); err != nil {
|
|
return "", fmt.Errorf("create destination directory: %w", err)
|
|
}
|
|
if err := os.Rename(fromPath, toPath); err != nil {
|
|
return "", fmt.Errorf("move object: %w", err)
|
|
}
|
|
return toPath, nil
|
|
}
|
|
|
|
func (s *LocalStore) DeleteBucket(bucketID string) error {
|
|
bucketPath := filepath.Join(s.root, strings.TrimSpace(bucketID))
|
|
if err := os.RemoveAll(bucketPath); err != nil {
|
|
return fmt.Errorf("delete bucket directory: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sanitizeObjectKey(key string) (string, error) {
|
|
clean := filepath.Clean(strings.TrimSpace(key))
|
|
if clean == "." || clean == "" || strings.HasPrefix(clean, "../") || strings.Contains(clean, "/../") || strings.HasPrefix(clean, "/") {
|
|
return "", fmt.Errorf("invalid object key")
|
|
}
|
|
return clean, nil
|
|
}
|
|
|
|
func copyWithContext(ctx context.Context, dst io.Writer, src io.Reader) (int64, error) {
|
|
buffer := make([]byte, 32*1024)
|
|
var written int64
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return written, ctx.Err()
|
|
default:
|
|
}
|
|
nr, er := src.Read(buffer)
|
|
if nr > 0 {
|
|
nw, ew := dst.Write(buffer[:nr])
|
|
written += int64(nw)
|
|
if ew != nil {
|
|
return written, ew
|
|
}
|
|
if nr != nw {
|
|
return written, io.ErrShortWrite
|
|
}
|
|
}
|
|
if er != nil {
|
|
if er == io.EOF {
|
|
return written, nil
|
|
}
|
|
return written, er
|
|
}
|
|
}
|
|
}
|