243 lines · 9.0 KB · Rust
RawDownload
use std::path::Path; use anyhow::{Context as _, Result};use async_zip::base::read;use futures::{AsyncRead, io::BufReader}; #[cfg(windows)]pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) -> Result<()> {    let mut reader = read::stream::ZipFileReader::new(BufReader::new(reader));     let destination = &destination        .canonicalize()        .unwrap_or_else(|_| destination.to_path_buf());     while let Some(mut item) = reader.next_with_entry().await? {        let entry_reader = item.reader_mut();        let entry = entry_reader.entry();        let path = destination.join(            entry                .filename()                .as_str()                .context("reading zip entry file name")?,        );         if entry            .dir()            .with_context(|| format!("reading zip entry metadata for path {path:?}"))?        {            std::fs::create_dir_all(&path)                .with_context(|| format!("creating directory {path:?}"))?;        } else {            let parent_dir = path                .parent()                .with_context(|| format!("no parent directory for {path:?}"))?;            std::fs::create_dir_all(parent_dir)                .with_context(|| format!("creating parent directory {parent_dir:?}"))?;            let mut file = smol::fs::File::create(&path)                .await                .with_context(|| format!("creating file {path:?}"))?;            futures::io::copy(entry_reader, &mut file)                .await                .with_context(|| format!("extracting into file {path:?}"))?;        }         reader = item.skip().await.context("reading next zip entry")?;    }     Ok(())} #[cfg(not(windows))]pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) -> Result<()> {    // Unix needs file permissions copied when extracting.    // This is only possible to do when a reader impls `AsyncSeek` and `seek::ZipFileReader` is used.    // `stream::ZipFileReader` also has the `unix_permissions` method, but it will always return `Some(0)`.    //    // A typical `reader` comes from a streaming network response, so cannot be sought right away,    // and reading the entire archive into the memory seems wasteful.    //    // So, save the stream into a temporary file first and then get it read with a seeking reader.    let mut file = async_fs::File::from(tempfile::tempfile().context("creating a temporary file")?);    futures::io::copy(&mut BufReader::new(reader), &mut file)        .await        .context("saving archive contents into the temporary file")?;    let mut reader = read::seek::ZipFileReader::new(BufReader::new(file))        .await        .context("reading the zip archive")?;    let destination = &destination        .canonicalize()        .unwrap_or_else(|_| destination.to_path_buf());    for (i, entry) in reader.file().entries().to_vec().into_iter().enumerate() {        let path = destination.join(            entry                .filename()                .as_str()                .context("reading zip entry file name")?,        );         if entry            .dir()            .with_context(|| format!("reading zip entry metadata for path {path:?}"))?        {            std::fs::create_dir_all(&path)                .with_context(|| format!("creating directory {path:?}"))?;        } else {            let parent_dir = path                .parent()                .with_context(|| format!("no parent directory for {path:?}"))?;            std::fs::create_dir_all(parent_dir)                .with_context(|| format!("creating parent directory {parent_dir:?}"))?;            let mut file = smol::fs::File::create(&path)                .await                .with_context(|| format!("creating file {path:?}"))?;            let mut entry_reader = reader                .reader_with_entry(i)                .await                .with_context(|| format!("reading entry for path {path:?}"))?;            futures::io::copy(&mut entry_reader, &mut file)                .await                .with_context(|| format!("extracting into file {path:?}"))?;             if let Some(perms) = entry.unix_permissions() {                use std::os::unix::fs::PermissionsExt;                let permissions = std::fs::Permissions::from_mode(u32::from(perms));                file.set_permissions(permissions)                    .await                    .with_context(|| format!("setting permissions for file {path:?}"))?;            }        }    }     Ok(())} #[cfg(test)]mod tests {    use async_zip::ZipEntryBuilder;    use async_zip::base::write::ZipFileWriter;    use futures::{AsyncSeek, AsyncWriteExt};    use smol::io::Cursor;    use tempfile::TempDir;     use super::*;     async fn compress_zip(src_dir: &Path, dst: &Path) -> Result<()> {        let mut out = smol::fs::File::create(dst).await?;        let mut writer = ZipFileWriter::new(&mut out);         for entry in walkdir::WalkDir::new(src_dir) {            let entry = entry?;            let path = entry.path();             if path.is_dir() {                continue;            }             let relative_path = path.strip_prefix(src_dir)?;            let data = smol::fs::read(&path).await?;             let filename = relative_path.display().to_string();             #[cfg(unix)]            {                let mut builder =                    ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);                use std::os::unix::fs::PermissionsExt;                let metadata = std::fs::metadata(&path)?;                let perms = metadata.permissions().mode() as u16;                builder = builder.unix_permissions(perms);                writer.write_entry_whole(builder, &data).await?;            }            #[cfg(not(unix))]            {                let builder =                    ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);                writer.write_entry_whole(builder, &data).await?;            }        }         writer.close().await?;        out.flush().await?;         Ok(())    }     #[track_caller]    fn assert_file_content(path: &Path, content: &str) {        assert!(path.exists(), "file not found: {:?}", path);        let actual = std::fs::read_to_string(path).unwrap();        assert_eq!(actual, content);    }     #[track_caller]    fn make_test_data() -> TempDir {        let dir = tempfile::tempdir().unwrap();        let dst = dir.path();         std::fs::write(dst.join("test"), "Hello world.").unwrap();        std::fs::create_dir_all(dst.join("foo/bar")).unwrap();        std::fs::write(dst.join("foo/bar.txt"), "Foo bar.").unwrap();        std::fs::write(dst.join("foo/dar.md"), "Bar dar.").unwrap();        std::fs::write(dst.join("foo/bar/dar你好.txt"), "你好世界").unwrap();         dir    }     async fn read_archive(path: &Path) -> impl AsyncRead + AsyncSeek + Unpin {        let data = smol::fs::read(&path).await.unwrap();        Cursor::new(data)    }     #[test]    fn test_extract_zip() {        let test_dir = make_test_data();        let zip_file = test_dir.path().join("test.zip");         smol::block_on(async {            compress_zip(test_dir.path(), &zip_file).await.unwrap();            let reader = read_archive(&zip_file).await;             let dir = tempfile::tempdir().unwrap();            let dst = dir.path();            extract_zip(dst, reader).await.unwrap();             assert_file_content(&dst.join("test"), "Hello world.");            assert_file_content(&dst.join("foo/bar.txt"), "Foo bar.");            assert_file_content(&dst.join("foo/dar.md"), "Bar dar.");            assert_file_content(&dst.join("foo/bar/dar你好.txt"), "你好世界");        });    }     #[cfg(unix)]    #[test]    fn test_extract_zip_preserves_executable_permissions() {        use std::os::unix::fs::PermissionsExt;         smol::block_on(async {            let test_dir = tempfile::tempdir().unwrap();            let executable_path = test_dir.path().join("my_script");             // Create an executable file            std::fs::write(&executable_path, "#!/bin/bash\necho 'Hello'").unwrap();            let mut perms = std::fs::metadata(&executable_path).unwrap().permissions();            perms.set_mode(0o755); // rwxr-xr-x            std::fs::set_permissions(&executable_path, perms).unwrap();             // Create zip            let zip_file = test_dir.path().join("test.zip");            compress_zip(test_dir.path(), &zip_file).await.unwrap();             // Extract to new location            let extract_dir = tempfile::tempdir().unwrap();            let reader = read_archive(&zip_file).await;            extract_zip(extract_dir.path(), reader).await.unwrap();             // Check permissions are preserved            let extracted_path = extract_dir.path().join("my_script");            assert!(extracted_path.exists());            let extracted_perms = std::fs::metadata(&extracted_path).unwrap().permissions();            assert_eq!(extracted_perms.mode() & 0o777, 0o755);        });    }}