Repository: comtrya/comtrya (language: rust)

Issue:
Feature: file.copy should support downloads
It's pretty common to want to download software (or maybe config) from http, even ftp.
`wget` can be run from command.run, but you have to dump it in /tmp etc etc.
It feels nice to model this by supporting http etc URL schemes in file.copy (file:// can be the default in the case of no scheme)


Discussion / hints:
@mt-inside I added a HttpDownload atom recently that would provide the basis for this. I'll work on adding a user facing action for it this week 👍🏻 

Fix patch:
diff --git a/examples/file/file-download.yaml b/examples/file/file-download.yaml
new file mode 100644
index 00000000..b8d53fae
--- /dev/null
+++ b/examples/file/file-download.yaml
@@ -0,0 +1,5 @@
+actions:
+  # This will be rendered with contexts
+  - action: file.download
+    from: https://google.com/robots.txt
+    to: /tmp/google-robots.txt
diff --git a/src/actions/file/download.rs b/src/actions/file/download.rs
new file mode 100644
index 00000000..3d1e4116
--- /dev/null
+++ b/src/actions/file/download.rs
@@ -0,0 +1,103 @@
+use super::FileAction;
+use crate::actions::Action;
+use crate::manifests::Manifest;
+use crate::steps::Step;
+use anyhow::Result;
+use serde::{de::Error, Deserialize, Deserializer, Serialize};
+use std::{path::PathBuf, u32};
+use tera::Context;
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct FileDownload {
+    pub from: String,
+    pub to: String,
+
+    #[serde(default = "default_chmod", deserialize_with = "from_octal")]
+    pub chmod: u32,
+
+    #[serde(default = "default_template")]
+    pub template: bool,
+}
+
+fn from_octal<'de, D>(deserializer: D) -> Result<u32, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    let chmod: u32 = Deserialize::deserialize(deserializer)?;
+    u32::from_str_radix(&chmod.to_string(), 8).map_err(D::Error::custom)
+}
+
+fn default_chmod() -> u32 {
+    0o644
+}
+
+fn default_template() -> bool {
+    false
+}
+
+impl FileDownload {}
+
+impl FileAction for FileDownload {}
+
+impl Action for FileDownload {
+    fn plan(&self, _manifest: &Manifest, _context: &Context) -> Vec<Step> {
+        use crate::atoms::directory::Create as DirCreate;
+        use crate::atoms::file::Chmod;
+        use crate::atoms::http::Download;
+
+        let path = PathBuf::from(&self.to);
+        let parent = path.clone();
+
+        vec![
+            Step {
+                atom: Box::new(DirCreate {
+                    path: parent.parent().unwrap().into(),
+                }),
+                initializers: vec![],
+                finalizers: vec![],
+            },
+            Step {
+                atom: Box::new(Download {
+                    url: self.from.clone(),
+                    to: path.clone(),
+                }),
+                initializers: vec![],
+                finalizers: vec![],
+            },
+            Step {
+                atom: Box::new(Chmod {
+                    path: path.clone(),
+                    mode: self.chmod,
+                }),
+                initializers: vec![],
+                finalizers: vec![],
+            },
+        ]
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::actions::Actions;
+
+    #[test]
+    fn it_can_be_deserialized() {
+        let yaml = r#"
+- action: file.download
+  from: a
+  to: b
+"#;
+
+        let mut actions: Vec<Actions> = serde_yaml::from_str(yaml).unwrap();
+
+        match actions.pop() {
+            Some(Actions::FileDownload(file_download)) => {
+                assert_eq!("a", file_download.from);
+                assert_eq!("b", file_download.to);
+            }
+            _ => {
+                panic!("FileDownload didn't deserialize to the correct type");
+            }
+        };
+    }
+}
diff --git a/src/actions/file/mod.rs b/src/actions/file/mod.rs
index 1c1b525d..7b706359 100644
--- a/src/actions/file/mod.rs
+++ b/src/actions/file/mod.rs
@@ -1,4 +1,5 @@
 pub mod copy;
+pub mod download;
 pub mod link;
 
 use crate::actions::Action;
diff --git a/src/actions/mod.rs b/src/actions/mod.rs
index 47bf4a25..812f21e8 100644
--- a/src/actions/mod.rs
+++ b/src/actions/mod.rs
@@ -8,6 +8,7 @@ use crate::steps::Step;
 use command::run::RunCommand;
 use directory::{DirectoryCopy, DirectoryCreate};
 use file::copy::FileCopy;
+use file::download::FileDownload;
 use file::link::FileLink;
 use package::install::PackageInstall;
 use serde::{Deserialize, Serialize};
@@ -28,6 +29,9 @@ pub enum Actions {
     #[serde(alias = "file.copy")]
     FileCopy(FileCopy),
 
+    #[serde(alias = "file.download")]
+    FileDownload(FileDownload),
+
     #[serde(alias = "file.link")]
     FileLink(FileLink),
 
@@ -42,6 +46,7 @@ impl Actions {
             Actions::DirectoryCopy(a) => a,
             Actions::DirectoryCreate(a) => a,
             Actions::FileCopy(a) => a,
+            Actions::FileDownload(a) => a,
             Actions::FileLink(a) => a,
             Actions::PackageInstall(a) => a,
         }
diff --git a/src/atoms/http/download.rs b/src/atoms/http/download.rs
index baba564e..c17c6cac 100644
--- a/src/atoms/http/download.rs
+++ b/src/atoms/http/download.rs
@@ -1,18 +1,20 @@
 use super::super::Atom;
 use std::io::Write;
-use std::{
-    fs::File,
-    path::{Path, PathBuf},
-};
+use std::{fs::File, path::PathBuf};
 
 pub struct Download {
     pub url: String,
-    pub to: String,
+    pub to: PathBuf,
 }
 
 impl std::fmt::Display for Download {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "HttpDownload from {} to {}", self.url, self.to,)
+        write!(
+            f,
+            "HttpDownload from {} to {}",
+            self.url,
+            self.to.to_str().unwrap()
+        )
     }
 }
 
@@ -27,8 +29,7 @@ impl Atom for Download {
     fn execute(&mut self) -> anyhow::Result<()> {
         let response = reqwest::blocking::get(&self.url)?;
 
-        let path = Path::new(&self.to);
-        let mut file = File::create(&path)?;
+        let mut file = File::create(&self.to)?;
 
         let content = response.bytes()?;
         file.write_all(&content)?;
@@ -45,7 +46,7 @@ mod tests {
     #[test]
     fn it_can() {
         let tmpdir = tempdir().unwrap();
-        let to_file = String::from(tmpdir.path().join("download").to_str().unwrap());
+        let to_file = tmpdir.path().join("download");
 
         let mut atom = Download {
             url: String::from("https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"),


Test patch: