package storage
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
)
type Disk struct {
root string
}
func NewDisk(root string) (*Disk, error) {
if root == "" {
return nil, fmt.Errorf("storage: disk backend requires a directory")
}
if err := os.MkdirAll(root, 0o755); err != nil {
return nil, fmt.Errorf("storage: create disk root: %w", err)
}
return &Disk{root: filepath.Clean(root)}, nil
}
func (d *Disk) path(key string) (string, error) {
if err := ValidateKey(key); err != nil {
return "", err
}
return filepath.Join(d.root, filepath.FromSlash(key)), nil
}
func (d *Disk) Get(_ context.Context, key string) (io.ReadCloser, error) {
p, err := d.path(key)
if err != nil {
return nil, err
}
f, err := os.Open(p)
if err != nil {
if os.IsNotExist(err) {
return nil, ErrNotExist
}
return nil, fmt.Errorf("storage: get %q: %w", key, err)
}
return f, nil
}
func (d *Disk) Put(_ context.Context, key string, r io.Reader) error {
p, err := d.path(key)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
return fmt.Errorf("storage: put %q: %w", key, err)
}
// dont let readers see a partial object
tmp, err := os.CreateTemp(filepath.Dir(p), ".tmp-*")
if err != nil {
return fmt.Errorf("storage: put %q: %w", key, err)
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if _, err := io.Copy(tmp, r); err != nil {
_ = tmp.Close()
return fmt.Errorf("storage: put %q: %w", key, err)
}
if err := tmp.Sync(); err != nil {
_ = tmp.Close()
return fmt.Errorf("storage: put %q: %w", key, err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("storage: put %q: %w", key, err)
}
if err := os.Rename(tmpName, p); err != nil {
return fmt.Errorf("storage: put %q: %w", key, err)
}
return nil
}
func (d *Disk) Stat(_ context.Context, key string) (int64, error) {
p, err := d.path(key)
if err != nil {
return 0, err
}
info, err := os.Stat(p)
if err != nil {
if os.IsNotExist(err) {
return 0, ErrNotExist
}
return 0, fmt.Errorf("storage: stat %q: %w", key, err)
}
return info.Size(), nil
}
func (d *Disk) PutIfAbsent(_ context.Context, key string, r io.Reader) error {
p, err := d.path(key)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
return err
}
tmp, err := os.CreateTemp(filepath.Dir(p), ".tmp-*")
if err != nil {
return fmt.Errorf("storage: put %q: %w", key, err)
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if _, err := io.Copy(tmp, r); err != nil {
_ = tmp.Close()
return fmt.Errorf("storage: put %q: %w", key, err)
}
if err := tmp.Sync(); err != nil {
_ = tmp.Close()
return fmt.Errorf("storage: put %q: %w", key, err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("storage: put %q: %w", key, err)
}
if err := os.Link(tmpName, p); err != nil {
return fmt.Errorf("storage: put %q: %w", key, err)
}
return nil
}
func (d *Disk) Delete(_ context.Context, key string) error {
p, err := d.path(key)
if err != nil {
return err
}
if err := os.Remove(p); err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("storage: delete %q: %w", key, err)
}
return nil
}