#[derive(Clone, Default)] pub struct Archive {} use std::{ io::{Bytes, Cursor}, path::Path, }; use anyhow::Context; use super::file_reader::Files; impl Archive { pub fn new() -> Self { Self {} } pub async fn create_archive(&self, prefix: &Path, files: Files) -> anyhow::Result { tracing::trace!("archiving files: {}", files.len()); let buffer = Vec::new(); let cursor = Cursor::new(buffer); let mut tar_builder = tar::Builder::new(cursor); for file in files { let abs_file_path = file.path; let mut fd = std::fs::File::open(&abs_file_path)?; if let Some(rel) = file.relative { tracing::trace!("archiving rel file: {}", rel.display()); tar_builder.append_file(&rel, &mut fd)?; } else { tracing::trace!("archiving file: {}", abs_file_path.display()); tar_builder.append_file( abs_file_path .strip_prefix(prefix) .context("failed to strip prefix from path")?, &mut fd, )?; } } tar_builder.finish()?; let cursor = tar_builder.into_inner()?; let buffer = cursor.into_inner(); Ok(buffer.into()) } pub async fn unpack_archive(&self, archive: &ArchiveFile, dest: &Path) -> anyhow::Result<()> { tracing::trace!("unpacking archive: {}", dest.display()); let cursor = Cursor::new(archive.content.clone()); let mut arc = tar::Archive::new(cursor); arc.unpack(dest)?; Ok(()) } } pub struct ArchiveFile { pub content: Vec, } impl From> for ArchiveFile { fn from(value: Vec) -> Self { Self { content: value } } } pub mod extensions { use std::{env::temp_dir, path::PathBuf}; use async_trait::async_trait; use tokio::io::AsyncWriteExt; use uuid::Uuid; use crate::app::SharedLocalApp; use crate::{app::SharedApp, services::release_manager::models::ArtifactID}; use crate::services::file_store::FileStore; use super::Archive; use super::ArchiveFile; pub trait ArchiveExt { fn archive(&self) -> Archive; } impl ArchiveExt for SharedApp { fn archive(&self) -> Archive { Archive::new() } } impl ArchiveExt for SharedLocalApp { fn archive(&self) -> Archive { Archive::new() } } #[async_trait] pub trait ArchiveUploadExt { async fn upload_archive( &self, artifact_id: ArtifactID, archive: ArchiveFile, ) -> anyhow::Result<()>; } #[async_trait] impl ArchiveUploadExt for FileStore { async fn upload_archive( &self, artifact_id: ArtifactID, archive: ArchiveFile, ) -> anyhow::Result<()> { tracing::trace!("uploading archive: {}", artifact_id.to_string()); let suffix = Uuid::new_v4(); let temp_root = temp_dir(); let archive_path = temp_root .join("flux-releaser") .join("archives") .join(format!("{}-{}.tar", &artifact_id.to_string(), suffix)); let archive_file_guard = ArchiveFilePath(archive_path.clone()); tokio::fs::create_dir_all(archive_path.parent().unwrap()).await?; { let mut archive_file = tokio::fs::File::create(&archive_path).await?; archive_file.write_all(&archive.content).await?; archive_file.flush().await?; } self.upload_file(artifact_id, archive_path).await?; drop(archive_file_guard); Ok(()) } } pub struct ArchiveFilePath(PathBuf); // make sure we delete the archive file when we're done with it, we don't want it sticking around longer than it needs to impl Drop for ArchiveFilePath { fn drop(&mut self) { let file_path = self.0.clone(); tokio::spawn(async move { if file_path.exists() { tracing::trace!("deleting archive: {}", file_path.display()); if let Err(e) = tokio::fs::remove_file(&file_path).await { tracing::error!("failed to delete archive: {}", e); } } }); } } }