Browse Source

dadk支持预编译源+代码源支持从Archive导入 (#10)

* 基本完成了DADK的预编译源与代码源的archive代码,暂时只支持tar.gz的代码以及同名文件

* 通过cd *来匹配解压后的文件,暂时解决了解压后名字与压缩包名字不同的问题

* 通过ArichiveType类型枚举和正则表达式完成了重构,支持灵活添加对不同类型的压缩文件的支持

* dadk支持预编译源+代码源支持从Archive导入

* dadk支持预编译源+代码源支持从Archive导入

* 如果taskname 包括空格就修改为下划线

* 修改了一部分系统指令为rust代码

* 将除了git外的命令行指令替换为了rust库实现,修复了一些bug,封装了FileUtils

* 修改了一处unwarp为expect。下载文件增加超时检测

* 修改了一处unwarp为expect。下载文件增加超时检测

* 将bash tar改为直接tar调用
Chiichen 1 year ago
parent
commit
674005c978
12 changed files with 333 additions and 89 deletions
  1. 4 1
      .gitignore
  2. 4 2
      Cargo.toml
  3. 5 5
      src/console/mod.rs
  4. 11 4
      src/executor/cache.rs
  5. 39 43
      src/executor/mod.rs
  6. 180 8
      src/executor/source.rs
  7. 22 22
      src/main.rs
  8. 1 1
      src/parser/mod.rs
  9. 5 2
      src/parser/task.rs
  10. 1 1
      src/scheduler/mod.rs
  11. 60 0
      src/utils/file.rs
  12. 1 0
      src/utils/mod.rs

+ 4 - 1
.gitignore

@@ -9,4 +9,7 @@ Cargo.lock
 # These are backup files generated by rustfmt
 **/*.rs.bk
 
-/run/
+/run/
+
+/bin
+/user

+ 4 - 2
Cargo.toml

@@ -1,7 +1,7 @@
 [package]
 name = "dadk"
-authors = ["longjin <longjin@DragonOS.org>"]
-version = "0.1.1"
+authors = ["longjin <longjin@DragonOS.org>", "chikejian <chikejian@DragonOS.org>"]
+version = "0.1.2"
 edition = "2021"
 description = "DragonOS Application Development Kit\nDragonOS应用开发工具"
 license = "GPL-2.0-only"
@@ -19,7 +19,9 @@ doc = true
 clap = { version = "4.2.4", features = ["derive"] }
 lazy_static = "1.4.0"
 log = "0.4.17"
+regex = "1.9.1"
 reqwest = { version = "0.11", features = ["blocking", "json"] }
 serde = { version = "1.0.160", features = ["serde_derive"] }
 serde_json = "1.0.96"
 simple_logger = "4.1.0"
+zip = "0.6"

+ 5 - 5
src/console/mod.rs

@@ -1,15 +1,15 @@
 //! # DADK控制台
-//! 
+//!
 //! DADK控制台能够让用户通过命令行交互的方式使用DADK。
-//! 
+//!
 //! ## 创建配置文件
-//! 
+//!
 //! DADK控制台提供了一个命令,用于创建一个配置文件。您可以通过以下命令创建一个配置文件:
-//! 
+//!
 //! ```bash
 //! dadk new
 //! ```
-//! 
+//!
 
 pub mod clean;
 pub mod elements;

+ 11 - 4
src/executor/cache.rs

@@ -132,12 +132,20 @@ impl CacheDir {
 
     pub fn build_dir_env_key(entity: &Rc<SchedEntity>) -> Result<String, ExecutorError> {
         let name_version_env = entity.task().name_version_env();
-        return Ok(format!("{}_{}", Self::DADK_BUILD_CACHE_DIR_ENV_KEY_PREFIX,name_version_env));
+        return Ok(format!(
+            "{}_{}",
+            Self::DADK_BUILD_CACHE_DIR_ENV_KEY_PREFIX,
+            name_version_env
+        ));
     }
 
     pub fn source_dir_env_key(entity: &Rc<SchedEntity>) -> Result<String, ExecutorError> {
         let name_version_env = entity.task().name_version_env();
-        return Ok(format!("{}_{}", Self::DADK_SOURCE_CACHE_DIR_ENV_KEY_PREFIX,name_version_env));
+        return Ok(format!(
+            "{}_{}",
+            Self::DADK_SOURCE_CACHE_DIR_ENV_KEY_PREFIX,
+            name_version_env
+        ));
     }
 
     pub fn need_source_cache(entity: &Rc<SchedEntity>) -> bool {
@@ -154,11 +162,10 @@ impl CacheDir {
             }
         } else if let TaskType::InstallFromPrebuilt(ps) = task_type {
             match ps {
-                crate::parser::task::PrebuiltSource::Archive(_) => return true,
+                crate::parser::task::PrebuiltSource::Archive(_) => return false,
                 crate::parser::task::PrebuiltSource::Local(_) => return false,
             }
         }
-
         unimplemented!("Not fully implemented task type: {:?}", task_type);
     }
 

+ 39 - 43
src/executor/mod.rs

@@ -7,14 +7,14 @@ use std::{
     sync::RwLock,
 };
 
-use log::{error, info, warn, debug};
+use log::{debug, error, info, warn};
 
 use crate::{
     console::{clean::CleanLevel, Action},
     executor::cache::CacheDir,
     parser::task::{CodeSource, PrebuiltSource, TaskEnv, TaskType},
     scheduler::{SchedEntities, SchedEntity},
-    utils::stdio::StdioUtils,
+    utils::file::FileUtils,
 };
 
 use self::cache::CacheDirType;
@@ -107,7 +107,11 @@ impl Executor {
                 // 清理构建结果
                 let r = self.clean();
                 if let Err(e) = r {
-                    error!("Failed to clean task {}: {:?}", self.entity.task().name_version(), e);
+                    error!(
+                        "Failed to clean task {}: {:?}",
+                        self.entity.task().name_version(),
+                        e
+                    );
                 }
             }
             _ => {
@@ -165,36 +169,8 @@ impl Executor {
 
         // 拷贝构建结果到安装路径
         let build_dir: PathBuf = self.build_dir.path.clone();
-
-        let cmd = Command::new("cp")
-            .arg("-r")
-            .arg(build_dir.to_string_lossy().to_string() + "/.")
-            .arg(install_path)
-            .stdout(Stdio::null())
-            .stderr(Stdio::piped())
-            .spawn()
-            .map_err(|e| {
-                ExecutorError::InstallError(format!(
-                    "Failed to install, error message: {}",
-                    e.to_string()
-                ))
-            })?;
-
-        let output = cmd.wait_with_output().map_err(|e| {
-            ExecutorError::InstallError(format!(
-                "Failed to install, error message: {}",
-                e.to_string()
-            ))
-        })?;
-
-        if !output.status.success() {
-            let err_msg = StdioUtils::tail_n_str(StdioUtils::stderr_to_lines(&output.stderr), 10);
-            return Err(ExecutorError::InstallError(format!(
-                "Failed to install, error message: {}",
-                err_msg
-            )));
-        }
-
+        FileUtils::copy_dir_all(&build_dir, &install_path)
+            .map_err(|e| ExecutorError::InstallError(e))?;
         info!("Task {} installed.", self.entity.task().name_version());
 
         return Ok(());
@@ -292,7 +268,6 @@ impl Executor {
         if let Some(local_path) = self.entity.task().source_path() {
             return local_path;
         }
-
         return self.source_dir.as_ref().unwrap().path.clone();
     }
 
@@ -308,7 +283,15 @@ impl Executor {
                     self.action
                 ),
             },
-            _ => None,
+
+            TaskType::InstallFromPrebuilt(_) => match self.action {
+                Action::Build => self.entity.task().build.build_command.clone(),
+                Action::Clean(_) => self.entity.task().clean.clean_command.clone(),
+                _ => unimplemented!(
+                    "create_command: Action {:?} not supported yet.",
+                    self.action
+                ),
+            },
         };
 
         if raw_cmd.is_none() {
@@ -359,14 +342,13 @@ impl Executor {
 
     fn prepare_input(&self) -> Result<(), ExecutorError> {
         // 拉取源文件
-        if self.source_dir.is_none() {
-            return Ok(());
-        }
         let task = self.entity.task();
-        let source_dir = self.source_dir.as_ref().unwrap();
-
         match &task.task_type {
             TaskType::BuildFromSource(cs) => {
+                if self.source_dir.is_none() {
+                    return Ok(());
+                }
+                let source_dir = self.source_dir.as_ref().unwrap();
                 match cs {
                     CodeSource::Git(git) => {
                         git.prepare(source_dir)
@@ -375,15 +357,29 @@ impl Executor {
                     // 本地源文件,不需要拉取
                     CodeSource::Local(_) => return Ok(()),
                     // 在线压缩包,需要下载
-                    CodeSource::Archive(_) => todo!(),
+                    CodeSource::Archive(archive) => {
+                        archive
+                            .download_unzip(source_dir)
+                            .map_err(|e| ExecutorError::PrepareEnvError(e))?;
+                    }
                 }
             }
             TaskType::InstallFromPrebuilt(pb) => {
                 match pb {
                     // 本地源文件,不需要拉取
-                    PrebuiltSource::Local(_) => return Ok(()),
+                    PrebuiltSource::Local(local_source) => {
+                        let local_path = local_source.path();
+                        let target_path = &self.build_dir.path;
+                        FileUtils::copy_dir_all(&local_path, &target_path)
+                            .map_err(|e| ExecutorError::TaskFailed(e))?; // let mut cmd = "cp -r ".to_string();
+                        return Ok(());
+                    }
                     // 在线压缩包,需要下载
-                    PrebuiltSource::Archive(_) => todo!(),
+                    PrebuiltSource::Archive(archive) => {
+                        archive
+                            .download_unzip(&self.build_dir)
+                            .map_err(|e| ExecutorError::PrepareEnvError(e))?;
+                    }
                 }
             }
         }

+ 180 - 8
src/executor/source.rs

@@ -1,13 +1,16 @@
+use log::info;
+use regex::Regex;
+use reqwest::Url;
+use serde::{Deserialize, Serialize};
+use std::os::unix::fs::PermissionsExt;
 use std::{
+    fs::File,
     path::PathBuf,
     process::{Command, Stdio},
 };
+use zip::ZipArchive;
 
-use log::info;
-use reqwest::Url;
-use serde::{Deserialize, Serialize};
-
-use crate::utils::stdio::StdioUtils;
+use crate::utils::{file::FileUtils, stdio::StdioUtils};
 
 use super::cache::CacheDir;
 
@@ -32,7 +35,6 @@ impl GitSource {
             revision,
         }
     }
-
     /// # 验证参数合法性
     ///
     /// 仅进行形式校验,不会检查Git仓库是否存在,以及分支是否存在、是否有权限访问等
@@ -218,7 +220,6 @@ impl GitSource {
         }
         return Ok(());
     }
-
     /// # 把浅克隆的仓库变成深克隆
     fn unshallow(&self, target_dir: &CacheDir) -> Result<(), String> {
         let mut cmd = Command::new("git");
@@ -356,7 +357,6 @@ impl ArchiveSource {
     pub fn new(url: String) -> Self {
         Self { url }
     }
-
     pub fn validate(&self) -> Result<(), String> {
         if self.url.is_empty() {
             return Err("url is empty".to_string());
@@ -376,4 +376,176 @@ impl ArchiveSource {
     pub fn trim(&mut self) {
         self.url = self.url.trim().to_string();
     }
+
+    /// @brief 下载压缩包并把其中的文件提取至target_dir目录下
+    ///
+    ///从URL中下载压缩包到临时文件夹 target_dir/DRAGONOS_ARCHIVE_TEMP 后
+    ///原地解压,提取文件后删除下载的压缩包。如果 target_dir 非空,就直接使用
+    ///其中内容,不进行重复下载和覆盖
+    ///
+    /// @param target_dir 文件缓存目录
+    ///
+    /// @return 根据结果返回OK或Err
+    pub fn download_unzip(&self, target_dir: &CacheDir) -> Result<(), String> {
+        let url = Url::parse(&self.url).unwrap();
+        let archive_name = url.path_segments().unwrap().last().unwrap();
+        let path = &(target_dir.path.join("DRAGONOS_ARCHIVE_TEMP"));
+        //如果source目录没有临时文件夹,且不为空,说明之前成功执行过一次,那么就直接使用之前的缓存
+        if !path.exists()
+            && !target_dir.is_empty().map_err(|e| {
+                format!(
+                    "Failed to check if target dir is empty: {}, message: {e:?}",
+                    target_dir.path.display()
+                )
+            })?
+        {
+            //如果source文件夹非空,就直接使用,不再重复下载压缩文件,这里可以考虑加入交互
+            info!("Source files already exist. Using previous source file cache. You should clean {:?} before re-download the archive ",target_dir);
+            return Ok(());
+        }
+
+        if path.exists() {
+            std::fs::remove_dir_all(path).map_err(|e| e.to_string())?;
+        }
+        //创建临时目录
+        std::fs::create_dir(path).map_err(|e| e.to_string())?;
+        info!("downloading {:?}", archive_name);
+        FileUtils::download_file(&self.url, path).map_err(|e| e.to_string())?;
+        //下载成功,开始尝试解压
+        info!("download {:?} finished, start unzip", archive_name);
+        let archive_file = ArchiveFile::new(&path.join(archive_name));
+        archive_file.unzip()?;
+        //删除创建的临时文件夹
+        std::fs::remove_dir_all(path).map_err(|e| e.to_string())?;
+        return Ok(());
+    }
+}
+
+pub struct ArchiveFile {
+    archive_path: PathBuf,
+    archive_name: String,
+    archive_type: ArchiveType,
+}
+
+impl ArchiveFile {
+    pub fn new(archive_path: &PathBuf) -> Self {
+        //匹配压缩文件类型
+        let archive_name = archive_path.file_name().unwrap().to_str().unwrap();
+        for (regex, archivetype) in [
+            (Regex::new(r"^(.+)\.tar\.gz$").unwrap(), ArchiveType::TarGz),
+            (Regex::new(r"^(.+)\.zip$").unwrap(), ArchiveType::Zip),
+        ] {
+            if regex.is_match(archive_name) {
+                return Self {
+                    archive_path: archive_path.parent().unwrap().to_path_buf(),
+                    archive_name: archive_name.to_string(),
+                    archive_type: archivetype,
+                };
+            }
+        }
+        Self {
+            archive_path: archive_path.parent().unwrap().to_path_buf(),
+            archive_name: archive_name.to_string(),
+            archive_type: ArchiveType::Undefined,
+        }
+    }
+
+    /// @brief 对self.archive_path路径下名为self.archive_name的压缩文件(tar.gz或zip)进行解压缩
+    ///
+    /// 在此函数中进行路径和文件名有效性的判断,如果有效的话就开始解压缩,根据ArchiveType枚举类型来
+    /// 生成不同的命令来对压缩文件进行解压缩,暂时只支持tar.gz和zip格式,并且都是通过调用bash来解压缩
+    /// 没有引入第三方rust库
+    ///
+    ///
+    /// @return 根据结果返回OK或Err
+
+    pub fn unzip(&self) -> Result<(), String> {
+        let path = &self.archive_path;
+        if !path.is_dir() {
+            return Err(format!("Archive directory {:?} is wrong", path));
+        }
+        if !path.join(&self.archive_name).is_file() {
+            return Err(format!(
+                " {:?} is not a file",
+                path.join(&self.archive_name)
+            ));
+        }
+        //根据压缩文件的类型生成cmd指令
+        match &self.archive_type {
+            ArchiveType::TarGz => {
+                let mut cmd = Command::new("tar -xzf");
+                cmd.arg(&self.archive_name);
+                let proc: std::process::Child = cmd
+                    .stderr(Stdio::piped())
+                    .stdout(Stdio::inherit())
+                    .spawn()
+                    .map_err(|e| e.to_string())?;
+                let output = proc.wait_with_output().map_err(|e| e.to_string())?;
+                if !output.status.success() {
+                    return Err(format!(
+                        "unzip failed, status: {:?},  stderr: {:?}",
+                        output.status,
+                        StdioUtils::tail_n_str(StdioUtils::stderr_to_lines(&output.stderr), 5)
+                    ));
+                }
+            }
+
+            ArchiveType::Zip => {
+                let file = File::open(&self.archive_path.join(&self.archive_name))
+                    .map_err(|e| e.to_string())?;
+                let mut archive = ZipArchive::new(file).map_err(|e| e.to_string())?;
+                for i in 0..archive.len() {
+                    let mut file = archive.by_index(i).map_err(|e| e.to_string())?;
+                    let outpath = match file.enclosed_name() {
+                        Some(path) => self.archive_path.join(path),
+                        None => continue,
+                    };
+                    if (*file.name()).ends_with('/') {
+                        std::fs::create_dir_all(&outpath).map_err(|e| e.to_string())?;
+                    } else {
+                        if let Some(p) = outpath.parent() {
+                            if !p.exists() {
+                                std::fs::create_dir_all(&p).map_err(|e| e.to_string())?;
+                            }
+                        }
+                        let mut outfile = File::create(&outpath).map_err(|e| e.to_string())?;
+                        std::io::copy(&mut file, &mut outfile).map_err(|e| e.to_string())?;
+                    }
+                    //设置解压后权限,在Linux中Unzip会丢失权限
+                    #[cfg(unix)]
+                    {
+                        if let Some(mode) = file.unix_mode() {
+                            std::fs::set_permissions(
+                                &outpath,
+                                std::fs::Permissions::from_mode(mode),
+                            )
+                            .map_err(|e| e.to_string())?;
+                        }
+                    }
+                }
+            }
+            _ => {
+                return Err("unsupported archive type".to_string());
+            }
+        }
+        //删除下载的压缩包
+        info!("unzip successfully, removing archive ");
+        std::fs::remove_file(path.join(&self.archive_name)).map_err(|e| e.to_string())?;
+        //从解压的文件夹中提取出文件并删除下载的压缩包等价于指令"cd *;mv ./* ../../"
+        for entry in path.read_dir().map_err(|e| e.to_string())? {
+            let entry = entry.map_err(|e| e.to_string())?;
+            let path = entry.path();
+            FileUtils::move_files(&path, &self.archive_path.parent().unwrap())
+                .map_err(|e| e.to_string())?;
+            //删除空的单独文件夹
+            std::fs::remove_dir_all(&path).map_err(|e| e.to_string())?;
+        }
+        return Ok(());
+    }
+}
+
+pub enum ArchiveType {
+    TarGz,
+    Zip,
+    Undefined,
 }

+ 22 - 22
src/main.rs

@@ -20,51 +20,51 @@
 //! ## License
 //!
 //! DADK is licensed under the [GPLv2 License](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html).
-//! 
+//!
 //! ## 快速开始
-//! 
+//!
 //! ### 安装DADK
-//! 
+//!
 //! DADK是一个Rust程序,您可以通过Cargo来安装DADK。
-//! 
+//!
 //! ```shell
 //! # 从GitHub安装最新版
 //! cargo install --git https://github.com/DragonOS-Community/DADK.git
-//! 
+//!
 //! # 从crates.io下载
 //! cargo install dadk
-//! 
+//!
 //! ```
-//! 
+//!
 //! ## DADK的工作原理
-//! 
+//!
 //! DADK使用(任务名,任务版本)来标识每个构建目标。当使用DADK构建DragonOS应用时,DADK会根据用户的配置文件,自动完成以下工作:
-//! 
+//!
 //! - 解析配置文件,生成DADK任务列表
 //! - 根据DADK任务列表,进行拓扑排序。这一步会自动处理软件库的依赖关系。
 //! - 收集环境变量信息,并根据DADK任务列表,设置全局环境变量、任务环境变量。
 //! - 根据拓扑排序后的DADK任务列表,自动执行任务。
-//! 
+//!
 //! ### DADK与环境变量
-//! 
+//!
 //! 环境变量的设置是DADK能正常工作的关键因素之一,您可以在您的编译脚本中,通过引用环境变量,来获得其他软件库的编译信息。
 //! 这是使得您的应用能够自动依赖其他软件库的关键一步。
-//! 
+//!
 //! 只要您的编译脚本能够正确地引用环境变量,DADK就能够自动处理软件库的依赖关系。
-//! 
+//!
 //! DADK会设置以下全局环境变量:
-//! 
+//!
 //! - `DADK_CACHE_ROOT`:DADK的缓存根目录。您可以在编译脚本中,通过引用该环境变量,来获得DADK的缓存根目录。
 //! - `DADK_BUILD_CACHE_DIR_任务名_任务版本`:DADK的任务构建结果缓存目录。当您要引用其他软件库的构建结果时,可以通过该环境变量来获得。
 //! 同时,您也要在构建您的app时,把构建结果放到您的软件库的构建结果缓存目录(通过对应的环境变量获得)中。
 //! - `DADK_SOURCE_CACHE_DIR_任务名_任务版本`:DADK的某个任务的源码目录。当您要引用其他软件库的源码目录时,可以通过该环境变量来获得。
-//! 
+//!
 //! 同时,DADK会为每个任务设置其自身在配置文件中指定的环境变量。
-//! 
+//!
 //! #### 全局环境变量命名格式
-//! 
+//!
 //! 全局环境变量中的任务名和任务版本,都会被转换为大写字母,并对特殊字符进行替换。替换表如下:
-//! 
+//!
 //! | 原字符 | 替换字符 |
 //! | ------ | -------- |
 //! | `.`    | `_`      |
@@ -73,12 +73,12 @@
 //! | 空格   | `_`      |
 //! | `+`    | `_`      |
 //! | `*`    | `_`      |
-//! 
+//!
 //! **举例**:对于任务`libc-0.1.0`,其构建结果的全局环境变量名为`DADK_BUILD_CACHE_DIR_LIBC_0_1_0`。
-//! 
-//! 
+//!
+//!
 //! ## TODO
-//! 
+//!
 //! - 支持从在线归档文件下载源码、构建好的软件库
 //! - 支持自动更新
 //! - 完善clean命令的逻辑

+ 1 - 1
src/parser/mod.rs

@@ -28,7 +28,7 @@ use std::{
     path::PathBuf,
 };
 
-use log::{error, info, debug};
+use log::{debug, error, info};
 
 use self::task::DADKTask;
 pub mod task;

+ 5 - 2
src/parser/task.rs

@@ -142,7 +142,11 @@ impl DADKTask {
     }
 
     pub fn name_version(&self) -> String {
-        return format!("{}-{}", self.name, self.version);
+        let mut name_version = format!("{}-{}", self.name, self.version);
+        for (src, dst) in &NAME_VERSION_REPLACE_TABLE {
+            name_version = name_version.replace(src, dst);
+        }
+        return name_version;
     }
 
     pub fn name_version_env(&self) -> String {
@@ -332,7 +336,6 @@ impl CodeSource {
             CodeSource::Archive(source) => source.validate(),
         }
     }
-
     pub fn trim(&mut self) {
         match self {
             CodeSource::Git(source) => source.trim(),

+ 1 - 1
src/scheduler/mod.rs

@@ -294,7 +294,7 @@ impl Scheduler {
                 self.run_with_topo_sort()?;
             }
             Action::Clean(_) => self.run_without_topo_sort()?,
-            _ => unimplemented!(),            
+            _ => unimplemented!(),
         }
 
         return Ok(());

+ 60 - 0
src/utils/file.rs

@@ -0,0 +1,60 @@
+use std::{fs::File, path::Path};
+
+use reqwest::{blocking::ClientBuilder, Url};
+
+pub struct FileUtils;
+
+impl FileUtils {
+    ///从指定url下载文件到指定路径
+    pub fn download_file(url: &str, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
+        let tempurl = Url::parse(url).expect("failed to parse the url");
+        let file_name = tempurl
+            .path_segments()
+            .expect("connot be base url")
+            .last()
+            .expect("failed to get the filename from the url");
+        let client = ClientBuilder::new()
+            .timeout(std::time::Duration::from_secs(10))
+            .build()?;
+        let mut response = client.get(url).send()?;
+        let mut file = File::create(path.join(file_name))?;
+        response.copy_to(&mut file)?;
+        Ok(())
+    }
+
+    /// 把指定路径下所有文件和文件夹递归地移动到另一个文件中
+    pub fn move_files(src: &Path, dst: &Path) -> std::io::Result<()> {
+        for entry in src.read_dir()? {
+            let entry = entry?;
+            let path = entry.path();
+            let new_path = dst.join(path.file_name().unwrap());
+            if entry.file_type()?.is_dir() {
+                std::fs::create_dir_all(&new_path)?;
+                FileUtils::move_files(&path, &new_path)?;
+            } else {
+                std::fs::rename(&path, &new_path)?;
+            }
+        }
+        Ok(())
+    }
+
+    /// 递归地复制给定目录下所有文件到另一个文件夹中
+    pub fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), String> {
+        if src.is_dir() {
+            std::fs::create_dir_all(dst).map_err(|e| e.to_string())?;
+            for entry in std::fs::read_dir(src).map_err(|e| e.to_string())? {
+                let entry = entry.map_err(|e| e.to_string())?;
+                let path = entry.path();
+                let dst_path = dst.join(path.file_name().unwrap());
+                if path.is_dir() {
+                    FileUtils::copy_dir_all(&path, &dst_path).map_err(|e| e.to_string())?;
+                } else {
+                    std::fs::copy(&path, &dst_path).map_err(|e| e.to_string())?;
+                }
+            }
+        } else {
+            return Err(format!("No such source directory:{:?}", src));
+        }
+        Ok(())
+    }
+}

+ 1 - 0
src/utils/mod.rs

@@ -1,2 +1,3 @@
+pub mod file;
 pub mod lazy_init;
 pub mod stdio;