atsh_lib/
lib.rs

1use std::io::Error;
2use std::path::{Path, PathBuf};
3use std::sync::{LazyLock, OnceLock};
4
5mod config;
6mod db;
7mod ssh;
8
9static WORK_DIR: OnceLock<PathBuf> = OnceLock::new();
10pub(crate) static WORK_DIR_FILE: LazyLock<fn(&str) -> PathBuf> = LazyLock::new(|| {
11    |n| {
12        if cfg!(test) {
13            let work_dir =
14                std::env::current_exe().expect("failed to get current execute directory");
15            work_dir.with_file_name("test.atsh.d")
16        } else {
17            let work_dir = WORK_DIR.get().expect("WORK_DIR not initialized");
18            work_dir.join(n)
19        }
20    }
21});
22
23fn setup_logging(work_dir: &Path) -> Result<(), Error> {
24    use tracing_appender::rolling::{RollingFileAppender, Rotation};
25    use tracing_subscriber::{
26        filter::LevelFilter,
27        fmt::{
28            self,
29            time::{LocalTime, UtcTime},
30        },
31        layer::SubscriberExt,
32        util::SubscriberInitExt,
33        EnvFilter,
34    };
35
36    let log_dir = work_dir.join("logs");
37    if !log_dir.is_dir() {
38        std::fs::create_dir_all(&log_dir)?;
39    }
40
41    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
42    let debug = filter
43        .max_level_hint()
44        .map(|level| level >= LevelFilter::DEBUG)
45        .unwrap_or(false);
46
47    let subscriber = tracing_subscriber::registry()
48        .with(filter)
49        .with(
50            fmt::Layer::new()
51                .with_writer(std::io::stderr)
52                .with_target(debug)
53                .with_line_number(debug)
54                .with_timer(LocalTime::rfc_3339()),
55        )
56        .with(
57            fmt::Layer::new()
58                .json()
59                .with_writer(
60                    RollingFileAppender::builder()
61                        .rotation(Rotation::DAILY)
62                        // .filename_prefix("atsh.log")
63                        .filename_suffix("json")
64                        .build(log_dir)
65                        .expect("Failed to create log file"),
66                )
67                .with_target(debug)
68                .with_line_number(debug)
69                .with_timer(UtcTime::rfc_3339()),
70        );
71
72    subscriber.init();
73    tracing::debug!(
74        "Logging initialized (RUST_LOG={})",
75        std::env::var("RUST_LOG").unwrap_or_default()
76    );
77    Ok(())
78}
79
80pub mod atsh {
81    use std::io::{Error, ErrorKind};
82    use std::path::Path;
83    use tracing::debug;
84
85    use crate::ssh::remote::Remotes;
86    use crate::{setup_logging, WORK_DIR};
87
88    pub use crate::ssh::{remote::Remote, secure::set_atshkey};
89
90    type Result<T> = std::result::Result<T, Error>;
91
92    pub fn initialize(work_dir: Option<&Path>) -> Result<()> {
93        let work_dir = work_dir.map(|p| p.to_path_buf()).unwrap_or({
94            let work_dir =
95                std::env::current_exe().expect("failed to get current execute directory");
96            // work_dir.pop();
97            // work_dir
98            work_dir.with_file_name(".atsh.d")
99        });
100        debug!("work_dir: {}", work_dir.display());
101
102        if !work_dir.exists() {
103            std::fs::create_dir_all(&work_dir)?;
104        }
105        setup_logging(&work_dir)?;
106
107        WORK_DIR
108            .set(work_dir)
109            .expect("WORK_DIR already initialized");
110
111        // info!(work=?WORK_DIR.get(), "success initialize");
112        Ok(())
113    }
114
115    pub fn add(
116        user: &str,
117        password: &str,
118        ip: &str,
119        port: u16,
120        name: &Option<impl AsRef<str>>,
121        note: &Option<impl AsRef<str>>,
122    ) -> Result<usize> {
123        Remotes::add(user, password, ip, port, name, note)
124    }
125
126    pub fn add_remote(remote: &Remote) -> Result<usize> {
127        add(
128            &remote.user,
129            &remote.password,
130            &remote.ip,
131            remote.port,
132            &remote.name,
133            &remote.note,
134        )
135    }
136
137    pub fn remove(index: &Vec<usize>) -> Result<usize> {
138        Remotes::delete(index)
139    }
140
141    pub fn get(index: usize) -> Result<Option<Remote>> {
142        Remotes::get(index)
143    }
144
145    pub fn try_get(index: usize) -> Result<Remote> {
146        Remotes::try_get(index)
147    }
148
149    pub fn get_all() -> Result<Vec<Remote>> {
150        let remotes = Remotes::get_all()?;
151        Ok(remotes.0)
152    }
153
154    pub fn pprint(all: bool) -> Result<()> {
155        let remotes = Remotes::get_all()?;
156        remotes.pprint(all);
157        Ok(())
158    }
159
160    // pub fn list(all: bool) -> Result<()> {
161    //     if all {
162    //         Remotes::list_all()
163    //     } else {
164    //         Remotes::list()
165    //     }
166    // }
167
168    // auth params means try auth against the server
169    pub fn login(index: usize, auth: bool) -> Result<()> {
170        let remote = Remotes::try_get(index)?;
171        remote.login(auth)
172    }
173
174    pub fn copy(index: usize, path: &str) -> Result<()> {
175        let paths = path.split('=').collect::<Vec<&str>>();
176        if paths.len() != 2 {
177            return Err(Error::new(
178                ErrorKind::InvalidInput,
179                "path format error, like `from=to`",
180            ));
181        }
182        let remote = Remotes::try_get(index)?;
183        if std::path::PathBuf::from(paths[0]).exists() {
184            remote.upload(paths[0], paths[1])
185        } else {
186            remote.download(paths[0], paths[1])
187        }
188    }
189
190    pub fn upload(index: usize, path: &Vec<impl AsRef<str>>) -> Result<()> {
191        if path.len() != 2 {
192            return Err(Error::new(
193                ErrorKind::InvalidInput,
194                "path format error, like `upload -p /local/path /remote/path`",
195            ));
196        }
197        let (local, remote) = (path[0].as_ref(), path[1].as_ref());
198        if !Path::new(local).exists() {
199            return Err(Error::new(ErrorKind::NotFound, "the upload file not found"));
200        }
201
202        Remotes::try_get(index)?.upload(local, remote)
203    }
204
205    pub fn download(index: usize, path: &Vec<impl AsRef<str>>) -> Result<()> {
206        if path.len() != 2 {
207            return Err(Error::new(
208                ErrorKind::InvalidInput,
209                "path format error, like `upload -p /remote/path /local/path`",
210            ));
211        }
212        let (remote, local) = (path[0].as_ref(), path[1].as_ref());
213
214        Remotes::try_get(index)?.download(remote, local)
215    }
216}
217
218// #[cfg(test)]
219// mod tests {
220
221//     use super::*;
222
223//     #[test]
224//     fn test_lib() {
225//     }
226// }