use std::collections::HashSet;
use std::time::Duration;
use crate::error::Error;
const PATH_SEGMENT_ENCODE: &percent_encoding::AsciiSet = &percent_encoding::NON_ALPHANUMERIC
.remove(b'-')
.remove(b'_')
.remove(b'.')
.remove(b'~');
pub const TUS_VERSION: &str = "1.0.0";
pub const TUS_RESUMABLE: &str = "1.0.0";
pub const DEFAULT_MAX_INTAKE_BUFFER: u64 = 8 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Config {
max_size: Option<u64>,
extensions: HashSet<Extension>,
checksum_algorithms: HashSet<ChecksumAlgorithm>,
expiration: Option<Duration>,
lock_timeout: Duration,
base_path: String,
base_url: Option<String>,
respect_forwarded_headers: bool,
max_chunk_size: Option<u64>,
max_intake_buffer: Option<u64>,
disable_download: bool,
allow_empty_creation: bool,
}
impl Default for Config {
fn default() -> Self {
let mut extensions = HashSet::new();
extensions.insert(Extension::Creation);
extensions.insert(Extension::Termination);
extensions.insert(Extension::CreationDeferLength);
Self {
max_size: None,
extensions,
checksum_algorithms: HashSet::new(),
expiration: None,
lock_timeout: Duration::from_secs(30),
base_path: "/files".to_string(),
base_url: None,
respect_forwarded_headers: false,
max_chunk_size: None,
max_intake_buffer: Some(DEFAULT_MAX_INTAKE_BUFFER),
disable_download: false,
allow_empty_creation: true,
}
}
}
impl Config {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn all_extensions() -> Self {
Self {
extensions: Extension::supported().iter().copied().collect(),
checksum_algorithms: [
ChecksumAlgorithm::Sha1,
ChecksumAlgorithm::Sha256,
ChecksumAlgorithm::Md5,
ChecksumAlgorithm::Crc32,
]
.into_iter()
.collect(),
..Self::default()
}
}
#[must_use]
pub fn with_max_size(mut self, size: u64) -> Self {
self.max_size = Some(size);
self
}
#[must_use]
pub fn with_extension(self, ext: Extension) -> Self {
self.try_with_extension(ext).unwrap_or_else(|err| {
panic!("{err}");
})
}
pub fn try_with_extension(mut self, ext: Extension) -> Result<Self, Error> {
#[cfg(not(feature = "checksum"))]
if matches!(ext, Extension::Checksum | Extension::ChecksumTrailer) {
return Err(Error::ExtensionNotSupported(format!(
"{}: enable the `checksum` feature of tus-protocol",
ext.as_str()
)));
}
self.extensions.insert(ext);
#[cfg(feature = "checksum")]
if matches!(ext, Extension::Checksum | Extension::ChecksumTrailer) {
self.extensions.insert(Extension::Checksum);
if self.checksum_algorithms.is_empty() {
self.checksum_algorithms.insert(ChecksumAlgorithm::Sha1);
}
}
Ok(self)
}
#[must_use]
pub fn without_extension(mut self, ext: Extension) -> Self {
self.extensions.remove(&ext);
self
}
#[must_use]
pub fn with_expiration(mut self, duration: Duration) -> Self {
self.expiration = Some(duration);
self.extensions.insert(Extension::Expiration);
self
}
#[must_use]
pub fn with_lock_timeout(mut self, timeout: Duration) -> Self {
self.lock_timeout = timeout;
self
}
#[must_use]
pub fn with_base_path(mut self, path: impl Into<String>) -> Self {
self.base_path = path.into();
self
}
#[must_use]
pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = Some(url.into());
self
}
#[must_use]
pub fn with_respect_forwarded_headers(mut self) -> Self {
self.respect_forwarded_headers = true;
self
}
#[must_use]
pub fn with_max_chunk_size(mut self, size: u64) -> Self {
self.max_chunk_size = Some(size);
self
}
#[must_use]
pub fn with_max_intake_buffer(mut self, size: u64) -> Self {
self.max_intake_buffer = Some(size);
self
}
#[must_use]
pub fn without_intake_buffer_limit(mut self) -> Self {
self.max_intake_buffer = None;
self
}
#[must_use]
pub fn without_download(mut self) -> Self {
self.disable_download = true;
self
}
#[cfg(feature = "checksum")]
#[must_use]
pub fn with_checksum(mut self, algorithm: ChecksumAlgorithm) -> Self {
self.checksum_algorithms.insert(algorithm);
self.checksum_algorithms.insert(ChecksumAlgorithm::Sha1);
self.extensions.insert(Extension::Checksum);
self
}
#[must_use]
pub fn without_empty_creation(mut self) -> Self {
self.allow_empty_creation = false;
self
}
pub fn has_extension(&self, ext: Extension) -> bool {
self.extensions.contains(&ext)
}
pub fn max_size(&self) -> Option<u64> {
self.max_size
}
pub fn checksum_algorithms(&self) -> Vec<ChecksumAlgorithm> {
let mut algorithms: Vec<_> = self.checksum_algorithms.iter().copied().collect();
algorithms.sort_by_key(|algorithm| algorithm.as_str());
algorithms
}
pub fn supports_checksum_algorithm(&self, algorithm: ChecksumAlgorithm) -> bool {
self.checksum_algorithms.contains(&algorithm)
}
pub fn expiration(&self) -> Option<Duration> {
self.expiration
}
pub fn lock_timeout(&self) -> Duration {
self.lock_timeout
}
pub fn base_path(&self) -> &str {
&self.base_path
}
pub fn base_url(&self) -> Option<&str> {
self.base_url.as_deref()
}
pub fn respects_forwarded_headers(&self) -> bool {
self.respect_forwarded_headers
}
pub fn max_chunk_size(&self) -> Option<u64> {
self.max_chunk_size
}
pub fn max_intake_buffer(&self) -> Option<u64> {
self.max_intake_buffer
}
pub fn is_download_disabled(&self) -> bool {
self.disable_download
}
pub fn allows_empty_creation(&self) -> bool {
self.allow_empty_creation
}
pub fn extensions_string(&self) -> String {
let mut exts: Vec<_> = self.extensions.iter().map(|e| e.as_str()).collect();
exts.sort();
exts.join(",")
}
pub fn checksum_algorithms_string(&self) -> String {
self.checksum_algorithms()
.iter()
.map(|a| a.as_str())
.collect::<Vec<_>>()
.join(",")
}
pub fn upload_url(&self, upload_id: &str, request_base_url: Option<&str>) -> String {
let upload_id = percent_encoding::utf8_percent_encode(upload_id, PATH_SEGMENT_ENCODE);
let base = self.base_url.as_deref().or(request_base_url);
match base {
Some(base) => format!("{}{}/{}", base, self.base_path, upload_id),
None => format!("{}/{}", self.base_path, upload_id),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Extension {
Creation,
CreationWithUpload,
CreationDeferLength,
Termination,
Expiration,
Concatenation,
ConcatenationUnfinished,
Checksum,
ChecksumTrailer,
}
impl Extension {
pub fn as_str(self) -> &'static str {
match self {
Extension::Creation => "creation",
Extension::CreationWithUpload => "creation-with-upload",
Extension::CreationDeferLength => "creation-defer-length",
Extension::Termination => "termination",
Extension::Expiration => "expiration",
Extension::Concatenation => "concatenation",
Extension::ConcatenationUnfinished => "concatenation-unfinished",
Extension::Checksum => "checksum",
Extension::ChecksumTrailer => "checksum-trailer",
}
}
pub fn supported() -> &'static [Extension] {
&[
Extension::Creation,
Extension::CreationWithUpload,
Extension::CreationDeferLength,
Extension::Termination,
Extension::Expiration,
Extension::Concatenation,
Extension::ConcatenationUnfinished,
#[cfg(feature = "checksum")]
Extension::Checksum,
#[cfg(feature = "checksum")]
Extension::ChecksumTrailer,
]
}
}
impl std::fmt::Display for Extension {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for Extension {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"creation" => Ok(Extension::Creation),
"creation-with-upload" => Ok(Extension::CreationWithUpload),
"creation-defer-length" => Ok(Extension::CreationDeferLength),
"termination" => Ok(Extension::Termination),
"expiration" => Ok(Extension::Expiration),
"concatenation" => Ok(Extension::Concatenation),
"concatenation-unfinished" => Ok(Extension::ConcatenationUnfinished),
"checksum" => Ok(Extension::Checksum),
"checksum-trailer" => Ok(Extension::ChecksumTrailer),
other => Err(Error::ExtensionNotSupported(other.to_string())),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ChecksumAlgorithm {
Sha1,
Sha256,
Md5,
Crc32,
}
impl ChecksumAlgorithm {
pub fn as_str(self) -> &'static str {
match self {
ChecksumAlgorithm::Sha1 => "sha1",
ChecksumAlgorithm::Sha256 => "sha256",
ChecksumAlgorithm::Md5 => "md5",
ChecksumAlgorithm::Crc32 => "crc32",
}
}
}
impl std::fmt::Display for ChecksumAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for ChecksumAlgorithm {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"sha1" => Ok(ChecksumAlgorithm::Sha1),
"sha256" => Ok(ChecksumAlgorithm::Sha256),
"md5" => Ok(ChecksumAlgorithm::Md5),
"crc32" => Ok(ChecksumAlgorithm::Crc32),
other => Err(Error::UnsupportedChecksum(other.to_string())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_supported_extensions_match_build_features() {
let extensions = Extension::supported();
assert!(extensions.contains(&Extension::Creation));
assert!(extensions.contains(&Extension::ConcatenationUnfinished));
#[cfg(feature = "checksum")]
{
assert!(extensions.contains(&Extension::Checksum));
assert!(extensions.contains(&Extension::ChecksumTrailer));
}
#[cfg(not(feature = "checksum"))]
{
assert!(!extensions.contains(&Extension::Checksum));
assert!(!extensions.contains(&Extension::ChecksumTrailer));
}
}
#[test]
fn test_default_config() {
let config = Config::default();
assert!(config.has_extension(Extension::Creation));
assert!(config.has_extension(Extension::Termination));
assert!(!config.has_extension(Extension::Checksum));
}
#[cfg(feature = "checksum")]
#[test]
fn test_builder_pattern() {
let config = Config::new()
.with_max_size(1024 * 1024 * 100) .with_extension(Extension::Checksum)
.with_checksum(ChecksumAlgorithm::Sha256)
.with_expiration(Duration::from_secs(3600))
.with_base_path("/uploads");
assert_eq!(config.max_size(), Some(100 * 1024 * 1024));
assert!(config.has_extension(Extension::Checksum));
assert!(config.has_extension(Extension::Expiration));
assert!(config.supports_checksum_algorithm(ChecksumAlgorithm::Sha256));
assert_eq!(config.base_path(), "/uploads");
}
#[cfg(not(feature = "checksum"))]
#[test]
#[should_panic(expected = "enable the `checksum` feature")]
fn with_extension_panics_for_checksum_without_feature() {
let _ = Config::new().with_extension(Extension::Checksum);
}
#[cfg(not(feature = "checksum"))]
#[test]
#[should_panic(expected = "enable the `checksum` feature")]
fn with_extension_panics_for_checksum_trailer_without_feature() {
let _ = Config::new().with_extension(Extension::ChecksumTrailer);
}
#[cfg(not(feature = "checksum"))]
#[test]
fn try_with_extension_errors_for_checksum_without_feature() {
let err = Config::new()
.try_with_extension(Extension::Checksum)
.unwrap_err();
assert!(matches!(err, Error::ExtensionNotSupported(_)));
}
#[test]
fn try_with_extension_accepts_non_checksum_extensions() {
let config = Config::new()
.try_with_extension(Extension::Concatenation)
.unwrap();
assert!(config.has_extension(Extension::Concatenation));
}
#[cfg(feature = "checksum")]
#[test]
fn checksum_algorithms_are_returned_sorted_by_name() {
let config = Config::new()
.with_checksum(ChecksumAlgorithm::Sha256)
.with_checksum(ChecksumAlgorithm::Crc32);
let names: Vec<&str> = config
.checksum_algorithms()
.into_iter()
.map(|algorithm| algorithm.as_str())
.collect();
assert_eq!(names, vec!["crc32", "sha1", "sha256"]);
}
#[test]
fn test_extensions_string() {
let config = Config::new()
.with_extension(Extension::Creation)
.with_extension(Extension::Termination);
let exts = config.extensions_string();
assert!(exts.contains("creation"));
assert!(exts.contains("termination"));
}
}