///|
/// Reference store for tests, examples and single-process adapters.
///
/// The protocol engine only needs snapshot, create and compare-and-swap append
/// operations. Production adapters can map those same operations to files,
/// object storage or a database without importing an HTTP framework.
pub struct MemoryStore {
priv records : Array[UploadRecord]
priv mut next_sequence : Int64
}
///|
pub fn MemoryStore::new(seed? : Int64 = 1L) -> MemoryStore {
{ records: [], next_sequence: seed, }
}
///|
pub fn MemoryStore::count(self : MemoryStore) -> Int {
self.records.length()
}
///|
pub fn MemoryStore::is_empty(self : MemoryStore) -> Bool {
self.records.length() == 0
}
///|
pub fn MemoryStore::contains(self : MemoryStore, identifier : String) -> Bool {
self.index_of(identifier) is Some(_)
}
///|
pub fn MemoryStore::get(
self : MemoryStore,
identifier : String,
) -> Result[UploadRecord, TusError] {
match self.index_of(identifier) {
None => Err(upload_not_found(identifier))
Some(index) => Ok(clone_record(self.records[index]))
}
}
///|
pub fn MemoryStore::list(self : MemoryStore) -> Array[UploadRecord] {
let output : Array[UploadRecord] = []
for record in self.records {
output.push(clone_record(record))
}
output
}
///|
pub fn MemoryStore::create(
self : MemoryStore,
plan : CreationPlan,
limits : TusLimits,
) -> Result[UploadRecord, TusError] {
let mut attempt = 0
while attempt < limits.max_identifier_attempts {
let identifier = sequence_identifier(self.next_sequence)
match
checked_add_i64(self.next_sequence, 1L, field_name="identifier-sequence") {
Err(_) =>
return Err(
tus_error(
IdentifierExhausted,
"TUS_IDENTIFIER_EXHAUSTED",
"reference store identifier sequence is exhausted",
status=507,
),
)
Ok(next) => self.next_sequence = next
}
if !self.contains(identifier) {
let record = upload_record(
identifier,
plan.length,
metadata=plan.metadata,
)
match validate_record(record, limits) {
Err(error) => return Err(error)
Ok(_) => ()
}
self.records.push(record)
return Ok(clone_record(record))
}
attempt = attempt + 1
}
Err(
tus_error(
IdentifierExhausted,
"TUS_IDENTIFIER_COLLISION_LIMIT",
"reference store could not allocate a unique identifier",
status=507,
expected=Some(limits.max_identifier_attempts.to_string()),
),
)
}
///|
/// Insert an explicit record for recovery/import workflows. It is also useful
/// to conformance tests that need precise revision and offset states.
pub fn MemoryStore::import_record(
self : MemoryStore,
record : UploadRecord,
limits : TusLimits,
) -> Result[Unit, TusError] {
if self.contains(record.identifier) {
return Err(storage_conflict(record.identifier))
}
match validate_record(record, limits) {
Err(error) => Err(error)
Ok(_) => {
self.records.push(clone_record(record))
Ok(())
}
}
}
///|
pub fn validate_record(
record : UploadRecord,
limits : TusLimits,
) -> Result[Unit, TusError] {
if !is_safe_identifier(record.identifier) {
return Err(store_invariant("stored identifier is not a safe path segment"))
}
if record.offset < 0L || record.revision < 0L {
return Err(
store_invariant("stored offset and revision must be non-negative"),
)
}
if record.data.length().to_int64() != record.offset {
return Err(
store_invariant("stored byte length does not equal Upload-Offset"),
)
}
if record.offset > limits.max_upload_size {
return Err(upload_too_large(limits.max_upload_size, record.offset))
}
match record.length {
Deferred =>
if record.lifecycle != Active {
return Err(store_invariant("deferred upload cannot be complete"))
}
Known(total) => {
if total < record.offset {
return Err(store_invariant("stored offset exceeds Upload-Length"))
}
if total > limits.max_upload_size {
return Err(upload_too_large(limits.max_upload_size, total))
}
if record.lifecycle != lifecycle(record.offset, record.length) {
return Err(
store_invariant("stored lifecycle disagrees with offset and length"),
)
}
}
}
if record.metadata.length() > limits.max_metadata_entries ||
metadata_decoded_size(record.metadata) > limits.max_metadata_bytes {
return Err(store_invariant("stored metadata exceeds configured limits"))
}
Ok(())
}
///|
fn MemoryStore::index_of(self : MemoryStore, identifier : String) -> Int? {
for index, record in self.records {
if record.identifier == identifier {
return Some(index)
}
}
None
}
///|
fn clone_record(record : UploadRecord) -> UploadRecord {
{
identifier: record.identifier,
offset: record.offset,
length: record.length,
metadata: record.metadata.copy(),
revision: record.revision,
lifecycle: record.lifecycle,
data: record.data,
}
}
///|
fn sequence_identifier(sequence : Int64) -> String {
if sequence < 0L {
return "u-invalid"
}
let alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
let reversed = StringBuilder()
let mut value = sequence
if value == 0L {
reversed.write_char('0')
}
while value > 0L {
let digit = (value % 36L).to_int()
reversed.write_char(alphabet[digit].to_int().unsafe_to_char())
value = value / 36L
}
let body = reversed.to_string()
let output = StringBuilder()
output.write_char('u')
let mut pad = body.length()
while pad < 12 {
output.write_char('0')
pad = pad + 1
}
let mut index = body.length()
while index > 0 {
output.write_char(body[index - 1].to_int().unsafe_to_char())
index = index - 1
}
output.to_string()
}
///|
fn store_invariant(message : String) -> TusError {
tus_error(StorageFailure, "TUS_STORAGE_INVARIANT", message, status=500)
}