1mod 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
50pub trait TendrilsApi {
55 fn get_default_repo_path(&self) -> Result<Option<PathBuf>, GetConfigError>;
61
62 fn get_default_profiles(&self) -> Result<Option<Vec<String>>, GetConfigError>;
67
68 fn init_tendrils_repo(&self, dir: &UniPath, force: bool) -> Result<(), InitError>;
77
78 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 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 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 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 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, ©_opts), to_existed) {
333 (Ok(_v), true) => Ok(TendrilActionSuccess::Overwrite),
334 (Ok(_v), false) => Ok(TendrilActionSuccess::New),
335 (Err(e), _) => match e.kind {
336 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
390fn 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
425fn 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 #[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, 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 format!("{:?}", e_kind).contains("ReadOnlyFilesystem")
496}
497
498fn 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), );
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 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), );
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), );
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
709fn 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 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}