Open an FTP or FTPS server as a folder in Memona
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
package memona:filesystem@0.1.0;
|
||||
|
||||
/// Host services scoped to the current connection. No ambient filesystem,
|
||||
/// environment, sockets, credentials, or network permissions are inherited.
|
||||
interface host {
|
||||
/// Stable failures without backend messages or credential-bearing URLs.
|
||||
enum host-error { denied, unavailable, invalid-input, cancelled, internal }
|
||||
/// Severity of a diagnostic event. The host may discard message contents.
|
||||
enum log-level { debug, info, warn, error }
|
||||
|
||||
/// Every method the host will send. A verb outside this list cannot be
|
||||
/// spelled, so no plugin can smuggle one past the transport rules.
|
||||
enum method {
|
||||
get, head, post, put, patch, delete,
|
||||
propfind, proppatch, mkcol, copy, move, lock, unlock, report,
|
||||
}
|
||||
/// An HTTP header. Host-controlled transport headers cannot be overridden.
|
||||
record header { name: string, value: string }
|
||||
/// Request head. At most 64 headers of 8 KiB each; the body is streamed.
|
||||
record request { method: method, url: string, headers: list<header> }
|
||||
/// Response head. The body is read from the exchange that produced it.
|
||||
record response { status: u16, headers: list<header> }
|
||||
/// One HTTP request and its response, streamed in bounded chunks so a
|
||||
/// transfer is never held whole in memory. A body of any size crosses in
|
||||
/// pieces, which is what multipart uploads of at least 5 MiB require.
|
||||
/// Dropping the handle abandons the request.
|
||||
resource exchange {
|
||||
/// Appends at most 1,048,576 bytes to the request body. Every push
|
||||
/// must land before `respond`.
|
||||
push: func(data: list<u8>) -> result<_, host-error>;
|
||||
/// Ends the request body and waits for the response head. Calling it
|
||||
/// twice is invalid-input.
|
||||
respond: func() -> result<response, host-error>;
|
||||
/// Reads at most 1,048,576 bytes of the response body, after
|
||||
/// `respond`. An empty result means the body is complete.
|
||||
pull: func(limit: u32) -> result<list<u8>, host-error>;
|
||||
}
|
||||
/// Begins a request to a host both declared by the package and granted on
|
||||
/// the connection. Each redirect and DNS result is checked, and DNS is
|
||||
/// pinned. Private/LAN addresses additionally require the private-network
|
||||
/// grant. Cross-origin redirects remove request headers; HTTPS cannot
|
||||
/// downgrade. The host bounds total bytes per exchange, not buffer size.
|
||||
http: func(request: request) -> result<exchange, host-error>;
|
||||
|
||||
/// One open TCP connection. Dropping the handle closes the socket, and the
|
||||
/// host closes every socket when the connection is disposed.
|
||||
resource tcp {
|
||||
/// Reads at most `limit` bytes, between 1 and 1,048,576. An empty
|
||||
/// result means the peer closed its side, not that data is pending.
|
||||
read: func(limit: u32) -> result<list<u8>, host-error>;
|
||||
/// Writes at most 1,048,576 bytes and returns how many were accepted.
|
||||
write: func(data: list<u8>) -> result<u32, host-error>;
|
||||
}
|
||||
/// Opens a socket to a host and port both declared by the package and
|
||||
/// granted on the connection. The host resolves and pins DNS, applies the
|
||||
/// same private-address rule as `http`, and starts TLS when asked. Ports
|
||||
/// negotiated at runtime, such as an FTP data channel, still need a grant.
|
||||
connect: func(host: string, port: u16, tls: bool) -> result<tcp, host-error>;
|
||||
|
||||
/// One datagram socket connected to a single granted peer.
|
||||
resource udp {
|
||||
/// Sends one datagram of at most 65,507 bytes.
|
||||
send: func(data: list<u8>) -> result<u32, host-error>;
|
||||
/// Receives one datagram of at most `limit` bytes. An empty result
|
||||
/// means none arrived inside the host's wait, not that the peer left.
|
||||
receive: func(limit: u32) -> result<list<u8>, host-error>;
|
||||
}
|
||||
/// Connects a datagram socket under the same declaration and grant rules
|
||||
/// as `connect`, for the few filesystem protocols that still need one.
|
||||
bind: func(host: string, port: u16) -> result<udp, host-error>;
|
||||
|
||||
/// A configuration value, typed as the package's manifest declared it.
|
||||
/// A secret is never logged by the host and never leaves the connection.
|
||||
variant setting { text(string), secret(string), toggle(bool) }
|
||||
/// Reads configuration or a session-only credential for this connection.
|
||||
config: func(key: string) -> option<setting>;
|
||||
/// Reports that the current access token was rejected. The host refreshes
|
||||
/// it once; a second rejection surfaces to the caller as denied.
|
||||
invalidate-token: func();
|
||||
|
||||
/// Emits a diagnostic event. Never include secrets or private document data.
|
||||
log: func(level: log-level, message: string);
|
||||
}
|
||||
|
||||
/// Filesystem operations for one connection. Paths are absolute within its
|
||||
/// root, starting with `/`; they are never local OS paths or resource URIs.
|
||||
/// Dot segments, backslashes, percent escapes, colons, and controls are invalid.
|
||||
/// Every invocation is bounded by host fuel, memory, time, and cancellation.
|
||||
interface provider {
|
||||
use host.{setting};
|
||||
|
||||
/// Stable operation failures. A mutation interrupted after dispatch can
|
||||
/// report outcome-unknown: inspect remote state before deciding to retry.
|
||||
enum error-code {
|
||||
not-found, denied, unsupported, conflict, unavailable, invalid-input,
|
||||
cancelled, outcome-unknown, internal
|
||||
}
|
||||
/// Anything that is neither a file nor a directory, such as a link, is
|
||||
/// reported as other rather than flattened into one of the two.
|
||||
enum entry-kind { file, directory, other }
|
||||
|
||||
/// An opaque version token, at most 4096 UTF-8 bytes.
|
||||
type revision = string;
|
||||
/// An opaque backend identity, at most 4096 UTF-8 bytes. The host never
|
||||
/// parses, orders, or shows one, and never keeps one past the connection,
|
||||
/// because a backend may reuse it.
|
||||
type resource-id = string;
|
||||
|
||||
/// How one resource is named. A provider that hands out ids must accept
|
||||
/// them back; one that does not may reject every id as invalid-input.
|
||||
variant locator { path(string), id(resource-id) }
|
||||
|
||||
/// Optional operations. False capabilities must return unsupported.
|
||||
/// Conditional-write promises atomic create-only and revision comparisons,
|
||||
/// and cannot be combined with staged-write, where nothing to compare
|
||||
/// exists until a publish lands. Virtual-directories says a directory is
|
||||
/// only ever a prefix of something stored, as in a flat object store: the
|
||||
/// host then never offers to create an empty one, warns that removing the
|
||||
/// last child removes the folder, and does not call a vanished folder an
|
||||
/// error. Such a provider may still accept mkdir by writing a marker, and
|
||||
/// must answer unsupported when asked to rename a directory.
|
||||
record capabilities {
|
||||
write: bool, mkdir: bool, rename: bool, remove: bool,
|
||||
conditional-write: bool, watch: bool,
|
||||
staged-write: bool, publishable: bool,
|
||||
virtual-directories: bool,
|
||||
}
|
||||
/// Byte length, an opaque revision, and a backend identity, each if known.
|
||||
record file-info {
|
||||
kind: entry-kind,
|
||||
size: u64,
|
||||
revision: option<revision>,
|
||||
id: option<resource-id>,
|
||||
}
|
||||
/// One child name, without slashes, dot segments, or control characters.
|
||||
record entry { name: string, info: file-info }
|
||||
/// At most the requested number of entries. A cursor (maximum 4096 bytes)
|
||||
/// identifies another page; absence means enumeration is complete.
|
||||
record list-page { entries: list<entry>, cursor: option<string> }
|
||||
/// Atomic preconditions: absent file, exact opaque revision, or unconditional
|
||||
/// replacement. Providers must never simulate comparisons non-atomically.
|
||||
variant write-condition { create-only, if-match(revision), overwrite }
|
||||
/// Changes since a previous polling cursor.
|
||||
enum change-kind { created, modified, removed }
|
||||
/// A changed resource path relative to the connection root.
|
||||
record change { path: string, kind: change-kind }
|
||||
/// At most 256 changes and a cursor of at most 4096 UTF-8 bytes.
|
||||
record change-page { changes: list<change>, cursor: option<string> }
|
||||
|
||||
/// One transfer being read. The handle survives between host calls, so a
|
||||
/// file of any size is read as a series of separately bounded calls.
|
||||
/// Dropping it releases whatever the provider held open.
|
||||
resource reader {
|
||||
/// Reads onward from the last position, at most `limit` bytes, with
|
||||
/// `limit` between 1 and 1,048,576. An empty result means end of file.
|
||||
pull: func(limit: u32) -> result<list<u8>, error-code>;
|
||||
/// The revision this transfer is reading, once the provider knows it.
|
||||
/// A provider that sees the revision change mid-transfer must fail the
|
||||
/// next pull with conflict rather than mix two versions.
|
||||
revision: func() -> option<revision>;
|
||||
}
|
||||
/// One transfer being written. Dropping the handle before it finishes
|
||||
/// abandons the transfer, and the provider must leave no partial file.
|
||||
resource writer {
|
||||
/// Appends at most 1,048,576 bytes. Oversized pushes are rejected,
|
||||
/// never truncated.
|
||||
push: func(data: list<u8>) -> result<_, error-code>;
|
||||
/// Commits the transfer and evaluates the write condition now, against
|
||||
/// the state the caller last saw. A none result means the commit needs
|
||||
/// another call: keep calling until it answers some, and treat an
|
||||
/// interruption in between as outcome-unknown.
|
||||
finish: func() -> result<option<file-info>, error-code>;
|
||||
}
|
||||
|
||||
/// Opens exactly one host-owned connection per component instance. The host
|
||||
/// passes only this connection's configuration and in-memory credentials.
|
||||
open: func(provider-id: string, config: list<tuple<string, setting>>) -> result<capabilities, error-code>;
|
||||
/// Best-effort cleanup before disposal. The host may dispose immediately
|
||||
/// after a trap, cancellation, permission change, or process shutdown.
|
||||
close: func();
|
||||
/// Reads metadata without changing the resource.
|
||||
stat: func(target: locator) -> result<file-info, error-code>;
|
||||
/// Enumerates 1-256 direct children per request, without recursive traversal.
|
||||
%list: func(target: locator, cursor: option<string>, limit: u32) -> result<list-page, error-code>;
|
||||
/// Begins reading at a byte offset. Streaming, rather than one capped read,
|
||||
/// is what lets a provider serve a file larger than a single call allows.
|
||||
open-read: func(target: locator, offset: u64) -> result<reader, error-code>;
|
||||
/// Begins replacing one complete file if the write capability is present.
|
||||
/// The size hint, when the caller knows it, lets a provider choose between
|
||||
/// a single request and a multipart upload before the first byte.
|
||||
open-write: func(target: locator, condition: write-condition, size-hint: option<u64>) -> result<writer, error-code>;
|
||||
/// Creates one directory if the mkdir capability is present.
|
||||
mkdir: func(target: locator) -> result<file-info, error-code>;
|
||||
/// Renames within this connection if the rename capability is present.
|
||||
rename: func(target: locator, destination: string) -> result<file-info, error-code>;
|
||||
/// Removes one file or empty directory if the remove capability is present.
|
||||
remove: func(target: locator) -> result<_, error-code>;
|
||||
/// Polls for a bounded change page if the watch capability is present. No
|
||||
/// background callback or persistent subscription is installed by the host.
|
||||
poll: func(cursor: option<string>) -> result<change-page, error-code>;
|
||||
/// Makes every staged write visible as one new root, if the publishable
|
||||
/// capability is present, and names it, for example as a content
|
||||
/// identifier. A none result means the publish needs another call.
|
||||
publish: func() -> result<option<revision>, error-code>;
|
||||
}
|
||||
|
||||
/// Memona filesystem extension ABI 0.1.0, distributed as a WebAssembly component.
|
||||
/// Rust guests can target wasm32-wasip2. Selected WASI 0.2 CLI, clocks, random,
|
||||
/// IO and filesystem interfaces are allowed for the standard library, with
|
||||
/// empty arguments/environment, empty preopens, and discarded stdio. WASI
|
||||
/// sockets and HTTP imports are not allowed; networking uses the host interface.
|
||||
world filesystem {
|
||||
import host;
|
||||
export provider;
|
||||
}
|
||||
Reference in New Issue
Block a user