dt_core/item.rs
1use std::{
2 os::unix::prelude::PermissionsExt,
3 path::{Path, PathBuf},
4 rc::Rc,
5};
6
7use path_clean::PathClean;
8use url::Url;
9
10use crate::{
11 config::{Group, LocalGroup, RenamingRule, SyncMethod},
12 error::{Error as AppError, Result},
13 registry::Register,
14 utils,
15};
16
17/// Defines shared behaviours for an item (a path to a file) used in [DT].
18///
19/// [DT]: https://github.com/blurgyy/dt
20#[allow(unused_variables)]
21pub trait Operate
22where
23 Self: Sized,
24{
25 /// Checks if the item is for another machine.
26 fn is_for_other_host(&self, hostname_sep: &str) -> bool {
27 unimplemented!()
28 }
29 /// Gets the absolute location of `self`, if applicable.
30 fn absolute(self) -> Result<Self> {
31 unimplemented!()
32 }
33 /// Gets the host-specific counterpart of `self`, if applicable. If
34 /// `self` is already host-specific, returns `self` directly.
35 fn host_specific(self, hostname_sep: &str) -> Self {
36 unimplemented!()
37 }
38 /// Gets the non-host-specific counterpart of `self`, if applicable. If
39 /// `self` is already non-host-specific, returns `self` directly.
40 fn non_host_specific(self, hostname_sep: &str) -> Self {
41 unimplemented!()
42 }
43 /// Gets the nearest existing parent component of `self`.
44 fn nearest_existing_parent(&self) -> Self {
45 unimplemented!()
46 }
47 /// Checks whether any of the component above `self` is readonly.
48 fn is_parent_readonly(&self) -> bool {
49 unimplemented!()
50 }
51 /// Checks whether any component of `self`'s parent is not a directory.
52 fn has_file_as_parent(&self) -> bool {
53 unimplemented!()
54 }
55 /// Checks whether any of the component references its parent.
56 fn is_twisted(&self) -> bool {
57 unimplemented!()
58 }
59 /// Given a `hostname_sep`, a `base`, a `targetbase`, and optionally a
60 /// list of [renaming rule]s, creates the path where `self` would be
61 /// synced to. Renaming rules are applied after host-specific suffixes
62 /// are stripped.
63 fn make_target<P>(
64 self,
65 hostname_sep: &str,
66 base: &Self,
67 targetbase: P,
68 renaming_rules: Vec<RenamingRule>,
69 ) -> Result<Self>
70 where
71 P: AsRef<Path>,
72 {
73 unimplemented!()
74 }
75 /// Renders this item with given context to the `dest` path.
76 fn get_content<R: Register, O: Operate>(
77 &self,
78 registry: &Rc<R>,
79 group: &Rc<Group<O>>,
80 ) -> Result<Vec<u8>> {
81 unimplemented!()
82 }
83 /// Populate this item with given group config. The given group config is
84 /// expected to be the group where this item belongs to.
85 fn populate<T: Register>(&self, group: Rc<Group<Self>>, registry: Rc<T>) -> Result<()> {
86 unimplemented!()
87 }
88 /// Show what is to be done if this item is to be populated with given
89 /// group config. The given group config is expected to be the group
90 /// where this item belongs to.
91 fn populate_dry(&self, group: Rc<LocalGroup>) -> Result<()> {
92 unimplemented!()
93 }
94}
95
96impl Operate for PathBuf {
97 /// Checks if the item is for another machine (by checking its name).
98 ///
99 /// A host-specific item is considered for another machine, when its
100 /// filename contains only 1 [`hostname_sep`], and after the
101 /// [`hostname_sep`] should not be current machine's hostname.
102 ///
103 /// A non-host-specific item is always considered **not** for another
104 /// machine (because it is non-host-specific, i.e. for all machines).
105 ///
106 /// An item with filename containing more than 1 [`hostname_sep`] causes
107 /// this function to panic.
108 ///
109 /// [`hostname_sep`]: crate::config::GlobalConfig::hostname_sep
110 fn is_for_other_host(&self, hostname_sep: &str) -> bool {
111 let filename = self
112 .file_name()
113 .unwrap_or_else(
114 || panic!("Failed extracting file name from path '{}'", self.display(),),
115 )
116 .to_str()
117 .unwrap_or_else(|| {
118 panic!(
119 "Failed converting &OsStr to &str for path '{}'",
120 self.display(),
121 )
122 });
123 let split: Vec<_> = filename.split(hostname_sep).collect();
124
125 assert!(
126 split.len() <= 2,
127 "There appears to be more than 1 occurrences of hostname_sep ({}) in this path: {}",
128 hostname_sep,
129 self.display(),
130 );
131 assert!(
132 !split.first().unwrap().is_empty(),
133 "hostname_sep ({}) appears to be a prefix of this path: {}",
134 hostname_sep,
135 self.display(),
136 );
137
138 split.len() > 1 && *split.last().unwrap() != gethostname::gethostname().to_string_lossy()
139 }
140
141 /// Gets the absolute path of `self`, **without** traversing symlinks.
142 ///
143 /// Reference: <https://stackoverflow.com/a/54817755/13482274>
144 fn absolute(self) -> Result<Self> {
145 let absolute_path = if self.is_absolute() {
146 self
147 } else {
148 std::env::current_dir()?.join(self)
149 }
150 .clean();
151
152 Ok(absolute_path)
153 }
154
155 /// Gets the host-specific counterpart of `self`. If `self` is already
156 /// host-specific, returns `self` directly.
157 fn host_specific(self, hostname_sep: &str) -> Self {
158 if self.ends_with(utils::host_specific_suffix(hostname_sep)) {
159 self
160 } else {
161 let hs_filename = self
162 .file_name()
163 .unwrap_or_else(|| {
164 panic!("Failed extracting file name from path '{}'", self.display(),)
165 })
166 .to_str()
167 .unwrap_or_else(|| {
168 panic!(
169 "Failed converting &OsStr to &str for path: '{}'",
170 self.display(),
171 )
172 })
173 .to_owned()
174 + &utils::host_specific_suffix(hostname_sep);
175
176 self.with_file_name(hs_filename)
177 }
178 }
179
180 /// Converts a path to a non-host-specific path. If the input path is
181 /// already non-host-specific, returns itself; Otherwise returns a
182 /// path where _every component_ of the path is converted to a
183 /// non-host-specific one.
184 ///
185 /// # Example
186 ///
187 /// ```rust
188 /// # use dt_core::item::Operate;
189 /// # use std::path::PathBuf;
190 /// # use std::str::FromStr;
191 /// let itm: PathBuf = "/some/long/path".into();
192 /// assert_eq!(
193 /// itm.non_host_specific("@@"),
194 /// PathBuf::from_str("/some/long/path").unwrap(),
195 /// );
196 ///
197 /// let itm: PathBuf = "/some@@john/long/path@@watson".into();
198 /// assert_eq!(
199 /// itm.non_host_specific("@@"),
200 /// PathBuf::from_str("/some/long/path").unwrap(),
201 /// );
202 /// ```
203 fn non_host_specific(self, hostname_sep: &str) -> Self {
204 self.iter()
205 .map(std::ffi::OsStr::to_str)
206 .map(|s| {
207 s.unwrap_or_else(|| {
208 panic!(
209 "Failed extracting path components from '{}'",
210 self.display(),
211 )
212 })
213 })
214 .map(|s| {
215 s.split(hostname_sep)
216 .collect::<Vec<_>>()
217 .first()
218 .unwrap_or_else(|| {
219 panic!(
220 "Failed extracting basename from component '{}' of path '{}'",
221 s,
222 self.display(),
223 )
224 })
225 .to_owned()
226 })
227 .collect::<PathBuf>()
228 }
229
230 /// Gets the nearest existing parent component of `self`.
231 fn nearest_existing_parent(&self) -> Self {
232 let mut p: &Path = self.as_ref();
233 let p = loop {
234 if p.exists() {
235 break p;
236 }
237 p = p.parent().unwrap();
238 };
239 p.into()
240 }
241
242 /// Checks whether any of the component above `self` is readonly.
243 fn is_parent_readonly(&self) -> bool {
244 self.nearest_existing_parent()
245 .metadata()
246 .unwrap()
247 .permissions()
248 .readonly()
249 }
250
251 /// Checks whether any component of `self`'s parent is not a directory.
252 fn has_file_as_parent(&self) -> bool {
253 self.nearest_existing_parent().metadata().unwrap().is_file()
254 }
255
256 /// Checks whether any of the component references its parent.
257 fn is_twisted(&self) -> bool {
258 self.iter().any(|comp| comp == "..")
259 }
260
261 /// Given a `hostname_sep`, a `base`, a `targetbase`, and optionally a
262 /// list of [renaming rule]s, create the path where `self` would be synced
263 /// to. Renaming rules are applied after host-specific suffixes are
264 /// stripped.
265 ///
266 /// # Example
267 ///
268 /// ## No renaming rule
269 ///
270 /// ```rust
271 /// # use dt_core::{
272 /// # config::RenamingRule,
273 /// # error::Error as AppError,
274 /// # item::Operate
275 /// # };
276 /// # use std::path::PathBuf;
277 /// # use std::str::FromStr;
278 /// let itm: PathBuf = "/path/to/source@@john/item".into();
279 /// let base: PathBuf = "/path/to/source".into();
280 /// let targetbase: PathBuf = "/path/to/target".into();
281 ///
282 /// assert_eq!(
283 /// itm.make_target("@@", &base, &targetbase, vec![])?,
284 /// PathBuf::from_str("/path/to/target/item").unwrap(),
285 /// );
286 /// # Ok::<(), AppError>(())
287 /// ```
288 ///
289 /// ## Single renaming rule
290 ///
291 /// ```rust
292 /// # use dt_core::{
293 /// # config::RenamingRule,
294 /// # error::Error as AppError,
295 /// # item::Operate
296 /// # };
297 /// # use std::path::PathBuf;
298 /// # use std::str::FromStr;
299 /// let itm: PathBuf = "/path/to/source@@john/_dot_item".into();
300 /// let base: PathBuf = "/path/to/source".into();
301 /// let targetbase: PathBuf = "/path/to/target".into();
302 /// let rules = vec![
303 /// RenamingRule{
304 /// pattern: regex::Regex::new("^_dot_").unwrap(),
305 /// substitution: ".".into(),
306 /// },
307 /// ];
308 ///
309 /// assert_eq!(
310 /// itm.make_target("@@", &base, &targetbase, rules)?,
311 /// PathBuf::from_str("/path/to/target/.item").unwrap(),
312 /// );
313 /// # Ok::<(), AppError>(())
314 /// ```
315 ///
316 /// ## Multiple renaming rules
317 ///
318 /// When multiple renaming rules are supplied, they are applied one after
319 /// another.
320 ///
321 /// ```rust
322 /// # use dt_core::{
323 /// # config::RenamingRule,
324 /// # error::Error as AppError,
325 /// # item::Operate
326 /// # };
327 /// # use std::path::PathBuf;
328 /// # use std::str::FromStr;
329 /// let itm: PathBuf = "/path/to/source@@john/_dot_item.ext".into();
330 /// let base: PathBuf = "/path/to/source".into();
331 /// let targetbase: PathBuf = "/path/to/target".into();
332 /// let rules = vec![
333 /// RenamingRule{
334 /// pattern: regex::Regex::new("^_dot_").unwrap(),
335 /// substitution: ".".into(),
336 /// },
337 /// RenamingRule{
338 /// pattern: regex::Regex::new("^.").unwrap(),
339 /// substitution: "_dotted_".into(),
340 /// },
341 /// ];
342 ///
343 /// assert_eq!(
344 /// itm.make_target("@@", &base, &targetbase, rules)?,
345 /// PathBuf::from_str("/path/to/target/_dotted_item.ext").unwrap(),
346 /// );
347 /// # Ok::<(), AppError>(())
348 /// ```
349 ///
350 /// ## Capture groups
351 ///
352 /// ```rust
353 /// # use dt_core::{
354 /// # config::RenamingRule,
355 /// # error::Error as AppError,
356 /// # item::Operate
357 /// # };
358 /// # use std::path::PathBuf;
359 /// # use std::str::FromStr;
360 /// let itm: PathBuf = "/path/to/source@@john/_dot_item.ext".into();
361 /// let base: PathBuf = "/path/to/source".into();
362 /// let targetbase: PathBuf = "/path/to/target".into();
363 ///
364 /// let named_capture = RenamingRule{
365 /// // Named capture group, captures "dot" into a group with name
366 /// // "prefix".
367 /// pattern: regex::Regex::new("^_(?P<prefix>.*)_").unwrap(),
368 /// substitution: ".${prefix}.".into(),
369 /// };
370 /// assert_eq!(
371 /// itm.to_owned().make_target(
372 /// "@@",
373 /// &base,
374 /// &targetbase,
375 /// vec![named_capture]
376 /// )?,
377 /// PathBuf::from_str("/path/to/target/.dot.item.ext").unwrap(),
378 /// );
379 ///
380 /// let numbered_capture = RenamingRule{
381 /// // Numbered capture group, where `${0}` references the whole match,
382 /// // other groups are indexed from 1.
383 /// pattern: regex::Regex::new(r#"\.(.*?)$"#).unwrap(),
384 /// substitution: "_${1}_${0}".into(),
385 /// };
386 /// assert_eq!(
387 /// itm.to_owned().make_target(
388 /// "@@",
389 /// &base,
390 /// &targetbase,
391 /// vec![numbered_capture]
392 /// )?,
393 /// PathBuf::from_str("/path/to/target/_dot_item_ext_.ext").unwrap(),
394 /// );
395 /// # Ok::<(), AppError>(())
396 /// ```
397 ///
398 /// [renaming rule]: crate::config::RenamingRule
399 fn make_target<P: AsRef<Path>>(
400 self,
401 hostname_sep: &str,
402 base: &Self,
403 targetbase: P,
404 renaming_rules: Vec<RenamingRule>,
405 ) -> Result<Self> {
406 // Get non-host-specific counterpart of `self`
407 let nhself = self.non_host_specific(hostname_sep);
408
409 // Get non-host-specific counterpart of `base`
410 let base = base.to_owned().non_host_specific(hostname_sep);
411
412 // The tail of the target path, which is the non-host-specific `self`
413 // without its `base` prefix path
414 let mut tail = nhself.strip_prefix(base)?.to_owned();
415
416 // Apply renaming rules to the tail component
417 for rr in renaming_rules {
418 log::trace!("Processing renaming rule: {:#?}", rr);
419 log::debug!("Before renaming: '{}'", tail.display());
420
421 let RenamingRule {
422 pattern,
423 substitution,
424 } = rr;
425 tail = tail
426 .iter()
427 .map(|comp| {
428 pattern
429 .replace(&comp.to_string_lossy(), &substitution)
430 .into_owned()
431 })
432 .collect();
433
434 log::debug!("After renaming: '{}'", tail.display());
435 }
436
437 // The target is the target base appended with `tail`
438 Ok(targetbase.as_ref().join(tail))
439 }
440
441 fn get_content<R: Register, O: Operate>(
442 &self,
443 registry: &Rc<R>,
444 group: &Rc<Group<O>>,
445 ) -> Result<Vec<u8>> {
446 let name = self.to_string_lossy();
447 if group.is_renderable() {
448 registry.get(&name)
449 } else {
450 Ok(std::fs::read(self)?)
451 }
452 }
453
454 /// Populate this item with given group config. The given group config is
455 /// expected to be the group where this item belongs to.
456 fn populate<T: Register>(&self, group: Rc<LocalGroup>, registry: Rc<T>) -> Result<()> {
457 // Create possibly missing parent directories along target's path.
458 let tpath = self.to_owned().make_target(
459 &group.get_hostname_sep(),
460 &group.base,
461 &group.target,
462 group.get_renaming_rules(),
463 )?;
464 let tparent = tpath.parent().unwrap().to_owned();
465 if tparent.has_file_as_parent() {
466 return Err(AppError::PathError(format!(
467 "target path's parent '{}' contains one or more file components thus can not be created as a directory",
468 tparent.display()
469 )));
470 }
471 std::fs::create_dir_all(tparent)?;
472 if group.target.canonicalize()? == group.base.canonicalize()? {
473 return Err(AppError::PathError(format!(
474 "base directory and its target point to the same path in group '{}'",
475 group.name,
476 )));
477 }
478
479 match group.get_method() {
480 SyncMethod::Copy => {
481 // `self` is _always_ a file. If its target path `tpath` is a
482 // directory, we should return an error.
483 if tpath.is_dir() {
484 return Err(AppError::SyncingError(format!(
485 "a directory '{}' exists at the target path of a source file '{}'",
486 tpath.display(),
487 self.display(),
488 )));
489 }
490 if tpath.is_symlink() {
491 log::debug!(
492 "SYNC::COPY [{}]> '{}' is a symlink, removing it",
493 group.name,
494 tpath.display(),
495 );
496 std::fs::remove_file(&tpath)?;
497 }
498
499 // Get content of this item
500 let src_content: Vec<u8> = self.get_content(®istry, &group)?;
501
502 if let Ok(dest_content) = std::fs::read(&tpath) {
503 // Check target file's contents, if it has identical
504 // contents as self, there is no need to write to it.
505 if src_content == dest_content {
506 log::debug!(
507 "SYNC::COPY::SKIP [{}]> '{}' has identical content as '{}'",
508 group.name,
509 tpath.display(),
510 self.display(),
511 );
512 } else if std::fs::write(&tpath, &src_content).is_err() {
513 // Contents of target file differs from content of
514 // self, but writing to it failed. It might be due to
515 // target file being readonly. Attempt to remove it
516 // and try again.
517 log::warn!(
518 "SYNC::COPY::OVERWRITE [{}]> '{}' seems to be readonly, trying to remove it first ..",
519 group.name,
520 tpath.display(),
521 );
522 std::fs::remove_file(&tpath)?;
523 log::debug!(
524 "SYNC::COPY::OVERWRITE [{}]> '{}' => '{}'",
525 group.name,
526 self.display(),
527 tpath.display(),
528 );
529 std::fs::write(&tpath, src_content)?;
530 }
531 } else if tpath.exists() {
532 // If read of target file failed but it does exist, then
533 // the target file is probably unreadable. Attempt to
534 // remove it first, then write contents to `tpath`.
535 log::warn!(
536 "SYNC::COPY::OVERWRITE [{}]> Could not read content of target file ('{}'), trying to remove it first ..",
537 group.name,
538 tpath.display(),
539 );
540 std::fs::remove_file(&tpath)?;
541 log::debug!(
542 "SYNC::COPY::OVERWRITE [{}]> '{}' => '{}'",
543 group.name,
544 self.display(),
545 tpath.display(),
546 );
547 std::fs::write(&tpath, src_content)?;
548 }
549 // If the target file does not exist --- this is the simplest
550 // case --- we just write the contents to `tpath`.
551 else {
552 log::debug!(
553 "SYNC::COPY [{}]> '{}' => '{}'",
554 group.name,
555 self.display(),
556 tpath.display(),
557 );
558 std::fs::write(&tpath, src_content)?;
559 }
560
561 // Copy permissions to target if permission bits do not match.
562 let src_perm = self.metadata()?.permissions();
563 let dest_perm = tpath.metadata()?.permissions();
564 if dest_perm != src_perm {
565 log::debug!(
566 "SYNC::COPY::SETPERM [{}]> source('{:o}') => target('{:o}')",
567 group.name,
568 src_perm.mode(),
569 dest_perm.mode()
570 );
571 if let Err(e) = std::fs::set_permissions(tpath, src_perm) {
572 log::warn!("'{}': Could not set permission: {}", self.display(), e,);
573 }
574 }
575 }
576 SyncMethod::Symlink => {
577 let staging_path = self.to_owned().make_target(
578 &group.get_hostname_sep(),
579 &group.base,
580 &group.get_staging_dir(),
581 Vec::new(), // Do not apply renaming on staging path
582 )?;
583 let sparent = staging_path.parent().unwrap().to_owned();
584 if sparent.has_file_as_parent() {
585 return Err(AppError::PathError(format!(
586 "staging path's parent '{}' contains one or more file component thus can not be created as a directory",
587 sparent.display()
588 )));
589 }
590 std::fs::create_dir_all(sparent)?;
591 if group.global.staging.0.canonicalize()? == group.base.canonicalize()? {
592 return Err(AppError::PathError(format!(
593 "base directory and its target point to the same path in group '{}'",
594 group.name,
595 )));
596 }
597 if group.global.staging.0.canonicalize()? == group.target.canonicalize()? {
598 return Err(AppError::PathError(format!(
599 "target directory and staging directory point to the same path in group '{}'",
600 group.name,
601 )));
602 }
603
604 // `self` is _always_ a file. If its target path `tpath` is a
605 // directory, we should return an error.
606 if tpath.is_dir() {
607 return Err(AppError::SyncingError(format!(
608 "a directory '{}' exists at the target path of a source file '{}'",
609 tpath.display(),
610 self.display(),
611 )));
612 }
613
614 if tpath.exists() && !group.is_overwrite_allowed() {
615 log::warn!(
616 "SYNC::SKIP [{}]> Target path ('{}') exists while `allow_overwrite` is set to false",
617 group.name,
618 tpath.display(),
619 );
620 } else {
621 // In this block, either:
622 //
623 // - `tpath` does not exist
624 // - `allow_overwrite` is true
625 //
626 // or both are true.
627 //
628 // 1. Staging:
629 //
630 // Check if the content of destination is already the
631 // same as source first. When the file is large, this
632 // operation is significantly faster than copying to an
633 // existing target file.
634
635 // Get content of this item
636 let src_content: Vec<u8> = self.get_content(®istry, &group)?;
637
638 if let Ok(dest_content) = std::fs::read(&staging_path) {
639 // Check staging file's contents, if it has identical
640 // contents as self, there is no need to write to it.
641 if src_content == dest_content {
642 log::debug!(
643 "SYNC::STAGE::SKIP [{}]> '{}' has identical content as '{}'",
644 group.name,
645 staging_path.display(),
646 self.display(),
647 );
648 } else if std::fs::write(&staging_path, &src_content).is_err() {
649 // Contents of staging file differs from content
650 // of self, but writing to it failed. It might be
651 // due to staging file being readonly. Attempt to
652 // remove it and try again.
653 log::warn!(
654 "SYNC::STAGE::OVERWRITE [{}]> '{}' seems to be readonly, trying to remove it first ..",
655 group.name,
656 staging_path.display(),
657 );
658 std::fs::remove_file(&staging_path)?;
659 log::debug!(
660 "SYNC::STAGE [{}]> '{}' => '{}'",
661 group.name,
662 self.display(),
663 staging_path.display(),
664 );
665 std::fs::write(&staging_path, src_content)?;
666 }
667 } else if staging_path.exists() {
668 // If read of staging file failed but it does exist,
669 // then the staging file is probably unreadable.
670 // Attempt to remove it first, then write contents to
671 // `staging_path`.
672 log::warn!(
673 "SYNC::STAGE::OVERWRITE [{}]> Could not read content of staging file ('{}'), trying to remove it first ..",
674 group.name,
675 staging_path.display(),
676 );
677 std::fs::remove_file(&staging_path)?;
678 log::debug!(
679 "SYNC::STAGE::OVERWRITE [{}]> '{}' => '{}'",
680 group.name,
681 self.display(),
682 staging_path.display(),
683 );
684 std::fs::write(&staging_path, src_content)?;
685 }
686 // If the staging file does not exist --- this is the
687 // simplest case --- we just write the contents to
688 // `staging_path`.
689 else {
690 log::debug!(
691 "SYNC::STAGE [{}]> '{}' => '{}'",
692 group.name,
693 self.display(),
694 staging_path.display(),
695 );
696 std::fs::write(&staging_path, src_content)?;
697 }
698
699 // Copy permissions to staging file if permission bits do
700 // not match.
701 let src_perm = self.metadata()?.permissions();
702 let dest_perm = staging_path.metadata()?.permissions();
703 if dest_perm != src_perm {
704 log::debug!(
705 "SYNC::STAGE::SETPERM [{}]> source('{:o}') => staging('{:o}')",
706 group.name,
707 src_perm.mode(),
708 dest_perm.mode()
709 );
710 if let Err(e) = std::fs::set_permissions(&staging_path, src_perm) {
711 log::warn!("'{}': Could not set permission: {}", self.display(), e,);
712 }
713 }
714
715 // 2. Symlinking
716 //
717 // Do not remove target file if it is already a symlink
718 // that points to the correct location.
719 if let Ok(dest) = std::fs::read_link(&tpath) {
720 if dest == staging_path {
721 log::debug!(
722 "SYNC::SYMLINK::SKIP [{}]> '{}' is already a symlink pointing to '{}'",
723 group.name,
724 tpath.display(),
725 staging_path.display(),
726 );
727 } else {
728 log::debug!(
729 "SYNC::SYMLINK::OVERWRITE [{}]> '{}' => '{}'",
730 group.name,
731 staging_path.display(),
732 tpath.display(),
733 );
734 std::fs::remove_file(&tpath)?;
735 std::os::unix::fs::symlink(&staging_path, &tpath)?;
736 }
737 }
738 // If target file exists but is not a symlink, try to
739 // remove it first, then make a symlink from
740 // `staging_path` to `tpath`.
741 else if tpath.exists() {
742 log::debug!(
743 "SYNC::SYMLINK::OVERWRITE [{}]> '{}' => '{}'",
744 group.name,
745 staging_path.display(),
746 tpath.display(),
747 );
748 std::fs::remove_file(&tpath)?;
749 std::os::unix::fs::symlink(&staging_path, &tpath)?;
750 }
751 // The final case is that when `tpath` does not exist
752 // yet, we can then directly create a symlink.
753 else {
754 log::debug!(
755 "SYNC::SYMLINK [{}]> '{}' => '{}'",
756 group.name,
757 staging_path.display(),
758 tpath.display(),
759 );
760 std::os::unix::fs::symlink(&staging_path, &tpath)?;
761 }
762 }
763 }
764 }
765
766 Ok(())
767 }
768
769 /// Show what is to be done if this item is to be populated with given
770 /// group config. The given group config is expected to be the group
771 /// where this item belongs to.
772 fn populate_dry(&self, group: Rc<LocalGroup>) -> Result<()> {
773 let tpath = self.to_owned().make_target(
774 &group.get_hostname_sep(),
775 &group.base,
776 &group.target,
777 group.get_renaming_rules(),
778 )?;
779 if tpath.exists() {
780 if group.is_overwrite_allowed() {
781 if tpath.is_dir() {
782 log::error!(
783 "DRYRUN [{}]> A directory ('{}') exists at the target path of a source file ('{}')",
784 group.name,
785 tpath.display(),
786 self.display(),
787 );
788 } else {
789 log::debug!(
790 "DRYRUN [{}]> '{}' -> '{}'",
791 group.name,
792 self.display(),
793 tpath.display(),
794 );
795 }
796 } else {
797 log::error!(
798 "DRYRUN [{}]> Target path ('{}') exists while `allow_overwrite` is set to false",
799 group.name,
800 tpath.display(),
801 );
802 }
803 } else {
804 log::debug!(
805 "DRYRUN [{}]> '{}' -> '{}'",
806 group.name,
807 self.display(),
808 tpath.display(),
809 );
810 }
811
812 Ok(())
813 }
814}
815
816impl Operate for Url {}
817
818// Author: Blurgy <gy@blurgy.xyz>
819// Date: Oct 29 2021, 22:56 [CST]