Browse Source

well-known mime, favicon

Ondřej Hruška 1 year ago
parent
commit
0ecfdbb7f9
Signed by: Ondřej Hruška <ondra@ondrovo.com> GPG key ID: 2C5FD5035250423D
8 changed files with 1267 additions and 136 deletions
  1. 2 0
      .gitignore
  2. 173 4
      Cargo.lock
  3. 7 0
      Cargo.toml
  4. BIN
      postit.db
  5. 22 15
      src/config.rs
  6. BIN
      src/favicon.ico
  7. 321 117
      src/main.rs
  8. 742 0
      src/well_known_mime.rs

+ 2 - 0
.gitignore View File

@@ -1 +1,3 @@
1 1
 /target
2
+.idea/
3
+postit.json

+ 173 - 4
Cargo.lock View File

@@ -12,7 +12,7 @@ version = "0.7.10"
12 12
 source = "registry+https://github.com/rust-lang/crates.io-index"
13 13
 checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada"
14 14
 dependencies = [
15
- "memchr",
15
+ "memchr 2.3.3",
16 16
 ]
17 17
 
18 18
 [[package]]
@@ -88,6 +88,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
88 88
 checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
89 89
 
90 90
 [[package]]
91
+name = "bincode"
92
+version = "1.2.1"
93
+source = "registry+https://github.com/rust-lang/crates.io-index"
94
+checksum = "5753e2a71534719bf3f4e57006c3a4f0d2c672a4b676eec84161f763eca87dbf"
95
+dependencies = [
96
+ "byteorder",
97
+ "serde",
98
+]
99
+
100
+[[package]]
91 101
 name = "bitflags"
92 102
 version = "1.2.1"
93 103
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -151,7 +161,7 @@ version = "0.8.4"
151 161
 source = "registry+https://github.com/rust-lang/crates.io-index"
152 162
 checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
153 163
 dependencies = [
154
- "memchr",
164
+ "memchr 2.3.3",
155 165
  "safemem",
156 166
 ]
157 167
 
@@ -324,6 +334,30 @@ dependencies = [
324 334
 ]
325 335
 
326 336
 [[package]]
337
+name = "fixedbitset"
338
+version = "0.2.0"
339
+source = "registry+https://github.com/rust-lang/crates.io-index"
340
+checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
341
+
342
+[[package]]
343
+name = "flate2"
344
+version = "1.0.14"
345
+source = "registry+https://github.com/rust-lang/crates.io-index"
346
+checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42"
347
+dependencies = [
348
+ "cfg-if",
349
+ "crc32fast",
350
+ "libc",
351
+ "miniz_oxide",
352
+]
353
+
354
+[[package]]
355
+name = "fnv"
356
+version = "1.0.6"
357
+source = "registry+https://github.com/rust-lang/crates.io-index"
358
+checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
359
+
360
+[[package]]
327 361
 name = "fuchsia-cprng"
328 362
 version = "0.1.1"
329 363
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -394,6 +428,15 @@ dependencies = [
394 428
 ]
395 429
 
396 430
 [[package]]
431
+name = "indexmap"
432
+version = "1.3.2"
433
+source = "registry+https://github.com/rust-lang/crates.io-index"
434
+checksum = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292"
435
+dependencies = [
436
+ "autocfg 1.0.0",
437
+]
438
+
439
+[[package]]
397 440
 name = "itoa"
398 441
 version = "0.4.5"
399 442
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -463,6 +506,15 @@ checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
463 506
 
464 507
 [[package]]
465 508
 name = "memchr"
509
+version = "1.0.2"
510
+source = "registry+https://github.com/rust-lang/crates.io-index"
511
+checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a"
512
+dependencies = [
513
+ "libc",
514
+]
515
+
516
+[[package]]
517
+name = "memchr"
466 518
 version = "2.3.3"
467 519
 source = "registry+https://github.com/rust-lang/crates.io-index"
468 520
 checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
@@ -489,6 +541,15 @@ dependencies = [
489 541
 ]
490 542
 
491 543
 [[package]]
544
+name = "miniz_oxide"
545
+version = "0.3.6"
546
+source = "registry+https://github.com/rust-lang/crates.io-index"
547
+checksum = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5"
548
+dependencies = [
549
+ "adler32",
550
+]
551
+
552
+[[package]]
492 553
 name = "multipart"
493 554
 version = "0.15.4"
494 555
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -507,6 +568,61 @@ dependencies = [
507 568
 ]
508 569
 
509 570
 [[package]]
571
+name = "nom"
572
+version = "3.2.1"
573
+source = "registry+https://github.com/rust-lang/crates.io-index"
574
+checksum = "05aec50c70fd288702bcd93284a8444607f3292dbdf2a30de5ea5dcdbe72287b"
575
+dependencies = [
576
+ "memchr 1.0.2",
577
+]
578
+
579
+[[package]]
580
+name = "num"
581
+version = "0.2.1"
582
+source = "registry+https://github.com/rust-lang/crates.io-index"
583
+checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36"
584
+dependencies = [
585
+ "num-bigint",
586
+ "num-complex",
587
+ "num-integer",
588
+ "num-iter",
589
+ "num-rational",
590
+ "num-traits",
591
+]
592
+
593
+[[package]]
594
+name = "num-bigint"
595
+version = "0.2.6"
596
+source = "registry+https://github.com/rust-lang/crates.io-index"
597
+checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
598
+dependencies = [
599
+ "autocfg 1.0.0",
600
+ "num-integer",
601
+ "num-traits",
602
+]
603
+
604
+[[package]]
605
+name = "num-complex"
606
+version = "0.2.4"
607
+source = "registry+https://github.com/rust-lang/crates.io-index"
608
+checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95"
609
+dependencies = [
610
+ "autocfg 1.0.0",
611
+ "num-traits",
612
+]
613
+
614
+[[package]]
615
+name = "num-derive"
616
+version = "0.3.0"
617
+source = "registry+https://github.com/rust-lang/crates.io-index"
618
+checksum = "0c8b15b261814f992e33760b1fca9fe8b693d8a65299f20c9901688636cfb746"
619
+dependencies = [
620
+ "proc-macro2",
621
+ "quote",
622
+ "syn",
623
+]
624
+
625
+[[package]]
510 626
 name = "num-integer"
511 627
 version = "0.1.42"
512 628
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -517,6 +633,29 @@ dependencies = [
517 633
 ]
518 634
 
519 635
 [[package]]
636
+name = "num-iter"
637
+version = "0.1.40"
638
+source = "registry+https://github.com/rust-lang/crates.io-index"
639
+checksum = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00"
640
+dependencies = [
641
+ "autocfg 1.0.0",
642
+ "num-integer",
643
+ "num-traits",
644
+]
645
+
646
+[[package]]
647
+name = "num-rational"
648
+version = "0.2.4"
649
+source = "registry+https://github.com/rust-lang/crates.io-index"
650
+checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef"
651
+dependencies = [
652
+ "autocfg 1.0.0",
653
+ "num-bigint",
654
+ "num-integer",
655
+ "num-traits",
656
+]
657
+
658
+[[package]]
520 659
 name = "num-traits"
521 660
 version = "0.2.11"
522 661
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -615,6 +754,16 @@ dependencies = [
615 754
 ]
616 755
 
617 756
 [[package]]
757
+name = "petgraph"
758
+version = "0.5.0"
759
+source = "registry+https://github.com/rust-lang/crates.io-index"
760
+checksum = "29c127eea4a29ec6c85d153c59dc1213f33ec74cead30fe4730aecc88cc1fd92"
761
+dependencies = [
762
+ "fixedbitset",
763
+ "indexmap",
764
+]
765
+
766
+[[package]]
618 767
 name = "phf"
619 768
 version = "0.7.24"
