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.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) 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 }