aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSven-Hendrik Haase <svenstaro@gmail.com>2019-03-23 12:49:46 +0000
committerGitHub <noreply@github.com>2019-03-23 12:49:46 +0000
commitc47a7773228e4e1beddfdcc6719cbc10acfbfdb5 (patch)
treeb5ebbb5a4c80f9736484f9cd9112ef30956e4178
parentMerge pull request #49 from svenstaro/dependabot/cargo/structopt-0.2.15 (diff)
parentUpdated README with known limitations (diff)
downloadminiserve-c47a7773228e4e1beddfdcc6719cbc10acfbfdb5.tar.gz
miniserve-c47a7773228e4e1beddfdcc6719cbc10acfbfdb5.zip
Merge pull request #48 from boastful-squirrel/targz
Download folders in .tar.gz format
-rw-r--r--Cargo.lock75
-rw-r--r--Cargo.toml6
-rw-r--r--README.md5
-rw-r--r--src/archive.rs145
-rw-r--r--src/errors.rs96
-rw-r--r--src/listing.rs76
-rw-r--r--src/main.rs21
-rw-r--r--src/renderer.rs42
8 files changed, 424 insertions, 42 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 5cc26d2..26f6848 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7,7 +7,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"actix_derive 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam-channel 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -35,7 +35,7 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"actix 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -64,7 +64,7 @@ dependencies = [
"bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
"brotli2 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"cookie 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -243,7 +243,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bytes"
-version = "0.4.11"
+version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -478,6 +478,16 @@ dependencies = [
]
[[package]]
+name = "filetime"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "flate2"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -532,7 +542,7 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
"http 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -570,7 +580,7 @@ name = "http"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
- "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -651,6 +661,16 @@ version = "0.2.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "libflate"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crc32fast 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "linked-hash-map"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -752,17 +772,23 @@ dependencies = [
"actix-web 0.7.18 (registry+https://github.com/rust-lang/crates.io-index)",
"alphanumeric-sort 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"bytesize 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono-humanize 0.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
"htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libflate 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"maud 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)",
"nanoid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
"simplelog 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
"structopt 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tar 0.4.22 (registry+https://github.com/rust-lang/crates.io-index)",
"yansi 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1344,6 +1370,17 @@ dependencies = [
]
[[package]]
+name = "tar"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "filetime 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
+ "xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "term"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1393,7 +1430,7 @@ name = "tokio"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
- "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
"mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1416,7 +1453,7 @@ name = "tokio-codec"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
- "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1454,7 +1491,7 @@ name = "tokio-io"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
- "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1505,7 +1542,7 @@ name = "tokio-tcp"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
- "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
"iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1546,7 +1583,7 @@ name = "tokio-udp"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
- "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1560,7 +1597,7 @@ name = "tokio-uds"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
- "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
"iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1825,6 +1862,14 @@ dependencies = [
]
[[package]]
+name = "xattr"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "yansi"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1851,7 +1896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum brotli2 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e"
"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
"checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb"
-"checksum bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "40ade3d27603c2cb345eb0912aec461a6dec7e06a4ae48589904e808335c7afa"
+"checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c"
"checksum bytesize 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "716960a18f978640f25101b5cbf1c6f6b0d3192fab36a2d98ca96f0ecbe41010"
"checksum cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)" = "4390a3b5f4f6bce9c1d0c00128379df433e53777fdd30e92f16a529332baec4e"
"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4"
@@ -1878,6 +1923,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum error-chain 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6930e04918388a9a2e41d518c25cf679ccafe26733fb4127dbf21993f2575d46"
"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"
"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1"
+"checksum filetime 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a2df5c1a8c4be27e7707789dc42ae65976e60b394afd293d1419ab915833e646"
"checksum flate2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2291c165c8e703ee54ef3055ad6188e3d51108e2ded18e9f2476e774fc5ad3d4"
"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
@@ -1901,6 +1947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
"checksum lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f"
"checksum libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)" = "413f3dfc802c5dc91dc570b05125b6cda9855edfaa9825c9849807876376e70e"
+"checksum libflate 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "7346a83e8a2c3958d44d24225d905385dc31fc16e89dffb356c457b278914d20"
"checksum linked-hash-map 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7860ec297f7008ff7a1e3382d7f7e1dcd69efc94751a2284bafc3d013c2aa939"
"checksum literalext 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2f42dd699527975a1e0d722e0707998671188a0125f2051d2d192fc201184a81"
"checksum lock_api 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c"
@@ -1982,6 +2029,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum structopt-derive 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)" = "528aeb7351d042e6ffbc2a6fb76a86f9b622fdf7c25932798e7a82cb03bc94c6"
"checksum syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)" = "f92e629aa1d9c827b2bb8297046c1ccffc57c99b947a680d3ccff1f136a3bee9"
"checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015"
+"checksum tar 0.4.22 (registry+https://github.com/rust-lang/crates.io-index)" = "c2167ff53da2a661702b3299f71a91b61b1dffef36b4b2884b1f9c67254c0133"
"checksum term 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5e6b677dd1e8214ea1ef4297f85dbcbed8e8cdddb561040cc998ca2551c37561"
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
"checksum textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "307686869c93e71f94da64286f9a9524c0f308a9e1c87a583de8e9c9039ad3f6"
@@ -2031,4 +2079,5 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum winreg 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a"
"checksum winutil 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e"
"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
+"checksum xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c"
"checksum yansi 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71"
diff --git a/Cargo.toml b/Cargo.toml
index 54dba89..6f4aa16 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -37,3 +37,9 @@ chrono = "0.4.6"
chrono-humanize = "0.0.11"
maud = { version = "0.20.0", features = ["actix-web"] }
serde = { version = "1.0.89", features = ["derive"] }
+tar = "0.4.22"
+bytes = "0.4.12"
+futures = "0.1.25"
+libflate = "0.1.21"
+failure = "0.1.5"
+log = "0.4.6" \ No newline at end of file
diff --git a/README.md b/README.md
index c30007c..2b8e8b3 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,11 @@ Sometimes this is just a more practical and quick way than doing things properly
- Single binary drop-in with no extra dependencies required
- Authentication support with username and password
- Mega fast and highly parallel (thanks to [Rust](https://www.rust-lang.org/) and [Actix](https://actix.rs/))
+- Folder download (compressed in .tar.gz)
+
+## Known limitations
+
+- **For now**, the tar.gz compression is not async-ready, which means that the whole archive needs to be created (in memory) before the download starts. While it should not be a problem for small folders, the download feature can really get resource-heavy for large folders.
## How to install
diff --git a/src/archive.rs b/src/archive.rs
new file mode 100644
index 0000000..206d252
--- /dev/null
+++ b/src/archive.rs
@@ -0,0 +1,145 @@
+use actix_web::http::ContentEncoding;
+use bytes::Bytes;
+use failure::ResultExt;
+use libflate::gzip::Encoder;
+use serde::Deserialize;
+use std::io;
+use std::path::PathBuf;
+use tar::Builder;
+
+use crate::errors;
+
+/// Available compression methods
+#[derive(Debug, Deserialize, Clone)]
+pub enum CompressionMethod {
+ /// TAR GZ
+ #[serde(alias = "targz")]
+ TarGz,
+}
+
+impl CompressionMethod {
+ pub fn to_string(&self) -> String {
+ match &self {
+ CompressionMethod::TarGz => "targz",
+ }
+ .to_string()
+ }
+
+ pub fn extension(&self) -> String {
+ match &self {
+ CompressionMethod::TarGz => "tar.gz",
+ }
+ .to_string()
+ }
+
+ pub fn content_type(&self) -> String {
+ match &self {
+ CompressionMethod::TarGz => "application/gzip",
+ }
+ .to_string()
+ }
+
+ pub fn content_encoding(&self) -> ContentEncoding {
+ match &self {
+ CompressionMethod::TarGz => ContentEncoding::Gzip,
+ }
+ }
+}
+
+/// Creates an archive of a folder, using the algorithm the user chose from the web interface
+/// This method returns the archive as a stream of bytes
+pub fn create_archive(
+ method: &CompressionMethod,
+ dir: &PathBuf,
+ skip_symlinks: bool,
+) -> Result<(String, Bytes), errors::CompressionError> {
+ match method {
+ CompressionMethod::TarGz => tgz_compress(&dir, skip_symlinks),
+ }
+}
+
+/// Compresses a given folder in .tar.gz format, and returns the result as a stream of bytes
+fn tgz_compress(
+ dir: &PathBuf,
+ skip_symlinks: bool,
+) -> Result<(String, Bytes), errors::CompressionError> {
+ let src_dir = dir.display().to_string();
+ let inner_folder = match dir.file_name() {
+ Some(directory_name) => match directory_name.to_str() {
+ Some(directory) => directory,
+ None => {
+ return Err(errors::CompressionError::new(
+ errors::CompressionErrorKind::InvalidUTF8DirectoryName,
+ ))
+ }
+ },
+ None => {
+ return Err(errors::CompressionError::new(
+ errors::CompressionErrorKind::InvalidDirectoryName,
+ ))
+ }
+ };
+ let dst_filename = format!("{}.tar", inner_folder);
+ let dst_tgz_filename = format!("{}.gz", dst_filename);
+
+ let tar_content = tar(src_dir, inner_folder.to_string(), skip_symlinks).context(
+ errors::CompressionErrorKind::TarBuildingError {
+ message: "an error occured while writing the TAR archive".to_string(),
+ },
+ )?;
+ let gz_data = gzip(&tar_content).context(errors::CompressionErrorKind::GZipBuildingError {
+ message: "an error occured while writing the GZIP archive".to_string(),
+ })?;
+
+ let mut data = Bytes::new();
+ data.extend_from_slice(&gz_data);
+
+ Ok((dst_tgz_filename, data))
+}
+
+/// Creates a TAR archive of a folder, and returns it as a stream of bytes
+fn tar(
+ src_dir: String,
+ inner_folder: String,
+ skip_symlinks: bool,
+) -> Result<Vec<u8>, errors::CompressionError> {
+ let mut tar_builder = Builder::new(Vec::new());
+
+ tar_builder.follow_symlinks(!skip_symlinks);
+ // Recursively adds the content of src_dir into the archive stream
+ tar_builder.append_dir_all(inner_folder, &src_dir).context(
+ errors::CompressionErrorKind::TarBuildingError {
+ message: format!(
+ "failed to append the content of {} to the TAR archive",
+ &src_dir
+ ),
+ },
+ )?;
+
+ let tar_content =
+ tar_builder
+ .into_inner()
+ .context(errors::CompressionErrorKind::TarBuildingError {
+ message: "failed to finish writing the TAR archive".to_string(),
+ })?;
+
+ Ok(tar_content)
+}
+
+/// Compresses a stream of bytes using the GZIP algorithm, and returns the resulting stream
+fn gzip(mut data: &[u8]) -> Result<Vec<u8>, errors::CompressionError> {
+ let mut encoder =
+ Encoder::new(Vec::new()).context(errors::CompressionErrorKind::GZipBuildingError {
+ message: "failed to create GZIP encoder".to_string(),
+ })?;
+ io::copy(&mut data, &mut encoder).context(errors::CompressionErrorKind::GZipBuildingError {
+ message: "failed to write GZIP data".to_string(),
+ })?;
+ let data = encoder.finish().into_result().context(
+ errors::CompressionErrorKind::GZipBuildingError {
+ message: "failed to write GZIP trailer".to_string(),
+ },
+ )?;
+
+ Ok(data)
+}
diff --git a/src/errors.rs b/src/errors.rs
new file mode 100644
index 0000000..2aa5f58
--- /dev/null
+++ b/src/errors.rs
@@ -0,0 +1,96 @@
+use failure::{Backtrace, Context, Fail};
+use std::fmt::{self, Debug, Display};
+
+/// Kinds of errors which might happen during the generation of an archive
+#[derive(Debug, Fail)]
+pub enum CompressionErrorKind {
+ /// This error will occur if the directory name could not be retrieved from the path
+ /// See https://doc.rust-lang.org/std/path/struct.Path.html#method.file_name
+ #[fail(display = "Invalid path: directory name terminates in \"..\"")]
+ InvalidDirectoryName,
+ /// This error will occur when trying to convert an OSString into a String, if the path
+ /// contains invalid UTF-8 characters
+ /// See https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_str
+ #[fail(display = "Invalid path: directory name contains invalid UTF-8 characters")]
+ InvalidUTF8DirectoryName,
+ /// This error might occur while building a TAR archive, or while writing the termination sections
+ /// See https://docs.rs/tar/0.4.22/tar/struct.Builder.html#method.append_dir_all
+ /// and https://docs.rs/tar/0.4.22/tar/struct.Builder.html#method.into_inner
+ #[fail(display = "Failed to create the TAR archive: {}", message)]
+ TarBuildingError { message: String },
+ /// This error might occur while building a GZIP archive, or while writing the GZIP trailer
+ /// See https://docs.rs/libflate/0.1.21/libflate/gzip/struct.Encoder.html#method.finish
+ #[fail(display = "Failed to create the GZIP archive: {}", message)]
+ GZipBuildingError { message: String },
+}
+
+/// Prints the full chain of error, up to the root cause.
+/// If RUST_BACKTRACE is set to 1, also prints the backtrace for each error
+pub fn print_error_chain(err: CompressionError) {
+ log::error!("{}", &err);
+ print_backtrace(&err);
+ for cause in Fail::iter_causes(&err) {
+ log::error!("caused by: {}", cause);
+ print_backtrace(cause);
+ }
+}
+
+/// Prints the backtrace of an error
+/// RUST_BACKTRACE needs to be set to 1 to display the backtrace
+fn print_backtrace(err: &dyn Fail) {
+ if let Some(backtrace) = err.backtrace() {
+ let backtrace = backtrace.to_string();
+ if backtrace != "" {
+ log::error!("{}", backtrace);
+ }
+ }
+}
+
+/// Based on https://boats.gitlab.io/failure/error-errorkind.html
+pub struct CompressionError {
+ inner: Context<CompressionErrorKind>,
+}
+
+impl CompressionError {
+ pub fn new(kind: CompressionErrorKind) -> CompressionError {
+ CompressionError {
+ inner: Context::new(kind),
+ }
+ }
+}
+
+impl Fail for CompressionError {
+ fn cause(&self) -> Option<&Fail> {
+ self.inner.cause()
+ }
+
+ fn backtrace(&self) -> Option<&Backtrace> {
+ self.inner.backtrace()
+ }
+}
+
+impl Display for CompressionError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ Display::fmt(&self.inner, f)
+ }
+}
+
+impl Debug for CompressionError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ Debug::fmt(&self.inner, f)
+ }
+}
+
+impl From<Context<CompressionErrorKind>> for CompressionError {
+ fn from(inner: Context<CompressionErrorKind>) -> CompressionError {
+ CompressionError { inner }
+ }
+}
+
+impl From<CompressionErrorKind> for CompressionError {
+ fn from(kind: CompressionErrorKind) -> CompressionError {
+ CompressionError {
+ inner: Context::new(kind),
+ }
+ }
+}
diff --git a/src/listing.rs b/src/listing.rs
index 57bef17..c4daf88 100644
--- a/src/listing.rs
+++ b/src/listing.rs
@@ -1,5 +1,6 @@
-use actix_web::{fs, FromRequest, HttpRequest, HttpResponse, Query, Result};
+use actix_web::{fs, http, Body, FromRequest, HttpRequest, HttpResponse, Query, Result};
use bytesize::ByteSize;
+use futures::stream::once;
use htmlescape::encode_minimal as escape_html_entity;
use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
use serde::Deserialize;
@@ -7,6 +8,8 @@ use std::io;
use std::path::Path;
use std::time::SystemTime;
+use crate::archive;
+use crate::errors;
use crate::renderer;
/// Query parameters
@@ -14,6 +17,7 @@ use crate::renderer;
struct QueryParameters {
sort: Option<SortingMethod>,
order: Option<SortingOrder>,
+ download: Option<archive::CompressionMethod>,
}
/// Available sorting methods
@@ -134,11 +138,16 @@ pub fn directory_listing<S>(
let is_root = base.parent().is_none() || req.path() == random_route;
let page_parent = base.parent().map(|p| p.display().to_string());
- let (sort_method, sort_order) = if let Ok(query) = Query::<QueryParameters>::extract(req) {
- (query.sort.clone(), query.order.clone())
- } else {
- (None, None)
- };
+ let (sort_method, sort_order, download) =
+ if let Ok(query) = Query::<QueryParameters>::extract(req) {
+ (
+ query.sort.clone(),
+ query.order.clone(),
+ query.download.clone(),
+ )
+ } else {
+ (None, None, None)
+ };
let mut entries: Vec<Entry> = Vec::new();
@@ -218,17 +227,46 @@ pub fn directory_listing<S>(
}
}
- Ok(HttpResponse::Ok()
- .content_type("text/html; charset=utf-8")
- .body(
- renderer::page(
- &title,
- entries,
- is_root,
- page_parent,
- sort_method,
- sort_order,
- )
- .into_string(),
- ))
+ if let Some(compression_method) = &download {
+ log::info!(
+ "Creating an archive ({extension}) of {path}...",
+ extension = compression_method.extension(),
+ path = &dir.path.display().to_string()
+ );
+ match archive::create_archive(&compression_method, &dir.path, skip_symlinks) {
+ Ok((filename, content)) => {
+ log::info!("{file} successfully created !", file = &filename);
+ Ok(HttpResponse::Ok()
+ .content_type(compression_method.content_type())
+ .content_encoding(compression_method.content_encoding())
+ .header("Content-Transfer-Encoding", "binary")
+ .header(
+ "Content-Disposition",
+ format!("attachment; filename={:?}", filename),
+ )
+ .chunked()
+ .body(Body::Streaming(Box::new(once(Ok(content))))))
+ }
+ Err(err) => {
+ errors::print_error_chain(err);
+ Ok(HttpResponse::Ok()
+ .status(http::StatusCode::INTERNAL_SERVER_ERROR)
+ .body(""))
+ }
+ }
+ } else {
+ Ok(HttpResponse::Ok()
+ .content_type("text/html; charset=utf-8")
+ .body(
+ renderer::page(
+ &title,
+ entries,
+ is_root,
+ page_parent,
+ sort_method,
+ sort_order,
+ )
+ .into_string(),
+ ))
+ }
}
diff --git a/src/main.rs b/src/main.rs
index b15088c..f662a73 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,8 +9,10 @@ use std::thread;
use std::time::Duration;
use yansi::{Color, Paint};
+mod archive;
mod args;
mod auth;
+mod errors;
mod listing;
mod renderer;
@@ -48,6 +50,13 @@ fn main() {
}
let miniserve_config = args::parse_args();
+
+ let _ = if miniserve_config.verbose {
+ TermLogger::init(LevelFilter::Info, Config::default())
+ } else {
+ TermLogger::init(LevelFilter::Error, Config::default())
+ };
+
if miniserve_config.no_symlinks
&& miniserve_config
.path
@@ -56,16 +65,10 @@ fn main() {
.file_type()
.is_symlink()
{
- println!(
- "{error} The no-symlinks option cannot be used with a symlink path",
- error = Paint::red("error:").bold(),
- );
+ log::error!("The no-symlinks option cannot be used with a symlink path");
return;
}
- if miniserve_config.verbose {
- let _ = TermLogger::init(LevelFilter::Info, Config::default());
- }
let sys = actix::System::new("miniserve");
let inside_config = miniserve_config.clone();
@@ -119,7 +122,7 @@ fn main() {
version = crate_version!()
);
if !miniserve_config.path_explicitly_chosen {
- println!("{info} miniserve has been invoked without an explicit path so it will serve the current directory.", info=Color::Blue.paint("Info:").bold());
+ println!("{warning} miniserve has been invoked without an explicit path so it will serve the current directory.", warning=Color::RGB(255, 192, 0).paint("Notice:").bold());
println!(
" Invoke with -h|--help to see options or invoke as `miniserve .` to hide this advice."
);
@@ -164,7 +167,7 @@ fn main() {
path = Color::Yellow.paint(path_string).bold(),
addresses = addresses,
);
- println!("Quit by pressing CTRL-C");
+ println!("\nQuit by pressing CTRL-C");
let _ = sys.run();
}
diff --git a/src/renderer.rs b/src/renderer.rs
index 89a9248..66fc714 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -3,6 +3,7 @@ use chrono_humanize::{Accuracy, HumanTime, Tense};
use maud::{html, Markup, PreEscaped, DOCTYPE};
use std::time::SystemTime;
+use crate::archive;
use crate::listing;
/// Renders the file listing
@@ -18,7 +19,10 @@ pub fn page(
(page_header(page_title))
body {
span #top { }
- h1 { (page_title) }
+ h1.title { (page_title) }
+ div.download {
+ (archive_button(archive::CompressionMethod::TarGz))
+ }
table {
thead {
th { (build_link("name", "Name", &sort_method, &sort_order)) }
@@ -50,6 +54,18 @@ pub fn page(
}
}
+/// Partial: archive button
+fn archive_button(compress_method: archive::CompressionMethod) -> Markup {
+ let link = format!("?download={}", compress_method.to_string());
+ let text = format!("Download .{}", compress_method.extension());
+
+ html! {
+ a href=(link) {
+ (text)
+ }
+ }
+}
+
/// Partial: table header link
fn build_link(
name: &str,
@@ -166,6 +182,9 @@ fn css() -> Markup {
margin: 0;
padding: 0;
}
+ h1 {
+ font-size: 1.5rem;
+ }
table {
margin-top: 2rem;
width: 100%;
@@ -259,6 +278,27 @@ fn css() -> Markup {
color: #3498db;
text-decoration: none;
}
+ .download {
+ display: flex;
+ flex-wrap: wrap;
+ margin-top: .5rem;
+ padding: 0.125rem;
+ }
+ .download a, .download a:visited {
+ color: #3498db;
+ }
+ .download a {
+ background: #efefef;
+ padding: 0.5rem;
+ border-radius: 0.2rem;
+ margin-top: 1rem;
+ }
+ .download a:hover {
+ background: #deeef7a6;
+ }
+ .download a:not(:last-of-type) {
+ margin-right: 1rem;
+ }
@media (max-width: 600px) {
h1 {
font-size: 1.375em;