Skip to content
This repository has been archived by the owner on Mar 27, 2023. It is now read-only.

Download directory as zip #50

Merged
merged 8 commits into from
Sep 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
859 changes: 450 additions & 409 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ serde = { version = "1.0", features = ["derive"] } # For tera serializing variab
ignore = "0.4" # Respect to .gitignore while listing directories.
# Logging
chrono = "0.4"
# Directory Download
qstring = "0.7"
zip = { version = "0.5", default-features = false, features = ["deflate"] }

[dev-dependencies]
tempdir = "0.3"
Expand Down
3 changes: 3 additions & 0 deletions src/server/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
{% endif %}
<span class="separator">/</span>
{% endfor %}
<a href="?action=zip" title="Download folder as a .zip file">
<svg height="16" width="16" viewBox="0 0 24 24" ><path fill-rule="evenodd" d="M20.54 5.23l-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6.5c0-.48-.17-.93-.46-1.27zM12 17.5L6.5 12H10v-2h4v2h3.5L12 17.5zM5.12 5l.81-1h12l.94 1H5.12z"></path></svg>
</a>
</div>
<ul>
{% for file in files %}
Expand Down
98 changes: 90 additions & 8 deletions src/server/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@

use std::convert::AsRef;
use std::fs::File;
use std::io::{self, BufReader};
use std::io::{self, BufReader, Read, Seek, SeekFrom};
use std::path::Path;

use ignore::WalkBuilder;
use serde::Serialize;
use tera::{Context, Tera};
use zip::ZipWriter;

use crate::extensions::PathExt;
use crate::server::PathType;
Expand All @@ -34,6 +35,21 @@ struct Breadcrumb<'a> {
path: String,
}

/// Walking inside a directory recursively
fn get_dir_contents<P: AsRef<Path>>(
dir_path: P,
with_ignore: bool,
show_all: bool,
depth: Option<usize>,
) -> ignore::Walk {
WalkBuilder::new(dir_path)
.standard_filters(false) // Disable all standard filters.
.git_ignore(with_ignore)
.hidden(!show_all) // Filter out hidden entries on demand.
.max_depth(depth) // Do not traverse subpaths.
.build()
}

/// Send a HTML page of all files under the path.
///
/// # Parameters
Expand All @@ -59,12 +75,7 @@ pub fn send_dir<P1: AsRef<Path>, P2: AsRef<Path>>(
let breadcrumbs = create_breadcrumbs(dir_path, base_path, prefix);

// Collect filename and there links.
let files_iter = WalkBuilder::new(dir_path)
.standard_filters(false) // Disable all standard filters.
.git_ignore(with_ignore)
.hidden(!show_all) // Filter out hidden entries on demand.
.max_depth(Some(1)) // Do not traverse subpaths.
.build()
let files_iter = get_dir_contents(dir_path, with_ignore, show_all, Some(1))
.filter_map(|entry| entry.ok())
.filter(|entry| dir_path != entry.path()) // Exclude `.`
.map(|entry| {
Expand All @@ -86,6 +97,7 @@ pub fn send_dir<P1: AsRef<Path>, P2: AsRef<Path>>(
} else {
// CWD == sub dir of base dir
// Append an item for popping back to parent directory.

let path = format!(
"{}/{}",
prefix,
Expand All @@ -97,6 +109,7 @@ pub fn send_dir<P1: AsRef<Path>, P2: AsRef<Path>>(
.to_str()
.unwrap()
);

vec![Item {
name: "..".to_owned(),
path,
Expand All @@ -118,9 +131,59 @@ pub fn send_file<P: AsRef<Path>>(file_path: P) -> io::Result<Vec<u8>> {
let f = File::open(file_path)?;
let mut buffer = Vec::new();
BufReader::new(f).read_to_end(&mut buffer)?;

Ok(buffer)
}

/// Sending a directory as zip buffer
pub fn send_dir_as_zip<P: AsRef<Path>>(
dir_path: P,
show_all: bool,
with_ignore: bool,
) -> io::Result<Vec<u8>> {
let dir_path = dir_path.as_ref();

// Creating a memory buffer to make zip file
let mut zip_buffer = Vec::new();
let cursor = std::io::Cursor::new(&mut zip_buffer);

let mut zip_writer = ZipWriter::new(cursor);
let zip_options = zip::write::FileOptions::default()
.compression_method(zip::CompressionMethod::Stored)
.unix_permissions(0o755);
whizsid marked this conversation as resolved.
Show resolved Hide resolved

// Recursively finding files and directories
let files_iter = get_dir_contents(dir_path, with_ignore, show_all, None)
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path() != dir_path);

for dir_entry in files_iter {
let file_path = dir_entry.path();
let name = file_path.strip_prefix(dir_path).unwrap().to_str().unwrap();

if file_path.is_dir() {
zip_writer
.add_directory(name, zip_options)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
whizsid marked this conversation as resolved.
Show resolved Hide resolved
} else {
zip_writer
.start_file(name, zip_options)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let mut file = File::open(file_path)?;

std::io::copy(&mut file, &mut zip_writer)?;
}
}

let mut zip = zip_writer
.finish()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;

zip.seek(SeekFrom::Start(0))?;

zip.bytes().collect()
}

/// Send a buffer with specific range.
///
/// # Parameters
Expand All @@ -132,7 +195,6 @@ pub fn send_file_with_range<P: AsRef<Path>>(
range: (u64, u64),
) -> io::Result<Vec<u8>> {
use std::io::prelude::*;
use std::io::SeekFrom;
let (start, end) = range; // TODO: should return HTTP 416
if end < start {
return Err(io::Error::from(io::ErrorKind::InvalidInput));
Expand Down Expand Up @@ -282,6 +344,12 @@ mod t_send {
path
}

fn dir_with_sub_dir_path() -> std::path::PathBuf {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("./tests/dir_with_sub_dirs/");
path
}

fn missing_file_path() -> std::path::PathBuf {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("./missing/file");
Expand Down Expand Up @@ -338,4 +406,18 @@ mod t_send {
let buf = send_file_with_range(file_txt_path(), (1, 0));
assert_eq!(buf.unwrap_err().kind(), std::io::ErrorKind::InvalidInput);
}

#[test]
fn t_send_dir_as_zip() {
let buf = send_dir_as_zip(dir_with_sub_dir_path(), true, false);

assert_eq!(buf.is_ok(), true);

let buf = buf.unwrap();

assert_eq!(buf.len() > 0, true);

// https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html#localheader
assert_eq!(&buf[0..4], &[0x50, 0x4b, 0x03, 0x04]);
}
}
Loading