diff --git a/Cargo.toml b/Cargo.toml index f3cd918386e8398fbc285448c25bc5e3c1f99ff1..c538a529c2d89118b89e6e2702b8cd1b203f4e83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "tuser" version = "0.1.0" edition = "2021" +description = "TUS protocol implementation written in Rust." # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [[bin]] diff --git a/src/config.rs b/src/config.rs index e1c4102b3d7c02aea285a9461652376ccdc331a0..8e626c8e5cd52df214b5139f9c90792c340033c8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; use structopt::StructOpt; +use crate::errors::TuserError; use crate::storages::AvailableStores; #[derive(Debug, StructOpt, Clone)] @@ -38,10 +39,99 @@ pub struct TuserConf { /// Number of actix workers default value = number of cpu cores. #[structopt(long, short)] pub workers: Option<usize>, + + /// Enabled extensions for TUS protocol. + #[structopt(long, default_value = "creation,creation-with-upload")] + pub extensions: String, +} + +/// Enum of available Protocol Extensions +#[derive(PartialEq, PartialOrd, Ord, Eq)] +pub enum ProtocolExtensions { + CreationWithUpload, + Creation, + Termination, +} + +impl TryFrom<String> for ProtocolExtensions { + type Error = TuserError; + + /// Parse string to protocol extension. + /// + /// This function raises an error if unknown protocol was passed. + fn try_from(value: String) -> Result<Self, Self::Error> { + match value.as_str() { + "creation" => Ok(ProtocolExtensions::Creation), + "creation-with-upload" => Ok(ProtocolExtensions::CreationWithUpload), + "termination" => Ok(ProtocolExtensions::Termination), + _ => Err(TuserError::UnknownExtension(value.clone())), + } + } +} + +impl From<ProtocolExtensions> for String { + /// Mapping protocol extensions to their + /// original names. + fn from(ext: ProtocolExtensions) -> Self { + match ext { + ProtocolExtensions::Creation => Self::from("creation"), + ProtocolExtensions::CreationWithUpload => Self::from("creation-with-upload"), + ProtocolExtensions::Termination => Self::from("termination"), + } + } } impl TuserConf { + /// Function to parse CLI parametes. + /// + /// This is a workaround for issue mentioned + /// [here](https://www.reddit.com/r/rust/comments/8ddd19/confusion_with_splitting_mainrs_into_smaller/). pub fn from_args() -> TuserConf { <TuserConf as StructOpt>::from_args() } + + /// Base API url. + pub fn base_url(&self) -> String { + format!( + "/{}", + self.url + .strip_prefix('/') + .unwrap_or_else(|| self.url.as_str()) + ) + } + + /// URL for a particular file. + pub fn file_url(&self) -> String { + let base_url = self.base_url(); + format!( + "{}/{{file_id}}", + base_url + .strip_suffix('/') + .unwrap_or_else(|| base_url.as_str()) + ) + } + + /// List of extensions. + /// + /// This function will parse list of extensions from CLI + /// and sort them. + /// + /// Protocol extensions must be sorted, + /// because Actix doesn't override + /// existing methods. + pub fn extensions_vec(&self) -> Vec<ProtocolExtensions> { + let mut ext = self + .extensions + .split(',') + .flat_map(|ext| ProtocolExtensions::try_from(String::from(ext))) + .collect::<Vec<ProtocolExtensions>>(); + // creation-with-upload + if ext.contains(&ProtocolExtensions::CreationWithUpload) + && !ext.contains(&ProtocolExtensions::Creation) + { + ext.push(ProtocolExtensions::Creation); + } + ext.sort(); + ext + } } diff --git a/src/errors.rs b/src/errors.rs index f62354fe2fac6df47afc55bace59d15d0298a802..3a70a22f99e631bea61689b3e3d4a82669b93724 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,8 +1,8 @@ use std::io::{Error, ErrorKind}; -use actix_web::{HttpResponse, ResponseError}; use actix_web::dev::HttpResponseBuilder; use actix_web::http::StatusCode; +use actix_web::{HttpResponse, ResponseError}; use thiserror::Error; pub type TuserResult<T> = Result<T, TuserError>; @@ -23,8 +23,12 @@ pub enum TuserError { UnableToReadInfo, #[error("Unable to write file {0}")] UnableToWrite(String), + #[error("Unable to remove file {0}")] + UnableToRemove(String), #[error("Unable to prepare storage. Reason: {0}")] UnableToPrepareStorage(String), + #[error("Unknown extension: {0}")] + UnknownExtension(String), } impl From<TuserError> for Error { diff --git a/src/main.rs b/src/main.rs index ffb625402295f48d3a78f9d3ee37e7e3d470a707..bbfd9b5a2868870d0b7e6c45e7a2ed15016fe026 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,10 @@ use std::str::FromStr; +use actix_web::http::Method; use actix_web::{ - App, - dev::{Server, Service}, guard, HttpServer, middleware, web, + dev::{Server, Service}, + middleware, web, App, HttpServer, }; -use actix_web::http::Method; use log::error; use config::TuserConf; @@ -13,6 +13,7 @@ use crate::storages::Storage; mod config; mod errors; +mod protocol; mod routes; mod storages; @@ -29,70 +30,24 @@ mod storages; /// This function may throw an error /// if the server can't be bound to the /// given address. -pub fn create_server<T: Storage + 'static + Send>( - storage: T, +pub fn create_server<S: Storage + 'static + Send>( + storage: S, app_conf: TuserConf, ) -> Result<Server, std::io::Error> { let host = app_conf.host.clone(); let port = app_conf.port; let workers = app_conf.workers; - let base_url = format!( - "/{}", - app_conf - .url - .strip_prefix('/') - .unwrap_or_else(|| app_conf.url.as_str()) - ); - let file_url = format!( - "{}/{{file_id}}", - base_url - .strip_suffix('/') - .unwrap_or_else(|| base_url.as_str()) - ); let mut server = HttpServer::new(move || { App::new() .data(app_conf.clone()) .data(storage.clone()) - .service( - // PATCH /base/{file_id} - // Main URL for uploading files. - web::resource(base_url.as_str()) - .name("server_info") - .guard(guard::Options()) - .to(routes::server_info), - ) - - .service( - // PATCH /base/{file_id} - // Main URL for uploading files. - web::resource(file_url.as_str()) - .name("write_bytes") - .guard(guard::Patch()) - .to(routes::write_bytes::<T>), - ) - .service( - // HEAD /base/{file_id} - // Main URL for getting info about files. - web::resource(file_url.as_str()) - .name("file_info") - .guard(guard::Head()) - // Header to prevent the client and/or proxies from caching the response. - .wrap(middleware::DefaultHeaders::new().header("Cache-Control", "no-store")) - .to(routes::get_file_info::<T>), - ) - .service( - // Post /base/{file_id} - // URL for creating files. - web::resource(base_url.as_str()) - .name("create_file") - .guard(guard::Post()) - .to(routes::create_file::<T>), - ) + // Adds all routes. + .configure(protocol::setup::<S>(app_conf.clone())) // Main middleware that appends TUS headers. .wrap( middleware::DefaultHeaders::new() .header("Tus-Resumable", "1.0.0") - .header("Tus-Version", "1.0.0") + .header("Tus-Version", "1.0.0"), ) .wrap(middleware::Logger::new("\"%r\" \"-\" \"%s\" \"%a\" \"%D\"")) // Middleware that overrides method of a request if @@ -111,7 +66,7 @@ pub fn create_server<T: Storage + 'static + Send>( // It returns 404 status_code. .default_service(web::route().to(routes::not_found)) }) - .bind((host, port))?; + .bind((host, port))?; // If custom workers count variable is provided. if let Some(workers_count) = workers { @@ -123,15 +78,14 @@ pub fn create_server<T: Storage + 'static + Send>( /// Main program entrypoint. #[actix_web::main] async fn main() -> std::io::Result<()> { - let args = TuserConf::from_args(); - simple_logging::log_to_stderr(args.log_level); + let app_conf = TuserConf::from_args(); + simple_logging::log_to_stderr(app_conf.log_level); - let storage_conf = args.clone(); - let storage = args.storage.get_storage(storage_conf); + let storage = app_conf.storage.get_storage(&app_conf); if let Err(err) = storage.prepare().await { error!("{}", err); return Err(err.into()); } - let server = create_server(storage, args)?; + let server = create_server(storage, app_conf)?; server.await } diff --git a/src/protocol/core/mod.rs b/src/protocol/core/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..b2248f46f17bfd92e83814d8efb0e0947a462697 --- /dev/null +++ b/src/protocol/core/mod.rs @@ -0,0 +1,46 @@ +use actix_web::{guard, middleware, web}; + +use crate::{Storage, TuserConf}; + +mod routes; + +/// Add core TUS protocol endpoints. +/// +/// This part of a protocol +/// has several endpoints. +/// +/// OPTIONS /api - to get info about the app. +/// HEAD /api/file - to get info about the file. +/// PATCH /api/file - to add bytes to file. +pub fn add_extension<S: Storage + 'static + Send>( + web_app: &mut web::ServiceConfig, + app_conf: &TuserConf, +) { + web_app + .service( + // PATCH /base/{file_id} + // Main URL for uploading files. + web::resource(app_conf.base_url().as_str()) + .name("core:server_info") + .guard(guard::Options()) + .to(routes::server_info), + ) + .service( + // PATCH /base/{file_id} + // Main URL for uploading files. + web::resource(app_conf.file_url().as_str()) + .name("core:write_bytes") + .guard(guard::Patch()) + .to(routes::write_bytes::<S>), + ) + .service( + // HEAD /base/{file_id} + // Main URL for getting info about files. + web::resource(app_conf.file_url().as_str()) + .name("core:file_info") + .guard(guard::Head()) + // Header to prevent the client and/or proxies from caching the response. + .wrap(middleware::DefaultHeaders::new().header("Cache-Control", "no-store")) + .to(routes::get_file_info::<S>), + ); +} diff --git a/src/protocol/core/routes.rs b/src/protocol/core/routes.rs new file mode 100644 index 0000000000000000000000000000000000000000..b43a63b967f6d101179857736d1d8d3f9b7ef218 --- /dev/null +++ b/src/protocol/core/routes.rs @@ -0,0 +1,80 @@ +use actix_web::{ + dev::HttpResponseBuilder, + http::StatusCode, + web, + web::{Buf, Bytes}, + HttpRequest, HttpResponse, +}; + +use crate::{Storage, TuserConf}; + +#[allow(clippy::needless_pass_by_value)] +pub fn server_info(app_conf: web::Data<TuserConf>) -> HttpResponse { + let ext_str = app_conf + .extensions_vec() + .into_iter() + .map(String::from) + .collect::<Vec<String>>() + .join(","); + HttpResponseBuilder::new(StatusCode::OK) + .set_header("Tus-Extension", ext_str.as_str()) + .body("") +} + +pub async fn get_file_info<T: Storage>( + storage: web::Data<T>, + request: HttpRequest, +) -> actix_web::Result<HttpResponse> { + let resp = if let Some(file_id) = request.match_info().get("file_id") { + let file_info = storage.get_file_info(file_id).await?; + HttpResponseBuilder::new(StatusCode::OK) + .set_header("Upload-Offset", file_info.offset.to_string()) + .set_header("Upload-Length", file_info.length.to_string()) + .body("") + } else { + HttpResponseBuilder::new(StatusCode::NOT_FOUND).body("") + }; + Ok(resp) +} + +pub async fn write_bytes<T: Storage>( + request: HttpRequest, + bytes: Bytes, + storage: web::Data<T>, +) -> actix_web::Result<HttpResponse> { + let content_type = + request + .headers() + .get("Content-Type") + .and_then(|header_val| match header_val.to_str() { + Ok(val) => Some(val == "application/offset+octet-stream"), + Err(_) => None, + }); + if Some(true) != content_type { + return Ok(HttpResponseBuilder::new(StatusCode::UNSUPPORTED_MEDIA_TYPE).body("")); + } + let offset = request + .headers() + .get("Upload-Offset") + .and_then(|header_val| match header_val.to_str() { + Ok(val) => Some(String::from(val)), + Err(_) => None, + }) + .and_then(|val| match val.parse::<usize>() { + Ok(offset) => Some(offset), + Err(_) => None, + }); + if offset.is_none() { + return Ok(HttpResponseBuilder::new(StatusCode::UNSUPPORTED_MEDIA_TYPE).body("")); + } + if let Some(file_id) = request.match_info().get("file_id") { + let offset = storage + .add_bytes(file_id, offset.unwrap(), bytes.bytes()) + .await?; + Ok(HttpResponseBuilder::new(StatusCode::NO_CONTENT) + .set_header("Upload-Offset", offset.to_string()) + .body("")) + } else { + Ok(HttpResponseBuilder::new(StatusCode::NOT_FOUND).body("")) + } +} diff --git a/src/protocol/creation/mod.rs b/src/protocol/creation/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..69214dc27555183200aae0bab7ff92c82302a40d --- /dev/null +++ b/src/protocol/creation/mod.rs @@ -0,0 +1,23 @@ +use actix_web::{guard, web}; + +use crate::{Storage, TuserConf}; + +mod routes; + +/// Add creation extensions. +/// +/// This extension allows you +/// to create file before sending data. +pub fn add_extension<S: Storage + 'static + Send>( + web_app: &mut web::ServiceConfig, + app_conf: &TuserConf, +) { + web_app.service( + // Post /base + // URL for creating files. + web::resource(app_conf.base_url().as_str()) + .name("creation:create_file") + .guard(guard::Post()) + .to(routes::create_file::<S>), + ); +} diff --git a/src/protocol/creation/routes.rs b/src/protocol/creation/routes.rs new file mode 100644 index 0000000000000000000000000000000000000000..d1177196b60e2c4c793282adcbf8e15c431b72f6 --- /dev/null +++ b/src/protocol/creation/routes.rs @@ -0,0 +1,28 @@ +use actix_web::dev::HttpResponseBuilder; +use actix_web::http::StatusCode; +use actix_web::{web, HttpRequest, HttpResponse}; + +use crate::Storage; + +pub async fn create_file<T: Storage>( + storage: web::Data<T>, + request: HttpRequest, +) -> actix_web::Result<HttpResponse> { + let length = request + .headers() + .get("Upload-Length") + .and_then(|value| match value.to_str() { + Ok(header_str) => Some(String::from(header_str)), + Err(_) => None, + }) + .and_then(|val| match val.parse::<usize>() { + Ok(num) => Some(num), + Err(_) => None, + }); + let file_id = storage.create_file(length, None).await?; + let upload_url = request.url_for("core:write_bytes", &[file_id])?; + Ok(HttpResponseBuilder::new(StatusCode::CREATED) + .set_header("Location", upload_url.as_str()) + .set_header("Upload-Offset", "0") + .body("")) +} diff --git a/src/protocol/creation_with_upload/mod.rs b/src/protocol/creation_with_upload/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..a0b3707c169b7d6556bf283c548ee6050747f3a2 --- /dev/null +++ b/src/protocol/creation_with_upload/mod.rs @@ -0,0 +1,19 @@ +use actix_web::{guard, web}; + +use crate::{Storage, TuserConf}; + +mod routes; + +pub fn add_extension<S: Storage + 'static + Send>( + web_app: &mut web::ServiceConfig, + app_conf: &TuserConf, +) { + web_app.service( + // Post /base + // URL for creating files. + web::resource(app_conf.base_url().as_str()) + .name("creation-with-upload:create_file") + .guard(guard::Post()) + .to(routes::create_file::<S>), + ); +} diff --git a/src/protocol/creation_with_upload/routes.rs b/src/protocol/creation_with_upload/routes.rs new file mode 100644 index 0000000000000000000000000000000000000000..c3b38a00a7e04879ad439a20804187dee3581c8b --- /dev/null +++ b/src/protocol/creation_with_upload/routes.rs @@ -0,0 +1,69 @@ +use actix_web::dev::HttpResponseBuilder; +use actix_web::http::StatusCode; +use actix_web::web::{Buf, Bytes}; +use actix_web::{web, HttpRequest, HttpResponse}; + +use crate::Storage; + +/// Creates files with initial bytes. +/// +/// This function is similar to +/// creation:create_file, +/// except that it can write bytes +/// right after it created a data file. +pub async fn create_file<T: Storage>( + storage: web::Data<T>, + request: HttpRequest, + bytes: Bytes, +) -> actix_web::Result<HttpResponse> { + let length = request + .headers() + .get("Upload-Length") + .and_then(|value| match value.to_str() { + Ok(header_str) => Some(String::from(header_str)), + Err(_) => None, + }) + .and_then(|val| match val.parse::<usize>() { + Ok(num) => Some(num), + Err(_) => None, + }); + let file_id = storage.create_file(length, None).await?; + let mut upload_offset = 0; + if !bytes.is_empty() { + // Checking if content type matches. + let content_type = request + .headers() + .get("Content-Type") + .and_then(|header_val| match header_val.to_str() { + Ok(val) => Some(val == "application/offset+octet-stream"), + Err(_) => None, + }); + if Some(true) != content_type { + return Ok(HttpResponseBuilder::new(StatusCode::UNSUPPORTED_MEDIA_TYPE).body("")); + } + // Checking if + let offset = request + .headers() + .get("Upload-Offset") + .and_then(|header_val| match header_val.to_str() { + Ok(val) => Some(String::from(val)), + Err(_) => None, + }) + .and_then(|val| match val.parse::<usize>() { + Ok(offset) => Some(offset), + Err(_) => None, + }); + if offset.is_none() { + return Ok(HttpResponseBuilder::new(StatusCode::UNSUPPORTED_MEDIA_TYPE).body("")); + } + + upload_offset = storage + .add_bytes(file_id.as_str(), offset.unwrap(), bytes.bytes()) + .await?; + } + let upload_url = request.url_for("core:write_bytes", &[file_id])?; + Ok(HttpResponseBuilder::new(StatusCode::CREATED) + .set_header("Location", upload_url.as_str()) + .set_header("Upload-Offset", upload_offset.to_string()) + .body("")) +} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..b41c1475af3f43fa1e5f8ec852b3971b04ab31cf --- /dev/null +++ b/src/protocol/mod.rs @@ -0,0 +1,32 @@ +use actix_web::web; + +use crate::config::ProtocolExtensions; +use crate::{Storage, TuserConf}; + +mod core; +mod creation; +mod creation_with_upload; +mod termination; + +/// Configure TUS web application. +/// +/// This function resolves all protocol extensions +/// provided by CLI into services and adds it to the application. +pub fn setup<S: Storage + 'static + Send>( + app_conf: TuserConf, +) -> Box<dyn Fn(&mut web::ServiceConfig)> { + Box::new(move |web_app| { + for extension in app_conf.extensions_vec() { + match extension { + ProtocolExtensions::Creation => creation::add_extension::<S>(web_app, &app_conf), + ProtocolExtensions::CreationWithUpload => { + creation_with_upload::add_extension::<S>(web_app, &app_conf); + } + ProtocolExtensions::Termination => { + termination::add_extension::<S>(web_app, &app_conf); + } + } + } + core::add_extension::<S>(web_app, &app_conf); + }) +} diff --git a/src/protocol/termination/mod.rs b/src/protocol/termination/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..7ed341d05ad5a48c9b28866d81199ae171a42545 --- /dev/null +++ b/src/protocol/termination/mod.rs @@ -0,0 +1,22 @@ +use actix_web::{guard, web}; + +use crate::{Storage, TuserConf}; + +mod routes; + +/// Add termination extension. +/// +/// This extension allows you +/// to terminate file upload. +pub fn add_extension<S: Storage + 'static + Send>( + web_app: &mut web::ServiceConfig, + app_conf: &TuserConf, +) { + web_app.service( + // DELETE /base/file + web::resource(app_conf.file_url().as_str()) + .name("termination:terminate") + .guard(guard::Delete()) + .to(routes::terminate::<S>), + ); +} diff --git a/src/protocol/termination/routes.rs b/src/protocol/termination/routes.rs new file mode 100644 index 0000000000000000000000000000000000000000..0aa971ce962e985810e74013579067879d74b2b7 --- /dev/null +++ b/src/protocol/termination/routes.rs @@ -0,0 +1,21 @@ +use actix_web::dev::HttpResponseBuilder; +use actix_web::http::StatusCode; +use actix_web::{web, HttpRequest, HttpResponse}; + +use crate::errors::TuserResult; +use crate::Storage; + +/// Terminate uploading. +/// +/// This method will remove all +/// files by id. +pub async fn terminate<S: Storage>( + storage: web::Data<S>, + request: HttpRequest, +) -> TuserResult<HttpResponse> { + let file_id_opt = request.match_info().get("file_id").map(String::from); + if let Some(file_id) = file_id_opt { + storage.remove_file(file_id.as_str()).await?; + } + Ok(HttpResponseBuilder::new(StatusCode::NO_CONTENT).body("")) +} diff --git a/src/routes.rs b/src/routes.rs index 1413c2d0d29976ebb821c7f16d827d30347176fb..d510bfea02e86dffdb06c5a81b196876b01755e2 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,102 +1,10 @@ -use actix_web::{HttpRequest, HttpResponse}; use actix_web::dev::HttpResponseBuilder; use actix_web::http::StatusCode; -use actix_web::web; -use actix_web::web::{Buf, Bytes}; - -use crate::storages::Storage; +use actix_web::HttpResponse; /// Default response to all unknown URLs. -#[allow(clippy::needless_pass_by_value)] pub fn not_found() -> HttpResponse { HttpResponseBuilder::new(StatusCode::NOT_FOUND) .set_header("Content-Type", "text/html; charset=utf-8") .body("Not found") } - -pub fn server_info() -> HttpResponse { - HttpResponseBuilder::new(StatusCode::OK) - .set_header("Tus-Extension", "creation") - .body("") -} - -pub async fn get_file_info<T: Storage>( - storage: web::Data<T>, - request: HttpRequest, -) -> actix_web::Result<HttpResponse> { - let resp = if let Some(file_id) = request.match_info().get("file_id") { - let file_info = storage.get_file_info(file_id).await?; - HttpResponseBuilder::new(StatusCode::OK) - .set_header("Upload-Offset", file_info.offset.to_string()) - .set_header("Upload-Length", file_info.length.to_string()) - .body("") - } else { - HttpResponseBuilder::new(StatusCode::NOT_FOUND).body("") - }; - Ok(resp) -} - -pub async fn create_file<T: Storage>( - storage: web::Data<T>, - request: HttpRequest, -) -> actix_web::Result<HttpResponse> { - let length = request - .headers() - .get("Upload-Length") - .and_then(|value| match value.to_str() { - Ok(header_str) => Some(String::from(header_str)), - Err(_) => None, - }) - .and_then(|val| match val.parse::<usize>() { - Ok(num) => Some(num), - Err(_) => None, - }); - let file_id = storage.create_file(length, None).await?; - let upload_url = request.url_for("write_bytes", &[file_id])?; - Ok(HttpResponseBuilder::new(StatusCode::CREATED) - .set_header("Location", upload_url.as_str()) - .body("")) -} - -pub async fn write_bytes<T: Storage>( - request: HttpRequest, - bytes: Bytes, - storage: web::Data<T>, -) -> actix_web::Result<HttpResponse> { - let conflict_response = HttpResponseBuilder::new(StatusCode::UNSUPPORTED_MEDIA_TYPE).body(""); - let content_type = - request - .headers() - .get("Content-Type") - .and_then(|header_val| match header_val.to_str() { - Ok(val) => Some(val == "application/offset+octet-stream"), - Err(_) => None, - }); - if Some(true) != content_type { - return Ok(conflict_response); - } - let offset = - request - .headers() - .get("Upload-Offset") - .and_then(|header_val| match header_val.to_str() { - Ok(val) => Some(String::from(val)), - Err(_) => None, - }).and_then(|val| { - match val.parse::<usize>() { - Ok(offset) => Some(offset), - Err(_) => None - } - }); - if offset.is_none() { - return Ok(conflict_response); - } - if let Some(file_id) = request.match_info().get("file_id") { - let offset = storage.add_bytes(file_id, offset.unwrap(), bytes.bytes()).await?; - Ok(HttpResponseBuilder::new(StatusCode::NO_CONTENT) - .set_header("Upload-Offset", offset.to_string()) - .body("")) - } else { - Ok(HttpResponseBuilder::new(StatusCode::NOT_FOUND).body("")) - } -} diff --git a/src/storages/file_storage.rs b/src/storages/file_storage.rs index 80aee961fea71882f8d5ffbe0fe54e12f2dca82d..1f6abeb63b2a8c7626e77c4c6ce4e303079a81ab 100644 --- a/src/storages/file_storage.rs +++ b/src/storages/file_storage.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; +use std::path::PathBuf; use actix_files::NamedFile; -use async_std::fs::{DirBuilder, OpenOptions, read_to_string}; +use async_std::fs::{DirBuilder, OpenOptions, read_to_string, remove_file}; use async_std::prelude::*; use async_trait::async_trait; use log::error; @@ -20,6 +21,14 @@ impl FileStorage { pub fn new(app_conf: TuserConf) -> FileStorage { FileStorage { app_conf } } + + pub fn info_file_path(&self, file_id: &str) -> PathBuf { + self.app_conf.data.join(format!("{}.info", file_id)) + } + + pub fn data_file_path(&self, file_id: &str) -> PathBuf { + self.app_conf.data.join(file_id.to_string()) + } } #[async_trait] @@ -35,8 +44,11 @@ impl Storage for FileStorage { } async fn get_file_info(&self, file_id: &str) -> TuserResult<FileInfo> { - let info_file_path = self.app_conf.data.join(format!("{}.info", file_id)); - let contents = read_to_string(info_file_path).await.map_err(|err| { + let info_path = self.info_file_path(file_id); + if !info_path.exists() { + return Err(TuserError::FileNotFound(String::from(file_id))); + } + let contents = read_to_string(info_path).await.map_err(|err| { error!("{:?}", err); TuserError::UnableToReadInfo })?; @@ -44,11 +56,10 @@ impl Storage for FileStorage { } async fn set_file_info(&self, file_info: &FileInfo) -> TuserResult<()> { - let info_file_path = self.app_conf.data.join(format!("{}.info", file_info.id)); let mut file = OpenOptions::new() .write(true) .create(true) - .open(info_file_path.as_path()) + .open(self.info_file_path(file_info.id.as_str()).as_path()) .await .map_err(|err| { error!("{:?}", err); @@ -58,7 +69,12 @@ impl Storage for FileStorage { .await .map_err(|err| { error!("{:?}", err); - TuserError::UnableToWrite(info_file_path.as_path().display().to_string()) + TuserError::UnableToWrite( + self.info_file_path(file_info.id.as_str()) + .as_path() + .display() + .to_string(), + ) })?; Ok(()) } @@ -67,8 +83,12 @@ impl Storage for FileStorage { Err(TuserError::FileNotFound(String::from(file_id))) } - async fn add_bytes(&self, file_id: &str, request_offset: usize, bytes: &[u8]) -> TuserResult<usize> { - let file_path = self.app_conf.data.join(file_id); + async fn add_bytes( + &self, + file_id: &str, + request_offset: usize, + bytes: &[u8], + ) -> TuserResult<usize> { let mut info = self.get_file_info(file_id).await?; if info.offset != request_offset { return Err(TuserError::WrongOffset); @@ -77,7 +97,7 @@ impl Storage for FileStorage { .write(true) .append(true) .create(false) - .open(file_path.as_path()) + .open(self.data_file_path(file_id)) .await .map_err(|err| { error!("{:?}", err); @@ -85,7 +105,7 @@ impl Storage for FileStorage { })?; file.write_all(bytes).await.map_err(|err| { error!("{:?}", err); - TuserError::UnableToWrite(file_path.as_path().display().to_string()) + TuserError::UnableToWrite(self.data_file_path(file_id).as_path().display().to_string()) })?; info.offset += bytes.len(); self.set_file_info(&info).await?; @@ -98,12 +118,12 @@ impl Storage for FileStorage { metadata: Option<HashMap<String, String>>, ) -> TuserResult<String> { let file_id = Uuid::new_v4().simple().to_string(); - let file_path = self.app_conf.data.join(file_id.as_str()); + let mut file = OpenOptions::new() .write(true) .create(true) .create_new(true) - .open(file_path.as_path()) + .open(self.data_file_path(file_id.as_str()).as_path()) .await .map_err(|err| { error!("{:?}", err); @@ -113,13 +133,21 @@ impl Storage for FileStorage { // We write empty file here. file.write_all(b"").await.map_err(|err| { error!("{:?}", err); - TuserError::UnableToWrite(file_path.as_path().display().to_string()) + TuserError::UnableToWrite( + self.data_file_path(file_id.as_str()) + .as_path() + .display() + .to_string(), + ) })?; let file_info = FileInfo::new( file_id.as_str(), file_size, - file_path.as_path().display().to_string(), + self.data_file_path(file_id.as_str()) + .as_path() + .display() + .to_string(), metadata, ); @@ -127,4 +155,24 @@ impl Storage for FileStorage { Ok(file_id) } + + async fn remove_file(&self, file_id: &str) -> TuserResult<()> { + let info_path = self.info_file_path(file_id); + if !info_path.exists() { + return Err(TuserError::FileNotFound(String::from(file_id))); + } + let data_path = self.data_file_path(file_id); + if !data_path.exists() { + return Err(TuserError::FileNotFound(String::from(file_id))); + } + remove_file(info_path).await.map_err(|err| { + error!("{:?}", err); + TuserError::UnableToRemove(String::from(file_id)) + })?; + remove_file(data_path).await.map_err(|err| { + error!("{:?}", err); + TuserError::UnableToRemove(String::from(file_id)) + })?; + Ok(()) + } } diff --git a/src/storages/mod.rs b/src/storages/mod.rs index dd641fa15df6cfe8cb6029c7f3577eece3bb7721..613d77f47a5b07d21f11729cd7d609ff317fbbc9 100644 --- a/src/storages/mod.rs +++ b/src/storages/mod.rs @@ -3,8 +3,8 @@ use std::str::FromStr; use actix_files::NamedFile; use async_trait::async_trait; -use chrono::{DateTime, Utc}; use chrono::serde::ts_seconds; +use chrono::{DateTime, Utc}; use derive_more::{Display, From}; use serde::{Deserialize, Serialize}; @@ -23,7 +23,7 @@ pub enum AvailableStores { impl FromStr for AvailableStores { type Err = String; - /// This function converts string to the AvailableStore item. + /// This function converts string to the `AvailableStore` item. /// This function is used by structopt to parse CLI parameters. /// /// # Params @@ -37,13 +37,14 @@ impl FromStr for AvailableStores { } impl AvailableStores { - /// Convert AvailableStores to the Storage. + /// Convert `AvailableStores` to the Storage. /// /// # Params /// `config` - Tuser configuration. /// - pub fn get_storage(&self, config: TuserConf) -> impl Storage { - file_storage::FileStorage::new(config) + #[allow(clippy::unused_self)] + pub fn get_storage(&self, config: &TuserConf) -> impl Storage { + file_storage::FileStorage::new(config.clone()) } } @@ -62,7 +63,7 @@ pub struct FileInfo { } impl FileInfo { - /// Create new FileInfo + /// Creates new `FileInfo`. /// /// # Params /// @@ -141,11 +142,15 @@ pub trait Storage: Clone { /// It returns new offset. /// /// # Params - /// /// `file_id` - unique file identifier; /// `request_offset` - offset from the client. /// `bytes` - bytes to append to the file. - async fn add_bytes(&self, file_id: &str, request_offset: usize, bytes: &[u8]) -> TuserResult<usize>; + async fn add_bytes( + &self, + file_id: &str, + request_offset: usize, + bytes: &[u8], + ) -> TuserResult<usize>; /// Create file in storage. /// @@ -159,4 +164,13 @@ pub trait Storage: Clone { file_size: Option<usize>, metadata: Option<HashMap<String, String>>, ) -> TuserResult<String>; + + /// Remove file from storage + /// + /// This method removes file and all associated + /// object if any. + /// + /// # Params + /// `file_id` - unique file identifier; + async fn remove_file(&self, file_id: &str) -> TuserResult<()>; }