diff --git a/.gitignore b/.gitignore index ea8c4bf..765220b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +.idea/ +postit.json diff --git a/Cargo.lock b/Cargo.lock index 7ea609c..78ea7d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,7 +12,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" dependencies = [ - "memchr", + "memchr 2.3.3", ] [[package]] @@ -87,6 +87,16 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" +[[package]] +name = "bincode" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5753e2a71534719bf3f4e57006c3a4f0d2c672a4b676eec84161f763eca87dbf" +dependencies = [ + "byteorder", + "serde", +] + [[package]] name = "bitflags" version = "1.2.1" @@ -151,7 +161,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" dependencies = [ - "memchr", + "memchr 2.3.3", "safemem", ] @@ -323,6 +333,30 @@ dependencies = [ "winapi", ] +[[package]] +name = "fixedbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" + +[[package]] +name = "flate2" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -393,6 +427,15 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" +dependencies = [ + "autocfg 1.0.0", +] + [[package]] name = "itoa" version = "0.4.5" @@ -461,6 +504,15 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +[[package]] +name = "memchr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.3.3" @@ -488,6 +540,15 @@ dependencies = [ "unicase", ] +[[package]] +name = "miniz_oxide" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5" +dependencies = [ + "adler32", +] + [[package]] name = "multipart" version = "0.15.4" @@ -506,6 +567,61 @@ dependencies = [ "twoway", ] +[[package]] +name = "nom" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aec50c70fd288702bcd93284a8444607f3292dbdf2a30de5ea5dcdbe72287b" +dependencies = [ + "memchr 1.0.2", +] + +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg 1.0.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg 1.0.0", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8b15b261814f992e33760b1fca9fe8b693d8a65299f20c9901688636cfb746" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-integer" version = "0.1.42" @@ -516,6 +632,29 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00" +dependencies = [ + "autocfg 1.0.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg 1.0.0", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.11" @@ -614,6 +753,16 @@ dependencies = [ "sha-1", ] +[[package]] +name = "petgraph" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c127eea4a29ec6c85d153c59dc1213f33ec74cead30fe4730aecc88cc1fd92" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "phf" version = "0.7.24" @@ -657,8 +806,14 @@ dependencies = [ name = "postit" version = "0.1.0" dependencies = [ + "anyhow", + "bincode", + "chrono", "clappconfig", + "flate2", "log 0.4.8", + "num", + "num-derive", "parking_lot", "rand 0.7.3", "rouille", @@ -666,6 +821,7 @@ dependencies = [ "serde_derive", "serde_json", "siphasher 0.3.3", + "tree_magic", ] [[package]] @@ -904,7 +1060,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" dependencies = [ "aho-corasick", - "memchr", + "memchr 2.3.3", "regex-syntax", "thread_local", ] @@ -1140,13 +1296,26 @@ dependencies = [ "url", ] +[[package]] +name = "tree_magic" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d99367ce3e553a84738f73bd626ccca541ef90ae757fdcdc4cbe728e6cb629" +dependencies = [ + "fnv", + "lazy_static", + "nom", + "parking_lot", + "petgraph", +] + [[package]] name = "twoway" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" dependencies = [ - "memchr", + "memchr 2.3.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f2a8969..13667dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,10 @@ serde_derive = "1.0" log = "0.4.8" siphasher = "0.3.3" rand = "0.7.3" +chrono = "0.4.11" +bincode = "1.2.1" +flate2 = "1.0.14" +anyhow = "1.0.28" +tree_magic = { version = "0.2.3", default_features = false, features = ["staticmime"] } +num = "0.2.1" +num-derive = "0.3.0" diff --git a/postit.db b/postit.db new file mode 100644 index 0000000..156c822 Binary files /dev/null and b/postit.db differ diff --git a/src/config.rs b/src/config.rs index 85d641b..f03d461 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,8 @@ -use clappconfig::{AppConfig, anyhow, clap::ArgMatches}; +use clappconfig::{anyhow, clap::ArgMatches, AppConfig}; use std::collections::HashMap; use std::time::Duration; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(default)] #[serde(deny_unknown_fields)] pub(crate) struct Config { @@ -32,6 +32,15 @@ pub(crate) struct Config { /// Max uploaded file size in bytes pub(crate) max_file_size: usize, + + /// Enable persistence + pub(crate) persistence: bool, + + /// Enable compression when persisting/loading + pub(crate) compression: bool, + + /// Persistence file + pub(crate) persist_file : String, } impl Default for Config { @@ -44,7 +53,10 @@ impl Default for Config { default_expiry: Duration::from_secs(60 * 10), max_expiry: Duration::from_secs(60 * 10), expired_gc_interval: Duration::from_secs(60), - max_file_size: 1 * (1024 * 1024) // 1MB + max_file_size: 1 * (1024 * 1024), // 1MB + persistence: false, + compression: true, + persist_file: "postit.db".to_string(), } } } @@ -66,24 +78,19 @@ impl AppConfig for Config { } mod serde_duration_secs { - use serde::{self, Deserialize, Serializer, Deserializer}; + use serde::{self, Deserialize, Deserializer, Serializer}; use std::time::Duration; - pub fn serialize( - value: &Duration, - se: S, - ) -> Result - where - S: Serializer, + pub fn serialize(value: &Duration, se: S) -> Result + where + S: Serializer, { se.serialize_u64(value.as_secs()) } - pub fn deserialize<'de, D>( - de: D, - ) -> Result - where - D: Deserializer<'de>, + pub fn deserialize<'de, D>(de: D) -> Result + where + D: Deserializer<'de>, { let s: u64 = u64::deserialize(de)?; Ok(Duration::from_secs(s)) diff --git a/src/favicon.ico b/src/favicon.ico new file mode 100644 index 0000000..9678c57 Binary files /dev/null and b/src/favicon.ico differ diff --git a/src/main.rs b/src/main.rs index 413f788..f451c87 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,53 +1,63 @@ -#[macro_use] extern crate serde_derive; -#[macro_use] extern crate log; +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate log; +#[macro_use] +extern crate num_derive; -use std::time::{Instant, Duration}; +use crate::config::Config; +use clappconfig::{anyhow, AppConfig}; use parking_lot::Mutex; -use std::collections::HashMap; -use clappconfig::{AppConfig, anyhow}; -use std::io::Read; -use std::hash::{Hash, Hasher}; use rand::rngs::OsRng; use rand::Rng; use rouille::{Request, Response, ResponseBody}; use std::borrow::Cow; -use crate::config::Config; +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; +use std::io::Read; +use std::time::Duration; +use chrono::{Utc, DateTime}; +use std::fs::OpenOptions; +use std::fmt::Display; +use serde::export::Formatter; +use std::fmt; +use serde::{Serialize, Serializer, Deserialize, Deserializer}; +use serde::de::{DeserializeOwned, Visitor}; +use crate::well_known_mime::Mime; mod config; +mod well_known_mime; -fn error_with_text(code : u16, text : impl Into) -> Response { - Response { - status_code: code, - headers: vec![("Content-Type".into(), "text/plain; charset=utf8".into())], - data: rouille::ResponseBody::from_string(text), - upgrade: None, - } -} +const HDR_EXPIRES : &str = "X-Expires"; +const HDR_SECRET : &str = "X-Secret"; -fn empty_error(code : u16) -> Response { - Response { - status_code: code, - headers: vec![("Content-Type".into(), "text/plain; charset=utf8".into())], - data: rouille::ResponseBody::empty(), - upgrade: None, - } -} +const FAVICON : &[u8] = include_bytes!("favicon.ico"); +/// Post ID (represented as a 16-digit hex string) type PostId = u64; +/// Write token (represented as a 16-digit hex string) type Secret = u64; +/// Hash of a data record type DataHash = u64; -#[derive(Debug)] +/// Post stored in the repository +#[derive(Debug,Serialize,Deserialize)] struct Post { - mime : Cow<'static, str>, - hash : DataHash, - secret : Secret, - expires : Instant, + /// Content-Type + mime: Mime, + /// Data hash + hash: DataHash, + /// Secret key for editing or deleting + secret: Secret, + /// Expiration timestamp + #[serde(with = "serde_chrono_datetime_as_unix")] + expires: DateTime, } impl Post { + /// Check if the post expired pub fn is_expired(&self) -> bool { - self.expires < Instant::now() + self.expires < Utc::now() } } @@ -55,7 +65,15 @@ fn main() -> anyhow::Result<()> { let config = Config::init("postit", "postit.json", None)?; let serve_at = format!("{}:{}", config.host, config.port); - let store = Mutex::new(Repository::new(config)); + let store = Mutex::new({ + let mut store = Repository::new(config); + if store.config.persistence { + if let Err(e) = store.load() { + error!("Load failed: {}", e); + } + } + store + }); rouille::start_server(serve_at, move |req| { let mut store_w = store.lock(); @@ -63,52 +81,120 @@ fn main() -> anyhow::Result<()> { info!("{} {}", method, req.raw_url()); + if req.url() == "/favicon.ico" { + return Response::from_data("image/vnd.microsoft.icon", FAVICON); + } + store_w.gc_expired_posts_if_needed(); - match method { - "POST" | "PUT" => { - store_w.serve_post_put(req) - } - "GET" | "HEAD" => { - store_w.serve_get_head(req) - } - "DELETE" => { - store_w.serve_delete(req) - } - _ => { - rouille::Response::empty_400() + let resp = match method { + "POST" | "PUT" => store_w.serve_post_put(req), + "GET" | "HEAD" => store_w.serve_get_head(req), + "DELETE" => store_w.serve_delete(req), + _ => rouille::Response::empty_400(), + }; + + if store_w.config.persistence { + if let Err(e) = store_w.persist_if_needed() { + error!("Store failed: {}", e); } } + + if resp.is_error() { + warn!("Error resp: {}", resp.status_code); + } + + resp }); } type PostsMap = HashMap; type DataMap = HashMap)>; +#[derive(Debug,Serialize,Deserialize)] struct Repository { + #[serde(skip)] config: Config, + /// Flag that the repository needs saving + #[serde(skip)] + dirty: bool, + /// Stored posts posts: PostsMap, - /// (use_count, data) + /// Post data - (use_count, data) data: DataMap, /// Time of last expired posts GC - last_gc_time: Instant, + #[serde(with = "serde_chrono_datetime_as_unix")] + last_gc_time: DateTime, } impl Repository { + /// New instance fn new(config: Config) -> Self { Repository { config, + dirty: false, posts: Default::default(), data: Default::default(), - last_gc_time: Instant::now(), + last_gc_time: Utc::now(), + } + } + + fn persist_if_needed(&mut self) -> anyhow::Result<()> { + if self.dirty { + self.persist() + } else { + Ok(()) } } - fn serve_delete(&mut self, req : &Request) -> Response { + /// Store to a file + fn persist(&mut self) -> anyhow::Result<()> { + debug!("Persist to file: {}", self.config.persist_file); + + self.dirty = false; + let file = OpenOptions::new() + .truncate(true) + .write(true) + .create(true) + .open(&self.config.persist_file)?; + + if self.config.compression { + let flate = flate2::write::DeflateEncoder::new(file, flate2::Compression::best()); + bincode::serialize_into(flate, self)?; + } else { + bincode::serialize_into(file, self)?; + } + Ok(()) + } + + /// Load from a file + fn load(&mut self) -> anyhow::Result<()> { + debug!("Load from file: {}", self.config.persist_file); + + let file = OpenOptions::new() + .read(true) + .open(&self.config.persist_file)?; + + let result : Repository = if self.config.compression { + let flate = flate2::read::DeflateDecoder::new(file); + bincode::deserialize_from(flate)? + } else { + bincode::deserialize_from(file)? + }; + + let old_config = self.config.clone(); + std::mem::replace(self, result); + self.config = old_config; + self.dirty = false; + Ok(()) + } + + /// Serve a DELETE request + fn serve_delete(&mut self, req: &Request) -> Response { let post_id = match self.request_to_post_id(req, true) { Ok(Some(pid)) => pid, Ok(None) => return error_with_text(400, "Post ID required."), - Err(resp) => return resp + Err(resp) => return resp, }; self.delete_post(post_id); @@ -116,18 +202,25 @@ impl Repository { Response::text("Deleted.") } - fn serve_post_put(&mut self, req : &Request) -> Response { + /// Serve a POST or PUT request + /// + /// POST inserts a new record + /// PUT updates a record + fn serve_post_put(&mut self, req: &Request) -> Response { + // Post ID is empty for POST, set for PUT let post_id = match self.request_to_post_id(req, true) { Ok(pid) => { if req.method() == "PUT" && pid.is_none() { + warn!("PUT without ID!"); return error_with_text(400, "PUT requires a file ID!"); } else if req.method() == "POST" && pid.is_some() { + warn!("POST with ID!"); return error_with_text(400, "Use PUT to update a file!"); } pid - }, - Err(resp) => return resp + } + Err(resp) => return resp, }; debug!("Submit new data, post ID: {:?}", post_id); @@ -136,7 +229,9 @@ impl Repository { if let Some(body) = req.data() { // Read up to 1 byte past the limit to catch too large uploads. // We can't reply on the "Length" field, which is not present with chunked encoding. - body.take(self.config.max_file_size as u64 + 1).read_to_end(&mut data).unwrap(); + body.take(self.config.max_file_size as u64 + 1) + .read_to_end(&mut data) + .unwrap(); if data.len() > self.config.max_file_size { return empty_error(413); } @@ -144,38 +239,44 @@ impl Repository { return error_with_text(400, "Empty body!"); } + // Convert "application/x-www-form-urlencoded" to text/plain (CURL uses this) + // NOTE: rouille does NOT parse urlencoded, we will serve the encoded format back if really used. let mime = match req.header("Content-Type") { None => None, - Some("application/x-www-form-urlencoded") => Some("text/plain"), + Some("application/x-www-form-urlencoded") => None, Some(v) => Some(v), }; - let expiry = match req.header("X-Expires") { - Some(text) => { - match text.parse() { - Ok(v) => { - let dur = Duration::from_secs(v); - if dur > self.config.max_expiry { - return error_with_text(400, - format!("Expiration time {} out of allowed range 0-{} s", - v, - self.config.max_expiry.as_secs() - )); - } - Some(dur) - }, - Err(_) => { - return error_with_text(400, "Malformed \"X-Expires\", use relative time in seconds."); - }, + let expiry = match req.header(HDR_EXPIRES) { + Some(text) => match text.parse() { + Ok(v) => { + let dur = Duration::from_secs(v); + if dur > self.config.max_expiry { + return error_with_text( + 400, + format!( + "Expiration time {} out of allowed range 0-{} s", + v, + self.config.max_expiry.as_secs() + ), + ); + } + Some(dur) } - } - None => None + Err(_) => { + return error_with_text( + 400, + "Malformed \"X-Expires\", use relative time in seconds.", + ); + } + }, + None => None, }; if let Some(id) = post_id { // UPDATE self.update(id, data, mime, expiry); - Response::text("Updated.") + Response::text("Updated OK.") } else { // INSERT let (id, token) = self.insert(data, mime, expiry.unwrap_or(self.config.default_expiry)); @@ -185,11 +286,12 @@ impl Repository { } } - fn serve_get_head(&mut self, req : &Request) -> Response { + /// Serve a GET or HEAD request + fn serve_get_head(&mut self, req: &Request) -> Response { let post_id = match self.request_to_post_id(req, false) { Ok(Some(pid)) => pid, Ok(None) => return error_with_text(400, "Post ID required."), - Err(resp) => return resp + Err(resp) => return resp, }; if let Some(post) = self.posts.get(&post_id) { @@ -205,7 +307,10 @@ impl Repository { Response { status_code: 200, - headers: vec![("Content-Type".into(), format!("{}; charset=utf8", post.mime).into())], + headers: vec![( + "Content-Type".into(), + format!("{}; charset=utf8", post.mime).into(), + )], data: if req.method() == "HEAD" { ResponseBody::empty() } else { @@ -220,47 +325,64 @@ impl Repository { } } - fn request_to_post_id(&self, req : &Request, check_secret : bool) -> Result, Response> { + /// Extract post ID from a request. + /// + /// if `check_secret` is true, ensure a `X-Secret` header contains a valid write token + /// for the post ID. + fn request_to_post_id( + &self, + req: &Request, + check_secret: bool, + ) -> Result, Response> { let url = req.url(); let stripped = url.trim_matches('/'); if stripped.is_empty() { - // No ID given + debug!("No ID given"); return Ok(None); } + if stripped.len() != 16 { + warn!("Bad ID len!"); + return Err(Response::empty_404()); + } + let id = match u64::from_str_radix(stripped, 16) { Ok(bytes) => bytes, Err(_) => { - return Err(error_with_text(400, "Bad file ID format!")); - }, + warn!("ID parsing error: {}", stripped); + return Err(Response::empty_404()); + } }; if check_secret { // Check the write token match self.posts.get(&id) { - None/* | Some(_p) if _p.is_expired()*/ => { + None => { + warn!("ID {} does not exist!", id); return Err(error_with_text(404, "No file with this ID!")); }, Some(post) => { if post.is_expired() { - warn!("Access of expired post!"); + warn!("Access of expired file {}!", id); return Err(error_with_text(404, "No file with this ID!")); } - let secret: u64 = match req.header("X-Secret").map(|v| u64::from_str_radix(v, 16)) { + let secret: u64 = match req.header(HDR_SECRET).map(|v| u64::from_str_radix(v, 16)) { Some(Ok(bytes)) => bytes, None => { - return Err(error_with_text(400, "X-Secret required!")); + warn!("Missing secret token!"); + return Err(error_with_text(400, "Secret token required!")); } Some(Err(e)) => { - warn!("{:?}", e); - return Err(error_with_text(400, "Bad secret format!")); + warn!("Token parse error: {:?}", e); + return Err(error_with_text(400, "Bad secret token format!")); }, }; if post.secret != secret { - return Err(error_with_text(401, "Invalid secret!")); + warn!("Secret token mismatch"); + return Err(error_with_text(401, "Invalid secret token!")); } }, } @@ -270,15 +392,17 @@ impl Repository { Ok(Some(id)) } + /// Drop expired posts, if cleaning is due fn gc_expired_posts_if_needed(&mut self) { - if self.last_gc_time.elapsed() > self.config.expired_gc_interval { + if Utc::now().signed_duration_since(self.last_gc_time).to_std().unwrap_or_default() > self.config.expired_gc_interval { self.gc_expired_posts(); - self.last_gc_time = Instant::now(); + self.last_gc_time = Utc::now(); } } + /// Drop expired posts fn gc_expired_posts(&mut self) { - debug!("GC expired posts"); + debug!("GC expired uploads"); let mut to_rm = vec![]; for post in &self.posts { @@ -287,6 +411,10 @@ impl Repository { } } + if !to_rm.is_empty() { + self.dirty = true; + } + for id in to_rm { debug!("Drop post ID {:016x}", id); if let Some(post) = self.posts.remove(&id) { @@ -295,30 +423,37 @@ impl Repository { } } - fn hash_data(data : &Vec) -> DataHash { + /// Get hash of a byte vector (for deduplication) + fn hash_data(data: &Vec) -> DataHash { let mut hasher = siphasher::sip::SipHasher::new(); data.hash(&mut hasher); hasher.finish() } - fn store_data_or_increment_rc(data_map : &mut DataMap, hash : u64, data: Vec) { + /// Store a data buffer under a given hash. + /// If the buffer is already present in the repository, increment its use count. + fn store_data_or_increment_rc(data_map: &mut DataMap, hash: u64, data: Vec) { match data_map.get_mut(&hash) { None => { debug!("Store new data hash #{:016x}", hash); data_map.insert(hash, (1, data)); - }, + } Some(entry) => { debug!("Link new use of data hash #{:016x}", hash); entry.0 += 1; // increment use counter - }, + } } } - fn drop_data_or_decrement_rc(data_map : &mut DataMap, hash : u64) { + /// Drop a data record with the given hash, or decrement its use count if there are other uses + fn drop_data_or_decrement_rc(data_map: &mut DataMap, hash: u64) { if let Some(old_data) = data_map.get_mut(&hash) { if old_data.0 > 1 { old_data.0 -= 1; - debug!("Unlink use of data hash #{:016x} ({} remain)", hash, old_data.0); + debug!( + "Unlink use of data hash #{:016x} ({} remain)", + hash, old_data.0 + ); } else { debug!("Drop data hash #{:016x}", hash); data_map.remove(&hash); @@ -326,13 +461,26 @@ impl Repository { } } - fn insert(&mut self, data : Vec, mime : Option<&str>, expires : Duration) -> (PostId, Secret) { - info!("Insert post with data of len {} bytes, mime {}, expiry {:?}", - data.len(), mime.unwrap_or("unspecified"), - expires); + /// Insert a post + fn insert(&mut self, data: Vec, mime: Option<&str>, expires: Duration) -> (PostId, Secret) { + info!( + "Insert post with data of len {} bytes, mime {}, expiry {:?}", + data.len(), + mime.unwrap_or("unspecified"), + expires + ); let hash = Self::hash_data(&data); + let mime = match mime { + None => { + Mime::from(tree_magic::from_u8(&data)) + }, + Some(explicit) => { + Mime::from(explicit) + }, + }; + Self::store_data_or_increment_rc(&mut self.data, hash, data); let post_id = loop { @@ -344,26 +492,36 @@ impl Repository { let secret = OsRng.gen(); - debug!("Data hash = #{:016x}", hash); debug!("Post ID = #{:016x}", post_id); + debug!("Data hash = #{:016x}, mime {}", hash, mime); debug!("Secret = #{:016x}", secret); - self.posts.insert(post_id, Post { - mime: mime.map(ToString::to_string).map(Cow::Owned) - .unwrap_or(Cow::Borrowed("application/octet-stream")), - hash, - secret, - expires: Instant::now() + expires - }); + self.posts.insert( + post_id, + Post { + mime, + hash, + secret, + expires: Utc::now() + chrono::Duration::from_std(expires).unwrap(), // this is safe unless mis-configured + }, + ); + + self.dirty = true; (post_id, secret) } - fn update(&mut self, id : PostId, data : Vec, mime : Option<&str>, expires : Option) { - info!("Update post id #{:016x} with data of len {} bytes, mime {}, expiry {}", - id, data.len(), mime.unwrap_or("unchanged"), - expires.map(|v| Cow::Owned(format!("{:?}", v))) - .unwrap_or("unchanged".into())); + /// Update a post by ID + fn update(&mut self, id: PostId, data: Vec, mime: Option<&str>, expires: Option) { + info!( + "Update post id #{:016x} with data of len {} bytes, mime {}, expiry {}", + id, + data.len(), + mime.unwrap_or("unchanged"), + expires + .map(|v| Cow::Owned(format!("{:?}", v))) + .unwrap_or("unchanged".into()) + ); let hash = Self::hash_data(&data); let post = self.posts.get_mut(&id).unwrap(); // post existence was checked before @@ -374,27 +532,73 @@ impl Repository { Self::drop_data_or_decrement_rc(&mut self.data, post.hash); Self::store_data_or_increment_rc(&mut self.data, hash, data); post.hash = hash; + self.dirty = true; } else { debug!("Data hash = #{:016x} (no change)", hash); } if let Some(mime) = mime { - if &post.mime != mime { + let new_mime = Mime::from(mime); + if post.mime != new_mime { debug!("Content type changed to {}", mime); - post.mime = Cow::Owned(mime.to_string()); + post.mime = new_mime; + self.dirty = true; } } if let Some(exp) = expires { debug!("Expiration changed to {:?} from now", exp); - post.expires = Instant::now() + exp; + post.expires = Utc::now() + chrono::Duration::from_std(exp).unwrap(); // this is safe unless mis-configured; + self.dirty = true; } } - fn delete_post(&mut self, id : PostId) { + /// Delete a post by ID + fn delete_post(&mut self, id: PostId) { info!("Delete post id #{:016x}", id); let post = self.posts.remove(&id).unwrap(); // post existence was checked before Self::drop_data_or_decrement_rc(&mut self.data, post.hash); + + self.dirty = true; + } +} + +/// Serialize chrono unix timestamp as seconds +mod serde_chrono_datetime_as_unix { + use serde::{self, Deserialize, Deserializer, Serializer}; + use chrono::{DateTime, Utc, NaiveDateTime}; + + pub fn serialize(value: &DateTime, se: S) -> Result + where + S: Serializer, + { + se.serialize_i64(value.naive_utc().timestamp()) + } + + pub fn deserialize<'de, D>(de: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let ts: i64 = i64::deserialize(de)?; + Ok(DateTime::from_utc(NaiveDateTime::from_timestamp(ts, 0), Utc)) + } +} + +fn error_with_text(code: u16, text: impl Into) -> Response { + Response { + status_code: code, + headers: vec![("Content-Type".into(), "text/plain; charset=utf8".into())], + data: rouille::ResponseBody::from_string(text), + upgrade: None, + } +} + +fn empty_error(code: u16) -> Response { + Response { + status_code: code, + headers: vec![("Content-Type".into(), "text/plain; charset=utf8".into())], + data: rouille::ResponseBody::empty(), + upgrade: None, } } diff --git a/src/well_known_mime.rs b/src/well_known_mime.rs new file mode 100644 index 0000000..09cf482 --- /dev/null +++ b/src/well_known_mime.rs @@ -0,0 +1,742 @@ +use serde::{Serialize, Serializer, Deserialize, Deserializer}; +use std::fmt::{Write, Display, Formatter}; +use std::fmt; +use serde::de::Visitor; + +#[derive(Serialize,Deserialize,Debug,PartialEq,Eq,Hash)] +pub enum Mime { + WellKnown(usize), + Custom(String), +} + +impl From for Mime { + fn from(s: String) -> Self { + if let Ok(index) = WELL_KNOWN.binary_search(&s.as_str()) { + Mime::WellKnown(index) + } else { + Mime::Custom(s) + } + } +} + +impl<'a> From<&'a str> for Mime { + fn from(s: &'a str) -> Self { + if let Ok(index) = WELL_KNOWN.binary_search(&s) { + Mime::WellKnown(index) + } else { + Mime::Custom(s.to_string()) + } + } +} + +impl Display for Mime { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Mime::WellKnown(n) => { + if let Some(s) = WELL_KNOWN.get(*n) { + f.write_str(s) + } else { + f.write_str("application/octet-stream") + } + }, + Mime::Custom(s) => { + f.write_str(s) + }, + } + } +} + +// CAUTION!!!!! This list must be alphabetically sorted! +const WELL_KNOWN : &[&str] = &[ + "application/andrew-inset", + "application/applixware", + "application/atom+xml", + "application/atomcat+xml", + "application/atomsvc+xml", + "application/ccxml+xml,", + "application/cdmi-capability", + "application/cdmi-container", + "application/cdmi-domain", + "application/cdmi-object", + "application/cdmi-queue", + "application/cu-seeme", + "application/davmount+xml", + "application/dssc+der", + "application/dssc+xml", + "application/ecmascript", + "application/emma+xml", + "application/epub+zip", + "application/exi", + "application/font-tdpfr", + "application/hyperstudio", + "application/ipfix", + "application/java-archive", + "application/java-serialized-object", + "application/java-vm", + "application/javascript", + "application/json", + "application/mac-binhex40", + "application/mac-compactpro", + "application/mads+xml", + "application/marc", + "application/marcxml+xml", + "application/mathematica", + "application/mathml+xml", + "application/mbox", + "application/mediaservercontrol+xml", + "application/metalink4+xml", + "application/mets+xml", + "application/mods+xml", + "application/mp21", + "application/mp4", + "application/msword", + "application/mxf", + "application/octet-stream", + "application/oda", + "application/oebps-package+xml", + "application/ogg", + "application/onenote", + "application/patch-ops-error+xml", + "application/pdf", + "application/pgp-encrypted", + "application/pgp-signature", + "application/pics-rules", + "application/pkcs10", + "application/pkcs7-mime", + "application/pkcs7-signature", + "application/pkcs8", + "application/pkix-attr-cert", + "application/pkix-cert", + "application/pkix-crl", + "application/pkix-pkipath", + "application/pkixcmp", + "application/pls+xml", + "application/postscript", + "application/prs.cww", + "application/pskc+xml", + "application/rdf+xml", + "application/reginfo+xml", + "application/relax-ng-compact-syntax", + "application/resource-lists+xml", + "application/resource-lists-diff+xml", + "application/rls-services+xml", + "application/rsd+xml", + "application/rss+xml", + "application/rtf", + "application/sbml+xml", + "application/scvp-cv-request", + "application/scvp-cv-response", + "application/scvp-vp-request", + "application/scvp-vp-response", + "application/sdp", + "application/set-payment-initiation", + "application/set-registration-initiation", + "application/shf+xml", + "application/smil+xml", + "application/sparql-query", + "application/sparql-results+xml", + "application/srgs", + "application/srgs+xml", + "application/sru+xml", + "application/ssml+xml", + "application/tei+xml", + "application/thraud+xml", + "application/timestamped-data", + "application/vnd.3gpp.pic-bw-large", + "application/vnd.3gpp.pic-bw-small", + "application/vnd.3gpp.pic-bw-var", + "application/vnd.3gpp2.tcap", + "application/vnd.3m.post-it-notes", + "application/vnd.accpac.simply.aso", + "application/vnd.accpac.simply.imp", + "application/vnd.acucobol", + "application/vnd.acucorp", + "application/vnd.adobe.air-application-installer-package+zip", + "application/vnd.adobe.fxp", + "application/vnd.adobe.xdp+xml", + "application/vnd.adobe.xfdf", + "application/vnd.ahead.space", + "application/vnd.airzip.filesecure.azf", + "application/vnd.airzip.filesecure.azs", + "application/vnd.amazon.ebook", + "application/vnd.americandynamics.acc", + "application/vnd.amiga.ami", + "application/vnd.android.package-archive", + "application/vnd.anser-web-certificate-issue-initiation", + "application/vnd.anser-web-funds-transfer-initiation", + "application/vnd.antix.game-component", + "application/vnd.apple.installer+xml", + "application/vnd.apple.mpegurl", + "application/vnd.aristanetworks.swi", + "application/vnd.audiograph", + "application/vnd.blueice.multipass", + "application/vnd.bmi", + "application/vnd.businessobjects", + "application/vnd.chemdraw+xml", + "application/vnd.chipnuts.karaoke-mmd", + "application/vnd.cinderella", + "application/vnd.claymore", + "application/vnd.cloanto.rp9", + "application/vnd.clonk.c4group", + "application/vnd.cluetrust.cartomobile-config", + "application/vnd.cluetrust.cartomobile-config-pkg", + "application/vnd.commonspace", + "application/vnd.contact.cmsg", + "application/vnd.cosmocaller", + "application/vnd.crick.clicker", + "application/vnd.crick.clicker.keyboard", + "application/vnd.crick.clicker.palette", + "application/vnd.crick.clicker.template", + "application/vnd.crick.clicker.wordbank", + "application/vnd.criticaltools.wbs+xml", + "application/vnd.ctc-posml", + "application/vnd.cups-ppd", + "application/vnd.curl.car", + "application/vnd.curl.pcurl", + "application/vnd.data-vision.rdz", + "application/vnd.denovo.fcselayout-link", + "application/vnd.dna", + "application/vnd.dolby.mlp", + "application/vnd.dpgraph", + "application/vnd.dreamfactory", + "application/vnd.dvb.ait", + "application/vnd.dvb.service", + "application/vnd.dynageo", + "application/vnd.ecowin.chart", + "application/vnd.enliven", + "application/vnd.epson.esf", + "application/vnd.epson.msf", + "application/vnd.epson.quickanime", + "application/vnd.epson.salt", + "application/vnd.epson.ssf", + "application/vnd.eszigno3+xml", + "application/vnd.ezpix-album", + "application/vnd.ezpix-package", + "application/vnd.fdf", + "application/vnd.fdsn.seed", + "application/vnd.flographit", + "application/vnd.fluxtime.clip", + "application/vnd.framemaker", + "application/vnd.frogans.fnc", + "application/vnd.frogans.ltf", + "application/vnd.fsc.weblaunch", + "application/vnd.fujitsu.oasys", + "application/vnd.fujitsu.oasys2", + "application/vnd.fujitsu.oasys3", + "application/vnd.fujitsu.oasysgp", + "application/vnd.fujitsu.oasysprs", + "application/vnd.fujixerox.ddd", + "application/vnd.fujixerox.docuworks", + "application/vnd.fujixerox.docuworks.binder", + "application/vnd.fuzzysheet", + "application/vnd.genomatix.tuxedo", + "application/vnd.geogebra.file", + "application/vnd.geogebra.tool", + "application/vnd.geometry-explorer", + "application/vnd.geonext", + "application/vnd.geoplan", + "application/vnd.geospace", + "application/vnd.gmx", + "application/vnd.google-earth.kml+xml", + "application/vnd.google-earth.kmz", + "application/vnd.grafeq", + "application/vnd.groove-account", + "application/vnd.groove-help", + "application/vnd.groove-identity-message", + "application/vnd.groove-injector", + "application/vnd.groove-tool-message", + "application/vnd.groove-tool-template", + "application/vnd.groove-vcard", + "application/vnd.hal+xml", + "application/vnd.handheld-entertainment+xml", + "application/vnd.hbci", + "application/vnd.hhe.lesson-player", + "application/vnd.hp-hpgl", + "application/vnd.hp-hpid", + "application/vnd.hp-hps", + "application/vnd.hp-jlyt", + "application/vnd.hp-pcl", + "application/vnd.hp-pclxl", + "application/vnd.hydrostatix.sof-data", + "application/vnd.hzn-3d-crossword", + "application/vnd.ibm.minipay", + "application/vnd.ibm.modcap", + "application/vnd.ibm.rights-management", + "application/vnd.ibm.secure-container", + "application/vnd.iccprofile", + "application/vnd.igloader", + "application/vnd.immervision-ivp", + "application/vnd.immervision-ivu", + "application/vnd.insors.igm", + "application/vnd.intercon.formnet", + "application/vnd.intergeo", + "application/vnd.intu.qbo", + "application/vnd.intu.qfx", + "application/vnd.ipunplugged.rcprofile", + "application/vnd.irepository.package+xml", + "application/vnd.is-xpr", + "application/vnd.isac.fcs", + "application/vnd.jam", + "application/vnd.jcp.javame.midlet-rms", + "application/vnd.jisp", + "application/vnd.joost.joda-archive", + "application/vnd.kahootz", + "application/vnd.kde.karbon", + "application/vnd.kde.kchart", + "application/vnd.kde.kformula", + "application/vnd.kde.kivio", + "application/vnd.kde.kontour", + "application/vnd.kde.kpresenter", + "application/vnd.kde.kspread", + "application/vnd.kde.kword", + "application/vnd.kenameaapp", + "application/vnd.kidspiration", + "application/vnd.kinar", + "application/vnd.koan", + "application/vnd.kodak-descriptor", + "application/vnd.las.las+xml", + "application/vnd.llamagraphics.life-balance.desktop", + "application/vnd.llamagraphics.life-balance.exchange+xml", + "application/vnd.lotus-1-2-3", + "application/vnd.lotus-approach", + "application/vnd.lotus-freelance", + "application/vnd.lotus-notes", + "application/vnd.lotus-organizer", + "application/vnd.lotus-screencam", + "application/vnd.lotus-wordpro", + "application/vnd.macports.portpkg", + "application/vnd.mcd", + "application/vnd.medcalcdata", + "application/vnd.mediastation.cdkey", + "application/vnd.mfer", + "application/vnd.mfmp", + "application/vnd.micrografx.flo", + "application/vnd.micrografx.igx", + "application/vnd.mif", + "application/vnd.mobius.daf", + "application/vnd.mobius.dis", + "application/vnd.mobius.mbk", + "application/vnd.mobius.mqy", + "application/vnd.mobius.msl", + "application/vnd.mobius.plc", + "application/vnd.mobius.txf", + "application/vnd.mophun.application", + "application/vnd.mophun.certificate", + "application/vnd.mozilla.xul+xml", + "application/vnd.ms-artgalry", + "application/vnd.ms-cab-compressed", + "application/vnd.ms-excel", + "application/vnd.ms-excel.addin.macroenabled.12", + "application/vnd.ms-excel.sheet.binary.macroenabled.12", + "application/vnd.ms-excel.sheet.macroenabled.12", + "application/vnd.ms-excel.template.macroenabled.12", + "application/vnd.ms-fontobject", + "application/vnd.ms-htmlhelp", + "application/vnd.ms-ims", + "application/vnd.ms-lrm", + "application/vnd.ms-officetheme", + "application/vnd.ms-pki.seccat", + "application/vnd.ms-pki.stl", + "application/vnd.ms-powerpoint", + "application/vnd.ms-powerpoint.addin.macroenabled.12", + "application/vnd.ms-powerpoint.presentation.macroenabled.12", + "application/vnd.ms-powerpoint.slide.macroenabled.12", + "application/vnd.ms-powerpoint.slideshow.macroenabled.12", + "application/vnd.ms-powerpoint.template.macroenabled.12", + "application/vnd.ms-project", + "application/vnd.ms-word.document.macroenabled.12", + "application/vnd.ms-word.template.macroenabled.12", + "application/vnd.ms-works", + "application/vnd.ms-wpl", + "application/vnd.ms-xpsdocument", + "application/vnd.mseq", + "application/vnd.musician", + "application/vnd.muvee.style", + "application/vnd.neurolanguage.nlu", + "application/vnd.noblenet-directory", + "application/vnd.noblenet-sealer", + "application/vnd.noblenet-web", + "application/vnd.nokia.n-gage.data", + "application/vnd.nokia.n-gage.symbian.install", + "application/vnd.nokia.radio-preset", + "application/vnd.nokia.radio-presets", + "application/vnd.novadigm.edm", + "application/vnd.novadigm.edx", + "application/vnd.novadigm.ext", + "application/vnd.oasis.opendocument.chart", + "application/vnd.oasis.opendocument.chart-template", + "application/vnd.oasis.opendocument.database", + "application/vnd.oasis.opendocument.formula", + "application/vnd.oasis.opendocument.formula-template", + "application/vnd.oasis.opendocument.graphics", + "application/vnd.oasis.opendocument.graphics-template", + "application/vnd.oasis.opendocument.image", + "application/vnd.oasis.opendocument.image-template", + "application/vnd.oasis.opendocument.presentation", + "application/vnd.oasis.opendocument.presentation-template", + "application/vnd.oasis.opendocument.spreadsheet", + "application/vnd.oasis.opendocument.spreadsheet-template", + "application/vnd.oasis.opendocument.text", + "application/vnd.oasis.opendocument.text-master", + "application/vnd.oasis.opendocument.text-template", + "application/vnd.oasis.opendocument.text-web", + "application/vnd.olpc-sugar", + "application/vnd.oma.dd2+xml", + "application/vnd.openofficeorg.extension", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.openxmlformats-officedocument.presentationml.slide", + "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + "application/vnd.openxmlformats-officedocument.presentationml.template", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + "application/vnd.osgeo.mapguide.package", + "application/vnd.osgi.dp", + "application/vnd.palm", + "application/vnd.pawaafile", + "application/vnd.pg.format", + "application/vnd.pg.osasli", + "application/vnd.picsel", + "application/vnd.pmi.widget", + "application/vnd.pocketlearn", + "application/vnd.powerbuilder6", + "application/vnd.previewsystems.box", + "application/vnd.proteus.magazine", + "application/vnd.publishare-delta-tree", + "application/vnd.pvi.ptid1", + "application/vnd.quark.quarkxpress", + "application/vnd.realvnc.bed", + "application/vnd.recordare.musicxml", + "application/vnd.recordare.musicxml+xml", + "application/vnd.rig.cryptonote", + "application/vnd.rim.cod", + "application/vnd.rn-realmedia", + "application/vnd.route66.link66+xml", + "application/vnd.sailingtracker.track", + "application/vnd.seemail", + "application/vnd.sema", + "application/vnd.semd", + "application/vnd.semf", + "application/vnd.shana.informed.formdata", + "application/vnd.shana.informed.formtemplate", + "application/vnd.shana.informed.interchange", + "application/vnd.shana.informed.package", + "application/vnd.simtech-mindmapper", + "application/vnd.smaf", + "application/vnd.smart.teacher", + "application/vnd.solent.sdkm+xml", + "application/vnd.spotfire.dxp", + "application/vnd.spotfire.sfs", + "application/vnd.stardivision.calc", + "application/vnd.stardivision.draw", + "application/vnd.stardivision.impress", + "application/vnd.stardivision.math", + "application/vnd.stardivision.writer", + "application/vnd.stardivision.writer-global", + "application/vnd.stepmania.stepchart", + "application/vnd.sun.xml.calc", + "application/vnd.sun.xml.calc.template", + "application/vnd.sun.xml.draw", + "application/vnd.sun.xml.draw.template", + "application/vnd.sun.xml.impress", + "application/vnd.sun.xml.impress.template", + "application/vnd.sun.xml.math", + "application/vnd.sun.xml.writer", + "application/vnd.sun.xml.writer.global", + "application/vnd.sun.xml.writer.template", + "application/vnd.sus-calendar", + "application/vnd.svd", + "application/vnd.symbian.install", + "application/vnd.syncml+xml", + "application/vnd.syncml.dm+wbxml", + "application/vnd.syncml.dm+xml", + "application/vnd.tao.intent-module-archive", + "application/vnd.tmobile-livetv", + "application/vnd.trid.tpt", + "application/vnd.triscape.mxs", + "application/vnd.trueapp", + "application/vnd.ufdl", + "application/vnd.uiq.theme", + "application/vnd.umajin", + "application/vnd.unity", + "application/vnd.uoml+xml", + "application/vnd.vcx", + "application/vnd.visio", + "application/vnd.visio2013", + "application/vnd.visionary", + "application/vnd.vsf", + "application/vnd.wap.wbxml", + "application/vnd.wap.wmlc", + "application/vnd.wap.wmlscriptc", + "application/vnd.webturbo", + "application/vnd.wolfram.player", + "application/vnd.wordperfect", + "application/vnd.wqd", + "application/vnd.wt.stf", + "application/vnd.xara", + "application/vnd.xfdl", + "application/vnd.yamaha.hv-dic", + "application/vnd.yamaha.hv-script", + "application/vnd.yamaha.hv-voice", + "application/vnd.yamaha.openscoreformat", + "application/vnd.yamaha.openscoreformat.osfpvg+xml", + "application/vnd.yamaha.smaf-audio", + "application/vnd.yamaha.smaf-phrase", + "application/vnd.yellowriver-custom-menu", + "application/vnd.zul", + "application/vnd.zzazz.deck+xml", + "application/voicexml+xml", + "application/widget", + "application/winhlp", + "application/wsdl+xml", + "application/wspolicy+xml", + "application/x-7z-compressed", + "application/x-abiword", + "application/x-ace-compressed", + "application/x-apple-diskimage", + "application/x-authorware-bin", + "application/x-authorware-map", + "application/x-authorware-seg", + "application/x-bcpio", + "application/x-bittorrent", + "application/x-bzip", + "application/x-bzip2", + "application/x-cdlink", + "application/x-chat", + "application/x-chess-pgn", + "application/x-cpio", + "application/x-csh", + "application/x-debian-package", + "application/x-director", + "application/x-doom", + "application/x-dtbncx+xml", + "application/x-dtbook+xml", + "application/x-dtbresource+xml", + "application/x-dvi", + "application/x-font-bdf", + "application/x-font-ghostscript", + "application/x-font-linux-psf", + "application/x-font-otf", + "application/x-font-pcf", + "application/x-font-snf", + "application/x-font-ttf", + "application/x-font-type1", + "application/x-font-woff", + "application/x-futuresplash", + "application/x-gnumeric", + "application/x-gtar", + "application/x-hdf", + "application/x-java-jnlp-file", + "application/x-latex", + "application/x-mobipocket-ebook", + "application/x-ms-application", + "application/x-ms-wmd", + "application/x-ms-wmz", + "application/x-ms-xbap", + "application/x-msaccess", + "application/x-msbinder", + "application/x-mscardfile", + "application/x-msclip", + "application/x-msdownload", + "application/x-msmediaview", + "application/x-msmetafile", + "application/x-msmoney", + "application/x-mspublisher", + "application/x-msschedule", + "application/x-msterminal", + "application/x-mswrite", + "application/x-netcdf", + "application/x-pkcs12", + "application/x-pkcs7-certificates", + "application/x-pkcs7-certreqresp", + "application/x-rar-compressed", + "application/x-sh", + "application/x-shar", + "application/x-shockwave-flash", + "application/x-silverlight-app", + "application/x-stuffit", + "application/x-stuffitx", + "application/x-sv4cpio", + "application/x-sv4crc", + "application/x-tar", + "application/x-tcl", + "application/x-tex", + "application/x-tex-tfm", + "application/x-texinfo", + "application/x-ustar", + "application/x-wais-source", + "application/x-x509-ca-cert", + "application/x-xfig", + "application/x-xpinstall", + "application/xcap-diff+xml", + "application/xenc+xml", + "application/xhtml+xml", + "application/xml", + "application/xml-dtd", + "application/xop+xml", + "application/xslt+xml", + "application/xspf+xml", + "application/xv+xml", + "application/yang", + "application/yin+xml", + "application/zip", + "audio/adpcm", + "audio/basic", + "audio/midi", + "audio/mp4", + "audio/mpeg", + "audio/ogg", + "audio/vnd.dece.audio", + "audio/vnd.digital-winds", + "audio/vnd.dra", + "audio/vnd.dts", + "audio/vnd.dts.hd", + "audio/vnd.lucent.voice", + "audio/vnd.ms-playready.media.pya", + "audio/vnd.nuera.ecelp4800", + "audio/vnd.nuera.ecelp7470", + "audio/vnd.nuera.ecelp9600", + "audio/vnd.rip", + "audio/webm", + "audio/x-aac", + "audio/x-aiff", + "audio/x-mpegurl", + "audio/x-ms-wax", + "audio/x-ms-wma", + "audio/x-pn-realaudio", + "audio/x-pn-realaudio-plugin", + "audio/x-wav", + "chemical/x-cdx", + "chemical/x-cif", + "chemical/x-cmdf", + "chemical/x-cml", + "chemical/x-csml", + "chemical/x-xyz", + "image/bmp", + "image/cgm", + "image/g3fax", + "image/gif", + "image/ief", + "image/jpeg", + "image/ktx", + "image/pjpeg", + "image/png", + "image/prs.btif", + "image/svg+xml", + "image/tiff", + "image/vnd.adobe.photoshop", + "image/vnd.dece.graphic", + "image/vnd.djvu", + "image/vnd.dvb.subtitle", + "image/vnd.dwg", + "image/vnd.dxf", + "image/vnd.fastbidsheet", + "image/vnd.fpx", + "image/vnd.fst", + "image/vnd.fujixerox.edmics-mmr", + "image/vnd.fujixerox.edmics-rlc", + "image/vnd.ms-modi", + "image/vnd.net-fpx", + "image/vnd.wap.wbmp", + "image/vnd.xiff", + "image/webp", + "image/x-citrix-jpeg", + "image/x-citrix-png", + "image/x-cmu-raster", + "image/x-cmx", + "image/x-freehand", + "image/x-icon", + "image/x-pcx", + "image/x-pict", + "image/x-png", + "image/x-portable-anymap", + "image/x-portable-bitmap", + "image/x-portable-graymap", + "image/x-portable-pixmap", + "image/x-rgb", + "image/x-xbitmap", + "image/x-xpixmap", + "image/x-xwindowdump", + "message/rfc822", + "model/iges", + "model/mesh", + "model/vnd.collada+xml", + "model/vnd.dwf", + "model/vnd.gdl", + "model/vnd.gtw", + "model/vnd.mts", + "model/vnd.vtu", + "model/vrml", + "text/calendar", + "text/css", + "text/csv", + "text/html", + "text/n3", + "text/plain", + "text/plain-bas", + "text/prs.lines.tag", + "text/richtext", + "text/sgml", + "text/tab-separated-values", + "text/troff", + "text/turtle", + "text/uri-list", + "text/vnd.curl", + "text/vnd.curl.dcurl", + "text/vnd.curl.mcurl", + "text/vnd.curl.scurl", + "text/vnd.fly", + "text/vnd.fmi.flexstor", + "text/vnd.graphviz", + "text/vnd.in3d.3dml", + "text/vnd.in3d.spot", + "text/vnd.sun.j2me.app-descriptor", + "text/vnd.wap.wml", + "text/vnd.wap.wmlscript", + "text/x-asm", + "text/x-c", + "text/x-fortran", + "text/x-java-source,java", + "text/x-pascal", + "text/x-setext", + "text/x-uuencode", + "text/x-vcalendar", + "text/x-vcard", + "text/yaml", + "video/3gpp", + "video/3gpp2", + "video/h261", + "video/h263", + "video/h264", + "video/jpeg", + "video/jpm", + "video/mj2", + "video/mp4", + "video/mpeg", + "video/ogg", + "video/quicktime", + "video/vnd.dece.hd", + "video/vnd.dece.mobile", + "video/vnd.dece.pd", + "video/vnd.dece.sd", + "video/vnd.dece.video", + "video/vnd.fvt", + "video/vnd.mpegurl", + "video/vnd.ms-playready.media.pyv", + "video/vnd.uvvu.mp4", + "video/vnd.vivo", + "video/webm", + "video/x-f4v", + "video/x-fli", + "video/x-flv", + "video/x-m4v", + "video/x-ms-asf", + "video/x-ms-wm", + "video/x-ms-wmv", + "video/x-ms-wmx", + "video/x-ms-wvx", + "video/x-msvideo", + "video/x-sgi-movie", + "x-conference/x-cooltalk", +];