1use std::path::Path;
2
3use anyhow::Result;
4use async_zip::base::read::stream::ZipFileReader;
5use futures::{AsyncRead, io::BufReader};
6
7pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) -> Result<()> {
8 let mut reader = ZipFileReader::new(BufReader::new(reader));
9
10 let destination = &destination
11 .canonicalize()
12 .unwrap_or_else(|_| destination.to_path_buf());
13
14 while let Some(mut item) = reader.next_with_entry().await? {
15 let entry_reader = item.reader_mut();
16 let entry = entry_reader.entry();
17 let path = destination.join(entry.filename().as_str().unwrap());
18
19 if entry.dir().unwrap() {
20 std::fs::create_dir_all(&path)?;
21 } else {
22 let parent_dir = path.parent().expect("failed to get parent directory");
23 std::fs::create_dir_all(parent_dir)?;
24 let mut file = smol::fs::File::create(&path).await?;
25 futures::io::copy(entry_reader, &mut file).await?;
26 }
27
28 reader = item.skip().await?;
29 }
30
31 Ok(())
32}
33
34#[cfg(test)]
35mod tests {
36 use std::path::PathBuf;
37
38 use async_zip::ZipEntryBuilder;
39 use async_zip::base::write::ZipFileWriter;
40 use futures::AsyncWriteExt;
41 use smol::io::Cursor;
42 use tempfile::TempDir;
43
44 use super::*;
45
46 async fn compress_zip(src_dir: &Path, dst: &Path) -> Result<()> {
47 let mut out = smol::fs::File::create(dst).await?;
48 let mut writer = ZipFileWriter::new(&mut out);
49
50 for entry in walkdir::WalkDir::new(src_dir) {
51 let entry = entry?;
52 let path = entry.path();
53
54 if path.is_dir() {
55 continue;
56 }
57
58 let relative_path = path.strip_prefix(src_dir)?;
59 let data = smol::fs::read(&path).await?;
60
61 let filename = relative_path.display().to_string();
62 let builder = ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);
63
64 writer.write_entry_whole(builder, &data).await?;
65 }
66
67 writer.close().await?;
68 out.flush().await?;
69
70 Ok(())
71 }
72
73 #[track_caller]
74 fn assert_file_content(path: &Path, content: &str) {
75 assert!(path.exists(), "file not found: {:?}", path);
76 let actual = std::fs::read_to_string(path).unwrap();
77 assert_eq!(actual, content);
78 }
79
80 #[track_caller]
81 fn make_test_data() -> TempDir {
82 let dir = tempfile::tempdir().unwrap();
83 let dst = dir.path();
84
85 std::fs::write(dst.join("test"), "Hello world.").unwrap();
86 std::fs::create_dir_all(dst.join("foo/bar")).unwrap();
87 std::fs::write(dst.join("foo/bar.txt"), "Foo bar.").unwrap();
88 std::fs::write(dst.join("foo/dar.md"), "Bar dar.").unwrap();
89 std::fs::write(dst.join("foo/bar/dar你好.txt"), "你好世界").unwrap();
90
91 dir
92 }
93
94 async fn read_archive(path: &PathBuf) -> impl AsyncRead + Unpin {
95 let data = smol::fs::read(&path).await.unwrap();
96 Cursor::new(data)
97 }
98
99 #[test]
100 fn test_extract_zip() {
101 let test_dir = make_test_data();
102 let zip_file = test_dir.path().join("test.zip");
103
104 smol::block_on(async {
105 compress_zip(test_dir.path(), &zip_file).await.unwrap();
106 let reader = read_archive(&zip_file).await;
107
108 let dir = tempfile::tempdir().unwrap();
109 let dst = dir.path();
110 extract_zip(dst, reader).await.unwrap();
111
112 assert_file_content(&dst.join("test"), "Hello world.");
113 assert_file_content(&dst.join("foo/bar.txt"), "Foo bar.");
114 assert_file_content(&dst.join("foo/dar.md"), "Bar dar.");
115 assert_file_content(&dst.join("foo/bar/dar你好.txt"), "你好世界");
116 });
117 }
118}