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_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.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 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 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