smbcloud_cli/deploy/
mod.rs

1mod git;
2mod smb_config;
3
4use crate::{
5    account::lib::protected_request,
6    cli::CommandResult,
7    ui::{fail_message, fail_symbol, succeed_message, succeed_symbol},
8};
9use anyhow::Result;
10use console::style;
11use git::remote_deployment_setup;
12use git2::{Cred, PushOptions, RemoteCallbacks, Repository};
13use git_url_parse::{GitUrl, Scheme};
14use indicatif::{ProgressBar, ProgressState, ProgressStyle};
15use smb_config::check_config;
16use smbcloud_networking::environment::Environment;
17use spinners::Spinner;
18use ssh2_config::{ParseRule, SshConfig};
19use std::{cmp::min, fmt::Write, fs::File, io::BufReader};
20
21pub async fn process_deploy(env: Environment) -> Result<CommandResult> {
22    protected_request(env).await?;
23    let repo_name = check_config().await?;
24
25    let mut spinner = Spinner::new(
26        spinners::Spinners::SimpleDotsScrolling,
27        style("Deploying...").green().bold().to_string(),
28    );
29
30    let repo = match Repository::open(".") {
31        Ok(repo) => repo,
32        Err(_) => {
33            spinner.stop_and_persist("😩", "No git repository found.".to_owned());
34            return Ok(CommandResult {
35                spinner,
36                symbol: fail_symbol(),
37                msg: fail_message("No git repository found. Init with `git init` command."),
38            });
39        }
40    };
41
42    let _main_branch = match repo.head() {
43        Ok(branch) => branch,
44        Err(_) => {
45            spinner.stop_and_persist("😩", "No main branch found.".to_owned());
46            return Ok(CommandResult {
47                spinner,
48                symbol: fail_symbol(),
49                msg: fail_message(
50                    "No main branch found. Create with `git checkout -b <branch>` command.",
51                ),
52            });
53        }
54    };
55    let mut origin = remote_deployment_setup(&repo, repo_name).await?;
56    let remote_url = match origin.url() {
57        Some(url) => url,
58        None => {
59            spinner.stop_and_persist("😩", "No remote URL found.".to_owned());
60            return Ok(CommandResult {
61                spinner,
62                symbol: fail_symbol(),
63                msg: fail_message(
64                    "No remote URL found. Add with `git remote add origin <url>` command.",
65                ),
66            });
67        }
68    };
69    //println!("Remote URL: {:#?}", remote_url);
70    let parsed_url = match GitUrl::parse(remote_url) {
71        Ok(url) => url,
72        Err(e) => {
73            spinner.stop_and_persist("😩", e.to_string());
74            return Ok(CommandResult {
75                spinner,
76                symbol: fail_symbol(),
77                msg: "Invalid remote URL.".to_owned(),
78            });
79        }
80    };
81    //println!("Parsed URL: {:#?}", parsed_url);
82    match parsed_url.scheme {
83        Scheme::Ssh => {
84            // println!("SSH URL: {:#?}", parsed_url);
85        }
86        _ => {
87            // Only support ssh for now
88            return Ok(CommandResult {
89                spinner,
90                symbol: fail_symbol(),
91                msg: fail_message("Only ssh is supported."),
92            });
93        }
94    };
95
96    // Get ssh config from host
97    let host = match parsed_url.host {
98        Some(host) => host,
99        None => {
100            spinner.stop_and_persist("😩", "No host found.".to_owned());
101            return Ok(CommandResult {
102                spinner,
103                symbol: fail_symbol(),
104                msg: fail_message("No host found."),
105            });
106        }
107    };
108
109    // get ssh_config
110    let ssh_config_file = match home::home_dir() {
111        Some(home) => {
112            let ssh_config_path = home.join(".ssh/config");
113            if ssh_config_path.exists() {
114                // println!("SSH config path: {:#?}", ssh_config_path);
115                // Open the file and read it
116                File::open(ssh_config_path.clone()).expect("Unable to open ssh config file")
117            } else {
118                spinner.stop_and_persist("😩", "No ssh config found.".to_owned());
119                return Ok(CommandResult {
120                    spinner,
121                    symbol: fail_symbol(),
122                    msg: fail_message("No ssh config found."),
123                });
124            }
125        }
126        None => {
127            spinner.stop_and_persist("😩", "No home".to_owned());
128            return Ok(CommandResult {
129                spinner,
130                symbol: "😩".to_owned(),
131                msg: "No home directory found.".to_owned(),
132            });
133        }
134    };
135    //println!("SSH config path: {:#?}", ssh_config_file);
136    let mut reader = BufReader::new(ssh_config_file);
137    let config = SshConfig::default()
138        .parse(&mut reader, ParseRule::STRICT)
139        .expect("Failed to parse ssh config file");
140    //println!("SSH config: {:#?}", config);
141
142    // Get the host config
143    let host_config = config.query(host);
144    //println!("Host config: {:#?}", host_config);
145    // get identity_file
146    let identity_files = match host_config.identity_file {
147        Some(identity_files) => {
148            //println!("Identity file: {:#?}", identity_files);
149            identity_files
150        }
151        None => {
152            spinner.stop_and_persist("😩", "No identity file found.".to_owned());
153            return Ok(CommandResult {
154                spinner,
155                symbol: "😩".to_owned(),
156                msg: "No identity files found.".to_owned(),
157            });
158        }
159    };
160    // println!("Identity files: {:#?}", identity_files);
161    // get identity_file
162    let identity_file = match identity_files.first() {
163        Some(identity_file) => {
164            //println!("Identity file: {:#?}", identity_file);
165            identity_file
166        }
167        None => {
168            spinner.stop_and_persist("😩", "No identity file found.".to_owned());
169            return Ok(CommandResult {
170                spinner,
171                symbol: "😩".to_owned(),
172                msg: "No identity file found.".to_owned(),
173            });
174        }
175    };
176    let mut push_opts = PushOptions::new();
177    let mut callbacks = RemoteCallbacks::new();
178    // Set the credentials
179    callbacks.credentials(|_url, _username_from_url, _allowed_types| {
180        Cred::ssh_key("deploy", None, identity_file, None)
181    });
182    callbacks.push_transfer_progress(|current, total, bytes| {
183        // println!("push_transfer_progress: current -> {}, total -> {}, bytes -> {}", current, total, bytes);
184        let pb = ProgressBar::new(total.try_into().unwrap());
185        pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})")
186            .unwrap()
187            .with_key("eta", |state: &ProgressState, w: &mut dyn Write| write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap())
188            .progress_chars("#>-"));
189        let mut current = current;
190        while current < total {
191            let new = min(current + 223211, total);
192            current = new;
193            pb.set_position(new.try_into().unwrap());
194        }
195    
196        pb.finish_with_message("downloaded");
197    });
198    callbacks.pack_progress(|stage, current, total| {
199        println!("pack_progress: current -> {}, total -> {}", current, total);
200    });
201    callbacks.sideband_progress(|x| {
202        //println!("{x}");
203        return true;
204    });
205    callbacks.push_update_reference(|x, y| match y {
206        Some(e) => {
207            println!("{}", e);
208            Ok(())
209        }
210        None => {
211            println!("Done");
212            Ok(())
213        }
214    });
215    push_opts.remote_callbacks(callbacks);
216
217    return match origin.push(&["refs/heads/main:refs/heads/main"], Some(&mut push_opts)) {
218        Ok(_) => {
219            println!("Succeed!!!");
220            Ok(CommandResult {
221                spinner,
222                symbol: succeed_symbol(),
223                msg: succeed_message("Your app has been deployed successfully."),
224            })
225        }
226        Err(e) => {
227            println!("Failed to push to remote: {:#?}", e);
228            spinner.stop_and_persist("😩", e.to_string());
229            Ok(CommandResult {
230                spinner,
231                symbol: fail_symbol(),
232                msg: fail_message(&e.to_string()),
233            })
234        }
235    };
236}