Skip to main content

tendrils_core/
lib.rs

1//! - Provides core functionality for the [`tendrils-cli`](https://crates.io/crates/tendrils-cli) crate and its `td` CLI tool
2//! - See documentation at <https://github.com/TendrilApps/tendrils-cli>
3
4mod config;
5mod enums;
6use config::{get_config, LazyCachedGlobalConfig};
7pub use enums::{
8    ActionMode,
9    ConfigType,
10    FsoType,
11    GetConfigError,
12    GetTendrilsRepoError,
13    InitError,
14    InvalidTendrilError,
15    Location,
16    TendrilActionError,
17    TendrilActionSuccess,
18    SetupError,
19    TendrilMode,
20};
21mod env_ext;
22use env_ext::can_symlink;
23mod filtering;
24use filtering::filter_tendrils;
25pub use filtering::FilterSpec;
26mod path_ext;
27use path_ext::PathExt;
28pub use path_ext::UniPath;
29use std::fs::{create_dir_all, remove_dir_all, remove_file};
30use std::path::{Path, PathBuf};
31mod tendril;
32use tendril::Tendril;
33pub use tendril::RawTendril;
34mod tendril_report;
35pub use tendril_report::{
36    ActionLog,
37    CallbackUpdater,
38    ListLog,
39    TendrilLog,
40    TendrilReport,
41    UpdateHandler
42};
43
44#[cfg(test)]
45mod tests;
46
47#[cfg(any(test, feature = "_test_utils"))]
48pub mod test_utils;
49
50/// Represents the public Tendrils API.
51/// Although the API functions are not static (i.e. they
52/// require an API instance), this is mainly to facilitate easier mocking
53/// for testing. The actual API implementation should have little to no state.
54pub trait TendrilsApi {
55    /// Returns the `default-repo-path` value stored in
56    /// `~/.tendrils/global-config.json` or any [errors](GetConfigError) that
57    /// occur. Returns `None` if the value is blank or absent, or if the config
58    /// file does not exist. Note: This does *not* check whether the folder
59    /// [is a tendrils repo](`TendrilsApi::is_tendrils_repo`).
60    fn get_default_repo_path(&self) -> Result<Option<PathBuf>, GetConfigError>;
61
62    /// Returns the `default-profiles` stored in
63    /// `~/.tendrils/global-config.json` or any [errors](GetConfigError) that
64    /// occur. Returns `None` if the value is blank or absent, or if the config
65    /// file does not exist.
66    fn get_default_profiles(&self) -> Result<Option<Vec<String>>, GetConfigError>;
67
68    /// Initializes a Tendrils repo with a `.tendrils` folder and a
69    /// pre-populated `tendrils.json` file. This will fail if the folder is
70    /// already a Tendrils repo or if there are general file-system errors.
71    /// This will also fail if the folder is not empty and `force` is false.
72    ///
73    /// # Arguments
74    /// - `dir` - The folder to initialize
75    /// - `force` - Ignores the [`InitError::NotEmpty`] error
76    fn init_tendrils_repo(&self, dir: &UniPath, force: bool) -> Result<(), InitError>;
77
78    /// Returns `true` if the given folder is a Tendrils repo, otherwise
79    /// `false`.
80    /// - A Tendrils repo is defined by having a `.tendrils` subfolder with
81    /// a `tendrils.json` file in it.
82    /// - Note: This does *not* check that the `tendrils.json` contents are valid.
83    fn is_tendrils_repo(&self, dir: &UniPath) -> bool;
84
85    fn list_tendrils(
86        &self,
87        td_repo: Option<&UniPath>,
88        filter: FilterSpec,
89    ) -> Result<Vec<TendrilReport<ListLog>>, SetupError>;
90
91    /// Reads the `tendrils.json` file in the given Tendrils repo, and
92    /// performs the action on each tendril that matches the
93    /// filter.
94    ///
95    /// The order of the actions maintains the order defined in
96    /// the `tendrils.json`, but each tendril set is expanded into individual tendrils for
97    /// each of its `remotes`. For example, for a
98    /// list of two tendril sets [t1, t2], each having multiple remotes [r1, r2], the
99    /// list will be expanded to:
100    /// - t1_r1
101    /// - t1_r2
102    /// - t2_r1
103    /// - t2_r2
104    ///
105    /// # Arguments
106    /// - `updater` - [`UpdateHandler`] to provide synchronous progress updates
107    /// to the caller.
108    /// - `mode` - The action mode to be performed.
109    /// - `td_repo` - The Tendrils repo to perform the actions on. If given
110    /// `None`, the [default repo](`TendrilsApi::get_default_repo_path`) will be checked for a
111    /// valid Tendrils repo. If neither the given `td_repo` or the default
112    /// folder are valid Tendrils folders, a
113    /// [`SetupError::NoValidTendrilsRepo`] is returned.
114    /// - `filter` - Only tendrils matching this filter will be included.
115    /// - `dry_run`
116    ///     - `true` will perform the internal checks for the action but does not
117    /// modify anything on the file system. If the action is expected to fail, the
118    /// expected [`TendrilActionError`] is returned. If it's expected to succeed,
119    /// it returns [`TendrilActionSuccess::NewSkipped`] or
120    /// [`TendrilActionSuccess::OverwriteSkipped`]. Note: It is still possible
121    /// for a successful dry run to fail in an actual run.
122    ///     - `false` will perform the action normally (modifying the file system),
123    /// and will return [`TendrilActionSuccess::New`] or
124    /// [`TendrilActionSuccess::Overwrite`] if successful.
125    /// - `force`
126    ///     - `true` will ignore any type mismatches and will force the operation.
127    ///     - `false` will simply return [`TendrilActionError::TypeMismatch`] if
128    /// there is a type mismatch.
129    ///
130    /// # Returns
131    /// A [`TendrilReport`] containing an [`ActionLog`] for each tendril action.
132    /// Returns a [`SetupError`] if there are any issues in setting up the
133    /// batch of actions.
134    fn tendril_action_updating<U> (
135        &self,
136        updater: U,
137        mode: ActionMode,
138        td_repo: Option<&UniPath>,
139        filter: FilterSpec,
140        dry_run: bool,
141        force: bool,
142    )
143    -> Result<(), SetupError>
144    where
145        U: UpdateHandler<ActionLog>;
146
147    /// Same behaviour as [`tendril_action_updating`](`TendrilsApi::tendril_action_updating`) except reports are only
148    /// returned once all actions have completed.
149    fn tendril_action(
150        &self,
151        mode: ActionMode,
152        td_repo: Option<&UniPath>,
153        filter: FilterSpec,
154        dry_run: bool,
155        force: bool,
156    ) -> Result<Vec<TendrilReport<ActionLog>>, SetupError>;
157}
158
159pub struct TendrilsActor {}
160
161impl TendrilsApi for TendrilsActor {
162    fn get_default_repo_path(&self) -> Result<Option<PathBuf>, GetConfigError> {
163        Ok(config::get_global_config()?.default_repo_path)
164    }
165
166    fn get_default_profiles(&self) -> Result<Option<Vec<String>>, GetConfigError> {
167        Ok(config::get_global_config()?.default_profiles)
168    }
169
170    fn init_tendrils_repo(&self, dir: &UniPath, force: bool) -> Result<(), InitError> {
171        if !dir.inner().exists() {
172            return Err(InitError::IoError { kind: std::io::ErrorKind::NotFound });
173        }
174        else if is_tendrils_repo(dir) {
175            return Err(InitError::AlreadyInitialized);
176        }
177        else if !force && std::fs::read_dir(dir.inner())?.count() > 0 {
178            return Err(InitError::NotEmpty);
179        }
180
181        let td_dot_json_dir = dir.inner().join(".tendrils");
182        let td_json_file = td_dot_json_dir.join("tendrils.json");
183        if !td_dot_json_dir.exists() {
184            std::fs::create_dir(td_dot_json_dir)?;
185        }
186        Ok(std::fs::write(td_json_file, INIT_TD_TENDRILS_JSON)?)
187    }
188
189    fn is_tendrils_repo(&self, dir: &UniPath) -> bool {
190        is_tendrils_repo(dir)
191    }
192
193    fn list_tendrils(
194        &self,
195        td_repo: Option<&UniPath>,
196        filter: FilterSpec,
197    ) -> Result<Vec<TendrilReport<ListLog>>, SetupError> {
198        let mut global_cfg = LazyCachedGlobalConfig::new();
199        let td_repo= get_tendrils_repo(td_repo, &mut global_cfg)?;
200        let all_tendrils = get_config(&td_repo)?.raw_tendrils;
201        let filtered_tendrils =
202            filter_tendrils(all_tendrils, filter, &mut global_cfg);
203
204        let reports = list_tendrils_inner(&td_repo, filtered_tendrils);
205        Ok(reports)
206    }
207
208    fn tendril_action_updating<U>(
209        &self,
210        updater: U,
211        mode: ActionMode,
212        td_repo: Option<&UniPath>,
213        filter: FilterSpec,
214        dry_run: bool,
215        force: bool,
216    ) -> Result<(), SetupError>
217    where
218        U: UpdateHandler<ActionLog>,
219    {
220        let mut global_cfg = LazyCachedGlobalConfig::new();
221        let td_repo= get_tendrils_repo(td_repo, &mut global_cfg)?;
222        let config = config::get_config(&td_repo)?;
223        let all_tendrils = config.raw_tendrils;
224
225        let mut filtered_tendrils =
226            filter_tendrils(all_tendrils, filter, &mut global_cfg);
227        if mode == ActionMode::Pull {
228            // Do not attempt to pull link-style tendrils
229            filtered_tendrils = filtered_tendrils.into_iter().filter(|t| !t.mode.requires_symlink()).collect();
230        }
231        if mode == ActionMode::Push
232            && filtered_tendrils.iter().any(|t| t.mode.requires_symlink())
233            && !can_symlink() {
234            // Do not continue if any symlinks are expected to fail
235            return Err(SetupError::CannotSymlink);
236        }
237
238        batch_tendril_action(updater, mode, &td_repo, filtered_tendrils, dry_run, force);
239        Ok(())
240    }
241
242    fn tendril_action(
243        &self,
244        mode: ActionMode,
245        td_repo: Option<&UniPath>,
246        filter: FilterSpec,
247        dry_run: bool,
248        force: bool,
249    ) -> Result<Vec<TendrilReport<ActionLog>>, SetupError> {
250        let mut reports = vec![];
251        let count_fn = |_| {};
252        let before_action_fn = |_| {};
253        let after_action_fn = |r| reports.push(r);
254        let updater = CallbackUpdater::<_, _, _, ActionLog>::new(
255            count_fn,
256            before_action_fn,
257            after_action_fn,
258        );
259
260        self.tendril_action_updating(updater, mode, td_repo, filter, dry_run, force)?;
261        Ok(reports)
262    }
263}
264
265const INIT_TD_TENDRILS_JSON: &str = r#"{
266    "tendrils": {
267        "SomeApp/SomeFile.ext": {
268            "remotes": "/path/to/SomeFile.ext"
269        },
270        "SomeApp2/SomeFolder": {
271            "remotes": [
272                "/path/to/SomeFolder",
273                "/path/to/DifferentName",
274                "~/path/in/home/dir/SomeFolder",
275                "/path/using/<MY-ENV-VAR>/SomeFolder"
276            ],
277            "dir-merge": false,
278            "link": true,
279            "profiles": ["home", "work"]
280        },
281        "SomeApp3/file.txt": [
282            {
283                "remotes": "~/unix/specific/path/file.txt",
284                "link": true,
285                "profiles": "unix"
286            },
287            {
288                "remotes": [
289                    "~/windows/specific/path/file.txt",
290                    "~/windows/another-specific/path/file.txt"
291                ],
292                "link": false,
293                "profiles": "windows"
294            }
295        ]
296    }
297}
298"#;
299
300fn is_tendrils_repo(dir: &UniPath) -> bool {
301    dir.inner().join(".tendrils/tendrils.json").is_file()
302}
303
304fn copy_fso(
305    from: &Path,
306    from_type: &Option<FsoType>,
307    mut to: &Path,
308    to_type: &Option<FsoType>,
309    dir_merge: bool,
310    dry_run: bool,
311    force: bool,
312) -> Result<TendrilActionSuccess, TendrilActionError> {
313    use std::io::ErrorKind::{NotFound, PermissionDenied};
314    let to_existed = to_type.is_some();
315
316    check_copy_types(from_type, to_type, force)?;
317
318    match (dry_run, to_existed) {
319        (true, true) => return Ok(TendrilActionSuccess::OverwriteSkipped),
320        (true, false) => return Ok(TendrilActionSuccess::NewSkipped),
321        _ => {}
322    }
323    match from_type {
324        Some(FsoType::Dir | FsoType::SymDir | FsoType::BrokenSym) => {
325            prepare_dest(to, to_type, dir_merge)?;
326
327            to = to.parent().unwrap_or(to);
328
329            let mut copy_opts = fs_extra::dir::CopyOptions::new();
330            copy_opts.overwrite = true;
331            copy_opts.skip_exist = false;
332            match (fs_extra::dir::copy(from, to, &copy_opts), to_existed) {
333                (Ok(_v), true) => Ok(TendrilActionSuccess::Overwrite),
334                (Ok(_v), false) => Ok(TendrilActionSuccess::New),
335                (Err(e), _) => match e.kind {
336                    // Convert fs_extra::errors
337                    fs_extra::error::ErrorKind::Io(e) => {
338                        if is_rofs_err(&e.kind()) {
339                            Err(TendrilActionError::IoError {
340                                kind: e.kind(),
341                                loc: Location::Dest,
342                            })
343                        }
344                        else {
345                            Err(TendrilActionError::from(e))
346                        }
347                    }
348                    fs_extra::error::ErrorKind::PermissionDenied => {
349                        let loc = which_copy_perm_failed(to);
350                        Err(TendrilActionError::IoError {
351                            kind: PermissionDenied,
352                            loc,
353                        })
354                    }
355                    _ => {
356                        Err(TendrilActionError::from(std::io::ErrorKind::Other))
357                    }
358                },
359            }
360        }
361        Some(FsoType::File | FsoType::SymFile) => {
362            prepare_dest(to, to_type, false)?;
363
364            match (std::fs::copy(from, to), to_existed) {
365                (Ok(_v), true) => Ok(TendrilActionSuccess::Overwrite),
366                (Ok(_v), false) => Ok(TendrilActionSuccess::New),
367                (Err(e), _) if e.kind() == PermissionDenied => {
368                    let loc = which_copy_perm_failed(to);
369                    Err(TendrilActionError::IoError {
370                        kind: PermissionDenied,
371                        loc,
372                    })
373                }
374                (Err(e), _) if is_rofs_err(&e.kind()) => {
375                    Err(TendrilActionError::IoError {
376                        kind: e.kind(),
377                        loc: Location::Dest,
378                    })
379                }
380                (Err(e), _) => Err(TendrilActionError::from(e)),
381            }
382        }
383        None => Err(TendrilActionError::IoError {
384            kind: NotFound,
385            loc: Location::Source,
386        }),
387    }
388}
389
390/// Returns [`Err(TendrilActionError::TypeMismatch)`](TendrilActionError::TypeMismatch)
391/// if the type (file vs folder) of the source and destination are mismatched,
392/// or if either the source or destination are symlinks. If `force` is true,
393/// type mismatches are ignored.
394/// Returns an [`Err(TendrilActionError::IoError)`](TendrilActionError::IoError)
395/// if the `source` does not exist.
396/// Otherwise, returns `Ok(())`.
397///
398/// No other invariants of [`TendrilActionError`] are returned.
399///
400/// Note: This is not applicable in link mode - see [`check_symlink_types`]
401/// instead.
402fn check_copy_types(
403    source: &Option<FsoType>,
404    dest: &Option<FsoType>,
405    force: bool,
406) -> Result<(), TendrilActionError> {
407    match (source, dest) {
408        (None | Some(FsoType::BrokenSym), _) => Err(TendrilActionError::IoError {
409            kind: std::io::ErrorKind::NotFound,
410            loc: Location::Source,
411        }),
412        (_, _) if force => Ok(()),
413        (Some(s), _) if s.is_symlink() => Err(TendrilActionError::TypeMismatch {
414            loc: Location::Source,
415            mistype: s.to_owned(),
416        }),
417        (Some(s), Some(d)) if s != d => Err(TendrilActionError::TypeMismatch {
418            loc: Location::Dest,
419            mistype: d.to_owned(),
420        }),
421        (Some(_), _) => Ok(()),
422    }
423}
424
425/// Prepares the destination before copying a file system object
426/// to it
427fn prepare_dest(
428    dest: &Path,
429    dest_type: &Option<FsoType>,
430    dir_merge: bool,
431) -> Result<(), TendrilActionError> {
432    match (dest_type, dir_merge) {
433        (Some(d), false) if d.is_dir() => {
434            if let Err(e) = remove_dir_all(dest) {
435                return Err(TendrilActionError::IoError {
436                    kind: e.kind(),
437                    loc: Location::Dest,
438                });
439            }
440        }
441        (Some(d), _) if d.is_file() => {
442            if let Err(e) = remove_file(dest) {
443                return Err(TendrilActionError::IoError {
444                    kind: e.kind(),
445                    loc: Location::Dest,
446                });
447            }
448        }
449        (Some(FsoType::BrokenSym), _) => remove_symlink(&dest)?,
450        (_, _) => {},
451    };
452
453    match create_dir_all(dest.parent().unwrap_or(dest)) {
454        Err(e) => Err(TendrilActionError::IoError {
455            kind: e.kind(),
456            loc: Location::Dest,
457        }),
458        _ => Ok(()),
459    }
460}
461
462fn remove_symlink(path: &Path) -> Result<(), std::io::Error> {
463    // Since there's no easy way to determine the type of a broken symlink,
464    // just try deleting it as a file then fall back to deleting it as a
465    // directory.
466    // Another potential option:
467    // https://gitlab.com/chris-morgan/symlink/-/blob/master/src/windows/mod.rs?ref_type=heads
468    #[cfg(windows)]
469    if remove_file(path).is_err() {
470        remove_dir_all(path)
471    }
472    else {
473        Ok(())
474    }
475
476    #[cfg(not(windows))]
477    remove_file(&path)
478}
479
480fn which_copy_perm_failed(to: &Path) -> Location {
481    match to.parent() {
482        Some(p) if p.parent().is_none() => Location::Dest, // Is root
483        Some(p) => match p.metadata() {
484            Ok(md) if md.permissions().readonly() => Location::Dest,
485            Ok(_) => Location::Source,
486            _ => Location::Unknown,
487        },
488        None => Location::Dest,
489    }
490}
491
492fn is_rofs_err(e_kind: &std::io::ErrorKind) -> bool {
493    // Possible bug where the std::io::ErrorKind::ReadOnlyFilesystem
494    // is only available in nightly but is being returned on Mac
495    format!("{:?}", e_kind).contains("ReadOnlyFilesystem")
496}
497
498/// Looks for a Tendrils repo (as defined by [`TendrilsApi::is_tendrils_repo`])
499/// - If given a `starting_path`, it begins looking in that folder.
500///     - If it is a Tendrils repo, `starting_path` is returned
501///     - Otherwise [`GetTendrilsRepoError::GivenInvalid`] is returned.
502/// - If a `starting_path` is not provided, the
503/// [default repo](`TendrilsApi::get_default_repo_path`) is used.
504///     - If it points to a valid repo, that path is returned
505///     - If it points to an invalid folder,
506/// [`GetTendrilsRepoError::DefaultInvalid`] is returned
507///     - If it is not set,
508/// [`GetTendrilsRepoError::DefaultNotSet`] is returned.
509// TODO: Recursively look through all parent folders before
510// checking global config?
511fn get_tendrils_repo(
512    starting_path: Option<&UniPath>,
513    global_cfg: &mut LazyCachedGlobalConfig,
514) -> Result<UniPath, GetTendrilsRepoError> {
515    match starting_path {
516        Some(v) => {
517            if is_tendrils_repo(&v) {
518                Ok(v.to_owned())
519            }
520            else {
521                Err(GetTendrilsRepoError::GivenInvalid {
522                    path: PathBuf::from(v.inner()),
523                })
524            }
525        }
526        None => match global_cfg.eval()?.default_repo_path {
527            Some(v) => {
528                let u_path = UniPath::from(v);
529                if is_tendrils_repo(&u_path) {
530                    Ok(u_path)
531                }
532                else {
533                    Err(GetTendrilsRepoError::DefaultInvalid {
534                        path: PathBuf::from(u_path.inner()),
535                    })
536                }
537            }
538            None => Err(GetTendrilsRepoError::DefaultNotSet),
539        }
540    }
541}
542
543fn link_tendril(
544    tendril: &Tendril,
545    dry_run: bool,
546    mut force: bool,
547) -> ActionLog {
548    let target = tendril.local_abs();
549    let create_at = tendril.remote().inner();
550
551    let mut log = ActionLog::new(
552        target.get_type(),
553        create_at.get_type(),
554        create_at.to_path_buf(),
555        Ok(TendrilActionSuccess::New), // Init only value
556    );
557    if tendril.mode != TendrilMode::Link {
558        log.result = Err(TendrilActionError::ModeMismatch);
559        return log;
560    }
561
562    let local_type;
563    if log.local_type().is_none()
564        || log.local_type() == &Some(FsoType::BrokenSym) {
565        if log.local_type() == &Some(FsoType::BrokenSym) {
566            if force {
567                if !dry_run {
568                    if let Err(e) = remove_symlink(&target) {
569                        log.result = Err(e.into());
570                        return log;
571                    }
572                }
573            }
574            else {
575                log.result = Err(TendrilActionError::TypeMismatch {
576                    mistype: FsoType::BrokenSym,
577                    loc: Location::Source
578                });
579                return log;
580            }
581        }
582
583        // Local does not exist - copy it first
584        if let Err(e) = copy_fso(
585            log.resolved_path(),
586            log.remote_type(),
587            &target,
588            &None,
589            false,
590            dry_run,
591            false,
592        ) {
593            log.result = Err(e);
594            return log;
595        };
596        local_type = log.remote_type();
597        force = true;
598    }
599    else {
600        local_type = log.local_type();
601    }
602
603    log.result = symlink(
604        log.resolved_path(),
605        log.remote_type(),
606        &target,
607        local_type,
608        dry_run,
609        force,
610    );
611
612    log
613}
614
615fn list_tendrils_inner(
616    td_repo: &UniPath,
617    raw_tendrils: Vec<RawTendril>,
618) -> Vec<TendrilReport<ListLog>> {
619    let mut reports = Vec::with_capacity(raw_tendrils.len());
620
621    for raw_tendril in raw_tendrils {
622        let log = match raw_tendril.resolve(&td_repo) {
623            Ok(v) => {
624                Ok(ListLog::new(
625                    v.local_abs().get_type(),
626                    v.remote().inner().get_type(),
627                    v.remote().inner().into()
628                ))
629            }
630            Err(e) => Err(e),
631        };
632
633        reports.push(TendrilReport {
634            raw_tendril,
635            log,
636        });
637    }
638
639    reports
640}
641
642fn pull_tendril(
643    tendril: &Tendril,
644    dry_run: bool,
645    force: bool,
646) -> ActionLog {
647    let dest = tendril.local_abs();
648    let source = tendril.remote().inner();
649
650    let mut log = ActionLog::new(
651        dest.get_type(),
652        source.get_type(),
653        source.to_path_buf(),
654        Ok(TendrilActionSuccess::New), // Init only value
655    );
656
657    if tendril.mode == TendrilMode::Link {
658        log.result = Err(TendrilActionError::ModeMismatch);
659        return log;
660    }
661
662    let dir_merge = tendril.mode == TendrilMode::CopyMerge;
663    log.result = copy_fso(
664        log.resolved_path(),
665        log.remote_type(),
666        &dest,
667        log.local_type(),
668        dir_merge,
669        dry_run,
670        force,
671    );
672
673    log
674}
675
676fn push_tendril(
677    tendril: &Tendril,
678    dry_run: bool,
679    force: bool,
680) -> ActionLog {
681    let source = tendril.local_abs();
682    let dest = tendril.remote().inner();
683
684    let mut log = ActionLog::new(
685        source.get_type(),
686        dest.get_type(),
687        dest.to_path_buf(),
688        Ok(TendrilActionSuccess::New), // Init only value
689    );
690    if tendril.mode == TendrilMode::Link {
691        log.result = Err(TendrilActionError::ModeMismatch);
692        return log;
693    }
694
695    let dir_merge = tendril.mode == TendrilMode::CopyMerge;
696    log.result = copy_fso(
697        &source,
698        log.local_type(),
699        log.resolved_path(),
700        log.remote_type(),
701        dir_merge,
702        dry_run,
703        force,
704    );
705
706    log
707}
708
709/// Returns [`Err(TendrilActionError::TypeMismatch)`](TendrilActionError::TypeMismatch)
710/// if the type of the source and destination are mismatched. If `force` is
711/// true, type mismatches are ignored.
712/// Returns an [`Err(TendrilActionError::IoError)`](TendrilActionError::IoError)
713/// if the `target` does not exist.
714/// Otherwise, returns `Ok(())`.
715///
716/// No other invariants of [`TendrilActionError`] are returned.
717///
718/// Note: This is not applicable in copy mode - see [`check_copy_types`]
719/// instead.
720fn check_symlink_types(
721    target: &Option<FsoType>,
722    create_at: &Option<FsoType>,
723    force: bool,
724) -> Result<(), TendrilActionError> {
725    match (target, create_at, force) {
726        (None, _, _) => Err(TendrilActionError::IoError {
727            kind: std::io::ErrorKind::NotFound,
728            loc: Location::Source,
729        }),
730        (Some(FsoType::SymFile), _, false) => {
731            Err(TendrilActionError::TypeMismatch {
732                loc: Location::Source,
733                mistype: FsoType::SymFile,
734            })
735        }
736        (Some(FsoType::SymDir), _, false) => {
737            Err(TendrilActionError::TypeMismatch {
738                loc: Location::Source,
739                mistype: FsoType::SymDir,
740            })
741        }
742        (_, Some(FsoType::File), false) => {
743            Err(TendrilActionError::TypeMismatch {
744                loc: Location::Dest,
745                mistype: FsoType::File,
746            })
747        }
748        (_, Some(FsoType::Dir), false) => {
749            Err(TendrilActionError::TypeMismatch {
750                loc: Location::Dest,
751                mistype: FsoType::Dir,
752            })
753        }
754        _ => Ok(()),
755    }
756}
757
758fn symlink(
759    create_at: &Path,
760    create_at_type: &Option<FsoType>,
761    target: &Path,
762    target_type: &Option<FsoType>,
763    dry_run: bool,
764    force: bool,
765) -> Result<TendrilActionSuccess, TendrilActionError> {
766    check_symlink_types(target_type, create_at_type, force)?;
767
768    let del_result = match (dry_run, &create_at_type) {
769        (true, Some(_)) => return Ok(TendrilActionSuccess::OverwriteSkipped),
770        (true, None) => return Ok(TendrilActionSuccess::NewSkipped),
771        (false, Some(FsoType::File | FsoType::SymFile)) => {
772            remove_file(create_at)
773        }
774        (false, Some(FsoType::BrokenSym)) => {
775            remove_symlink(create_at)
776        }
777        (false, Some(FsoType::Dir | FsoType::SymDir)) => {
778            remove_dir_all(create_at)
779        }
780        (false, None) => Ok(()),
781    };
782    match del_result {
783        Err(e) => Err(TendrilActionError::IoError {
784            kind: e.kind(),
785            loc: Location::Dest,
786        }),
787        _ => Ok(()),
788    }?;
789
790    if let Err(e) = create_dir_all(create_at.parent().unwrap_or(create_at)) {
791        return Err(TendrilActionError::IoError {
792            kind: e.kind(),
793            loc: Location::Dest,
794        });
795    };
796
797    #[cfg(windows)]
798    let sym_result = symlink_win(create_at, target);
799    #[cfg(unix)]
800    let sym_result = symlink_unix(create_at, target);
801    match sym_result {
802        Err(TendrilActionError::IoError { kind: k, loc: _ }) => {
803            Err(TendrilActionError::IoError { kind: k, loc: Location::Dest })
804        }
805        _ => Ok(()),
806    }?;
807
808    if create_at_type.is_none() {
809        Ok(TendrilActionSuccess::New)
810    }
811    else {
812        Ok(TendrilActionSuccess::Overwrite)
813    }
814}
815
816#[cfg(unix)]
817fn symlink_unix(
818    create_at: &Path,
819    target: &Path,
820) -> Result<(), TendrilActionError> {
821    std::os::unix::fs::symlink(target, create_at)?;
822
823    Ok(())
824}
825
826#[cfg(windows)]
827fn symlink_win(
828    create_at: &Path,
829    target: &Path,
830) -> Result<(), TendrilActionError> {
831    use std::os::windows::fs::{symlink_dir, symlink_file};
832
833    if target.is_dir() {
834        symlink_dir(target, create_at)?;
835    }
836    else {
837        symlink_file(target, create_at)?;
838    }
839
840    Ok(())
841}
842
843fn batch_tendril_action<U>(
844    mut updater: U,
845    mode: ActionMode,
846    td_repo: &UniPath,
847    raw_tendrils: Vec<RawTendril>,
848    dry_run: bool,
849    force: bool,
850)
851where
852    U: UpdateHandler<ActionLog>,
853{
854    updater.count(raw_tendrils.len() as i32);
855
856    for raw_tendril in raw_tendrils.into_iter() {
857        updater.before(raw_tendril.clone());
858        let tendril = raw_tendril.resolve(td_repo);
859
860        let log = match (tendril, &mode) {
861            (Ok(v), ActionMode::Pull) => {
862                Ok(pull_tendril(&v, dry_run, force))
863            }
864            (Ok(v), ActionMode::Push) => match v.mode {
865                TendrilMode::Link if !can_symlink() => {
866                    // Do not attempt to symlink if it has already been
867                    // determined that the process
868                    // does not have the required permissions.
869                    // This prevents deleting any of the remote files
870                    // unnecessarily.
871                    let remote = v.remote();
872                    Ok(ActionLog::new(
873                        v.local_abs().get_type(),
874                        remote.inner().get_type(),
875                        remote.inner().to_path_buf(),
876                        Err(TendrilActionError::IoError {
877                            kind: std::io::ErrorKind::PermissionDenied,
878                            loc: Location::Dest,
879                        }),
880                    ))
881                },
882                TendrilMode::Link => {
883                    Ok(link_tendril(&v, dry_run, force))
884                }
885                _ => {
886                    Ok(push_tendril(&v, dry_run, force))
887                },
888            }
889            (Err(e), _) => Err(e),
890        };
891
892        let report = TendrilReport {
893            raw_tendril,
894            log,
895        };
896
897        updater.after(report);
898    }
899}