1use std::{
2 ffi::{OsStr, OsString},
3 fs, io,
4 path::{Path, PathBuf},
5};
6
7use thiserror::Error;
8
9use kalavor::Katetime;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct SrcDstConfig {
18 pub allow_from_stdin: bool,
19 pub allow_to_stdout: bool,
20
21 pub auto_tnamed_dst_file: bool,
22 pub auto_tnamed_dst_dir: bool,
23
24 pub default_extension: OsString,
25
26 pub allow_inplace: bool,
28}
29
30impl SrcDstConfig {
31 pub fn new<S: AsRef<OsStr>>(default_extension: S) -> Self {
32 Self {
33 allow_from_stdin: true,
34 allow_to_stdout: true,
35 auto_tnamed_dst_file: true,
36 auto_tnamed_dst_dir: true,
37 default_extension: default_extension.as_ref().to_owned(),
38 allow_inplace: false,
39 }
40 }
41
42 pub fn new_with_allow_inplace<S: AsRef<OsStr>>(default_extension: S) -> Self {
43 Self {
44 allow_from_stdin: true,
45 allow_to_stdout: true,
46 auto_tnamed_dst_file: true,
47 auto_tnamed_dst_dir: true,
48 default_extension: default_extension.as_ref().to_owned(),
49 allow_inplace: true,
50 }
51 }
52
53 pub fn parse<P: AsRef<Path>>(
71 &self,
72 src: P,
73 dst: Option<P>,
74 ) -> io::Result<Result<SrcDstPairs, SrcDstError>> {
75 enum InnerSource {
76 Stdin,
77 File(PathBuf),
78 Dir(PathBuf),
79 }
80
81 enum InnerDrain {
82 Stdout,
83 File(PathBuf),
84 Dir(PathBuf),
85 NotExist(PathBuf),
86 NotProvided,
87 }
88
89 let src = src.as_ref();
90 let src = if src.as_os_str() == "-" {
91 InnerSource::Stdin
92 } else if !src.exists() {
93 return Err(io::Error::new(
94 io::ErrorKind::PermissionDenied,
95 format!("SRC '{}' does not exist", src.to_string_lossy()),
96 ));
97 } else {
98 let src = fs::canonicalize(src)?;
99 if src.is_file() {
100 InnerSource::File(src)
101 } else {
102 InnerSource::Dir(src)
103 }
104 };
105
106 let dst = dst.as_ref();
107 let mut dst = match dst {
108 None => InnerDrain::NotProvided,
109 Some(dst) => {
110 let dst = dst.as_ref();
111 if dst.as_os_str() == "-" {
112 InnerDrain::Stdout
113 } else if !dst.exists() {
114 InnerDrain::NotExist(dst.to_owned())
115 } else {
116 let dst = fs::canonicalize(dst)?;
117 if dst.is_file() {
118 InnerDrain::File(dst)
119 } else {
120 InnerDrain::Dir(dst)
121 }
122 }
123 }
124 };
125
126 if matches!(src, InnerSource::Stdin) && !self.allow_from_stdin {
127 return Ok(Err(SrcDstError::DisallowFromStdin)); }
129 if matches!(dst, InnerDrain::Stdout) && !self.allow_to_stdout {
130 return Ok(Err(SrcDstError::DisallowToStdout)); }
132 if matches!(dst, InnerDrain::NotProvided) {
133 if matches!(src, InnerSource::Dir(_)) && !self.auto_tnamed_dst_dir {
134 return Ok(Err(SrcDstError::ForbidAutoTnamedDstDir)); } else if !self.auto_tnamed_dst_file {
136 return Ok(Err(SrcDstError::ForbidAutoTnamedDstFile)); }
138 }
139 if let InnerDrain::Dir(parent) = &dst {
140 if let InnerSource::File(src) = &src {
141 if fs::canonicalize(parent)? == fs::canonicalize(src)?.parent().unwrap() {
142 dst = InnerDrain::NotProvided; }
144 } else if !self.allow_inplace {
145 if let InnerSource::Dir(src) = &src {
146 if fs::canonicalize(parent)? == fs::canonicalize(src)? {
147 return Ok(Err(SrcDstError::Inplaced));
148 }
149 }
150 }
151 }
152
153 let mut tnamed = false;
154 let (src, dst): (Source, Drain) = match src {
155 InnerSource::Stdin | InnerSource::File(_) => {
156 fn dst_parent_src_name(src: &InnerSource, dst: &InnerDrain) -> io::Result<PathBuf> {
157 let mut parent = match dst {
158 InnerDrain::Dir(parent) => parent.to_owned(),
159 InnerDrain::NotProvided => fs::canonicalize(std::env::current_dir()?)?,
160 _ => unreachable!(),
161 };
162 parent.push(match src {
163 InnerSource::Stdin => OsString::from("stdin"),
164 InnerSource::File(src) => src.file_name().unwrap().into(), InnerSource::Dir(_) => unreachable!(),
166 });
167
168 Ok(parent)
169 }
170
171 (
172 match &src {
173 InnerSource::Stdin => Source::Stdin,
174 InnerSource::File(src) => Source::File(src.clone()),
175 InnerSource::Dir(_) => unreachable!(),
176 },
177 match dst {
178 InnerDrain::Stdout => Drain::Stdout,
179 InnerDrain::File(dst) => Drain::Single(dst),
180 InnerDrain::Dir(_) => Drain::Single(dst_parent_src_name(&src, &dst)?),
181 InnerDrain::NotExist(dst) => Drain::Single(dst),
182 InnerDrain::NotProvided => {
183 let mut dst = dst_parent_src_name(&src, &dst)?;
187
188 dst.extension()
189 .and_then(|ext| Some(ext == self.default_extension))
190 .unwrap_or(false)
191 .then(|| dst.set_extension("")); dst.set_file_name(format!(
193 "{}-{}{}",
194 dst.as_os_str().to_string_lossy(),
195 Katetime::now_datetime(),
196 match self.default_extension.is_empty() {
197 true => String::with_capacity(0),
198 false =>
199 format!(".{}", self.default_extension.to_string_lossy()),
200 }
201 ));
202
203 Drain::Single(dst)
204 }
205 },
206 )
207 }
208
209 InnerSource::Dir(src) => {
210 fn shallow_walk<P: AsRef<Path>>(src: P) -> io::Result<Vec<PathBuf>> {
211 let mut files = fs::read_dir(src)?
212 .filter_map(Result::ok)
213 .filter_map(|p| {
214 p.metadata()
215 .ok()
216 .and_then(|m| m.is_file().then(|| p.path()))
217 })
218 .collect::<Vec<_>>();
219 files.sort_unstable_by(|a, b| b.cmp(a));
220 Ok(files)
221 }
222
223 match dst {
224 InnerDrain::Stdout => return Ok(Err(SrcDstError::ManyToOne)),
225 InnerDrain::File(_) => return Ok(Err(SrcDstError::ManyToOne)),
226 InnerDrain::Dir(dst) => (Source::Files(shallow_walk(src)?), Drain::Single(dst)),
227 InnerDrain::NotExist(_) => return Ok(Err(SrcDstError::DstDirNotExist)),
228 InnerDrain::NotProvided => {
229 let mut dst = src
231 .parent()
232 .ok_or_else(|| {
233 io::Error::new(
234 io::ErrorKind::PermissionDenied,
235 format!("parent directory of {src:?} are unavailable"),
236 )
237 })?
238 .to_owned();
239 dst.push(format!(
240 "{}-{}",
241 src.file_name().unwrap().to_string_lossy(),
242 Katetime::now_datetime()
243 ));
244
245 tnamed = true;
246 (Source::Files(shallow_walk(src)?), Drain::Single(dst))
247 }
248 }
249 }
250 };
251
252 Ok(Ok(SrcDstPairs {
253 src,
254 dst,
255 tnamed_dir: tnamed,
256 finished: false,
257 }))
258 }
259}
260
261#[non_exhaustive]
262#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
263pub enum SrcDstError {
264 #[error("disallow read from stdin")]
265 DisallowFromStdin = 1,
266 #[error("disallow write to stdout")]
267 DisallowToStdout,
268 #[error("forbid automatic time-based named DST file")]
269 ForbidAutoTnamedDstFile,
270 #[error("forbid automatic time-based named DST directory")]
271 ForbidAutoTnamedDstDir,
272
273 #[error("there may be a potential to `open` and `create` the same file at the same time")]
274 Inplaced,
275
276 #[error("unable to write multiple files to one file")]
277 ManyToOne,
278 #[error("specified DST directory does not exist")]
279 DstDirNotExist,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq, Hash)]
283pub enum Src {
284 File(PathBuf),
285 Stdin,
286}
287
288#[derive(Debug, Clone, PartialEq, Eq, Hash)]
289pub enum Dst {
290 File(PathBuf),
291 Stdout,
292}
293
294#[derive(Debug)]
295pub struct SrcDstPairs {
296 src: Source,
297 dst: Drain,
298
299 tnamed_dir: bool,
300 finished: bool,
301}
302
303impl SrcDstPairs {
304 pub fn create_tnamed_dir(&self) -> io::Result<()> {
306 if let Drain::Single(dir) = &self.dst {
307 if self.tnamed_dir {
308 fs::create_dir(dir)?;
309 }
310 }
311 Ok(())
312 }
313
314 pub fn is_batch(&self) -> bool {
315 matches!(self.src, Source::Files(_))
316 }
317}
318
319impl Iterator for SrcDstPairs {
320 type Item = (Src, Dst);
321
322 fn next(&mut self) -> Option<Self::Item> {
323 if self.finished {
324 return None;
325 }
326
327 match &self.dst {
328 Drain::Stdout => match &self.src {
329 Source::Stdin => {
330 self.finished = true;
331 Some((Src::Stdin, Dst::Stdout))
332 }
333 Source::File(src) => {
334 self.finished = true;
335 Some((Src::File(src.to_owned()), Dst::Stdout))
336 }
337 Source::Files(_) => unreachable!(),
338 },
339
340 Drain::Single(dst) => match &mut self.src {
341 Source::Stdin => {
342 self.finished = true;
343 Some((Src::Stdin, Dst::File(dst.to_owned())))
344 }
345 Source::File(src) => {
346 self.finished = true;
347 Some((Src::File(src.to_owned()), Dst::File(dst.to_owned())))
348 }
349 Source::Files(srcs) => match srcs.pop() {
350 None => None,
351 Some(src) => {
352 let dst = dst.join(src.file_name().unwrap());
353 Some((Src::File(src), Dst::File(dst)))
354 }
355 },
356 },
357 }
358 }
359}
360
361#[derive(Debug)]
362enum Source {
363 Stdin,
364 File(PathBuf),
365 Files(Vec<PathBuf>),
367}
368
369#[derive(Debug)]
370enum Drain {
371 Stdout,
372 Single(PathBuf),
374}
375
376#[cfg(test)]
378mod tests {
379 use super::*;
380
381 #[test]
382 fn test() {
383 match SrcDstConfig::new("png").parse(".", None) {
384 Err(e) => println!("{e}"),
385 Ok(p) => match p {
386 Err(e) => println!("{e}"),
387 Ok(p) => {
388 let p = p.collect::<Vec<_>>();
389 println!("{p:#?}")
390 }
391 },
392 };
393 }
394}