///|
/// Validate PATCH headers and current snapshot, then produce a compare-and-swap
/// plan. No bytes are mutated until a storage adapter commits this value.
pub fn plan_append(
request : TusRequest,
record : UploadRecord,
config : TusConfig,
) -> Result[AppendPlan, TusError] {
if request.http_method != Patch {
return Err(invalid_method(request.http_method.name()))
}
let identifier = match validate_request(request, config) {
Err(error) => return Err(error)
Ok(Collection) => return Err(invalid_path(request.path))
Ok(Resource(identifier)) => identifier
}
if identifier != record.identifier {
return Err(upload_not_found(identifier))
}
match validate_record(record, config.limits) {
Err(error) => return Err(error)
Ok(_) => ()
}
if record.is_complete() {
return Err(upload_complete(record.identifier))
}
let content_type = match
request.headers.required_singleton("content-type", MissingContentType) {
Err(error) => return Err(error)
Ok(value) => trim_ows(value)
}
if !ascii_equal_ignore_case(content_type, "application/offset+octet-stream") {
return Err(
tus_error(
InvalidContentType,
"TUS_PATCH_CONTENT_TYPE_INVALID",
"PATCH Content-Type must be application/offset+octet-stream",
status=415,
header_name=Some("content-type"),
expected=Some("application/offset+octet-stream"),
actual=Some(content_type),
),
)
}
match require_content_length(request) {
Err(error) => return Err(error)
Ok(_) => ()
}
match validate_patch_size(request.body, config.limits) {
Err(error) => return Err(error)
Ok(_) => ()
}
let supplied_offset = match required_upload_offset(request.headers) {
Err(error) => return Err(error)
Ok(value) => value
}
if supplied_offset != record.offset {
return Err(offset_mismatch(record.offset, supplied_offset))
}
let new_offset = match
checked_body_end(record.offset, request.body.length()) {
Err(error) => return Err(error)
Ok(value) => value
}
if new_offset > config.limits.max_upload_size {
return Err(upload_too_large(config.limits.max_upload_size, new_offset))
}
let patch_length = match optional_upload_length(request.headers) {
Err(error) => return Err(error)
Ok(value) => value
}
let resolved_length = match record.length {
Known(total) => {
if patch_length is Some(_) {
return Err(
tus_error(
ConflictingUploadLength,
"TUS_LENGTH_ALREADY_DECLARED",
"Upload-Length cannot change after creation",
header_name=Some("upload-length"),
),
)
}
if new_offset > total {
return Err(upload_too_large(total, new_offset))
}
Known(total)
}
Deferred =>
match patch_length {
None => Deferred
Some(total) => {
if total < new_offset {
return Err(
tus_error(
ConflictingUploadLength,
"TUS_DEFERRED_LENGTH_TOO_SMALL",
"declared Upload-Length is smaller than the resulting offset",
header_name=Some("upload-length"),
expected=Some(new_offset.to_string()),
actual=Some(total.to_string()),
),
)
}
match validate_declared_size(total, config.limits) {
Err(error) => return Err(error)
Ok(_) => Known(total)
}
}
}
}
let completes = match resolved_length {
Known(total) => new_offset == total
Deferred => false
}
Ok({
identifier: record.identifier,
expected_revision: record.revision,
old_offset: record.offset,
new_offset,
resolved_length,
body: request.body,
completes_upload: completes,
})
}
///|
pub fn append_trace(plan : AppendPlan) -> Array[TraceStep] {
let steps = [
trace(
"patch.offset.matched", "request Upload-Offset matches the committed resource offset",
"tus-1.0.0/core",
),
trace(
"patch.body.bounded",
plan.body.length().to_string() + " request bytes passed configured limits",
"moontuscore/resource-limits",
),
trace(
"patch.cas.prepared",
"storage must compare revision " + plan.expected_revision.to_string(),
"moontuscore/storage-contract",
),
]
if plan.completes_upload {
steps.push(
trace(
"patch.upload.complete", "the resulting offset reaches Upload-Length", "tus-1.0.0/core",
),
)
}
steps
}