feat: download search results as a ZIP file

pull/567/head
Mateusz Manowiecki 2025-04-06 10:43:30 +02:00
parent 4fbdec2878
commit 456fa004dc
2 changed files with 131 additions and 4 deletions

View File

@ -348,6 +348,11 @@ async function setupIndexPage() {
$download.href = baseUrl() + "?zip";
$download.title = "Download folder as a .zip file";
$download.classList.remove("hidden");
if (PARAMS.q) {
$download.href += "&q=" + PARAMS.q;
$download.title = "Download search results as a .zip file";
}
}
if (DATA.allow_upload) {

View File

@ -267,8 +267,14 @@ impl Server {
status_not_found(&mut res);
return Ok(res);
}
self.handle_zip_dir(path, head_only, access_paths, &mut res)
.await?;
if allow_search && query_params.contains_key("q") {
self.handle_search_zip(path, &query_params, head_only, access_paths, &mut res)
.await?;
} else {
self.handle_zip_dir(path, head_only, access_paths, &mut res)
.await?;
}
} else if allow_search && query_params.contains_key("q") {
self.handle_search_dir(
path,
@ -307,8 +313,14 @@ impl Server {
status_not_found(&mut res);
return Ok(res);
}
self.handle_zip_dir(path, head_only, access_paths, &mut res)
.await?;
if allow_search && query_params.contains_key("q") {
self.handle_search_zip(path, &query_params, head_only, access_paths, &mut res)
.await?;
} else {
self.handle_zip_dir(path, head_only, access_paths, &mut res)
.await?;
}
} else if allow_search && query_params.contains_key("q") {
self.handle_search_dir(
path,
@ -684,6 +696,116 @@ impl Server {
Ok(())
}
async fn handle_search_zip(
&self,
path: &Path,
query_params: &HashMap<String, String>,
head_only: bool,
access_paths: AccessPaths,
res: &mut Response,
) -> Result<()> {
let search = query_params
.get("q")
.ok_or_else(|| anyhow!("missing 'q' parameter"))?
.to_lowercase();
if search.is_empty() {
return Err(anyhow!("search term is empty"));
}
let (mut writer, reader) = tokio::io::duplex(BUF_SIZE);
let safe_name = search.replace(|c: char| !c.is_ascii_alphanumeric(), "_");
let filename = format!("search-{}.zip", safe_name);
set_content_disposition(res, false, &filename)?;
res.headers_mut()
.insert("content-type", HeaderValue::from_static("application/zip"));
if head_only {
return Ok(());
}
let path = path.to_owned();
let hidden = self.args.hidden.clone();
let running = self.running.clone();
let compression = self.args.compress.to_compression();
let access_paths = access_paths.clone();
let search = search.to_lowercase();
tokio::spawn(async move {
let mut zip_writer = ZipFileWriter::with_tokio(&mut writer);
let hidden = Arc::new(hidden);
let path_clone = path.clone();
let search_paths = tokio::task::spawn_blocking(move || {
let mut paths = vec![];
for dir in access_paths.entry_paths(&path_clone) {
let mut it = WalkDir::new(&dir).into_iter();
it.next();
while let Some(Ok(entry)) = it.next() {
if !running.load(atomic::Ordering::SeqCst) {
break;
}
let entry_path = entry.path();
let base_name = get_file_name(entry_path);
let file_type = entry.file_type();
if is_hidden(&hidden, base_name, file_type.is_dir()) {
if file_type.is_dir() {
it.skip_current_dir();
}
continue;
}
if !file_type.is_file() {
continue;
}
if base_name.to_lowercase().contains(&search) {
paths.push(entry_path.to_path_buf());
}
}
}
paths
})
.await;
let paths = match search_paths {
Ok(p) => p,
Err(e) => {
error!("Search failed: {}", e);
return Ok::<(), anyhow::Error>(());
}
};
for zip_path in paths {
let filename = match zip_path.strip_prefix(&path).ok().and_then(|v| v.to_str()) {
Some(v) => v,
None => continue,
};
let (datetime, mode) = get_file_mtime_and_mode(&zip_path).await?;
let builder = ZipEntryBuilder::new(filename.into(), compression)
.unix_permissions(mode)
.last_modification_date(ZipDateTime::from_chrono(&datetime));
let mut file = File::open(&zip_path).await?;
let mut file_writer = zip_writer.write_entry_stream(builder).await?.compat_write();
io::copy(&mut file, &mut file_writer).await?;
file_writer.into_inner().close().await?;
}
let _ = zip_writer.close().await;
Ok(())
});
let reader_stream = ReaderStream::with_capacity(reader, BUF_SIZE);
let stream_body = StreamBody::new(
reader_stream
.map_ok(Frame::data)
.map_err(|err| anyhow!("{err}")),
);
*res.body_mut() = stream_body.boxed();
Ok(())
}
async fn handle_render_index(
&self,
path: &Path,