/filesystem/write
One file into the container: the server sends the name of the file, its content in one or more pieces, and one empty message; the proxy sends exactly one answer, ok or an error, and closes the connection.
The server opens /filesystem/write and sends, in order, the request
defined below as JSON, naming the destination; the content, one piece
per message; and exactly one empty message, which ends the content.
The proxy sends exactly one message and closes the connection.
server → proxy: [request JSON] [bytes …] … [bytes …] []
proxy → server: [kind: u8][message …] once
- The end of the content. A piece is never empty. The first empty message ends the content. The file is the pieces concatenated, in order.
- The answer. A first byte of
0states that the file was written. A first byte of1is followed by a message stating why the file was not written; a destination whose parent directory does not exist is such a case. - An abandoned write. A close by the server before the empty message abandons the write, and the destination remains as it was. An abrupt end by either party is a write that failed, and the destination remains as it was.
- No repetition. No party retries a write.
The request is defined by
diverge-provider-sdk/src/container_proxy/filesystem/write/request/request.rs:
//! The destination.
use serde::{Deserialize, Serialize};
use crate::decode::Decode;
use crate::encode::{Encode, Writer};
/// The file to write, the first message on `/filesystem/write`.
///
/// No length and no mode: the content follows as
/// [`Frame`](super::Frame)s until the empty one, and what the file
/// looks like on disk is the container's business.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct Request {
/// The destination, as path components from the container's
/// root — the same meaning of "path" as everywhere else in this
/// crate, and the same frame of reference a
/// [`filetree`](crate::shared::filetree) stream uses. Components
/// rather than a joined string: a path is a sequence, and joining
/// it would invent a separator that then has to be escaped out
/// of names containing it.
pub path: Vec<String>,
}
impl Encode for Request {
/// The ordinary JSON failure.
type Error = serde_json::Error;
fn encode(&self, out: &mut Writer<'_>) -> Result<(), Self::Error> {
serde_json::to_writer(out, self)
}
}
impl Decode<'_> for Request {
/// The ordinary JSON failure.
type Error = serde_json::Error;
fn decode(bytes: &[u8]) -> Result<Self, Self::Error> {
serde_json::from_slice(bytes)
}
}
A piece is defined by
diverge-provider-sdk/src/container_proxy/filesystem/write/request/frame.rs:
//! A chunk of the content.
use std::convert::Infallible;
use crate::encode::{Encode, Writer};
/// One message of content on `/filesystem/write`, every message after the
/// first: bytes, verbatim, in order. An EMPTY one is not content —
/// it is the end of it, after which the container answers.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Frame<'a>(
/// The bytes, borrowed from the message they arrived in.
pub &'a [u8],
);
impl Encode for Frame<'_> {
/// [`Infallible`]: bytes copied.
type Error = Infallible;
fn encode(&self, out: &mut Writer<'_>) -> Result<(), Infallible> {
out.extend_from_slice(self.0);
Ok(())
}
}
impl<'a> Frame<'a> {
/// Decode one message: the bytes, kept.
pub fn decode(bytes: &'a [u8]) -> Result<Self, Infallible> {
Ok(Frame(bytes))
}
}
The answer is defined by
diverge-provider-sdk/src/container_proxy/filesystem/write/response/frame.rs:
//! Whether the file landed.
use std::convert::Infallible;
use super::FrameError;
use crate::encode::{Encode, Writer};
/// The one message the container sends on `/filesystem/write`, after the empty
/// chunk and before the close.
///
/// ```text
/// [kind: u8][message…]
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Frame<'a> {
/// Kind `0`. The file is written, whole.
Ok,
/// Kind `1`. It is not, and this says why, for a reader rather
/// than a program: a missing parent, a path that is a directory,
/// a disk that is full. Nothing here is enumerated, because what
/// a container refuses is its filesystem's business.
Error(&'a str),
}
impl Encode for Frame<'_> {
/// [`Infallible`]: a kind byte and bytes copied.
type Error = Infallible;
fn encode(&self, out: &mut Writer<'_>) -> Result<(), Infallible> {
match self {
Frame::Ok => out.extend_from_slice(&[0]),
Frame::Error(message) => {
out.extend_from_slice(&[1]);
out.extend_from_slice(message.as_bytes());
}
}
Ok(())
}
}
impl<'a> Frame<'a> {
/// Decode the one message. The message borrows from `bytes`.
pub fn decode(bytes: &'a [u8]) -> Result<Self, FrameError> {
let (kind, rest) = bytes.split_first().ok_or(FrameError::Empty)?;
match *kind {
0 => Ok(Frame::Ok),
1 => std::str::from_utf8(rest)
.map(Frame::Error)
.map_err(|_| FrameError::MessageUtf8),
other => Err(FrameError::UnknownKind(other)),
}
}
}
The ways in which the answer fails to decode are defined by
diverge-provider-sdk/src/container_proxy/filesystem/write/response/error.rs:
//! Why the answer on `/filesystem/write` could not be decoded.
use std::error;
use std::fmt;
/// An answer that could not be read.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FrameError {
/// No bytes at all, so not even a kind.
Empty,
/// A kind this answer does not define.
UnknownKind(u8),
/// An error message that is not UTF-8.
MessageUtf8,
}
impl fmt::Display for FrameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FrameError::Empty => f.write_str("write answer is empty"),
FrameError::UnknownKind(kind) => {
write!(f, "unknown write answer kind {kind}")
}
FrameError::MessageUtf8 => {
f.write_str("write error message is not utf-8")
}
}
}
}
impl error::Error for FrameError {}