pub struct Client { /* private fields */ }Expand description
Async HTTP client for LoonFS.
Cloning is cheap: clones share one connection pool and one capability cache.
Implementations§
Source§impl Client
impl Client
Sourcepub fn new(config: ClientConfig) -> Result<Self>
pub fn new(config: ClientConfig) -> Result<Self>
Creates a client, validating the config exactly as
ClientConfig::load does — direct Rust construction cannot bypass
validation.
Sourcepub async fn capabilities(&self) -> Result<CapabilityDocument>
pub async fn capabilities(&self) -> Result<CapabilityDocument>
Returns the server’s capability document, fetched once and cached for the life of this client and its clones (API spec, “Capability discovery”).
Feature keys that are not parented by an advertised profile are dropped rather than trusted, per the spec’s client guidance for malformed documents.
pub async fn create_namespace( &self, namespace_id: &NamespaceId, ) -> Result<NamespaceSummary>
pub async fn namespace_status( &self, namespace_id: &NamespaceId, ) -> Result<NamespaceStatusResponse>
Sourcepub async fn delete_namespace(
&self,
namespace_id: &NamespaceId,
expected_head_seq: Option<ChangeSeq>,
) -> Result<DeleteNamespaceResponse>
pub async fn delete_namespace( &self, namespace_id: &NamespaceId, expected_head_seq: Option<ChangeSeq>, ) -> Result<DeleteNamespaceResponse>
Deletes a namespace (feature core.namespaces.delete): terminal,
and the id is permanently retired. Pass expected_head_seq to delete
only if the namespace is still where you last observed it
(stale_head on mismatch). Deleting an already-deleted namespace
fails with namespace_deleted.
pub async fn fork_namespace( &self, source_namespace_id: &NamespaceId, new_namespace_id: &NamespaceId, ) -> Result<NamespaceSummary>
Sourcepub async fn list_path_entries_all(
&self,
spec: &NamespacePath,
) -> Result<ListPathEntriesResponse>
pub async fn list_path_entries_all( &self, spec: &NamespacePath, ) -> Result<ListPathEntriesResponse>
Lists a directory by aggregating every page into one response.
Listing cursors tolerate commits landing mid-listing — each page
resumes in name-key order against the head the server has loaded —
so aggregation never restarts. The envelope’s head_seq reports the
newest head that served a page. Use
Self::list_path_entries_page for page-level control.
pub async fn list_path_entries_page( &self, spec: &NamespacePath, limit: Option<u32>, cursor: Option<&str>, ) -> Result<ListPathEntriesResponse>
pub async fn stat_path( &self, spec: &NamespacePath, ) -> Result<AuthoritativePathEntry>
pub async fn get_file_bytes(&self, spec: &NamespacePath) -> Result<Vec<u8>>
pub async fn get_file_revision_bytes( &self, spec: &NamespacePath, revision_no: RevisionNo, ) -> Result<Vec<u8>>
Sourcepub async fn offers_direct_download(&self, size_bytes: u64) -> bool
pub async fn offers_direct_download(&self, size_bytes: u64) -> bool
Whether this deployment would refuse to proxy a file of this size but can authorize a direct read of it.
The two halves are one question. Under the advertised proxy cap the
proxied read is the simpler path and stays the default; over it the
proxied read answers content_too_large, and a deployment that
advertises core.downloads.direct_get can hand the object back
instead — which is the whole point of the capability, because that
same deployment is one that let a client create the object directly.
A deployment that advertises no cap is left on the proxied path: nothing here knows it would refuse.
Sourcepub async fn begin_download(
&self,
spec: &NamespacePath,
revision_no: Option<RevisionNo>,
) -> Result<BeginDownloadResponse>
pub async fn begin_download( &self, spec: &NamespacePath, revision_no: Option<RevisionNo>, ) -> Result<BeginDownloadResponse>
Asks for one short-lived capability to read a file’s content object straight from the store.
Sourcepub async fn open_direct_download(
&self,
download: &BeginDownloadResponse,
) -> Result<DirectDownloadStream>
pub async fn open_direct_download( &self, download: &BeginDownloadResponse, ) -> Result<DirectDownloadStream>
Opens the response body authorized by a download grant as a bounded, verified stream.
Sourcepub async fn open_direct_download_at(
&self,
download: &BeginDownloadResponse,
start_offset: u64,
) -> Result<DirectDownloadStream>
pub async fn open_direct_download_at( &self, download: &BeginDownloadResponse, start_offset: u64, ) -> Result<DirectDownloadStream>
Opens a download grant’s body from start_offset, for a caller that
already holds the bytes below it.
The offset rides a Range header, which the presigned signature does
not cover: one grant serves the whole object or any part of it, so a
resumed download needs no different grant than a fresh one. The
stream still reports on the whole object, so a nonzero offset obliges
the caller to hand over what it holds through
DirectDownloadStream::fold_resumed_prefix before driving it.
Sourcepub async fn download_via_presigned_url<W>(
&self,
download: &BeginDownloadResponse,
sink: &mut W,
) -> Result<u64>where
W: AsyncWrite + Unpin,
pub async fn download_via_presigned_url<W>(
&self,
download: &BeginDownloadResponse,
sink: &mut W,
) -> Result<u64>where
W: AsyncWrite + Unpin,
Streams a granted object’s bytes into sink, checking them against
the reference the grant carried, and reports how many arrived.
The payload is never held: each chunk is hashed and written as it arrives, so this costs one chunk of memory whatever the object’s length. That is the entire reason the grant exists — a file past the deployment’s proxy cap has no other way home.
Verification is what keeps a direct read no weaker than a proxied one: the length always, and the whole-file SHA-256 whenever the reference carries one. A direct-multipart object carries none — nobody ever hashed it that way — and its length is then the whole check, exactly as it is for the server’s own reads.
A failure is reported after the sink has already received bytes, because that is the only order a streamed read allows. Callers must treat the sink as provisional until this returns: write to a temporary and install it on success.
pub async fn list_file_revisions_page( &self, spec: &NamespacePath, limit: Option<u32>, cursor: Option<&str>, ) -> Result<ListFileRevisionsResponse>
pub async fn list_trash_page( &self, namespace_id: &NamespaceId, limit: Option<u32>, cursor: Option<&str>, ) -> Result<ListTrashResponse>
pub async fn health(&self) -> Result<()>
pub async fn begin_upload( &self, namespace_id: &NamespaceId, request: &BeginUploadRequest, ) -> Result<BeginUploadResponse>
Sourcepub async fn begin_direct_put(
&self,
namespace_id: &NamespaceId,
claim: DirectPutContentClaim,
) -> Result<BeginUploadResponse>
pub async fn begin_direct_put( &self, namespace_id: &NamespaceId, claim: DirectPutContentClaim, ) -> Result<BeginUploadResponse>
Starts a direct upload of bytes the caller already has.
The claim is what the client can know about its own bytes; the server answers with the content object it minted for them, and that reference — not this claim — is what completion and the later commit name.
Sourcepub async fn begin_direct_multipart(
&self,
namespace_id: &NamespaceId,
options: DirectMultipartUploadOptions,
) -> Result<BeginUploadResponse>
pub async fn begin_direct_multipart( &self, namespace_id: &NamespaceId, options: DirectMultipartUploadOptions, ) -> Result<BeginUploadResponse>
Opens a direct multipart upload session.
Nothing about the payload is declared here — not its length, not its digest — so one pass over the bytes is enough and a stream of unknown length can start uploading immediately. The server answers with the part geometry to cut to and nothing else: not the bucket, not the key, not the provider’s upload id, and not the content identity, which it names back at completion.
Sourcepub async fn sign_upload_parts(
&self,
namespace_id: &NamespaceId,
upload_id: &UploadId,
parts: Vec<UploadPartChecksumClaim>,
) -> Result<SignUploadPartsResponse>
pub async fn sign_upload_parts( &self, namespace_id: &NamespaceId, upload_id: &UploadId, parts: Vec<UploadPartChecksumClaim>, ) -> Result<SignUploadPartsResponse>
Asks for one wave of checksum-bound part-upload capabilities.
Asking again for a part already uploaded is how a client retries it: a repeated part is last-write-wins at the provider, and the object’s checksum follows the bytes that stuck.
Sourcepub async fn upload_part_via_presigned_url(
&self,
part_number: u32,
access: &ObjectTransferAccess,
crc64nvme: String,
bytes: Bytes,
) -> Result<CompletedUploadPart>
pub async fn upload_part_via_presigned_url( &self, part_number: u32, access: &ObjectTransferAccess, crc64nvme: String, bytes: Bytes, ) -> Result<CompletedUploadPart>
Uploads one part and reports what the provider recorded for it.
The etag comes back to the caller rather than to the server: parts are the uploader’s bookkeeping all the way to completion, exactly as they are in the provider’s own multipart API.
pub async fn upload_via_presigned_url( &self, access: &ObjectTransferAccess, bytes: &[u8], ) -> Result<()>
pub async fn upload_content( &self, namespace_id: &NamespaceId, upload_id: &UploadId, bytes: &[u8], ) -> Result<UploadContentResponse>
Sourcepub async fn upload_streamed_content(
&self,
namespace_id: &NamespaceId,
upload_id: &UploadId,
source: PayloadSource,
) -> Result<UploadContentResponse>
pub async fn upload_streamed_content( &self, namespace_id: &NamespaceId, upload_id: &UploadId, source: PayloadSource, ) -> Result<UploadContentResponse>
Stages a payload that arrives in pieces, forwarding it to the server as it is read.
This is Self::upload_content for a caller that does not hold its
bytes: the payload crosses the client in bounded chunks and the
server hashes it as it forwards it on, so neither side ever holds the
object. A source whose length is unknown is sent with chunked
transfer encoding, and the server’s own limit is what bounds it.
Unlike the buffered call this one never resends: a stream is consumed by the attempt that reads it, so a failure here is the caller’s to handle with a fresh source.
Sourcepub async fn abort_upload(
&self,
namespace_id: &NamespaceId,
upload_id: &UploadId,
) -> Result<AbortUploadResponse>
pub async fn abort_upload( &self, namespace_id: &NamespaceId, upload_id: &UploadId, ) -> Result<AbortUploadResponse>
Ends an open upload session and deletes the object it was writing.
Repeating it succeeds and reports the abort that stands. This is what a one-pass upload does when its source fails partway: the session it opened must not be left holding a half-written object.
Sourcepub async fn read_upload_status(
&self,
namespace_id: &NamespaceId,
upload_id: &UploadId,
) -> Result<UploadStatusResponse>
pub async fn read_upload_status( &self, namespace_id: &NamespaceId, upload_id: &UploadId, ) -> Result<UploadStatusResponse>
Reads one upload session back.
A completed session answers with the exact content reference it settled on and a freshly minted validation token, so a caller that lost its completion response — or the whole process — can commit that content without re-uploading a byte. The upload id is the only thing it has to have kept.
pub async fn complete_upload( &self, namespace_id: &NamespaceId, upload_id: &UploadId, request: &CompleteUploadRequest, ) -> Result<CompleteUploadResponse>
pub async fn list_changes( &self, namespace_id: &NamespaceId, after_seq: ChangeSeq, limit: Option<u32>, ) -> Result<ChangesResponse>
Sourcepub async fn create_checkpoint(
&self,
namespace_id: &NamespaceId,
request: &CreateCheckpointRequest,
) -> Result<CreateCheckpointResponse>
pub async fn create_checkpoint( &self, namespace_id: &NamespaceId, request: &CreateCheckpointRequest, ) -> Result<CreateCheckpointResponse>
Creates or reuses a named, user-owned checkpoint pinning the namespace’s current view (admin plane). This is a maintenance operation, not a file mutation. The record is a garbage-collection root until released or expired.
Sourcepub async fn list_checkpoints(
&self,
namespace_id: &NamespaceId,
) -> Result<ListCheckpointsResponse>
pub async fn list_checkpoints( &self, namespace_id: &NamespaceId, ) -> Result<ListCheckpointsResponse>
Lists the namespace’s active checkpoint records, oldest first (admin plane).
A checkpoint name is a label rather than a key, so this is how a pin is found again once its creation response is gone. An expired record that no collection pass has released yet is still listed, with its expiry in the entry.
Sourcepub async fn release_checkpoint(
&self,
namespace_id: &NamespaceId,
checkpoint_id: &CheckpointId,
) -> Result<ReleaseCheckpointResponse>
pub async fn release_checkpoint( &self, namespace_id: &NamespaceId, checkpoint_id: &CheckpointId, ) -> Result<ReleaseCheckpointResponse>
Releases a user-owned checkpoint pin by id (admin plane). Idempotent: releasing an already-released or reaped record succeeds.
Sourcepub async fn maintenance_step(
&self,
namespace_id: &NamespaceId,
request: &MaintenanceStepRequest,
) -> Result<MaintenanceStepResponse>
pub async fn maintenance_step( &self, namespace_id: &NamespaceId, request: &MaintenanceStepRequest, ) -> Result<MaintenanceStepResponse>
Runs one bounded maintenance step against a namespace (admin plane). Absent request fields use the server’s defaults; garbage collection runs only when the request opts in.
Sourcepub async fn probe_store(
&self,
request: &StoreProbeRequest,
) -> Result<StoreProbeResponse>
pub async fn probe_store( &self, request: &StoreProbeRequest, ) -> Result<StoreProbeResponse>
Proves the server’s backing store honours the object-store contract LoonFS depends on (admin plane).
The probe writes and deletes objects under a scratch prefix, so it runs only when asked. A store that fails a check answers with that check reported failed rather than with an error: the probe ran, and the answer is that the store is wrong.
Sourcepub async fn grep(
&self,
namespace_id: &NamespaceId,
request: &GrepRequest,
) -> Result<GrepResponse>
pub async fn grep( &self, namespace_id: &NamespaceId, request: &GrepRequest, ) -> Result<GrepResponse>
Content search over the namespace’s grep index (query plane).
Gate on the query.grep capability before calling against unknown
deployments; the namespace must also have a materialized steady-state
grep root or the server answers not_supported.
Sourcepub async fn grep_index_status(
&self,
namespace_id: &NamespaceId,
) -> Result<GrepIndexStatusResponse>
pub async fn grep_index_status( &self, namespace_id: &NamespaceId, ) -> Result<GrepIndexStatusResponse>
Reads the namespace’s grep-index lifecycle (admin plane): disabled, backfilling toward a captured sequence, or steady at a watermark. One grep root read on the server, with no side effects.
Sourcepub async fn enable_grep_index(
&self,
namespace_id: &NamespaceId,
) -> Result<EnableGrepIndexResponse>
pub async fn enable_grep_index( &self, namespace_id: &NamespaceId, ) -> Result<EnableGrepIndexResponse>
Enables the namespace’s grep root (admin plane); embedded mode starts that namespace’s event-driven backfill. Idempotent.
Sourcepub async fn disable_grep_index(
&self,
namespace_id: &NamespaceId,
) -> Result<DisableGrepIndexResponse>
pub async fn disable_grep_index( &self, namespace_id: &NamespaceId, ) -> Result<DisableGrepIndexResponse>
Disables the namespace’s grep root (admin plane); garbage collection reclaims the segments. Idempotent.
Sourcepub async fn gc_grep_index(
&self,
namespace_id: &NamespaceId,
request: &GrepGcRequest,
) -> Result<GrepGcResponse>
pub async fn gc_grep_index( &self, namespace_id: &NamespaceId, request: &GrepGcRequest, ) -> Result<GrepGcResponse>
Runs one explicit grep-index garbage-collection pass for a namespace.
max_objects bounds the reads the pass spends; when keys remain the
response carries a next_cursor to resume from.
Sourcepub async fn commit(
&self,
namespace_id: &NamespaceId,
request: &CommitRequest,
) -> Result<ApiCommitResponse>
pub async fn commit( &self, namespace_id: &NamespaceId, request: &CommitRequest, ) -> Result<ApiCommitResponse>
Applies one commit: its operations land together, in order, under one commit id.
The convenience methods below are the one-element case of this call.
Operations that introduce new external content carry their proofs in
the request’s content_tokens; stage the bytes with the upload
methods first.
Sourcepub async fn put_file_bytes(
&self,
spec: &NamespacePath,
bytes: &[u8],
options: &PutFileOptions,
) -> Result<ApiCommitResponse>
pub async fn put_file_bytes( &self, spec: &NamespacePath, bytes: &[u8], options: &PutFileOptions, ) -> Result<ApiCommitResponse>
Uploads bytes and commits them at a path.
Re-running this with a commit_id that already committed is safe
when the request is the same. A commit’s identity names which
content object it wrote, and a re-run necessarily uploads a fresh
one, so the server sees a different commit and reports
commit_id_reuse_conflict. This resolves that the only honest way
available: it reads back what the commit id actually committed and
compares it against the request just made. The same content under
the same message means the operation had already succeeded and the
original answer is returned; anything else — different bytes, a
different message, or content this build cannot compare — surfaces
the conflict.
The freshly uploaded duplicate object is then referenced by nothing. That is by design, not a leak: content garbage collection reclaims an unreferenced completed upload once its grace passes.
Sourcepub async fn put_file_stream(
&self,
spec: &NamespacePath,
source: PayloadSource,
options: &PutFileOptions,
) -> Result<ApiCommitResponse>
pub async fn put_file_stream( &self, spec: &NamespacePath, source: PayloadSource, options: &PutFileOptions, ) -> Result<ApiCommitResponse>
Uploads a payload read once from its source and commits it at a path.
This is Self::put_file_bytes for a caller that should not hold
its payload: the source is read forward in bounded pieces, hashed as
it goes, and never assembled. What the memory costs is the transport’s
window and nothing about the payload’s size — so a file larger than
this process could hold, or a pipe whose length nobody knows, both
upload the same way.
Where the deployment authorizes direct part uploads the payload goes
straight to object storage in parts, and otherwise it streams through
the server. One bound is worth knowing on the direct path: a provider
assembles at most 10,000 parts, so a session carries at most
part_size_bytes × 10_000 and a longer payload is refused when it
asks to authorize the part past that ceiling. A caller that knows its
payload is enormous should not be taking the default geometry.
Retrying with a commit_id that already committed is safe here in
the same way it is for Self::put_file_bytes, and by the same
evidence: this pass measured the payload’s length and folded its
digest, which is what the reconciliation compares. The digest is
CRC-64/NVME on the direct path, because that is what a provider
computes over an assembled object.
Sourcepub async fn put_file_stream_resumable(
&self,
spec: &NamespacePath,
source: PayloadSource,
options: &PutFileOptions,
journal: &dyn MultipartUploadJournal,
resume: Option<&MultipartUploadResume>,
) -> Result<ApiCommitResponse>
pub async fn put_file_stream_resumable( &self, spec: &NamespacePath, source: PayloadSource, options: &PutFileOptions, journal: &dyn MultipartUploadJournal, resume: Option<&MultipartUploadResume>, ) -> Result<ApiCommitResponse>
Self::put_file_stream for a caller that intends to survive an
interruption.
journal is told the session’s id and geometry when it opens and
told every part as it lands; resume hands back what an earlier run
of the same upload recorded, so this one sends only the parts still
missing. Both apply to the direct multipart transport alone: a
proxied upload is a single request with no session behind it and
nothing to resume from, and it ignores them.
The source is read from its first byte either way. That is not waste — the checksum the assembly is verified against covers the whole object, so every byte has to be folded whether or not it also has to be sent — and it means the source has to be one that can be opened again. A pipe cannot, which is why nothing that reads one should be calling this.
Sourcepub async fn commit_completed_upload(
&self,
spec: &NamespacePath,
content_ref: ContentRef,
validated_content_token: Option<String>,
options: &PutFileOptions,
) -> Result<ApiCommitResponse>
pub async fn commit_completed_upload( &self, spec: &NamespacePath, content_ref: ContentRef, validated_content_token: Option<String>, options: &PutFileOptions, ) -> Result<ApiCommitResponse>
Commits content an upload session already completed, for a caller that finished the transfer and was interrupted before the commit.
The reference and the token come from
Self::read_upload_status reporting the session Completed: the
bytes are in object storage and admitted, so the only thing left to
do is name them at a path. Nothing is re-uploaded.
pub async fn create_directory( &self, spec: &NamespacePath, options: &CreateDirectoryOptions, ) -> Result<ApiCommitResponse>
pub async fn delete_path( &self, spec: &NamespacePath, options: &DeleteOptions, ) -> Result<ApiCommitResponse>
pub async fn move_path( &self, from: &NamespacePath, to: &NamespacePath, options: &MoveOptions, ) -> Result<ApiCommitResponse>
pub async fn copy_path( &self, from: &NamespacePath, to: &NamespacePath, options: &CopyOptions, ) -> Result<ApiCommitResponse>
Sourcepub async fn undelete(
&self,
namespace: &NamespaceId,
inode_id: InodeId,
deleted_at_seq: ChangeSeq,
path: Option<&AbsolutePath>,
options: &UndeleteOptions,
) -> Result<ApiCommitResponse>
pub async fn undelete( &self, namespace: &NamespaceId, inode_id: InodeId, deleted_at_seq: ChangeSeq, path: Option<&AbsolutePath>, options: &UndeleteOptions, ) -> Result<ApiCommitResponse>
Recovers a deleted file or subtree: clears the tombstone rooted at
inode_id (the id the delete reported) and re-binds it at the spec’s
path.