///|
/// Supported tus protocol version. v0.1 intentionally exposes one version so
/// adapters cannot accidentally negotiate behavior that is not implemented.
pub const TUS_VERSION : String = "1.0.0"
///|
pub(all) enum HttpMethod {
Options
Post
Head
Patch
Other(String)
} derive(Debug, Eq)
///|
pub fn parse_http_method(value : String) -> HttpMethod {
match ascii_upper(value) {
"OPTIONS" => Options
"POST" => Post
"HEAD" => Head
"PATCH" => Patch
other => Other(other)
}
}
///|
pub fn HttpMethod::name(self : HttpMethod) -> String {
match self {
Options => "OPTIONS"
Post => "POST"
Head => "HEAD"
Patch => "PATCH"
Other(value) => value
}
}
///|
/// Immutable HTTP-shaped input accepted by the protocol engine. No socket or
/// framework type crosses this boundary.
pub(all) struct TusRequest {
http_method : HttpMethod
path : String
headers : Headers
body : Bytes
} derive(Debug, Eq)
///|
pub fn request(
http_method : String,
path : String,
headers? : Headers = Headers::empty(),
body? : Bytes = b"",
) -> TusRequest {
{ http_method: parse_http_method(http_method), path, headers, body, }
}
///|
pub fn options_request(path? : String = "/files") -> TusRequest {
request("OPTIONS", path)
}
///|
/// HTTP-shaped output. Response bodies are small protocol diagnostics only;
/// uploaded bytes are owned by the configured store.
pub(all) struct TusResponse {
status : Int
headers : Headers
body : Bytes
trace : Array[TraceStep]
error : TusError?
} derive(Debug, Eq)
///|
pub fn success_response(
status : Int,
headers? : Headers = Headers::empty(),
trace? : Array[TraceStep] = [],
) -> TusResponse {
{ status, headers, body: b"", trace: trace.copy(), error: None, }
}
///|
pub fn failure_response(
error : TusError,
headers? : Headers = Headers::empty(),
trace? : Array[TraceStep] = [],
) -> TusResponse {
{
status: error.status,
headers,
body: b"",
trace: trace.copy(),
error: Some(error),
}
}
///|
/// Normative explanation attached to every decision.
pub(all) struct TraceStep {
code : String
message : String
protocol_section : String
} derive(Debug, Eq)
///|
pub fn trace(
code : String,
message : String,
protocol_section : String,
) -> TraceStep {
{ code, message, protocol_section, }
}
///|
/// Declared total size or a promise that a later PATCH will declare it once.
pub(all) enum UploadLength {
Known(Int64)
Deferred
} derive(Debug, Eq)
///|
pub fn UploadLength::known(self : UploadLength) -> Int64? {
match self {
Known(value) => Some(value)
Deferred => None
}
}
///|
pub fn UploadLength::is_deferred(self : UploadLength) -> Bool {
self == Deferred
}
///|
pub(all) enum UploadLifecycle {
Active
Complete
} derive(Debug, Eq)
///|
pub fn lifecycle(offset : Int64, length : UploadLength) -> UploadLifecycle {
match length {
Known(total) if offset == total => Complete
_ => Active
}
}
///|
/// One decoded Upload-Metadata member. `value` contains decoded bytes and
/// `encoded_value` retains the canonical wire representation.
pub(all) struct MetadataEntry {
key : String
value : Bytes
encoded_value : String
} derive(Debug, Eq)
///|
/// Stored protocol state. The body is retained by the in-memory reference
/// store only; external stores may persist it elsewhere behind the same seam.
pub(all) struct UploadRecord {
identifier : String
offset : Int64
length : UploadLength
metadata : Array[MetadataEntry]
revision : Int64
lifecycle : UploadLifecycle
data : Bytes
} derive(Debug, Eq)
///|
pub fn upload_record(
identifier : String,
length : UploadLength,
metadata? : Array[MetadataEntry] = [],
) -> UploadRecord {
{
identifier,
offset: 0L,
length,
metadata: metadata.copy(),
revision: 0L,
lifecycle: lifecycle(0L, length),
data: b"",
}
}
///|
pub fn UploadRecord::is_complete(self : UploadRecord) -> Bool {
self.lifecycle == Complete
}
///|
pub fn UploadRecord::remaining(self : UploadRecord) -> Int64? {
match self.length {
Known(total) => Some(total - self.offset)
Deferred => None
}
}
///|
/// Limits are checked before mutation. Defaults are conservative enough for a
/// protocol adapter while remaining useful in tests and local tools.
pub(all) struct TusLimits {
max_upload_size : Int64
max_patch_bytes : Int
max_header_count : Int
max_header_bytes : Int
max_metadata_bytes : Int
max_metadata_entries : Int
max_path_bytes : Int
max_identifier_attempts : Int
} derive(Debug, Eq)
///|
pub fn TusLimits::default() -> TusLimits {
{
max_upload_size: 1024L * 1024L * 1024L,
max_patch_bytes: 16 * 1024 * 1024,
max_header_count: 128,
max_header_bytes: 64 * 1024,
max_metadata_bytes: 16 * 1024,
max_metadata_entries: 64,
max_path_bytes: 4096,
max_identifier_attempts: 1024,
}
}
///|
pub fn TusLimits::strict_test() -> TusLimits {
{
max_upload_size: 1024L,
max_patch_bytes: 256,
max_header_count: 32,
max_header_bytes: 4096,
max_metadata_bytes: 512,
max_metadata_entries: 8,
max_path_bytes: 256,
max_identifier_attempts: 16,
}
}
///|
pub(all) struct TusConfig {
collection_path : String
limits : TusLimits
allow_creation : Bool
allow_deferred_length : Bool
} derive(Debug, Eq)
///|
pub fn TusConfig::default() -> TusConfig {
{
collection_path: "/files",
limits: TusLimits::default(),
allow_creation: true,
allow_deferred_length: true,
}
}
///|
pub fn config(
collection_path? : String = "/files",
limits? : TusLimits = TusLimits::default(),
allow_creation? : Bool = true,
allow_deferred_length? : Bool = true,
) -> TusConfig {
{ collection_path, limits, allow_creation, allow_deferred_length, }
}
///|
/// Result of a validated append before it is committed to storage.
pub(all) struct AppendPlan {
identifier : String
expected_revision : Int64
old_offset : Int64
new_offset : Int64
resolved_length : UploadLength
body : Bytes
completes_upload : Bool
} derive(Debug, Eq)
///|
/// Minimal creation plan produced after header validation.
pub(all) struct CreationPlan {
length : UploadLength
metadata : Array[MetadataEntry]
} derive(Debug, Eq)
///|
pub fn lifecycle_name(value : UploadLifecycle) -> String {
match value {
Active => "active"
Complete => "complete"
}
}
///|
pub fn upload_length_name(value : UploadLength) -> String {
match value {
Known(total) => total.to_string()
Deferred => "deferred"
}
}
///|
fn ascii_upper(value : String) -> String {
let output = StringBuilder()
for byte in value.iter() {
if byte >= 'a' && byte <= 'z' {
output.write_char((byte.to_int() - 32).unsafe_to_char())
} else {
output.write_char(byte)
}
}
output.to_string()
}