///|
/// What a storage backend must be able to do.
///
/// This is the raw seam -- OpenDAL's `Access` rather than its `Operator`. Two
/// obligations on an implementor, and both are things a hand-written backend
/// gets wrong:
///
/// - **Paths arrive normalised and validated.** `Operator` runs every path
/// through `validate` before it gets here: no leading `/`, no `..`, no `//`,
/// no `.`, and a trailing `/` exactly when the caller meant a directory. A
/// backend must not re-normalise, and may concatenate the path onto its own
/// root without escaping it -- which is precisely why validation is not
/// optional and not the backend's job.
///
/// - **Nothing from the backend's own dependencies escapes.** Every failure
/// leaves as a `SosError`, or a caller's `catch` sees a type from a package it
/// never imported.
///
/// Implement it and hand the value to `Operator::new`; callers go through the
/// operator, not through this.
pub(open) trait Store {
/// Who this is and what it can do.
///
/// Must be cheap and pure: `Operator::new` calls it once and caches the
/// answer, so a backend cannot change its mind later.
fn info(Self) -> StoreInfo
/// The whole object, or the requested range of it.
///
/// `NotFound` when absent, `IsADirectory` when the path names a directory,
/// `RangeNotSatisfied` when the range starts past the end.
async fn read(Self, String, ReadOptions) -> Bytes raise SosError
/// Replace, append to, or create the object at this path.
///
/// Creates every missing ancestor directory. A filesystem needs `mkdir -p`
/// here; an object store needs nothing. Making that the backend's problem is
/// what lets `write("a/b/c.txt")` mean the same thing on all three.
async fn write(Self, String, Bytes, WriteOptions) -> Unit raise SosError
/// Metadata only. `NotFound` when absent.
async fn stat(Self, String) -> Metadata raise SosError
/// The contents of a directory, in ascending path order.
///
/// The path is a directory path; `""` is the root. Non-recursive returns the
/// immediate children, with a `Dir` entry per child directory whether or not
/// the backend has real ones. Recursive returns every descendant, directories
/// included, still in ascending path order.
///
/// A directory with nothing under it returns `[]`. It does NOT raise
/// `NotFound`: a filesystem could tell absent from empty and an object store
/// cannot, and the portable answer is the one both can give.
async fn list(Self, String, ListOptions) -> Array[Entry] raise SosError
/// Remove.
///
/// An absent path is success, not `NotFound` -- delete is idempotent, so a
/// retry after a half-seen failure does not have to distinguish. A non-empty
/// directory without `recursive` raises `DirectoryNotEmpty`.
async fn delete(Self, String, DeleteOptions) -> Unit raise SosError
/// `mkdir -p`. Idempotent. The path ends in `/`.
async fn create_dir(Self, String) -> Unit raise SosError
async fn copy(Self, String, String) -> Unit raise SosError = _
async fn rename(Self, String, String) -> Unit raise SosError = _
}
///|
/// Read the source and write it at the destination.
///
/// Correct for every backend and native to none, which is exactly why it is a
/// default rather than a required method: a backend without a server-side copy
/// would write this, and a backend with one should override. `FsStore` keeps it,
/// because `moonbitlang/async/fs` has no `copyfile`; `MemoryStore` and
/// `IdbStore` override, because for them a copy is one insert.
///
/// Neither atomic nor cheap: the whole object goes through memory, and a reader
/// at the destination sees the old bytes until the write lands.
/// `Capability::copy` says the operation exists, not that it is either of those.
impl Store with fn copy(self, from, to) {
let meta = self.stat(from)
guard meta.mode is File else {
raise SosError::new(
IsADirectory,
operation="copy",
path=from,
message="copy source is a directory",
)
}
let content = self.read(from, ReadOptions::default())
self.write(to, content, {
..WriteOptions::default(),
content_type: meta.content_type,
})
}
///|
/// Copy, then delete the source.
///
/// A backend with a real rename should override -- every filesystem has one, and
/// for the two object stores it is an insert plus a delete. Both because that is
/// one operation instead of three, and because this one has both copies present
/// in between.
impl Store with fn rename(self, from, to) {
self.copy(from, to)
self.delete(from, DeleteOptions::default())
}