620 769
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -657,8 +806,14 @@ dependencies = [
657 806
 name = "postit"
658 807
 version = "0.1.0"
659 808
 dependencies = [
809
+ "anyhow",
810
+ "bincode",
811
+ "chrono",
660 812
  "clappconfig",
813
+ "flate2",
661 814
  "log 0.4.8",
815
+ "num",
816
+ "num-derive",
662 817
  "parking_lot",
663 818
  "rand 0.7.3",
664 819
  "rouille",
@@ -666,6 +821,7 @@ dependencies = [
666 821
  "serde_derive",
667 822
  "serde_json",
668 823
  "siphasher 0.3.3",
824
+ "tree_magic",
669 825
 ]
670 826
 
671 827
 [[package]]
@@ -904,7 +1060,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
904 1060
 checksum = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692"
905 1061
 dependencies = [
906 1062
  "aho-corasick",
907
- "memchr",
1063
+ "memchr 2.3.3",
908 1064
  "regex-syntax",
909 1065
  "thread_local",
910 1066
 ]
@@ -1141,12 +1297,25 @@ dependencies = [
1141 1297
 ]
1142 1298
 
1143 1299
 [[package]]
1300
+name = "tree_magic"
1301
+version = "0.2.3"
1302
+source = "registry+https://github.com/rust-lang/crates.io-index"
1303
+checksum = "b1d99367ce3e553a84738f73bd626ccca541ef90ae757fdcdc4cbe728e6cb629"
1304
+dependencies = [
1305
+ "fnv",
1306
+ "lazy_static",
1307
+ "nom",
1308
+ "parking_lot",
1309
+ "petgraph",
1310
+]
1311
+
1312
+[[package]]
1144 1313
 name = "twoway"
1145 1314
 version = "0.1.8"
1146 1315
 source = "registry+https://github.com/rust-lang/crates.io-index"
1147 1316
 checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1"
1148 1317
 dependencies = [
1149
- "memchr",
1318
+ "memchr 2.3.3",
1150 1319
 ]
1151 1320
 
1152 1321
 [[package]]

+ 7 - 0
Cargo.toml View File

@@ -16,3 +16,10 @@ serde_derive = "1.0"
16 16
 log = "0.4.8"
17 17
 siphasher = "0.3.3"
18 18
 rand = "0.7.3"
19
+chrono = "0.4.11"
20
+bincode = "1.2.1"
21
+flate2 = "1.0.14"
22
+anyhow = "1.0.28"
23
+tree_magic = { version = "0.2.3", default_features = false, features = ["staticmime"] }
24
+num = "0.2.1"
25
+num-derive = "0.3.0"

BIN
postit.db View File


+ 22 - 15
src/config.rs View File

@@ -1,8 +1,8 @@
1
-use clappconfig::{AppConfig, anyhow, clap::ArgMatches};
1
+use clappconfig::{anyhow, clap::ArgMatches, AppConfig};
2 2
 use std::collections::HashMap;
3 3
 use std::time::Duration;
4 4
 
5
-#[derive(Debug, Serialize, Deserialize)]
5
+#[derive(Debug, Serialize, Deserialize, Clone)]
6 6
 #[serde(default)]
7 7
 #[serde(deny_unknown_fields)]
8 8
 pub(crate) struct Config {
@@ -32,6 +32,15 @@ pub(crate) struct Config {
32 32
 
33 33
     /// Max uploaded file size in bytes
34 34
     pub(crate) max_file_size: usize,
35
+
36
+    /// Enable persistence
37
+    pub(crate) persistence: bool,
38
+
39
+    /// Enable compression when persisting/loading
40
+    pub(crate) compression: bool,
41
+
42
+    /// Persistence file
43
+    pub(crate) persist_file : String,
35 44
 }
36 45
 
37 46
 impl Default for Config {
@@ -44,7 +53,10 @@ impl Default for Config {
44 53
             default_expiry: Duration::from_secs(60 * 10),
45 54
             max_expiry: Duration::from_secs(60 * 10),
46 55
             expired_gc_interval: Duration::from_secs(60),
47
-            max_file_size: 1 * (1024 * 1024) // 1MB
56
+            max_file_size: 1 * (1024 * 1024), // 1MB
57
+            persistence: false,
58
+            compression: true,
59
+            persist_file: "postit.db".to_string(),
48 60
         }
49 61
     }
50 62
 }
@@ -66,24 +78,19 @@ impl AppConfig for Config {
66 78
 }
67 79
 
68 80
 mod serde_duration_secs {
69
-    use serde::{self, Deserialize, Serializer, Deserializer};
81
+    use serde::{self, Deserialize, Deserializer, Serializer};
70 82
     use std::time::Duration;
71 83
 
72
-    pub fn serialize<S>(
73
-        value: &Duration,
74
-        se: S,
75
-    ) -> Result<S::Ok, S::Error>
76
-        where
77
-            S: Serializer,
84
+    pub fn serialize<S>(value: &Duration, se: S) -> Result<S::Ok, S::Error>
85
+    where
86
+        S: Serializer,
78 87
     {
79 88
         se.serialize_u64(value.as_secs())
80 89
     }
81 90
 
82
-    pub fn deserialize<'de, D>(
83
-        de: D,
84
-    ) -> Result<Duration, D::Error>
85
-        where
86
-            D: Deserializer<'de>,
91
+    pub fn deserialize<'de, D>(de: D) -> Result<Duration, D::Error>
92
+    where
93
+        D: Deserializer<'de>,
87 94
     {
88 95
         let s: u64 = u64::deserialize(de)?;
89 96
         Ok(Duration::from_secs(s))

BIN
src/favicon.ico View File


+ 321 - 117
src/main.rs View File

@@ -1,53 +1,63 @@
1
-#[macro_use] extern crate serde_derive;
2
-#[macro_use] extern crate log;
1
+#[macro_use]
2
+extern crate serde_derive;
3
+#[macro_use]
4
+extern crate log;
5
+#[macro_use]
6
+extern crate num_derive;
3 7
 
4
-use std::time::{Instant, Duration};
8
+use crate::config::Config;
9
+use clappconfig::{anyhow, AppConfig};
5 10
 use parking_lot::Mutex;
6
-use std::collections::HashMap;
7
-use clappconfig::{AppConfig, anyhow};
8
-use std::io::Read;
9
-use std::hash::{Hash, Hasher};
10 11
 use rand::rngs::OsRng;
11 12
 use rand::Rng;
12 13
 use rouille::{Request, Response, ResponseBody};
13 14
 use std::borrow::Cow;
14
-use crate::config::Config;
15
+use std::collections::HashMap;
16
+use std::hash::{Hash, Hasher};
17
+use std::io::Read;
18
+use std::time::Duration;
19
+use chrono::{Utc, DateTime};
20
+use std::fs::OpenOptions;
21
+use std::fmt::Display;
22
+use serde::export::Formatter;
23
+use std::fmt;
24
+use serde::{Serialize, Serializer, Deserialize, Deserializer};
25
+use serde::de::{DeserializeOwned, Visitor};
26
+use crate::well_known_mime::Mime;
15 27
 
16 28
 mod config;
29
+mod well_known_mime;
17 30
 
18
-fn error_with_text(code : u16, text : impl Into<String>) -> Response {
19
-    Response {
20
-        status_code: code,
21
-        headers: vec![("Content-Type".into(), "text/plain; charset=utf8".into())],
22
-        data: rouille::ResponseBody::from_string(text),
23
-        upgrade: None,
24
-    }
25
-}
31
+const HDR_EXPIRES : &str = "X-Expires";
32
+const HDR_SECRET : &str = "X-Secret";
26 33
 
27
-fn empty_error(code : u16) -> Response {
28
-    Response {
29
-        status_code: code,
30
-        headers: vec![("Content-Type".into(), "text/plain; charset=utf8".into())],
31
-        data: rouille::ResponseBody::empty(),
32
-        upgrade: None,
33
-    }
34
-}
34
+const FAVICON : &[u8] = include_bytes!("favicon.ico");
35 35
 
36
+/// Post ID (represented as a 16-digit hex string)
36 37
 type PostId = u64;
38
+/// Write token (represented as a 16-digit hex string)
37 39
 type Secret = u64;
40
+/// Hash of a data record
38 41
 type DataHash = u64;
39 42
 
40
-#[derive(Debug)]
43
+/// Post stored in the repository
44
+#[derive(Debug,Serialize,Deserialize)]
41 45
 struct Post {
42
-    mime : Cow<'static, str>,
43
-    hash : DataHash,
44
-    secret : Secret,
45
-    expires : Instant,
46
+    /// Content-Type
47
+    mime: Mime,
48
+    /// Data hash
49
+    hash: DataHash,
50
+    /// Secret key for editing or deleting
51
+    secret: Secret,
52
+    /// Expiration timestamp
53
+    #[serde(with = "serde_chrono_datetime_as_unix")]
54
+    expires: DateTime<Utc>,
46 55
 }
47 56
 
48 57
 impl Post {
58
+    /// Check if the post expired
49 59
     pub fn is_expired(&self) -> bool {
50
-        self.expires < Instant::now()
60
+        self.expires < Utc::now()
51 61
     }
52 62
 }
53 63
 
@@ -55,7 +65,15 @@ fn main() -> anyhow::Result<()> {
55 65
     let config = Config::init("postit", "postit.json", None)?;
56 66
     let serve_at = format!("{}:{}", config.host, config.port);
57 67
 
58
-    let store = Mutex::new(Repository::new(config));
68
+    let store = Mutex::new({
69
+        let mut store = Repository::new(config);
70
+        if store.config.persistence {
71
+            if let Err(e) = store.load() {
72
+                error!("Load failed: {}", e);
73
+            }
74
+        }
75
+        store
76
+    });
59 77
 
60 78
     rouille::start_server(serve_at, move |req| {
61 79
         let mut store_w = store.lock();
@@ -63,52 +81,120 @@ fn main() -> anyhow::Result<()> {
63 81
 
64 82
         info!("{} {}", method, req.raw_url());
65 83
 
84
+        if req.url() == "/favicon.ico" {
85
+            return Response::from_data("image/vnd.microsoft.icon", FAVICON);
86
+        }
87
+
66 88
         store_w.gc_expired_posts_if_needed();
67 89
 
68
-        match method {
69
-            "POST" | "PUT" => {
70
-                store_w.serve_post_put(req)
71
-            }
72
-            "GET" | "HEAD" => {
73
-                store_w.serve_get_head(req)
74
-            }
75
-            "DELETE" => {
76
-                store_w.serve_delete(req)
77
-            }
78
-            _ => {
79
-                rouille::Response::empty_400()
90
+        let resp = match method {
91
+            "POST" | "PUT" => store_w.serve_post_put(req),
92
+            "GET" | "HEAD" => store_w.serve_get_head(req),
93
+            "DELETE" => store_w.serve_delete(req),
94
+            _ => rouille::Response::empty_400(),
95
+        };
96
+
97
+        if store_w.config.persistence {
98
+            if let Err(e) = store_w.persist_if_needed() {
99
+                error!("Store failed: {}", e);
80 100
             }
81 101
         }
102
+
103
+        if resp.is_error() {
104
+            warn!("Error resp: {}", resp.status_code);
105
+        }
106
+
107
+        resp
82 108
     });
83 109
 }
84 110
 
85 111
 type PostsMap = HashMap<PostId, Post>;
86 112
 type DataMap = HashMap<DataHash, (usize, Vec<u8>)>;
87 113
 
114
+#[derive(Debug,Serialize,Deserialize)]
88 115
 struct Repository {
116
+    #[serde(skip)]
89 117
     config: Config,
118
+    /// Flag that the repository needs saving
119
+    #[serde(skip)]
120
+    dirty: bool,
121
+    /// Stored posts
90 122
     posts: PostsMap,
91
-    /// (use_count, data)
123
+    /// Post data - (use_count, data)
92 124
     data: DataMap,
93 125
     /// Time of last expired posts GC
94
-    last_gc_time: Instant,
126
+    #[serde(with = "serde_chrono_datetime_as_unix")]
127
+    last_gc_time: DateTime<Utc>,
95 128
 }
96 129
 
97 130
 impl Repository {
131
+    /// New instance
98 132
     fn new(config: Config) -> Self {
99 133
         Repository {
100 134
             config,
135
+            dirty: false,
101 136
             posts: Default::default(),
102 137
             data: Default::default(),
103
-            last_gc_time: Instant::now(),
138
+            last_gc_time: Utc::now(),
139
+        }
140
+    }
141
+
142
+    fn persist_if_needed(&mut self) -> anyhow::Result<()> {
143
+        if self.dirty {
144
+            self.persist()
145
+        } else {
146
+            Ok(())
104 147
         }
105 148
     }
106 149
 
107
-    fn serve_delete(&mut self, req : &Request) -> Response {
150
+    /// Store to a file
151
+    fn persist(&mut self) -> anyhow::Result<()> {
152
+        debug!("Persist to file: {}", self.config.persist_file);
153
+
154
+        self.dirty = false;
155
+        let file = OpenOptions::new()
156
+            .truncate(true)
157
+            .write(true)
158
+            .create(true)
159
+            .open(&self.config.persist_file)?;
160
+
161
+        if self.config.compression {
162
+            let flate = flate2::write::DeflateEncoder::new(file, flate2::Compression::best());
163
+            bincode::serialize_into(flate, self)?;
164
+        } else {
165
+            bincode::serialize_into(file, self)?;
166
+        }
167
+        Ok(())
168
+    }
169
+
170
+    /// Load from a file
171
+    fn load(&mut self) -> anyhow::Result<()> {
172
+        debug!("Load from file: {}", self.config.persist_file);
173
+
174
+        let file = OpenOptions::new()
175
+            .read(true)
176
+            .open(&self.config.persist_file)?;
177
+
178
+        let result : Repository = if self.config.compression {
179
+            let flate = flate2::read::DeflateDecoder::new(file);
180
+            bincode::deserialize_from(flate)?
181
+        } else {
182
+            bincode::deserialize_from(file)?
183
+        };
184
+
185
+        let old_config = self.config.clone();
186
+        std::mem::replace(self, result);
187
+        self.config = old_config;
188
+        self.dirty = false;
189
+        Ok(())
190
+    }
191
+
192
+    /// Serve a DELETE request
193
+    fn serve_delete(&mut self, req: &Request) -> Response {
108 194
         let post_id = match self.request_to_post_id(req, true) {
109 195
             Ok(Some(pid)) => pid,
110 196
             Ok(None) => return error_with_text(400, "Post ID required."),
111
-            Err(resp) => return resp
197
+            Err(resp) => return resp,
112 198
         };
113 199
 
114 200
         self.delete_post(post_id);
@@ -116,18 +202,25 @@ impl Repository {
116 202
         Response::text("Deleted.")
117 203
     }
118 204
 
119
-    fn serve_post_put(&mut self, req : &Request) -> Response {
205
+    /// Serve a POST or PUT request
206
+    ///
207
+    /// POST inserts a new record
208
+    /// PUT updates a record
209
+    fn serve_post_put(&mut self, req: &Request) -> Response {
210
+        // Post ID is empty for POST, set for PUT
120 211
         let post_id = match self.request_to_post_id(req, true) {
121 212
             Ok(pid) => {
122 213
                 if req.method() == "PUT" && pid.is_none() {
214
+                    warn!("PUT without ID!");
123 215
                     return error_with_text(400, "PUT requires a file ID!");
124 216
                 } else if req.method() == "POST" && pid.is_some() {
217
+                    warn!("POST with ID!");
125 218
                     return error_with_text(400, "Use PUT to update a file!");
126 219
                 }
127 220
 
128 221
                 pid
129
-            },
130
-            Err(resp) => return resp
222
+            }
223
+            Err(resp) => return resp,
131 224
         };
132 225
 
133 226
         debug!("Submit new data, post ID: {:?}", post_id);
@@ -136,7 +229,9 @@ impl Repository {
136 229
         if let Some(body) = req.data() {
137 230
             // Read up to 1 byte past the limit to catch too large uploads.
138 231
             // We can't reply on the "Length" field, which is not present with chunked encoding.
139
-            body.take(self.config.max_file_size as u64 + 1).read_to_end(&mut data).unwrap();
232
+            body.take(self.config.max_file_size as u64 + 1)
233
+                .read_to_end(&mut data)
234
+                .unwrap();
140 235
             if data.len() > self.config.max_file_size {
141 236
                 return empty_error(413);
142 237
             }
@@ -144,38 +239,44 @@ impl Repository {
144 239
             return error_with_text(400, "Empty body!");
145 240
         }
146 241
 
242
+        // Convert "application/x-www-form-urlencoded" to text/plain (CURL uses this)
243
+        // NOTE: rouille does NOT parse urlencoded, we will serve the encoded format back if really used.
147 244
         let mime = match req.header("Content-Type") {
148 245
             None => None,
149
-            Some("application/x-www-form-urlencoded") => Some("text/plain"),
246
+            Some("application/x-www-form-urlencoded") => None,
150 247
             Some(v) => Some(v),
151 248
         };
152 249
 
153
-        let expiry = match req.header("X-Expires") {
154
-            Some(text) => {
155
-                match text.parse() {
156
-                    Ok(v) => {
157
-                        let dur = Duration::from_secs(v);
158
-                        if dur > self.config.max_expiry {
159
-                            return error_with_text(400,
160
-                                                   format!("Expiration time {} out of allowed range 0-{} s",
161
-                                                           v,
162
-                                                           self.config.max_expiry.as_secs()
163
-                                                   ));
164
-                        }
165
-                        Some(dur)
166
-                    },
167
-                    Err(_) => {
168
-                        return error_with_text(400, "Malformed \"X-Expires\", use relative time in seconds.");
169
-                    },
250
+        let expiry = match req.header(HDR_EXPIRES) {
251
+            Some(text) => match text.parse() {
252
+                Ok(v) => {
253
+                    let dur = Duration::from_secs(v);
254
+                    if dur > self.config.max_expiry {
255
+                        return error_with_text(
256
+                            400,
257
+                            format!(
258
+                                "Expiration time {} out of allowed range 0-{} s",
259
+                                v,
260
+                                self.config.max_expiry.as_secs()
261
+                            ),
262
+                        );
263
+                    }
264
+                    Some(dur)
170 265
                 }
171
-            }
172
-            None => None
266
+                Err(_) => {
267
+                    return error_with_text(
268
+                        400,
269
+                        "Malformed \"X-Expires\", use relative time in seconds.",
270
+                    );
271
+                }
272
+            },
273
+            None => None,
173 274
         };
174 275
 
175 276
         if let Some(id) = post_id {
176 277
             // UPDATE
177 278
             self.update(id, data, mime, expiry);
178
-            Response::text("Updated.")
279
+            Response::text("Updated OK.")
179 280
         } else {
180 281
             // INSERT
181 282
             let (id, token) = self.insert(data, mime, expiry.unwrap_or(self.config.default_expiry));
@@ -185,11 +286,12 @@ impl Repository {
185 286
         }
186 287
     }
187 288
 
188
-    fn serve_get_head(&mut self, req : &Request) -> Response {
289
+    /// Serve a GET or HEAD request
290
+    fn serve_get_head(&mut self, req: &Request) -> Response {
189 291
         let post_id = match self.request_to_post_id(req, false) {
190 292
             Ok(Some(pid)) => pid,
191 293
             Ok(None) => return error_with_text(400, "Post ID required."),
192
-            Err(resp) => return resp
294
+            Err(resp) => return resp,
193 295
         };
194 296
 
195 297
         if let Some(post) = self.posts.get(&post_id) {
@@ -205,7 +307,10 @@ impl Repository {
205 307
 
206 308
                 Response {
207 309
                     status_code: 200,
208
-                    headers: vec![("Content-Type".into(), format!("{}; charset=utf8", post.mime).into())],
310
+                    headers: vec![(
311
+                        "Content-Type".into(),
312
+                        format!("{}; charset=utf8", post.mime).into(),
313
+                    )],
209 314
                     data: if req.method() == "HEAD" {
210 315
                         ResponseBody::empty()
211 316
                     } else {
@@ -220,47 +325,64 @@ impl Repository {
220 325
         }
221 326
     }
222 327
 
223
-    fn request_to_post_id(&self, req : &Request, check_secret : bool) -> Result<Option<PostId>, Response> {
328
+    /// Extract post ID from a request.
329
+    ///
330
+    /// if `check_secret` is true, ensure a `X-Secret` header contains a valid write token
331
+    /// for the post ID.
332
+    fn request_to_post_id(
333
+        &self,
334
+        req: &Request,
335
+        check_secret: bool,
336
+    ) -> Result<Option<PostId>, Response> {
224 337
         let url = req.url();
225 338
         let stripped = url.trim_matches('/');
226 339
 
227 340
         if stripped.is_empty() {
228
-            // No ID given
341
+            debug!("No ID given");
229 342
             return Ok(None);
230 343
         }
231 344
 
345
+        if stripped.len() != 16 {
346
+            warn!("Bad ID len!");
347
+            return Err(Response::empty_404());
348
+        }
349
+
232 350
         let id = match u64::from_str_radix(stripped, 16) {
233 351
             Ok(bytes) => bytes,
234 352
             Err(_) => {
235
-                return Err(error_with_text(400, "Bad file ID format!"));
236
-            },
353
+                warn!("ID parsing error: {}", stripped);
354
+                return Err(Response::empty_404());
355
+            }
237 356
         };
238 357
 
239 358
         if check_secret {
240 359
             // Check the write token
241 360
             match self.posts.get(&id) {
242
-                None/* | Some(_p) if _p.is_expired()*/ => {
361
+                None => {
362
+                    warn!("ID {} does not exist!", id);
243 363
                     return Err(error_with_text(404, "No file with this ID!"));
244 364
                 },
245 365
                 Some(post) => {
246 366
                     if post.is_expired() {
247
-                        warn!("Access of expired post!");
367
+                        warn!("Access of expired file {}!", id);
248 368
                         return Err(error_with_text(404, "No file with this ID!"));
249 369
                     }
250 370
 
251
-                    let secret: u64 = match req.header("X-Secret").map(|v| u64::from_str_radix(v, 16)) {
371
+                    let secret: u64 = match req.header(HDR_SECRET).map(|v| u64::from_str_radix(v, 16)) {
252 372
                         Some(Ok(bytes)) => bytes,
253 373
                         None => {
254
-                            return Err(error_with_text(400, "X-Secret required!"));
374
+                            warn!("Missing secret token!");
375
+                            return Err(error_with_text(400, "Secret token required!"));
255 376
                         }
256 377
                         Some(Err(e)) => {
257
-                            warn!("{:?}", e);
258
-                            return Err(error_with_text(400, "Bad secret format!"));
378
+                            warn!("Token parse error: {:?}", e);
379
+                            return Err(error_with_text(400, "Bad secret token format!"));
259 380
                         },
260 381
                     };
261 382
 
262 383
                     if post.secret != secret {
263
-                        return Err(error_with_text(401, "Invalid secret!"));
384
+                        warn!("Secret token mismatch");
385
+                        return Err(error_with_text(401, "Invalid secret token!"));
264 386
                     }
265 387
                 },
266 388
             }
@@ -270,15 +392,17 @@ impl Repository {
270 392
         Ok(Some(id))
271 393
     }
272 394
 
395
+    /// Drop expired posts, if cleaning is due
273 396
     fn gc_expired_posts_if_needed(&mut self) {
274
-        if self.last_gc_time.elapsed() > self.config.expired_gc_interval {
397
+        if Utc::now().signed_duration_since(self.last_gc_time).to_std().unwrap_or_default() > self.config.expired_gc_interval {
275 398
             self.gc_expired_posts();
276
-            self.last_gc_time = Instant::now();
399
+            self.last_gc_time = Utc::now();
277 400
         }
278 401
     }
279 402
 
403
+    /// Drop expired posts
280 404
     fn gc_expired_posts(&mut self) {
281
-        debug!("GC expired posts");
405
+        debug!("GC expired uploads");
282 406
 
283 407
         let mut to_rm = vec![];
284 408
         for post in &self.posts {
@@ -287,6 +411,10 @@ impl Repository {
287 411
             }
288 412
         }
289 413
 
414
+        if !to_rm.is_empty() {
415
+            self.dirty = true;
416
+        }
417
+
290 418
         for id in to_rm {
291 419
             debug!("Drop post ID {:016x}", id);
292 420
             if let Some(post) = self.posts.remove(&id) {
@@ -295,30 +423,37 @@ impl Repository {
295 423
         }
296 424
     }
297 425
 
298
-    fn hash_data(data : &Vec<u8>) -> DataHash {
426
+    /// Get hash of a byte vector (for deduplication)
427
+    fn hash_data(data: &Vec<u8>) -> DataHash {
299 428
         let mut hasher = siphasher::sip::SipHasher::new();
300 429
         data.hash(&mut hasher);
301 430
         hasher.finish()
302 431
     }
303 432
 
304
-    fn store_data_or_increment_rc(data_map : &mut DataMap, hash : u64, data: Vec<u8>) {
433
+    /// Store a data buffer under a given hash.
434
+    /// If the buffer is already present in the repository, increment its use count.
435
+    fn store_data_or_increment_rc(data_map: &mut DataMap, hash: u64, data: Vec<u8>) {
305 436
         match data_map.get_mut(&hash) {
306 437
             None => {
307 438
                 debug!("Store new data hash #{:016x}", hash);
308 439
                 data_map.insert(hash, (1, data));
309
-            },
440
+            }
310 441
             Some(entry) => {
311 442
                 debug!("Link new use of data hash #{:016x}", hash);
312 443
                 entry.0 += 1; // increment use counter
313
-            },
444
+            }
314 445
         }
315 446
     }
316 447
 
317
-    fn drop_data_or_decrement_rc(data_map : &mut DataMap, hash : u64) {
448
+    /// Drop a data record with the given hash, or decrement its use count if there are other uses
449
+    fn drop_data_or_decrement_rc(data_map: &mut DataMap, hash: u64) {
318 450
         if let Some(old_data) = data_map.get_mut(&hash) {
319 451
             if old_data.0 > 1 {
320 452
                 old_data.0 -= 1;
321
-                debug!("Unlink use of data hash #{:016x} ({} remain)", hash, old_data.0);
453
+                debug!(
454
+                    "Unlink use of data hash #{:016x} ({} remain)",
455
+                    hash, old_data.0
456
+                );
322 457
             } else {
323 458
                 debug!("Drop data hash #{:016x}", hash);
324 459
                 data_map.remove(&hash);
@@ -326,13 +461,26 @@ impl Repository {
326 461
         }
327 462
     }
328 463
 
329
-    fn insert(&mut self, data : Vec<u8>, mime : Option<&str>, expires : Duration) -> (PostId, Secret) {
330
-        info!("Insert post with data of len {} bytes, mime {}, expiry {:?}",
331
-              data.len(), mime.unwrap_or("unspecified"),
332
-              expires);
464
+    /// Insert a post
465
+    fn insert(&mut self, data: Vec<u8>, mime: Option<&str>, expires: Duration) -> (PostId, Secret) {
466
+        info!(
467
+            "Insert post with data of len {} bytes, mime {}, expiry {:?}",
468
+            data.len(),
469
+            mime.unwrap_or("unspecified"),
470
+            expires
471
+        );
333 472
 
334 473
         let hash = Self::hash_data(&data);
335 474
 
475
+        let mime = match mime {
476
+            None => {
477
+                Mime::from(tree_magic::from_u8(&data))
478
+            },
479
+            Some(explicit) => {
480
+                Mime::from(explicit)
481
+            },
482
+        };
483
+
336 484
         Self::store_data_or_increment_rc(&mut self.data, hash, data);
337 485
 
338 486
         let post_id = loop {
@@ -344,26 +492,36 @@ impl Repository {
344 492
 
345 493
         let secret = OsRng.gen();
346 494
 
347
-        debug!("Data hash = #{:016x}", hash);
348 495
         debug!("Post ID = #{:016x}", post_id);
496
+        debug!("Data hash = #{:016x}, mime {}", hash, mime);
349 497
         debug!("Secret = #{:016x}", secret);
350 498
 
351
-        self.posts.insert(post_id, Post {
352
-            mime: mime.map(ToString::to_string).map(Cow::Owned)
353
-                .unwrap_or(Cow::Borrowed("application/octet-stream")),
354
-            hash,
355
-            secret,
356
-            expires: Instant::now() + expires
357
-        });
499
+        self.posts.insert(
500
+            post_id,
501
+            Post {
502
+                mime,
503
+                hash,
504
+                secret,
505
+                expires: Utc::now() + chrono::Duration::from_std(expires).unwrap(), // this is safe unless mis-configured
506
+            },
507
+        );
508
+
509
+        self.dirty = true;
358 510
 
359 511
         (post_id, secret)
360 512
     }
361 513
 
362
-    fn update(&mut self, id : PostId, data : Vec<u8>, mime : Option<&str>, expires : Option<Duration>) {
363
-        info!("Update post id #{:016x} with data of len {} bytes, mime {}, expiry {}",
364
-               id, data.len(), mime.unwrap_or("unchanged"),
365
-               expires.map(|v| Cow::Owned(format!("{:?}", v)))
366
-                   .unwrap_or("unchanged".into()));
514
+    /// Update a post by ID
515
+    fn update(&mut self, id: PostId, data: Vec<u8>, mime: Option<&str>, expires: Option<Duration>) {
516
+        info!(
517
+            "Update post id #{:016x} with data of len {} bytes, mime {}, expiry {}",
518
+            id,
519
+            data.len(),
520
+            mime.unwrap_or("unchanged"),
521
+            expires
522
+                .map(|v| Cow::Owned(format!("{:?}", v)))
523
+                .unwrap_or("unchanged".into())
524
+        );
367 525
 
368 526
         let hash = Self::hash_data(&data);
369 527
         let post = self.posts.get_mut(&id).unwrap(); // post existence was checked before
@@ -374,27 +532,73 @@ impl Repository {
374 532
             Self::drop_data_or_decrement_rc(&mut self.data, post.hash);
375 533
             Self::store_data_or_increment_rc(&mut self.data, hash, data);
376 534
             post.hash = hash;
535
+            self.dirty = true;
377 536
         } else {
378 537
             debug!("Data hash = #{:016x} (no change)", hash);
379 538
         }
380 539
 
381 540
         if let Some(mime) = mime {
382
-            if &post.mime != mime {
541
+            let new_mime = Mime::from(mime);
542
+            if post.mime != new_mime {
383 543
                 debug!("Content type changed to {}", mime);
384
-                post.mime = Cow::Owned(mime.to_string());
544
+                post.mime = new_mime;
545
+                self.dirty = true;
385 546
             }
386 547
         }
387 548
 
388 549
         if let Some(exp) = expires {
389 550
             debug!("Expiration changed to {:?} from now", exp);
390
-            post.expires = Instant::now() + exp;
551
+            post.expires = Utc::now() + chrono::Duration::from_std(exp).unwrap(); // this is safe unless mis-configured;
552
+            self.dirty = true;
391 553
         }
392 554
     }
393 555
 
394
-    fn delete_post(&mut self, id : PostId) {
556
+    /// Delete a post by ID
557
+    fn delete_post(&mut self, id: PostId) {
395 558
         info!("Delete post id #{:016x}", id);
396 559
 
397 560
         let post = self.posts.remove(&id).unwrap(); // post existence was checked before
398 561
         Self::drop_data_or_decrement_rc(&mut self.data, post.hash);
562
+
563
+        self.dirty = true;
564
+    }
565
+}
566
+
567
+/// Serialize chrono unix timestamp as seconds
568
+mod serde_chrono_datetime_as_unix {
569
+    use serde::{self, Deserialize, Deserializer, Serializer};
570
+    use chrono::{DateTime, Utc, NaiveDateTime};
571
+
572
+    pub fn serialize<S>(value: &DateTime<Utc>, se: S) -> Result<S::Ok, S::Error>
573
+        where
574
+            S: Serializer,
575
+    {
576
+        se.serialize_i64(value.naive_utc().timestamp())
577
+    }
578
+
579
+    pub fn deserialize<'de, D>(de: D) -> Result<DateTime<Utc>, D::Error>
580
+        where
581
+            D: Deserializer<'de>,
582
+    {
583
+        let ts: i64 = i64::deserialize(de)?;
584
+        Ok(DateTime::from_utc(NaiveDateTime::from_timestamp(ts, 0), Utc))
585
+    }
586
+}
587
+
588
+fn error_with_text(code: u16, text: impl Into<String>) -> Response {
589
+    Response {
590
+        status_code: code,
591
+        headers: vec![("Content-Type".into(), "text/plain; charset=utf8".into())],
592
+        data: rouille::ResponseBody::from_string(text),
593
+        upgrade: None,
594
+    }
595
+}
596
+
597
+fn empty_error(code: u16) -> Response {
598
+    Response {
599
+        status_code: code,
600
+        headers: vec![("Content-Type".into(), "text/plain; charset=utf8".into())],
601
+        data: rouille::ResponseBody::empty(),
602
+        upgrade: None,
399 603
     }
400 604
 }

+ 742 - 0
src/well_known_mime.rs View File

@@ -0,0 +1,742 @@
1
+use serde::{Serialize, Serializer, Deserialize, Deserializer};
2
+use std::fmt::{Write, Display, Formatter};
3
+use std::fmt;
4
+use serde::de::Visitor;
5
+
6
+#[derive(Serialize,Deserialize,Debug,PartialEq,Eq,Hash)]
7
+pub enum Mime {
8
+    WellKnown(usize),
9
+    Custom(String),
10
+}
11
+
12
+impl From<String> for Mime {
13
+    fn from(s: String) -> Self {
14
+        if let Ok(index) = WELL_KNOWN.binary_search(&s.as_str()) {
15
+            Mime::WellKnown(index)
16
+        } else {
17
+            Mime::Custom(s)
18
+        }
19
+    }
20
+}
21
+
22
+impl<'a> From<&'a str> for Mime {
23
+    fn from(s: &'a str) -> Self {
24
+        if let Ok(index) = WELL_KNOWN.binary_search(&s) {
25
+            Mime::WellKnown(index)
26
+        } else {
27
+            Mime::Custom(s.to_string())
28
+        }
29
+    }
30
+}
31
+
32
+impl Display for Mime {
33
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
34
+        match self {
35
+            Mime::WellKnown(n) => {
36
+                if let Some(s) = WELL_KNOWN.get(*n) {
37
+                    f.write_str(s)
38
+                } else {
39
+                    f.write_str("application/octet-stream")
40
+                }
41
+            },
42
+            Mime::Custom(s) => {
43
+                f.write_str(s)
44
+            },
45
+        }
46
+    }
47
+}
48
+
49
+// CAUTION!!!!! This list must be alphabetically sorted!
50
+const WELL_KNOWN : &[&str] = &[
51
+    "application/andrew-inset",
52
+    "application/applixware",
53
+    "application/atom+xml",
54
+    "application/atomcat+xml",
55
+    "application/atomsvc+xml",
56
+    "application/ccxml+xml,",
57
+    "application/cdmi-capability",
58
+    "application/cdmi-container",
59
+    "application/cdmi-domain",
60
+    "application/cdmi-object",
61
+    "application/cdmi-queue",
62
+    "application/cu-seeme",
63
+    "application/davmount+xml",
64
+    "application/dssc+der",
65
+    "application/dssc+xml",
66
+    "application/ecmascript",
67
+    "application/emma+xml",
68
+    "application/epub+zip",
69
+    "application/exi",
70
+    "application/font-tdpfr",
71
+    "application/hyperstudio",
72
+    "application/ipfix",
73
+    "application/java-archive",
74
+    "application/java-serialized-object",
75
+    "application/java-vm",
76
+    "application/javascript",
77
+    "application/json",
78
+    "application/mac-binhex40",
79
+    "application/mac-compactpro",
80
+    "application/mads+xml",
81
+    "application/marc",
82
+    "application/marcxml+xml",
83
+    "application/mathematica",
84
+    "application/mathml+xml",
85
+    "application/mbox",
86
+    "application/mediaservercontrol+xml",
87
+    "application/metalink4+xml",
88
+    "application/mets+xml",
89
+    "application/mods+xml",
90
+    "application/mp21",
91
+    "application/mp4",
92
+    "application/msword",
93
+    "application/mxf",
94
+    "application/octet-stream",
95
+    "application/oda",
96
+    "application/oebps-package+xml",
97
+    "application/ogg",
98
+    "application/onenote",
99
+    "application/patch-ops-error+xml",
100
+    "application/pdf",
101
+    "application/pgp-encrypted",
102
+    "application/pgp-signature",
103
+    "application/pics-rules",
104
+    "application/pkcs10",
105
+    "application/pkcs7-mime",
106
+    "application/pkcs7-signature",
107
+    "application/pkcs8",
108
+    "application/pkix-attr-cert",
109
+    "application/pkix-cert",
110
+    "application/pkix-crl",
111
+    "application/pkix-pkipath",
112
+    "application/pkixcmp",
113
+    "application/pls+xml",
114
+    "application/postscript",
115
+    "application/prs.cww",
116
+    "application/pskc+xml",
117
+    "application/rdf+xml",
118
+    "application/reginfo+xml",
119
+    "application/relax-ng-compact-syntax",
120
+    "application/resource-lists+xml",
121
+    "application/resource-lists-diff+xml",
122
+    "application/rls-services+xml",
123
+    "application/rsd+xml",
124
+    "application/rss+xml",
125
+    "application/rtf",
126
+    "application/sbml+xml",
127
+    "application/scvp-cv-request",
128
+    "application/scvp-cv-response",
129
+    "application/scvp-vp-request",
130
+    "application/scvp-vp-response",
131
+    "application/sdp",
132
+    "application/set-payment-initiation",
133
+    "application/set-registration-initiation",
134
+    "application/shf+xml",
135
+    "application/smil+xml",
136
+    "application/sparql-query",
137
+    "application/sparql-results+xml",
138
+    "application/srgs",
139
+    "application/srgs+xml",
140
+    "application/sru+xml",
141
+    "application/ssml+xml",
142
+    "application/tei+xml",
143
+    "application/thraud+xml",
144
+    "application/timestamped-data",
145
+    "application/vnd.3gpp.pic-bw-large",
146
+    "application/vnd.3gpp.pic-bw-small",
147
+    "application/vnd.3gpp.pic-bw-var",
148
+    "application/vnd.3gpp2.tcap",
149
+    "application/vnd.3m.post-it-notes",
150
+    "application/vnd.accpac.simply.aso",
151
+    "application/vnd.accpac.simply.imp",
152
+    "application/vnd.acucobol",
153
+    "application/vnd.acucorp",
154
+    "application/vnd.adobe.air-application-installer-package+zip",
155
+    "application/vnd.adobe.fxp",
156
+    "application/vnd.adobe.xdp+xml",
157
+    "application/vnd.adobe.xfdf",
158
+    "application/vnd.ahead.space",
159
+    "application/vnd.airzip.filesecure.azf",
160
+    "application/vnd.airzip.filesecure.azs",
161
+    "application/vnd.amazon.ebook",
162
+    "application/vnd.americandynamics.acc",
163
+    "application/vnd.amiga.ami",
164
+    "application/vnd.android.package-archive",
165
+    "application/vnd.anser-web-certificate-issue-initiation",
166
+    "application/vnd.anser-web-funds-transfer-initiation",
167
+    "application/vnd.antix.game-component",
168
+    "application/vnd.apple.installer+xml",
169
+    "application/vnd.apple.mpegurl",
170
+    "application/vnd.aristanetworks.swi",
171
+    "application/vnd.audiograph",
172
+    "application/vnd.blueice.multipass",
173
+    "application/vnd.bmi",
174
+    "application/vnd.businessobjects",
175
+    "application/vnd.chemdraw+xml",
176
+    "application/vnd.chipnuts.karaoke-mmd",
177
+    "application/vnd.cinderella",
178
+    "application/vnd.claymore",
179
+    "application/vnd.cloanto.rp9",
180
+    "application/vnd.clonk.c4group",
181
+    "application/vnd.cluetrust.cartomobile-config",
182
+    "application/vnd.cluetrust.cartomobile-config-pkg",
183
+    "application/vnd.commonspace",
184
+    "application/vnd.contact.cmsg",
185
+    "application/vnd.cosmocaller",
186
+    "application/vnd.crick.clicker",
187
+    "application/vnd.crick.clicker.keyboard",
188
+    "application/vnd.crick.clicker.palette",
189
+    "application/vnd.crick.clicker.template",
190
+    "application/vnd.crick.clicker.wordbank",
191
+    "application/vnd.criticaltools.wbs+xml",
192
+    "application/vnd.ctc-posml",
193
+    "application/vnd.cups-ppd",
194
+    "application/vnd.curl.car",
195
+    "application/vnd.curl.pcurl",
196
+    "application/vnd.data-vision.rdz",
197
+    "application/vnd.denovo.fcselayout-link",
198
+    "application/vnd.dna",
199
+    "application/vnd.dolby.mlp",
200
+    "application/vnd.dpgraph",
201
+    "application/vnd.dreamfactory",
202
+    "application/vnd.dvb.ait",
203
+    "application/vnd.dvb.service",
204
+    "application/vnd.dynageo",
205
+    "application/vnd.ecowin.chart",
206
+    "application/vnd.enliven",
207
+    "application/vnd.epson.esf",
208
+    "application/vnd.epson.msf",
209
+    "application/vnd.epson.quickanime",
210
+    "application/vnd.epson.salt",
211
+    "application/vnd.epson.ssf",
212
+    "application/vnd.eszigno3+xml",
213
+    "application/vnd.ezpix-album",
214
+    "application/vnd.ezpix-package",
215
+    "application/vnd.fdf",
216
+    "application/vnd.fdsn.seed",
217
+    "application/vnd.flographit",
218
+    "application/vnd.fluxtime.clip",
219
+    "application/vnd.framemaker",
220
+    "application/vnd.frogans.fnc",
221
+    "application/vnd.frogans.ltf",
222
+    "application/vnd.fsc.weblaunch",
223
+    "application/vnd.fujitsu.oasys",
224
+    "application/vnd.fujitsu.oasys2",
225
+    "application/vnd.fujitsu.oasys3",
226
+    "application/vnd.fujitsu.oasysgp",
227
+    "application/vnd.fujitsu.oasysprs",
228
+    "application/vnd.fujixerox.ddd",
229
+    "application/vnd.fujixerox.docuworks",
230
+    "application/vnd.fujixerox.docuworks.binder",
231
+    "application/vnd.fuzzysheet",
232
+    "application/vnd.genomatix.tuxedo",
233
+    "application/vnd.geogebra.file",
234
+    "application/vnd.geogebra.tool",
235
+    "application/vnd.geometry-explorer",
236
+    "application/vnd.geonext",
237
+    "application/vnd.geoplan",
238
+    "application/vnd.geospace",
239
+    "application/vnd.gmx",
240
+    "application/vnd.google-earth.kml+xml",
241
+    "application/vnd.google-earth.kmz",
242
+    "application/vnd.grafeq",
243
+    "application/vnd.groove-account",
244
+    "application/vnd.groove-help",
245
+    "application/vnd.groove-identity-message",
246
+    "application/vnd.groove-injector",
247
+    "application/vnd.groove-tool-message",
248
+    "application/vnd.groove-tool-template",
249
+    "application/vnd.groove-vcard",
250
+    "application/vnd.hal+xml",
251
+    "application/vnd.handheld-entertainment+xml",
252
+    "application/vnd.hbci",
253
+    "application/vnd.hhe.lesson-player",
254
+    "application/vnd.hp-hpgl",
255
+    "application/vnd.hp-hpid",
256
+    "application/vnd.hp-hps",
257
+    "application/vnd.hp-jlyt",
258
+    "application/vnd.hp-pcl",
259
+    "application/vnd.hp-pclxl",
260
+    "application/vnd.hydrostatix.sof-data",
261
+    "application/vnd.hzn-3d-crossword",
262
+    "application/vnd.ibm.minipay",
263
+    "application/vnd.ibm.modcap",
264
+    "application/vnd.ibm.rights-management",
265
+    "application/vnd.ibm.secure-container",
266
+    "application/vnd.iccprofile",
267
+    "application/vnd.igloader",
268
+    "application/vnd.immervision-ivp",
269
+    "application/vnd.immervision-ivu",
270
+    "application/vnd.insors.igm",
271
+    "application/vnd.intercon.formnet",
272
+    "application/vnd.intergeo",
273
+    "application/vnd.intu.qbo",
274
+    "application/vnd.intu.qfx",
275
+    "application/vnd.ipunplugged.rcprofile",
276
+    "application/vnd.irepository.package+xml",
277
+    "application/vnd.is-xpr",
278
+    "application/vnd.isac.fcs",
279
+    "application/vnd.jam",
280
+    "application/vnd.jcp.javame.midlet-rms",
281
+    "application/vnd.jisp",
282
+    "application/vnd.joost.joda-archive",
283
+    "application/vnd.kahootz",
284
+    "application/vnd.kde.karbon",
285
+    "application/vnd.kde.kchart",
286
+    "application/vnd.kde.kformula",
287
+    "application/vnd.kde.kivio",
288
+    "application/vnd.kde.kontour",
289
+    "application/vnd.kde.kpresenter",
290
+    "application/vnd.kde.kspread",
291
+    "application/vnd.kde.kword",
292
+    "application/vnd.kenameaapp",
293
+    "application/vnd.kidspiration",
294
+    "application/vnd.kinar",
295
+    "application/vnd.koan",
296
+    "application/vnd.kodak-descriptor",
297
+    "application/vnd.las.las+xml",
298
+    "application/vnd.llamagraphics.life-balance.desktop",
299
+    "application/vnd.llamagraphics.life-balance.exchange+xml",
300
+    "application/vnd.lotus-1-2-3",
301
+    "application/vnd.lotus-approach",
302
+    "application/vnd.lotus-freelance",
303
+    "application/vnd.lotus-notes",
304
+    "application/vnd.lotus-organizer",
305
+    "application/vnd.lotus-screencam",
306
+    "application/vnd.lotus-wordpro",
307
+    "application/vnd.macports.portpkg",
308
+    "application/vnd.mcd",
309
+    "application/vnd.medcalcdata",
310
+    "application/vnd.mediastation.cdkey",
311
+    "application/vnd.mfer",
312
+    "application/vnd.mfmp",
313
+    "application/vnd.micrografx.flo",
314
+    "application/vnd.micrografx.igx",
315
+    "application/vnd.mif",
316
+    "application/vnd.mobius.daf",
317
+    "application/vnd.mobius.dis",
318
+    "application/vnd.mobius.mbk",
319
+    "application/vnd.mobius.mqy",
320
+    "application/vnd.mobius.msl",
321
+    "application/vnd.mobius.plc",
322
+    "application/vnd.mobius.txf",
323
+    "application/vnd.mophun.application",
324
+    "application/vnd.mophun.certificate",
325
+    "application/vnd.mozilla.xul+xml",
326
+    "application/vnd.ms-artgalry",
327
+    "application/vnd.ms-cab-compressed",
328
+    "application/vnd.ms-excel",
329
+    "application/vnd.ms-excel.addin.macroenabled.12",
330
+    "application/vnd.ms-excel.sheet.binary.macroenabled.12",
331
+    "application/vnd.ms-excel.sheet.macroenabled.12",
332
+    "application/vnd.ms-excel.template.macroenabled.12",
333
+    "application/vnd.ms-fontobject",
334
+    "application/vnd.ms-htmlhelp",
335
+    "application/vnd.ms-ims",
336
+    "application/vnd.ms-lrm",
337
+    "application/vnd.ms-officetheme",
338
+    "application/vnd.ms-pki.seccat",
339
+    "application/vnd.ms-pki.stl",
340
+    "application/vnd.ms-powerpoint",
341
+    "application/vnd.ms-powerpoint.addin.macroenabled.12",
342
+    "application/vnd.ms-powerpoint.presentation.macroenabled.12",
343
+    "application/vnd.ms-powerpoint.slide.macroenabled.12",
344
+    "application/vnd.ms-powerpoint.slideshow.macroenabled.12",
345
+    "application/vnd.ms-powerpoint.template.macroenabled.12",
346
+    "application/vnd.ms-project",
347
+    "application/vnd.ms-word.document.macroenabled.12",
348
+    "application/vnd.ms-word.template.macroenabled.12",
349
+    "application/vnd.ms-works",
350
+    "application/vnd.ms-wpl",
351
+    "application/vnd.ms-xpsdocument",
352
+    "application/vnd.mseq",
353
+    "application/vnd.musician",
354
+    "application/vnd.muvee.style",
355
+    "application/vnd.neurolanguage.nlu",
356
+    "application/vnd.noblenet-directory",
357
+    "application/vnd.noblenet-sealer",
358
+    "application/vnd.noblenet-web",
359
+    "application/vnd.nokia.n-gage.data",
360
+    "application/vnd.nokia.n-gage.symbian.install",
361
+    "application/vnd.nokia.radio-preset",
362
+    "application/vnd.nokia.radio-presets",
363
+    "application/vnd.novadigm.edm",
364
+    "application/vnd.novadigm.edx",
365
+    "application/vnd.novadigm.ext",
366
+    "application/vnd.oasis.opendocument.chart",
367
+    "application/vnd.oasis.opendocument.chart-template",
368
+    "application/vnd.oasis.opendocument.database",
369
+    "application/vnd.oasis.opendocument.formula",
370
+    "application/vnd.oasis.opendocument.formula-template",
371
+    "application/vnd.oasis.opendocument.graphics",
372
+    "application/vnd.oasis.opendocument.graphics-template",
373
+    "application/vnd.oasis.opendocument.image",
374
+    "application/vnd.oasis.opendocument.image-template",
375
+    "application/vnd.oasis.opendocument.presentation",
376
+    "application/vnd.oasis.opendocument.presentation-template",
377
+    "application/vnd.oasis.opendocument.spreadsheet",
378
+    "application/vnd.oasis.opendocument.spreadsheet-template",
379
+    "application/vnd.oasis.opendocument.text",
380
+    "application/vnd.oasis.opendocument.text-master",
381
+    "application/vnd.oasis.opendocument.text-template",
382
+    "application/vnd.oasis.opendocument.text-web",
383
+    "application/vnd.olpc-sugar",
384
+    "application/vnd.oma.dd2+xml",
385
+    "application/vnd.openofficeorg.extension",
386
+    "application/vnd.openxmlformats-officedocument.presentationml.presentation",
387
+    "application/vnd.openxmlformats-officedocument.presentationml.slide",
388
+    "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
389
+    "application/vnd.openxmlformats-officedocument.presentationml.template",
390
+    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
391
+    "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
392
+    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
393
+    "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
394
+    "application/vnd.osgeo.mapguide.package",
395
+    "application/vnd.osgi.dp",
396
+    "application/vnd.palm",
397
+    "application/vnd.pawaafile",
398
+    "application/vnd.pg.format",
399
+    "application/vnd.pg.osasli",
400
+    "application/vnd.picsel",
401
+    "application/vnd.pmi.widget",
402
+    "application/vnd.pocketlearn",
403
+    "application/vnd.powerbuilder6",
404
+    "application/vnd.previewsystems.box",
405
+    "application/vnd.proteus.magazine",
406
+    "application/vnd.publishare-delta-tree",
407
+    "application/vnd.pvi.ptid1",
408
+    "application/vnd.quark.quarkxpress",
409
+    "application/vnd.realvnc.bed",
410
+    "application/vnd.recordare.musicxml",
411
+    "application/vnd.recordare.musicxml+xml",
412
+    "application/vnd.rig.cryptonote",
413
+    "application/vnd.rim.cod",
414
+    "application/vnd.rn-realmedia",
415
+    "application/vnd.route66.link66+xml",
416
+    "application/vnd.sailingtracker.track",
417
+    "application/vnd.seemail",
418
+    "application/vnd.sema",
419
+    "application/vnd.semd",
420
+    "application/vnd.semf",
421
+    "application/vnd.shana.informed.formdata",
422
+    "application/vnd.shana.informed.formtemplate",
423
+    "application/vnd.shana.informed.interchange",
424
+    "application/vnd.shana.informed.package",
425
+    "application/vnd.simtech-mindmapper",
426
+    "application/vnd.smaf",
427
+    "application/vnd.smart.teacher",
428
+    "application/vnd.solent.sdkm+xml",
429
+    "application/vnd.spotfire.dxp",
430
+    "application/vnd.spotfire.sfs",
431
+    "application/vnd.stardivision.calc",
432
+    "application/vnd.stardivision.draw",
433
+    "application/vnd.stardivision.impress",
434
+    "application/vnd.stardivision.math",
435
+    "application/vnd.stardivision.writer",
436
+    "application/vnd.stardivision.writer-global",
437
+    "application/vnd.stepmania.stepchart",
438
+    "application/vnd.sun.xml.calc",
439
+    "application/vnd.sun.xml.calc.template",
440
+    "application/vnd.sun.xml.draw",
441
+    "application/vnd.sun.xml.draw.template",
442
+    "application/vnd.sun.xml.impress",
443
+    "application/vnd.sun.xml.impress.template",
444
+    "application/vnd.sun.xml.math",
445
+    "application/vnd.sun.xml.writer",
446
+    "application/vnd.sun.xml.writer.global",
447
+    "application/vnd.sun.xml.writer.template",
448
+    "application/vnd.sus-calendar",
449
+    "application/vnd.svd",
450
+    "application/vnd.symbian.install",
451
+    "application/vnd.syncml+xml",
452
+    "application/vnd.syncml.dm+wbxml",
453
+    "application/vnd.syncml.dm+xml",
454
+    "application/vnd.tao.intent-module-archive",
455
+    "application/vnd.tmobile-livetv",
456
+    "application/vnd.trid.tpt",
457
+    "application/vnd.triscape.mxs",
458
+    "application/vnd.trueapp",
459
+    "application/vnd.ufdl",
460
+    "application/vnd.uiq.theme",
461
+    "application/vnd.umajin",
462
+    "application/vnd.unity",
463
+    "application/vnd.uoml+xml",
464
+    "application/vnd.vcx",
465
+    "application/vnd.visio",
466
+    "application/vnd.visio2013",
467
+    "application/vnd.visionary",
468
+    "application/vnd.vsf",
469
+    "application/vnd.wap.wbxml",
470
+    "application/vnd.wap.wmlc",
471
+    "application/vnd.wap.wmlscriptc",
472
+    "application/vnd.webturbo",
473
+    "application/vnd.wolfram.player",
474
+    "application/vnd.wordperfect",
475
+    "application/vnd.wqd",
476
+    "application/vnd.wt.stf",
477
+    "application/vnd.xara",
478
+    "application/vnd.xfdl",
479
+    "application/vnd.yamaha.hv-dic",
480
+    "application/vnd.yamaha.hv-script",
481
+    "application/vnd.yamaha.hv-voice",
482
+    "application/vnd.yamaha.openscoreformat",
483
+    "application/vnd.yamaha.openscoreformat.osfpvg+xml",
484
+    "application/vnd.yamaha.smaf-audio",
485
+    "application/vnd.yamaha.smaf-phrase",
486
+    "application/vnd.yellowriver-custom-menu",
487
+    "application/vnd.zul",
488
+    "application/vnd.zzazz.deck+xml",
489
+    "application/voicexml+xml",
490
+    "application/widget",
491
+    "application/winhlp",
492
+    "application/wsdl+xml",
493
+    "application/wspolicy+xml",
494
+    "application/x-7z-compressed",
495
+    "application/x-abiword",
496
+    "application/x-ace-compressed",
497
+    "application/x-apple-diskimage",
498
+    "application/x-authorware-bin",
499
+    "application/x-authorware-map",
500
+    "application/x-authorware-seg",
501
+    "application/x-bcpio",
502
+    "application/x-bittorrent",
503
+    "application/x-bzip",
504
+    "application/x-bzip2",
505
+    "application/x-cdlink",
506
+    "application/x-chat",
507
+    "application/x-chess-pgn",
508
+    "application/x-cpio",
509
+    "application/x-csh",
510
+    "application/x-debian-package",
511
+    "application/x-director",
512
+    "application/x-doom",
513
+    "application/x-dtbncx+xml",
514
+    "application/x-dtbook+xml",
515
+    "application/x-dtbresource+xml",
516
+    "application/x-dvi",
517
+    "application/x-font-bdf",
518
+    "application/x-font-ghostscript",
519
+    "application/x-font-linux-psf",
520
+    "application/x-font-otf",
521
+    "application/x-font-pcf",
522
+    "application/x-font-snf",
523
+    "application/x-font-ttf",
524
+    "application/x-font-type1",
525
+    "application/x-font-woff",
526
+    "application/x-futuresplash",
527
+    "application/x-gnumeric",
528
+    "application/x-gtar",
529
+    "application/x-hdf",
530
+    "application/x-java-jnlp-file",
531
+    "application/x-latex",
532
+    "application/x-mobipocket-ebook",
533
+    "application/x-ms-application",
534
+    "application/x-ms-wmd",
535
+    "application/x-ms-wmz",
536
+    "application/x-ms-xbap",
537
+    "application/x-msaccess",
538
+    "application/x-msbinder",
539
+    "application/x-mscardfile",
540
+    "application/x-msclip",
541
+    "application/x-msdownload",
542
+    "application/x-msmediaview",
543
+    "application/x-msmetafile",
544
+    "application/x-msmoney",
545
+    "application/x-mspublisher",
546
+    "application/x-msschedule",
547
+    "application/x-msterminal",
548
+    "application/x-mswrite",
549
+    "application/x-netcdf",
550
+    "application/x-pkcs12",
551
+    "application/x-pkcs7-certificates",
552
+    "application/x-pkcs7-certreqresp",
553
+    "application/x-rar-compressed",
554
+    "application/x-sh",
555
+    "application/x-shar",
556
+    "application/x-shockwave-flash",
557
+    "application/x-silverlight-app",
558
+    "application/x-stuffit",
559
+    "application/x-stuffitx",
560
+    "application/x-sv4cpio",
561
+    "application/x-sv4crc",
562
+    "application/x-tar",
563
+    "application/x-tcl",
564
+    "application/x-tex",
565
+    "application/x-tex-tfm",
566
+    "application/x-texinfo",
567
+    "application/x-ustar",
568
+    "application/x-wais-source",
569
+    "application/x-x509-ca-cert",
570
+    "application/x-xfig",
571
+    "application/x-xpinstall",
572
+    "application/xcap-diff+xml",
573
+    "application/xenc+xml",
574
+    "application/xhtml+xml",
575
+    "application/xml",
576
+    "application/xml-dtd",
577
+    "application/xop+xml",
578
+    "application/xslt+xml",
579
+    "application/xspf+xml",
580
+    "application/xv+xml",
581
+    "application/yang",
582
+    "application/yin+xml",
583
+    "application/zip",
584
+    "audio/adpcm",
585
+    "audio/basic",
586
+    "audio/midi",
587
+    "audio/mp4",
588
+    "audio/mpeg",
589
+    "audio/ogg",
590
+    "audio/vnd.dece.audio",
591
+    "audio/vnd.digital-winds",
592
+    "audio/vnd.dra",
593
+    "audio/vnd.dts",
594
+    "audio/vnd.dts.hd",
595
+    "audio/vnd.lucent.voice",
596
+    "audio/vnd.ms-playready.media.pya",
597
+    "audio/vnd.nuera.ecelp4800",
598
+    "audio/vnd.nuera.ecelp7470",
599
+    "audio/vnd.nuera.ecelp9600",
600
+    "audio/vnd.rip",
601
+    "audio/webm",
602
+    "audio/x-aac",
603
+    "audio/x-aiff",
604
+    "audio/x-mpegurl",
605
+    "audio/x-ms-wax",
606
+    "audio/x-ms-wma",
607
+    "audio/x-pn-realaudio",
608
+    "audio/x-pn-realaudio-plugin",
609
+    "audio/x-wav",
610
+    "chemical/x-cdx",
611
+    "chemical/x-cif",
612
+    "chemical/x-cmdf",
613
+    "chemical/x-cml",
614
+    "chemical/x-csml",
615
+    "chemical/x-xyz",
616
+    "image/bmp",
617
+    "image/cgm",
618
+    "image/g3fax",
619
+    "image/gif",
620
+    "image/ief",
621
+    "image/jpeg",
622
+    "image/ktx",
623
+    "image/pjpeg",
624
+    "image/png",
625
+    "image/prs.btif",
626
+    "image/svg+xml",
627
+    "image/tiff",
628
+    "image/vnd.adobe.photoshop",
629
+    "image/vnd.dece.graphic",
630
+    "image/vnd.djvu",
631
+    "image/vnd.dvb.subtitle",
632
+    "image/vnd.dwg",
633
+    "image/vnd.dxf",
634
+    "image/vnd.fastbidsheet",
635
+    "image/vnd.fpx",
636
+    "image/vnd.fst",
637
+    "image/vnd.fujixerox.edmics-mmr",
638
+    "image/vnd.fujixerox.edmics-rlc",
639
+    "image/vnd.ms-modi",
640
+    "image/vnd.net-fpx",
641
+    "image/vnd.wap.wbmp",
642
+    "image/vnd.xiff",
643
+    "image/webp",
644
+    "image/x-citrix-jpeg",
645
+    "image/x-citrix-png",
646
+    "image/x-cmu-raster",
647
+    "image/x-cmx",
648
+    "image/x-freehand",
649
+    "image/x-icon",
650
+    "image/x-pcx",
651
+    "image/x-pict",
652
+    "image/x-png",
653
+    "image/x-portable-anymap",
654
+    "image/x-portable-bitmap",
655
+    "image/x-portable-graymap",
656
+    "image/x-portable-pixmap",
657
+    "image/x-rgb",
658
+    "image/x-xbitmap",
659
+    "image/x-xpixmap",
660
+    "image/x-xwindowdump",
661
+    "message/rfc822",
662
+    "model/iges",
663
+    "model/mesh",
664
+    "model/vnd.collada+xml",
665
+    "model/vnd.dwf",
666
+    "model/vnd.gdl",
667
+    "model/vnd.gtw",
668
+    "model/vnd.mts",
669
+    "model/vnd.vtu",
670
+    "model/vrml",
671
+    "text/calendar",
672
+    "text/css",
673
+    "text/csv",
674
+    "text/html",
675
+    "text/n3",
676
+    "text/plain",
677
+    "text/plain-bas",
678
+    "text/prs.lines.tag",
679
+    "text/richtext",
680
+    "text/sgml",
681
+    "text/tab-separated-values",
682
+    "text/troff",
683
+    "text/turtle",
684
+    "text/uri-list",
685
+    "text/vnd.curl",
686
+    "text/vnd.curl.dcurl",
687
+    "text/vnd.curl.mcurl",
688
+    "text/vnd.curl.scurl",
689
+    "text/vnd.fly",
690
+    "text/vnd.fmi.flexstor",
691
+    "text/vnd.graphviz",
692
+    "text/vnd.in3d.3dml",
693
+    "text/vnd.in3d.spot",
694
+    "text/vnd.sun.j2me.app-descriptor",
695
+    "text/vnd.wap.wml",
696
+    "text/vnd.wap.wmlscript",
697
+    "text/x-asm",
698
+    "text/x-c",
699
+    "text/x-fortran",
700
+    "text/x-java-source,java",
701
+    "text/x-pascal",
702
+    "text/x-setext",
703
+    "text/x-uuencode",
704
+    "text/x-vcalendar",
705
+    "text/x-vcard",
706
+    "text/yaml",
707
+    "video/3gpp",
708
+    "video/3gpp2",
709
+    "video/h261",
710
+    "video/h263",
711
+    "video/h264",
712
+    "video/jpeg",
713
+    "video/jpm",
714
+    "video/mj2",
715
+    "video/mp4",
716
+    "video/mpeg",
717
+    "video/ogg",
718
+    "video/quicktime",
719
+    "video/vnd.dece.hd",
720
+    "video/vnd.dece.mobile",
721
+    "video/vnd.dece.pd",
722
+    "video/vnd.dece.sd",
723
+    "video/vnd.dece.video",
724
+    "video/vnd.fvt",
725
+    "video/vnd.mpegurl",
726
+    "video/vnd.ms-playready.media.pyv",
727
+    "video/vnd.uvvu.mp4",
728
+    "video/vnd.vivo",
729
+    "video/webm",
730
+    "video/x-f4v",
731
+    "video/x-fli",
732
+    "video/x-flv",
733
+    "video/x-m4v",
734
+    "video/x-ms-asf",
735
+    "video/x-ms-wm",
736
+    "video/x-ms-wmv",
737
+    "video/x-ms-wmx",
738
+    "video/x-ms-wvx",
739
+    "video/x-msvideo",
740
+    "video/x-sgi-movie",
741
+    "x-conference/x-cooltalk",
742
+];