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(&registry, &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(&registry, &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]