archive.rs

  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}