diff --git a/apps/backend/internal/blob/err.go b/apps/backend/internal/blob/err.go new file mode 100644 index 0000000..d1e9614 --- /dev/null +++ b/apps/backend/internal/blob/err.go @@ -0,0 +1,8 @@ +package blob + +import "errors" + +var ( + ErrConflict = errors.New("key already used for a different blob") + ErrNotFound = errors.New("key not found") +) diff --git a/apps/backend/internal/blob/fs_store.go b/apps/backend/internal/blob/fs_store.go new file mode 100644 index 0000000..9053b21 --- /dev/null +++ b/apps/backend/internal/blob/fs_store.go @@ -0,0 +1,95 @@ +package blob + +import ( + "context" + "io" + "os" + "path/filepath" +) + +var _ Store = &FSStore{} + +type FSStore struct { + config FSStoreConfig +} + +type FSStoreConfig struct { + Root string +} + +func NewFSStore(config FSStoreConfig) *FSStore { + return &FSStore{config: config} +} + +func (s *FSStore) Put(ctx context.Context, key Key, reader io.Reader) error { + path := filepath.Join(s.config.Root, string(key)) + + err := os.MkdirAll(filepath.Dir(path), 0755) + if err != nil { + return err + } + + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644) + if err != nil { + if os.IsExist(err) { + return ErrConflict + } + return err + } + + defer f.Close() + _, err = io.Copy(f, reader) + if err != nil { + _ = os.Remove(path) + return err + } + + return nil +} + +func (s *FSStore) Retrieve(ctx context.Context, key Key) (io.ReadCloser, error) { + path := filepath.Join(s.config.Root, string(key)) + f, err := os.Open(path) + if err != nil { + if os.IsNotExist(err) { + return nil, ErrNotFound + } + return nil, err + } + return f, nil +} + +func (s *FSStore) Delete(ctx context.Context, key Key) error { + err := os.Remove(filepath.Join(s.config.Root, string(key))) + // no op if file does not exist + // swallow error if file does not exist + if err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +func (s *FSStore) Move(ctx context.Context, srcKey, dstKey Key) error { + oldPath := filepath.Join(s.config.Root, string(srcKey)) + newPath := filepath.Join(s.config.Root, string(dstKey)) + + _, err := os.Stat(newPath) + if err == nil { + return ErrConflict + } + + err = os.MkdirAll(filepath.Dir(newPath), 0755) + if err != nil { + return err + } + + err = os.Rename(oldPath, newPath) + if err != nil { + if os.IsNotExist(err) { + return ErrNotFound + } + return err + } + + return nil +} diff --git a/apps/backend/internal/blob/key.go b/apps/backend/internal/blob/key.go new file mode 100644 index 0000000..fc7a04d --- /dev/null +++ b/apps/backend/internal/blob/key.go @@ -0,0 +1,3 @@ +package blob + +type Key string diff --git a/apps/backend/internal/blob/store.go b/apps/backend/internal/blob/store.go new file mode 100644 index 0000000..c0996a0 --- /dev/null +++ b/apps/backend/internal/blob/store.go @@ -0,0 +1,13 @@ +package blob + +import ( + "context" + "io" +) + +type Store interface { + Put(ctx context.Context, key Key, reader io.Reader) error + Retrieve(ctx context.Context, key Key) (io.ReadCloser, error) + Delete(ctx context.Context, key Key) error + Move(ctx context.Context, srcKey, dstKey Key) error +}