diff --git a/.Rbuildignore b/.Rbuildignore index 3530468..6589a77 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -7,3 +7,5 @@ ^pkgdown$ ^\.github$ ^README\.Rmd$ +^cran-comments\.md$ +^CRAN-SUBMISSION$ diff --git a/.gitignore b/.gitignore index c39c9d6..a433f8a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ .DS_Store .quarto +inst/doc diff --git a/CRAN-SUBMISSION b/CRAN-SUBMISSION new file mode 100644 index 0000000..c73463d --- /dev/null +++ b/CRAN-SUBMISSION @@ -0,0 +1,3 @@ +Version: 0.1.0 +Date: 2026-01-12 14:34:58 UTC +SHA: 72741d1bba2d7c2eda7b330c0da1263cfd31cb3e diff --git a/DESCRIPTION b/DESCRIPTION index f5cd68f..26c0618 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,23 +1,31 @@ Package: syncdr -Title: Tool for Facilitating Directory Comparison and Updating -Version: 0.0.2.9001 +Title: Facilitate File Handling, Directory Comparison & Synchronization +Version: 0.1.1 Authors@R: - c(person(given = "R.Andres", - family = "Castaneda", - email = "acastanedaa@worldbank.org", - role = c("aut", "cre")), - person(given = "Rossana", - family = "Tatulli", - role = "aut", - email = "rtatulli@worldbank.org") - ) -Description: An R package for directory comparison and updates. It provides tools to facilitate file handling, comparison and synchronization of directories. + c( + person( + given = "R.Andres", + family = "Castaneda", + email = "acastanedaa@worldbank.org", + role = "aut" + ), + person( + given = "Rossana", + family = "Tatulli", + email = "rtatulli@worldbank.org", + role = c("aut", "cre") + ), + person( + given = "Global Poverty and Inequality Data Team", + family = "World Bank", + role = "cph" + ) + ) +Description: Compare directories flexibly (by date, content, or both) and synchronize files efficiently, with asymmetric and symmetric modes, helper tools, and visualization support for file management. License: MIT + file LICENSE Encoding: UTF-8 Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.2 -Depends: - R (>= 2.10) LazyData: true Imports: collapse, @@ -27,17 +35,20 @@ Imports: cli, DT, fs, - joyn (>= 0.1.6.9003), + joyn (>= 0.3.0), stats, knitr, utils, rstudioapi -Remotes: - github::randrescastaneda/joyn@DEV Suggests: fst, + withr, rmarkdown, + mockery, testthat (>= 3.0.0) +Depends: + R (>= 4.1.0) Config/testthat/edition: 3 VignetteBuilder: knitr -URL: https://rossanatat.github.io/syncdr/ +URL: https://rossanatat.github.io/syncdr/, https://github.com/RossanaTat/syncdr +BugReports: https://github.com/RossanaTat/syncdr/issues diff --git a/LICENSE b/LICENSE index d0b6ced..864093c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,2 +1,2 @@ -YEAR: 2024 +YEAR: 2025 COPYRIGHT HOLDER: syncdr authors diff --git a/LICENSE.md b/LICENSE.md index 938d9ed..c9f0a10 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # MIT License -Copyright (c) 2024 syncdr authors +Copyright (c) 2025 syncdr authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..0cc223c --- /dev/null +++ b/NEWS.md @@ -0,0 +1,35 @@ +# syncdr 0.1.1 + +# syncdr 0.1.0 + +Initial CRAN release. + +### Directory comparison +- Added `compare_directories()` to compare two directories by modification date, file content, or date-then-content. +- Detects newer/older/same-date files as well as files present only in one directory. +- Added `print.syncdr_status()` for formatted summaries of comparison results. + +### Visualization +- Added `display_sync_status()` for interactive visualization of comparison output. +- Added `display_dir_tree()` to show directory trees for one or two paths. + +### Asymmetric synchronization (left → right) +- Added `full_asym_sync_to_right()` for complete one-way synchronization. +- Added partial sync helpers: + - `common_files_asym_sync_to_right()` + - `update_missing_files_asym_to_right()` + - `partial_update_missing_files_asym_to_right()` + +### Symmetric synchronization +- Added `full_symmetric_sync()` for two-way synchronization with conflict handling. +- Added `partial_symmetric_sync_common_files()` for partial symmetric updates. + +### Utilities +- Added `toy_dirs()` to create reproducible example directory structures. +- Added `copy_temp_environment()` for safe testing of directory operations. +- Added `search_duplicates()` for content-based duplicate detection. +- Added `save_sync_status()` to persist comparison results. + +### Documentation and tests +- Added introductory and workflow vignettes. +- Added unit tests covering comparison logic, synchronization functions, directory-tree display, and duplicate detection. diff --git a/R/action_functions.R b/R/action_functions.R index 2946715..7204a72 100644 --- a/R/action_functions.R +++ b/R/action_functions.R @@ -36,13 +36,26 @@ copy_files_to_right <- function(left_dir, path_to = right_dir) } - # Copy files - mapply(fs::file_copy, - files_to_copy$path_from, - files_to_copy$path_to, - MoreArgs = list(overwrite = TRUE)) + # Ensure destination subdirectories exist + unique_dirs <- unique(fs::path_dir(files_to_copy$path_to)) + fs::dir_create(unique_dirs) + + # progress-enabled iteration + invisible( + lapply( + cli::cli_progress_along(files_to_copy$path_from, name = "Copying files"), + function(i) { + fs::file_copy( + path = files_to_copy$path_from[i], + new_path = files_to_copy$path_to[i], + overwrite = TRUE + ) + } + ) + ) invisible(TRUE) + } #' Copy files from right directory to left directory @@ -88,11 +101,23 @@ copy_files_to_left <- function(left_dir, } - # Copy files - mapply(fs::file_copy, - files_to_copy$path_from, - files_to_copy$path_to, - MoreArgs = list(overwrite = TRUE)) + # Ensure destination subdirectories exist + unique_dirs <- unique(fs::path_dir(files_to_copy$path_to)) + fs::dir_create(unique_dirs) + + # progress-enabled iteration + invisible( + lapply( + cli::cli_progress_along(files_to_copy$path_from, name = "Copying files"), + function(i) { + fs::file_copy( + path = files_to_copy$path_from[i], + new_path = files_to_copy$path_to[i], + overwrite = TRUE + ) + } + ) + ) invisible(TRUE) } diff --git a/R/asymmetric_sync.R b/R/asymmetric_sync.R index 803553e..bde71c6 100644 --- a/R/asymmetric_sync.R +++ b/R/asymmetric_sync.R @@ -18,6 +18,10 @@ #' @param recurse Logical. If TRUE (default), files are copied to corresponding subdirectories #' in the destination folder. If FALSE, files are copied to the top level of the destination folder #' without creating subdirectories if they do not exist. +#' @param delete_in_right Logical. If TRUE (default), files that exist only in the +#' right directory (i.e., absent from the left directory) are deleted during +#' synchronization. If FALSE, no files are removed from the right directory, +#' even if they are exclusive to it. #' @param force Logical. If TRUE (by default), directly perform synchronization of the directories. #' If FALSE, Displays a preview of actions and prompts the user for confirmation before proceeding. Synchronization is aborted if the user does not agree. #' @param backup Logical. If TRUE, creates a backup of the right directory before synchronization. The backup is stored in the location specified by `backup_dir`. @@ -27,34 +31,30 @@ #' #' @export #' @examples -#' # Create syncdr environment with toy directories -#' library(syncdr) -#' e <- toy_dirs() -#' -#' # Get left and right directories' paths +#' \donttest{ +#' e <- toy_dirs(fast = TRUE) #' left <- e$left #' right <- e$right -#' -#' # Synchronize by date & content -#' # Providing left and right paths to directories, as well as by_date and content -#' full_asym_sync_to_right(left_path = left, -#' right_path = right, -#' by_date = FALSE, -#' by_content = TRUE) -#' # Providing sync_status object -#' #sync_status = compare_directories(left_path = left, -#' # right_path = right) -#' #full_asym_sync_to_right(sync_status = sync_status) -full_asym_sync_to_right <- function(left_path = NULL, - right_path = NULL, - sync_status = NULL, - by_date = TRUE, - by_content = FALSE, - recurse = TRUE, - force = TRUE, - backup = FALSE, - backup_dir = "temp_dir", - verbose = getOption("syncdr.verbose")) { +#' full_asym_sync_to_right( +#' left_path = left, +#' right_path = right, +#' by_date = FALSE, +#' by_content = TRUE +#' ) +#' sync_status <- compare_directories(left_path = left, right_path = right) +#' full_asym_sync_to_right(sync_status = sync_status) +#' } +full_asym_sync_to_right <- function(left_path = NULL, + right_path = NULL, + sync_status = NULL, + by_date = TRUE, + by_content = FALSE, + recurse = TRUE, + force = TRUE, + delete_in_right = TRUE, + backup = FALSE, + backup_dir = "temp_dir", + verbose = getOption("syncdr.verbose")) { # Display folder structure before synchronization @@ -125,7 +125,7 @@ full_asym_sync_to_right <- function(left_path = NULL, # Copy right directory in backup directory if (backup) { - backup_dir <- fifelse(backup_dir == "temp_dir", # the default + backup_dir <- ifelse(backup_dir == "temp_dir", # the default #tempdir(), file.path(tempdir(), @@ -211,8 +211,22 @@ full_asym_sync_to_right <- function(left_path = NULL, recurse = recurse) - ## Delete Files #### - fs::file_delete(files_to_delete$path_right) + ## Delete Files + if (delete_in_right == TRUE) { + if (NROW(files_to_delete) > 0) { + invisible( + lapply( + cli::cli_progress_along( + files_to_delete$path_right, name = "Deleting files" + ), + function(i) fs::file_delete(files_to_delete$path_right[i]) + ) + ) + } else if (verbose) { + cli::cli_alert_info("No files deleted (all excluded or none to delete).") + } + } + if(verbose == TRUE) { @@ -259,20 +273,20 @@ full_asym_sync_to_right <- function(left_path = NULL, #' @return Invisible TRUE indicating successful synchronization. #' @export #' @examples -#' # Compare directories with 'compare_directories()' -#' e <- toy_dirs() +#' # Asymmetric synchronization of common files #' -#' # Get left and right directories' paths +#' \donttest{ +#' e <- toy_dirs() #' left <- e$left #' right <- e$right -#' -#' # Example: Synchronize by content only -#' # Option 1 -#' common_files_asym_sync_to_right(left_path = left, -#' right_path = right, -#' by_date = FALSE, -#' by_content = TRUE) -#' +#' # Synchronize common files by content only +#' common_files_asym_sync_to_right( +#' left_path = left, +#' right_path = right, +#' by_date = FALSE, +#' by_content = TRUE +#' ) +#' } common_files_asym_sync_to_right <- function(left_path = NULL, right_path = NULL, sync_status = NULL, @@ -378,7 +392,7 @@ common_files_asym_sync_to_right <- function(left_path = NULL, if (nrow(files_to_copy) > 0 ) { style_msgs("blue", - text = "These files will be COPIED (overwriting if present) to right \n") + text = "These files will be COPIED (overwriting if present) from left to right \n") display_file_actions(path_to_files = files_to_copy |> fselect(1), directory = left_path, @@ -437,6 +451,15 @@ common_files_asym_sync_to_right <- function(left_path = NULL, #' If recurse is TRUE: when copying a file from source folder to destination folder, the file will be copied into the corresponding (sub)directory. #' If the sub(directory) where the file is located does not exist in destination folder (or you are not sure), set recurse to FALSE, #' and the file will be copied at the top level +#' @param copy_to_right Logical, default is TRUE. +#' If TRUE, files that exist only in the left directory are copied to the right directory. +#' If FALSE, such files are not copied and remain absent from the right directory. +#' +#' @param delete_in_right Logical, default is TRUE. +#' If TRUE, files that exist only in the right directory (i.e., not present in the left) are deleted. +#' If FALSE, these right-only files are preserved. +#' @param exclude_delete Character vector of file names or dir names to protect from deletion. +#' These files will be kept in the right directory even if `delete = TRUE`. #' @param force Logical. If TRUE (by default), directly perform synchronization of the directories. #' If FALSE, Displays a preview of actions and prompts the user for confirmation before proceeding. Synchronization is aborted if the user does not agree. # @@ -446,21 +469,26 @@ common_files_asym_sync_to_right <- function(left_path = NULL, #' @return Invisible TRUE indicating successful synchronization. #' @export #' @examples -#' # Compare directories with 'compare_directories()' +#' # Create a temporary synchronization environment +#' \donttest{ #' e <- toy_dirs() -#' -#' # Get left and right directories' paths #' left <- e$left #' right <- e$right #' -#' # Option 1 -#' update_missing_files_asym_to_right(left_path = left, -#' right_path = right) -#' # Option 2 -#' sync_status = compare_directories(left, -#' right) +#' # Update missing files asymmetrically (left → right) +#' # Option 1: provide left and right paths +#' update_missing_files_asym_to_right( +#' left_path = left, +#' right_path = right +#' ) #' +#' # Option 2: provide a precomputed sync_status object +#' sync_status <- compare_directories( +#' left_path = left, +#' right_path = right +#' ) #' update_missing_files_asym_to_right(sync_status = sync_status) +#' } update_missing_files_asym_to_right <- function(left_path = NULL, right_path = NULL, sync_status = NULL, @@ -468,6 +496,9 @@ update_missing_files_asym_to_right <- function(left_path = NULL, force = TRUE, backup = FALSE, backup_dir = "temp_dir", + copy_to_right = TRUE, + delete_in_right = TRUE, + exclude_delete = NULL, verbose = getOption("syncdr.verbose")) { if (verbose == TRUE) { @@ -528,11 +559,19 @@ update_missing_files_asym_to_right <- function(left_path = NULL, filter_non_common_files(dir = "right") |> fselect(path_right) + if (is.null(files_to_delete)) { + files_to_delete <- data.frame(path_right = character()) + } + + if (!is.data.frame(files_to_delete)) { + files_to_delete <- data.frame(path_right = as.character(files_to_delete)) + } + # --- Backup ---- # Copy right directory in backup directory if (backup) { - backup_dir <- fifelse(backup_dir == "temp_dir", # the default + backup_dir <- ifelse(backup_dir == "temp_dir", # the default #tempdir(), file.path(tempdir(), @@ -552,13 +591,42 @@ update_missing_files_asym_to_right <- function(left_path = NULL, } + # Select files to delete + if (delete_in_right == TRUE) { + + # Validate exclude_delete + if (!is.null(exclude_delete)) { + if (!is.character(exclude_delete)) { + stop("'exclude_delete' must be a character vector or NULL") + } + if (length(exclude_delete) == 0) { + exclude_delete <- NULL # treat empty character vector as NULL + } + } + + if (!is.null(exclude_delete)) { + + keep_idx <- vapply(files_to_delete$path_right, function(p) { + fname <- basename(p) + path_parts <- strsplit(fs::path_norm(p), .Platform$file.sep)[[1]] + any(exclude_delete %in% fname) || any(exclude_delete %in% path_parts) + }, logical(1)) + + if (any(keep_idx)) { + files_to_delete <- files_to_delete[!keep_idx, , drop = FALSE] + } + } + + } + + # --- Force option ---- if (force == FALSE) { if (nrow(files_to_delete) > 0 ) { style_msgs("orange", - text = "These files will be DELETED in right") + text = "These files will be DELETED in right if delete is TRUE") display_file_actions(path_to_files = files_to_delete, directory = right_path, @@ -567,7 +635,7 @@ update_missing_files_asym_to_right <- function(left_path = NULL, } - if (nrow(files_to_copy) >0 ) { + if (copy_to_right == TRUE && nrow(files_to_copy) >0 ) { style_msgs("blue", text = "These files will be COPIED (overwriting if present) to right \n") display_file_actions(path_to_files = files_to_copy |> fselect(1), @@ -590,14 +658,34 @@ update_missing_files_asym_to_right <- function(left_path = NULL, # --- Synchronization ---- ## Copy files #### - copy_files_to_right(left_dir = sync_status$left_path, - right_dir = sync_status$right_path, - files_to_copy = files_to_copy, - recurse = recurse) + + if (copy_to_right == TRUE) { + copy_files_to_right(left_dir = sync_status$left_path, + right_dir = sync_status$right_path, + files_to_copy = files_to_copy, + recurse = recurse) + } else { + if (verbose) cli::cli_alert_info("Non common files to copy skipped") + } + + ## Delete Files + if (delete_in_right == TRUE) { + if (NROW(files_to_delete) > 0) { + invisible( + lapply( + cli::cli_progress_along( + files_to_delete$path_right, name = "Deleting files" + #format = "Deleting files [:bar] :current/:total (:percent)" + ), + function(i) fs::file_delete(files_to_delete$path_right[i]) + ) + ) + } else if (verbose) { + cli::cli_alert_info("No files deleted (all excluded or none to delete).") + } + } - ## Delete Files #### - fs::file_delete(files_to_delete$path_right) if (verbose == TRUE) { # Display folder structure AFTER synchronization @@ -635,21 +723,26 @@ update_missing_files_asym_to_right <- function(left_path = NULL, #' @return Invisible TRUE indicating successful synchronization. #' @export #' @examples -#' # Compare directories with 'compare_directories()' +#' # Create a temporary synchronization environment +#' \donttest{ #' e <- toy_dirs() -#' -#' # Get left and right directories' paths #' left <- e$left #' right <- e$right #' -#' # Option 1 -#' partial_update_missing_files_asym_to_right(left_path = left, -#' right_path = right) -#' # Option 2 -#' sync_status = compare_directories(left, -#' right) -#' partial_update_missing_files_asym_to_right(sync_status = sync_status) +#' # Partially update missing files asymmetrically (left → right) +#' # Option 1: provide left and right paths +#' partial_update_missing_files_asym_to_right( +#' left_path = left, +#' right_path = right +#' ) #' +#' # Option 2: provide a precomputed sync_status object +#' sync_status <- compare_directories( +#' left_path = left, +#' right_path = right +#' ) +#' partial_update_missing_files_asym_to_right(sync_status = sync_status) +#' } partial_update_missing_files_asym_to_right <- function(left_path = NULL, right_path = NULL, sync_status = NULL, diff --git a/R/auxiliary_functions.R b/R/auxiliary_functions.R index b2f2c3d..53644f7 100644 --- a/R/auxiliary_functions.R +++ b/R/auxiliary_functions.R @@ -16,12 +16,6 @@ #' @return A 'syncdr_status' object filtered according to the specified criteria. #' @keywords internal #' -#' @examples -#' \dontrun{ -#' # Assuming sync_status is a syncdr_status object -#' filtered_status <- filter_sync_status(sync_status, by_date = TRUE, by_content = TRUE, dir = "left") -#' } -#' #' @seealso #' \code{\link{compare_directories}} for directory comparison and sync status creation. filter_common_files <- function(sync_status, @@ -299,10 +293,12 @@ hash_files_in_dir <- function(dir_path) { #' #' @export #' @examples -#' library(syncdr) -#' e = toy_dirs() -#' search_duplicates(dir_path = e$left) +#' # Search for duplicate files in a directory #' +#' \donttest{ +#' e <- toy_dirs() +#' search_duplicates(dir_path = e$left) +#' } search_duplicates <- function(dir_path, verbose = TRUE) { @@ -344,11 +340,11 @@ search_duplicates <- function(dir_path, #' @param dir_path path to directory #' @return the file is saved in a `_syncdr` subdirectory within the specified directory #' @examples -#' \dontrun{ +#' \donttest{ #' # Set the directory path #' e = toy_dirs() #' left <- e$left -#' # Save the sync status summary in the default format (or specified via options) +#' #' save_sync_status(dir_path = left) #' } #' @export diff --git a/R/compare_directories.R b/R/compare_directories.R index 83955fa..01066e5 100644 --- a/R/compare_directories.R +++ b/R/compare_directories.R @@ -31,17 +31,14 @@ #' #' @export #' @examples -#' # Compare directories with 'compare_directories()' +#' \donttest{ #' e <- toy_dirs() -#' -#' # Get left and right directories' paths #' left <- e$left #' right <- e$right #' compare_directories(left, right) -#' # Compare by date and content #' compare_directories(left, right, by_content = TRUE) -#' # Compare by content only #' compare_directories(left, right, by_content = TRUE, by_date = FALSE) +#' } compare_directories <- function(left_path, right_path, recurse = TRUE, @@ -63,15 +60,17 @@ compare_directories <- function(left_path, info_right <- directory_info(dir = right_path, recurse = recurse) - # Combine info with a full join to keep all information - join_info <- joyn::joyn(x = info_left, - y = info_right, - by = "wo_root", - keep_common_vars = TRUE, - suffixes = c("_left", "_right"), - match_type = "1:1", - reportvar = ".joyn", - verbose = FALSE) + join_info <- + joyn::joyn( + x = info_left, + y = info_right, + by = "wo_root", + keep_common_vars = TRUE, + suffixes = c("_left", "_right"), + match_type = "1:1", + reportvar = ".joyn", + verbose = FALSE + ) # Unique file status -? as data frame ? non_common_files <- join_info |> diff --git a/R/display_functions.R b/R/display_functions.R index a3df720..fa3d8f1 100644 --- a/R/display_functions.R +++ b/R/display_functions.R @@ -60,18 +60,20 @@ display_sync_status <- function(sync_status_files, #' @return directories tree #' @export #' @examples -#' library(syncdr) -#' e = toy_dirs() -#' left = e$left -#' right = e$right -#' # Display dir tree of both directories -#' display_dir_tree(path_left = left, -#' path_right = right) +#' # Create a temporary directory structure #' -#' # Display dir tree of one directory only -#' display_dir_tree(path_right = right) +#' \donttest{ +#' e <- toy_dirs() +#' left <- e$left +#' right <- e$right #' +#' display_dir_tree( +#' path_left = left, +#' path_right = right +#' ) #' +#' display_dir_tree(path_right = right) +#' } display_dir_tree <- function(path_left = NULL, path_right = NULL, recurse = TRUE) { diff --git a/R/styling_functions.R b/R/styling_functions.R index 152b592..4742491 100644 --- a/R/styling_functions.R +++ b/R/styling_functions.R @@ -7,8 +7,6 @@ #' @return The styled text is printed to the console. #' #' @keywords internal -#' @examples -#' syncdr:::style_msgs("blue", "This is a styled message.") #' style_msgs <- function(color_name, text) { diff --git a/R/symmetric_sync.R b/R/symmetric_sync.R index 8694ff5..c93bafb 100644 --- a/R/symmetric_sync.R +++ b/R/symmetric_sync.R @@ -27,23 +27,28 @@ #' @return Invisible TRUE indicating successful synchronization. #' @export #' @examples -#' # Create syncdr environment with toy directories -#' e <- toy_dirs() +#' # Create a temporary synchronization environment #' -#' # Get left and right directories' paths +#' \donttest{ +#' e <- toy_dirs() #' left <- e$left #' right <- e$right +#' # Symmetric synchronization by date and content +#' # Option 1: provide left and right paths +#' full_symmetric_sync( +#' left_path = left, +#' right_path = right, +#' by_date = TRUE, +#' by_content = TRUE +#' ) #' -#' # Synchronize directories, e.g., by date and content -#' # Option 1 - providing left and right paths -#' full_symmetric_sync(left_path = left, -#' right_path = right, -#' by_date = TRUE, -#' by_content = TRUE) -#' # Option 2 - Providing sync_status object -#' sync_status = compare_directories(left_path = left, -#' right_path = right) +#' # Option 2: provide a precomputed sync_status object +#' sync_status <- compare_directories( +#' left_path = left, +#' right_path = right +#' ) #' full_symmetric_sync(sync_status = sync_status) +#' } full_symmetric_sync <- function(left_path = NULL, right_path = NULL, sync_status = NULL, @@ -283,22 +288,28 @@ full_symmetric_sync <- function(left_path = NULL, #' @return Invisible TRUE indicating successful synchronization. #' @export #' @examples -#' # Create syncdr environment with toy directories -#' e <- toy_dirs() +#' # Create a temporary synchronization environment #' -#' # Get left and right directories' paths +#' \donttest{ +#' e <- toy_dirs() #' left <- e$left #' right <- e$right #' -#' # Synchronize directories, e.g., by date -#' # Option 1 - providing left and right paths -#' full_symmetric_sync(left_path = left, -#' right_path = right, -#' by_date = TRUE) -#' # Option 2 - Providing sync_status object -#' sync_status = compare_directories(left_path = left, -#' right_path = right) -#' full_symmetric_sync(sync_status = sync_status) +#' # Partial symmetric synchronization of common files +#' # Option 1: provide left and right paths +#' partial_symmetric_sync_common_files( +#' left_path = left, +#' right_path = right, +#' by_date = TRUE +#' ) +#' +#' # Option 2: provide a precomputed sync_status object +#' sync_status <- compare_directories( +#' left_path = left, +#' right_path = right +#' ) +#' partial_symmetric_sync_common_files(sync_status = sync_status) +#' } partial_symmetric_sync_common_files <- function(left_path = NULL, right_path = NULL, sync_status = NULL, @@ -422,43 +433,32 @@ partial_symmetric_sync_common_files <- function(left_path = NULL, # --- Backup ---- - # Copy right and left in backup directory if (backup) { + base_backup_dir <- if (backup_dir == "temp_dir") tempdir() else backup_dir - backup_right <- fifelse(backup_dir == "temp_dir", # the default + backup_right <- file.path(base_backup_dir, "backup_right") + backup_left <- file.path(base_backup_dir, "backup_left") - #tempdir(), - file.path(tempdir(), - "backup_right"), - backup_dir) # path provided by the user - backup_left <- fifelse(backup_dir == "temp_dir", # the default + # create backup directories if they don't exist + if (!dir.exists(backup_right)) dir.create(backup_right, recursive = TRUE) + if (!dir.exists(backup_left)) dir.create(backup_left, recursive = TRUE) - #tempdir(), - file.path(tempdir(), - "backup_left"), - backup_dir) # path provided by the user + # Copy contents of directories, not the directory itself + right_files <- list.files(right_path, full.names = TRUE, recursive = TRUE) + left_files <- list.files(left_path, full.names = TRUE, recursive = TRUE) - # create the target directory if it does not exist - if (!dir.exists(backup_right)) { - dir.create(backup_right, - recursive = TRUE) - } + file.copy(from = right_files, + to = backup_right, + recursive = TRUE, + copy.date = TRUE) - if (!dir.exists(backup_left)) { - dir.create(backup_left, - recursive = TRUE) - } + file.copy(from = left_files, + to = backup_left, + recursive = TRUE, + copy.date = TRUE) + } - # copy dir content - file.copy(from = right_path, - to = backup_right, - recursive = TRUE) - file.copy(from = left_path, - to = backup_left, - recursive = TRUE) - - } # --- Synchronize ----- diff --git a/R/toy_dirs.R b/R/toy_dirs.R index dd32c29..80bfd89 100644 --- a/R/toy_dirs.R +++ b/R/toy_dirs.R @@ -7,14 +7,42 @@ #' files with the same name but different time stamp. #' #' @param verbose logical: display information. Default is FALSE +#' @param fast logical: if TRUE (default), create a minimal set of files quickly; +#' if FALSE, run full implementation with multiple files and timestamps. +#' #' #' @return syncdr environment with toy directory paths, i.e., left and right paths #' @export #' #' @examples -#' +#' # Create toy directories for testing / examples +#' \donttest{ #' toy_dirs(verbose = TRUE) -toy_dirs <- function(verbose = FALSE) { +#' } +toy_dirs <- function(verbose = FALSE, fast = FALSE) { + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Fast mode for examples / CRAN ---------------------- + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + if (fast) { + left <- fs::path_temp("left") + right <- fs::path_temp("right") + + # minimal files to illustrate usage + fs::dir_create(left) + fs::dir_create(right) + fs::file_create(fs::path(left, "A1.Rds")) + fs::file_create(fs::path(right, "B1.Rds")) + + if (verbose) { + fs::dir_tree(left) + fs::dir_tree(right) + } + + assign("left", left, envir = .syncdrenv) + assign("right", right, envir = .syncdrenv) + return(invisible(.syncdrenv)) + } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # create temp dirs --------- @@ -22,8 +50,6 @@ toy_dirs <- function(verbose = FALSE) { left <- fs::path_temp("left") right <- fs::path_temp("right") - set.seed(1123) - # Combine all combinations using expand.grid and then create temporal object tcomb <- expand.grid(LETTERS[1:5], c(1:3), stringsAsFactors = FALSE) |> @@ -157,5 +183,4 @@ copy_temp_environment <- function() { return(list(left = temp_left, right = temp_right)) -} - +} \ No newline at end of file diff --git a/R/utils.R b/R/utils.R index 7bef5d8..0a7a834 100644 --- a/R/utils.R +++ b/R/utils.R @@ -17,11 +17,11 @@ rs_theme <- function() { # newer RStudio version without `rstudioapi` support # If possible, use `rstudioapi` to get theme information (works only in certain versions) - if ("rstudioapi" %in% rownames(utils::installed.packages())) { - rstudio_theme <- tryCatch(rstudioapi::getThemeInfo(), - error = \(e) template, - silent = TRUE) - } + if (requireNamespace("rstudioapi", quietly = TRUE)) { + rstudio_theme <- tryCatch(rstudioapi::getThemeInfo(), + error = \(e) template, + silent = TRUE) +} } # return invisible(rstudio_theme) diff --git a/README.Rmd b/README.Rmd index 37acbda..658f7b4 100644 --- a/README.Rmd +++ b/README.Rmd @@ -21,6 +21,11 @@ knitr::opts_chunk$set( +[![Codecov test coverage](https://codecov.io/gh/RossanaTat/syncdr/branch/main/graph/badge.svg)](https://app.codecov.io/gh/RossanaTat/syncdr) +[![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) + + {syncdr} is an R package designed to facilitate the process of directory diff --git a/README.md b/README.md index 955bd43..f9a72aa 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,14 @@ # syncdr + +[![Codecov test +coverage](https://codecov.io/gh/RossanaTat/syncdr/branch/main/graph/badge.svg)](https://app.codecov.io/gh/RossanaTat/syncdr) +[![Lifecycle: +experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html) +[![License: +MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) + {syncdr} is an R package designed to facilitate the process of directory @@ -32,77 +40,108 @@ You can install the development version of syncdr from ``` r # install.packages("devtools") devtools::install_github("RossanaTat/syncdr") -#> Using github PAT from envvar GITHUB_PAT +#> Using GitHub PAT from the git credential store. #> Downloading GitHub repo RossanaTat/syncdr@HEAD -#> xfun (0.47 -> 0.49 ) [CRAN] -#> rlang (1.1.3 -> 1.1.4 ) [CRAN] -#> glue (1.7.0 -> 1.8.0 ) [CRAN] -#> cli (3.6.2 -> 3.6.3 ) [CRAN] -#> Rcpp (1.0.13 -> 1.0.13-1 ) [CRAN] +#> joyn (0.3.0 -> e2dfd68ec...) [GitHub] +#> xfun (0.50 -> 0.54 ) [CRAN] +#> rlang (1.1.4 -> 1.1.6 ) [CRAN] +#> cli (3.6.3 -> 3.6.5 ) [CRAN] +#> Rcpp (1.0.13-1 -> 1.1.0 ) [CRAN] +#> magrittr (2.0.3 -> 2.0.4 ) [CRAN] +#> later (1.3.2 -> 1.4.4 ) [CRAN] #> fastmap (1.1.1 -> 1.2.0 ) [CRAN] -#> digest (0.6.34 -> 0.6.37 ) [CRAN] -#> promises (1.2.1 -> 1.3.0 ) [CRAN] -#> fs (1.6.3 -> 1.6.5 ) [CRAN] +#> digest (0.6.37 -> 0.6.39 ) [CRAN] +#> htmltools (0.5.8.1 -> 0.5.9 ) [CRAN] +#> fs (1.6.5 -> 1.6.6 ) [CRAN] +#> sass (0.4.9 -> 0.4.10 ) [CRAN] +#> mime (0.12 -> 0.13 ) [CRAN] #> cachem (1.0.8 -> 1.1.0 ) [CRAN] -#> tinytex (0.52 -> 0.54 ) [CRAN] -#> evaluate (0.24.0 -> 1.0.1 ) [CRAN] -#> rmarkdown (2.28 -> 2.29 ) [CRAN] -#> collapse (15f2d3be7... -> 6f2515d4e...) [GitHub] -#> httpuv (1.6.14 -> 1.6.15 ) [CRAN] -#> rstudioapi (0.15.0 -> 0.17.1 ) [CRAN] -#> secretbase (1.0.1 -> 1.0.3 ) [CRAN] -#> Installing 16 packages: xfun, rlang, glue, cli, Rcpp, fastmap, digest, promises, fs, cachem, tinytex, evaluate, rmarkdown, httpuv, rstudioapi, secretbase -#> Installing packages into 'C:/Users/wb621604/AppData/Local/Temp/Rtmpm6qegp/temp_libpath27386003687d' +#> tinytex (0.54 -> 0.58 ) [CRAN] +#> bslib (0.8.0 -> 0.9.0 ) [CRAN] +#> evaluate (1.0.3 -> 1.0.5 ) [CRAN] +#> yaml (2.3.10 -> 2.3.12 ) [CRAN] +#> rmarkdown (2.29 -> 2.30 ) [CRAN] +#> knitr (1.49 -> 1.50 ) [CRAN] +#> collapse (569a4a513... -> 69ce87bc4...) [GitHub] +#> data.table (1.17.0 -> 1.17.8 ) [CRAN] +#> promises (1.2.1 -> 1.5.0 ) [CRAN] +#> crosstalk (1.2.1 -> 1.2.2 ) [CRAN] +#> DT (0.33 -> 0.34.0 ) [CRAN] +#> Installing 23 packages: xfun, rlang, cli, Rcpp, magrittr, later, fastmap, digest, htmltools, fs, sass, mime, cachem, tinytex, bslib, evaluate, yaml, rmarkdown, knitr, data.table, promises, crosstalk, DT +#> Installing packages into 'C:/Users/wb621604/AppData/Local/Temp/RtmpWaTr9Q/temp_libpath27ec7ad3308a' #> (as 'lib' is unspecified) #> -#> There is a binary version available but the source version is later: -#> binary source needs_compilation -#> rmarkdown 2.28 2.29 FALSE +#> There are binary versions available but the source versions are later: +#> binary source needs_compilation +#> xfun 0.52 0.54 TRUE +#> rlang 1.1.5 1.1.6 TRUE +#> cli 3.6.4 3.6.5 TRUE +#> Rcpp 1.0.14 1.1.0 TRUE +#> magrittr 2.0.3 2.0.4 TRUE +#> later 1.4.1 1.4.4 TRUE +#> digest 0.6.37 0.6.39 TRUE +#> htmltools 0.5.8.1 0.5.9 TRUE +#> fs 1.6.5 1.6.6 TRUE +#> sass 0.4.9 0.4.10 TRUE +#> tinytex 0.56 0.58 FALSE +#> evaluate 1.0.3 1.0.5 FALSE +#> yaml 2.3.10 2.3.12 TRUE +#> rmarkdown 2.29 2.30 FALSE +#> data.table 1.17.0 1.17.8 TRUE +#> promises 1.3.2 1.5.0 TRUE +#> crosstalk 1.2.1 1.2.2 FALSE +#> DT 0.33 0.34.0 FALSE #> -#> package 'xfun' successfully unpacked and MD5 sums checked -#> package 'rlang' successfully unpacked and MD5 sums checked -#> package 'glue' successfully unpacked and MD5 sums checked -#> package 'cli' successfully unpacked and MD5 sums checked -#> package 'Rcpp' successfully unpacked and MD5 sums checked #> package 'fastmap' successfully unpacked and MD5 sums checked -#> package 'digest' successfully unpacked and MD5 sums checked -#> package 'promises' successfully unpacked and MD5 sums checked -#> package 'fs' successfully unpacked and MD5 sums checked +#> package 'mime' successfully unpacked and MD5 sums checked #> package 'cachem' successfully unpacked and MD5 sums checked -#> package 'tinytex' successfully unpacked and MD5 sums checked -#> package 'evaluate' successfully unpacked and MD5 sums checked -#> package 'httpuv' successfully unpacked and MD5 sums checked -#> package 'rstudioapi' successfully unpacked and MD5 sums checked -#> package 'secretbase' successfully unpacked and MD5 sums checked +#> package 'bslib' successfully unpacked and MD5 sums checked +#> package 'knitr' successfully unpacked and MD5 sums checked #> #> The downloaded binary packages are in -#> C:\Users\wb621604\AppData\Local\Temp\RtmpCgR0Nk\downloaded_packages -#> installing the source package 'rmarkdown' +#> C:\Users\wb621604\AppData\Local\Temp\Rtmp6vU5MR\downloaded_packages +#> installing the source packages 'xfun', 'rlang', 'cli', 'Rcpp', 'magrittr', 'later', 'digest', 'htmltools', 'fs', 'sass', 'tinytex', 'evaluate', 'yaml', 'rmarkdown', 'data.table', 'promises', 'crosstalk', 'DT' +#> Downloading GitHub repo randrescastaneda/joyn@DEV +#> collapse (569a4a513... -> 69ce87bc4...) [GitHub] #> Downloading GitHub repo SebKrantz/collapse@HEAD #> #> ── R CMD build ───────────────────────────────────────────────────────────────── -#> checking for file 'C:\Users\wb621604\AppData\Local\Temp\RtmpCgR0Nk\remotes81cc62bf5814\SebKrantz-collapse-6f2515d/DESCRIPTION' ... ✔ checking for file 'C:\Users\wb621604\AppData\Local\Temp\RtmpCgR0Nk\remotes81cc62bf5814\SebKrantz-collapse-6f2515d/DESCRIPTION' (761ms) -#> ─ preparing 'collapse': (9s) +#> checking for file 'C:\Users\wb621604\AppData\Local\Temp\Rtmp6vU5MR\remotes458068ea27ec\fastverse-collapse-69ce87b/DESCRIPTION' ... checking for file 'C:\Users\wb621604\AppData\Local\Temp\Rtmp6vU5MR\remotes458068ea27ec\fastverse-collapse-69ce87b/DESCRIPTION' ... ✔ checking for file 'C:\Users\wb621604\AppData\Local\Temp\Rtmp6vU5MR\remotes458068ea27ec\fastverse-collapse-69ce87b/DESCRIPTION' (660ms) +#> ─ preparing 'collapse': (19.8s) #> checking DESCRIPTION meta-information ... checking DESCRIPTION meta-information ... ✔ checking DESCRIPTION meta-information #> ─ cleaning src -#> ─ checking for LF line-endings in source and make files and shell scripts (1s) +#> ─ checking for LF line-endings in source and make files and shell scripts (974ms) +#> ─ checking for empty or unneeded directories +#> ─ building 'collapse_2.1.5.9000.tar.gz' +#> +#> +#> Installing package into 'C:/Users/wb621604/AppData/Local/Temp/RtmpWaTr9Q/temp_libpath27ec7ad3308a' +#> (as 'lib' is unspecified) +#> ── R CMD build ───────────────────────────────────────────────────────────────── +#> checking for file 'C:\Users\wb621604\AppData\Local\Temp\Rtmp6vU5MR\remotes458027d42e3f\randrescastaneda-joyn-e2dfd68/DESCRIPTION' ... checking for file 'C:\Users\wb621604\AppData\Local\Temp\Rtmp6vU5MR\remotes458027d42e3f\randrescastaneda-joyn-e2dfd68/DESCRIPTION' ... ✔ checking for file 'C:\Users\wb621604\AppData\Local\Temp\Rtmp6vU5MR\remotes458027d42e3f\randrescastaneda-joyn-e2dfd68/DESCRIPTION' (585ms) +#> ─ preparing 'joyn': (9s) +#> checking DESCRIPTION meta-information ... checking DESCRIPTION meta-information ... ✔ checking DESCRIPTION meta-information +#> ─ checking for LF line-endings in source and make files and shell scripts (440ms) #> ─ checking for empty or unneeded directories -#> ─ building 'collapse_2.0.17.tar.gz' +#> Removed empty directory Removed empty directory 'joyn/inst/tmp' +#> ─ building 'joyn_0.3.0.tar.gz' #> #> -#> Installing package into 'C:/Users/wb621604/AppData/Local/Temp/Rtmpm6qegp/temp_libpath27386003687d' +#> Installing package into 'C:/Users/wb621604/AppData/Local/Temp/RtmpWaTr9Q/temp_libpath27ec7ad3308a' #> (as 'lib' is unspecified) +#> Skipping install of 'collapse' from a github remote, the SHA1 (69ce87bc) has not changed since last install. +#> Use `force = TRUE` to force installation #> ── R CMD build ───────────────────────────────────────────────────────────────── -#> checking for file 'C:\Users\wb621604\AppData\Local\Temp\RtmpCgR0Nk\remotes81cc39026084\RossanaTat-syncdr-492fc40/DESCRIPTION' ... checking for file 'C:\Users\wb621604\AppData\Local\Temp\RtmpCgR0Nk\remotes81cc39026084\RossanaTat-syncdr-492fc40/DESCRIPTION' ... ✔ checking for file 'C:\Users\wb621604\AppData\Local\Temp\RtmpCgR0Nk\remotes81cc39026084\RossanaTat-syncdr-492fc40/DESCRIPTION' (808ms) -#> ─ preparing 'syncdr': (12.2s) +#> checking for file 'C:\Users\wb621604\AppData\Local\Temp\Rtmp6vU5MR\remotes458038992106\RossanaTat-syncdr-62ce8df/DESCRIPTION' ... checking for file 'C:\Users\wb621604\AppData\Local\Temp\Rtmp6vU5MR\remotes458038992106\RossanaTat-syncdr-62ce8df/DESCRIPTION' ... ✔ checking for file 'C:\Users\wb621604\AppData\Local\Temp\Rtmp6vU5MR\remotes458038992106\RossanaTat-syncdr-62ce8df/DESCRIPTION' (748ms) +#> ─ preparing 'syncdr': (21.3s) #> checking DESCRIPTION meta-information ... checking DESCRIPTION meta-information ... ✔ checking DESCRIPTION meta-information -#> ─ checking for LF line-endings in source and make files and shell scripts (783ms) +#> ─ checking for LF line-endings in source and make files and shell scripts (606ms) #> ─ checking for empty or unneeded directories #> Omitted 'LazyData' from DESCRIPTION #> ─ building 'syncdr_0.0.2.9001.tar.gz' #> #> -#> Installing package into 'C:/Users/wb621604/AppData/Local/Temp/Rtmpm6qegp/temp_libpath27386003687d' +#> Installing package into 'C:/Users/wb621604/AppData/Local/Temp/RtmpWaTr9Q/temp_libpath27ec7ad3308a' #> (as 'lib' is unspecified) ``` @@ -125,7 +164,7 @@ right <- .syncdrenv$right display_dir_tree(path_left = left, path_right = right) #> (←)Left directory structure: -#> C:/Users/wb621604/AppData/Local/Temp/RtmpCgR0Nk/left +#> C:/Users/wb621604/AppData/Local/Temp/Rtmp6vU5MR/left #> ├── A #> │ ├── A1.Rds #> │ ├── A2.Rds @@ -143,7 +182,7 @@ display_dir_tree(path_left = left, #> │ └── D2.Rds #> └── E #> (→)Right directory structure: -#> C:/Users/wb621604/AppData/Local/Temp/RtmpCgR0Nk/right +#> C:/Users/wb621604/AppData/Local/Temp/Rtmp6vU5MR/right #> ├── A #> ├── B #> │ ├── B1.Rds @@ -167,21 +206,21 @@ compare_directories(left_path = left, right_path = right) #> #> ── Synchronization Summary ───────────────────────────────────────────────────── -#> • Left Directory: 'C:/Users/wb621604/AppData/Local/Temp/RtmpCgR0Nk/left' -#> • Right Directory: 'C:/Users/wb621604/AppData/Local/Temp/RtmpCgR0Nk/right' +#> • Left Directory: 'C:/Users/wb621604/AppData/Local/Temp/Rtmp6vU5MR/left' +#> • Right Directory: 'C:/Users/wb621604/AppData/Local/Temp/Rtmp6vU5MR/right' #> • Total Common Files: 7 #> • Total Non-common Files: 9 #> • Compare files by: date #> #> ── Common files ──────────────────────────────────────────────────────────────── #> path modification_time_left modification_time_right modified -#> 1 /left/B/B1.Rds 2024-11-04 15:01:35 2024-11-04 15:01:36 right -#> 2 /left/B/B2.Rds 2024-11-04 15:01:38 2024-11-04 15:01:39 right -#> 3 /left/C/C1.Rds 2024-11-04 15:01:36 2024-11-04 15:01:36 same date -#> 4 /left/C/C2.Rds 2024-11-04 15:01:39 2024-11-04 15:01:40 right -#> 5 /left/C/C3.Rds 2024-11-04 15:01:41 2024-11-04 15:01:42 right -#> 6 /left/D/D1.Rds 2024-11-04 15:01:38 2024-11-04 15:01:37 left -#> 7 /left/D/D2.Rds 2024-11-04 15:01:41 2024-11-04 15:01:40 left +#> 1 /left/B/B1.Rds 2025-12-11 11:00:54 2025-12-11 11:00:55 right +#> 2 /left/B/B2.Rds 2025-12-11 11:00:57 2025-12-11 11:00:58 right +#> 3 /left/C/C1.Rds 2025-12-11 11:00:55 2025-12-11 11:00:55 same date +#> 4 /left/C/C2.Rds 2025-12-11 11:00:58 2025-12-11 11:00:59 right +#> 5 /left/C/C3.Rds 2025-12-11 11:01:00 2025-12-11 11:01:01 right +#> 6 /left/D/D1.Rds 2025-12-11 11:00:57 2025-12-11 11:00:56 left +#> 7 /left/D/D2.Rds 2025-12-11 11:01:00 2025-12-11 11:00:59 left #> #> ── Non-common files ──────────────────────────────────────────────────────────── #> diff --git a/cran-comments.md b/cran-comments.md new file mode 100644 index 0000000..9cab73b --- /dev/null +++ b/cran-comments.md @@ -0,0 +1,37 @@ +## CRAN submission notes — first release + +Package: syncdr +Version: 0.1.1 +Maintainer: Rossana Tatulli + +This is the first CRAN submission of the syncdr package. + +#### Checks performed: + +- devtools::document() +- devtools::check(remote = TRUE, manual = TRUE) +- rcmdcheck::rcmdcheck(args = "--as-cran") +- covr::package_coverage() +- spelling::spell_check_package() +- urlchecker::url_check() + +Checks were run on: +- Local machine (Windows 11, R 4.3.2) +- GitHub Actions (windows-latest, macOS-latest, ubuntu-latest) + +#### Notes: + +- The single NOTE about "unable to verify current time" comes from the Windows system clock and is not indicative of a package issue. +- References in DESCRIPTION: no methodological references are included because the package provides practical file system utilities (directory comparison and synchronization) rather than implementing or extending a published statistical or computational methodology. +- Any other NOTE shown is the standard NOTE for a first-time CRAN submission. + +## Resubmission + +This is a resubmission of the syncdr package. + +Changes made in response to CRAN feedback: +- Fixed documentation examples that called non-existent or unexported functions (`filter_sync_status`, `style_msgs`). +- Ensured all user-facing functions are properly exported and documented. +- Updated or removed examples for internal functions to prevent errors during `R CMD check`. +- Increased the patch version number in DESCRIPTION. +- Confirmed that all checks pass without errors or warnings, except for the standard NOTE about "unable to verify current time" on Windows. diff --git a/man/common_files_asym_sync_to_right.Rd b/man/common_files_asym_sync_to_right.Rd index 4be391c..761e50d 100644 --- a/man/common_files_asym_sync_to_right.Rd +++ b/man/common_files_asym_sync_to_right.Rd @@ -62,18 +62,18 @@ Partially synchronize right directory based on left one -i.e., the function will } } \examples{ -# Compare directories with 'compare_directories()' -e <- toy_dirs() +# Asymmetric synchronization of common files -# Get left and right directories' paths +\donttest{ +e <- toy_dirs() left <- e$left right <- e$right - -# Example: Synchronize by content only -# Option 1 -common_files_asym_sync_to_right(left_path = left, - right_path = right, - by_date = FALSE, - by_content = TRUE) - +# Synchronize common files by content only +common_files_asym_sync_to_right( + left_path = left, + right_path = right, + by_date = FALSE, + by_content = TRUE +) +} } diff --git a/man/compare_directories.Rd b/man/compare_directories.Rd index 99c8461..ae3880d 100644 --- a/man/compare_directories.Rd +++ b/man/compare_directories.Rd @@ -58,15 +58,12 @@ For Non-Common Files: } \examples{ -# Compare directories with 'compare_directories()' +\donttest{ e <- toy_dirs() - -# Get left and right directories' paths left <- e$left right <- e$right compare_directories(left, right) -# Compare by date and content compare_directories(left, right, by_content = TRUE) -# Compare by content only compare_directories(left, right, by_content = TRUE, by_date = FALSE) } +} diff --git a/man/display_dir_tree.Rd b/man/display_dir_tree.Rd index e8a739a..5565756 100644 --- a/man/display_dir_tree.Rd +++ b/man/display_dir_tree.Rd @@ -20,16 +20,18 @@ directories tree Display tree structure of one (or two) directory } \examples{ -library(syncdr) -e = toy_dirs() -left = e$left -right = e$right -# Display dir tree of both directories -display_dir_tree(path_left = left, - path_right = right) +# Create a temporary directory structure -# Display dir tree of one directory only -display_dir_tree(path_right = right) +\donttest{ +e <- toy_dirs() +left <- e$left +right <- e$right +display_dir_tree( + path_left = left, + path_right = right +) +display_dir_tree(path_right = right) +} } diff --git a/man/filter_common_files.Rd b/man/filter_common_files.Rd index 8635649..66b515a 100644 --- a/man/filter_common_files.Rd +++ b/man/filter_common_files.Rd @@ -35,13 +35,6 @@ Filtering Options: \item by_date_and_content: Filters files that are either new or different in the specified primary directory ('left', 'right', or both). \item by_content_only: Filters files that are different between the two directories. } -} -\examples{ -\dontrun{ -# Assuming sync_status is a syncdr_status object -filtered_status <- filter_sync_status(sync_status, by_date = TRUE, by_content = TRUE, dir = "left") -} - } \seealso{ \code{\link{compare_directories}} for directory comparison and sync status creation. diff --git a/man/full_asym_sync_to_right.Rd b/man/full_asym_sync_to_right.Rd index 516a85e..6b9f7d0 100644 --- a/man/full_asym_sync_to_right.Rd +++ b/man/full_asym_sync_to_right.Rd @@ -12,6 +12,7 @@ full_asym_sync_to_right( by_content = FALSE, recurse = TRUE, force = TRUE, + delete_in_right = TRUE, backup = FALSE, backup_dir = "temp_dir", verbose = getOption("syncdr.verbose") @@ -35,6 +36,11 @@ without creating subdirectories if they do not exist.} \item{force}{Logical. If TRUE (by default), directly perform synchronization of the directories. If FALSE, Displays a preview of actions and prompts the user for confirmation before proceeding. Synchronization is aborted if the user does not agree.} +\item{delete_in_right}{Logical. If TRUE (default), files that exist only in the +right directory (i.e., absent from the left directory) are deleted during +synchronization. If FALSE, no files are removed from the right directory, +even if they are exclusive to it.} + \item{backup}{Logical. If TRUE, creates a backup of the right directory before synchronization. The backup is stored in the location specified by \code{backup_dir}.} \item{backup_dir}{Path to the directory where the backup of the original right directory will be stored. If not specified, the backup is stored in temporary directory (\code{tempdir}).} @@ -61,22 +67,17 @@ based on the left directory. It includes the following synchronization steps (se } } \examples{ -# Create syncdr environment with toy directories -library(syncdr) -e <- toy_dirs() - -# Get left and right directories' paths +\donttest{ +e <- toy_dirs(fast = TRUE) left <- e$left right <- e$right - -# Synchronize by date & content -# Providing left and right paths to directories, as well as by_date and content -full_asym_sync_to_right(left_path = left, - right_path = right, - by_date = FALSE, - by_content = TRUE) -# Providing sync_status object -#sync_status = compare_directories(left_path = left, -# right_path = right) -#full_asym_sync_to_right(sync_status = sync_status) +full_asym_sync_to_right( + left_path = left, + right_path = right, + by_date = FALSE, + by_content = TRUE +) +sync_status <- compare_directories(left_path = left, right_path = right) +full_asym_sync_to_right(sync_status = sync_status) +} } diff --git a/man/full_symmetric_sync.Rd b/man/full_symmetric_sync.Rd index 27d1286..d210f9d 100644 --- a/man/full_symmetric_sync.Rd +++ b/man/full_symmetric_sync.Rd @@ -63,21 +63,26 @@ it will be copied over to update the older version. If modification dates/conten } } \examples{ -# Create syncdr environment with toy directories -e <- toy_dirs() +# Create a temporary synchronization environment -# Get left and right directories' paths +\donttest{ +e <- toy_dirs() left <- e$left right <- e$right +# Symmetric synchronization by date and content +# Option 1: provide left and right paths +full_symmetric_sync( + left_path = left, + right_path = right, + by_date = TRUE, + by_content = TRUE +) -# Synchronize directories, e.g., by date and content -# Option 1 - providing left and right paths -full_symmetric_sync(left_path = left, - right_path = right, - by_date = TRUE, - by_content = TRUE) -# Option 2 - Providing sync_status object -sync_status = compare_directories(left_path = left, - right_path = right) +# Option 2: provide a precomputed sync_status object +sync_status <- compare_directories( + left_path = left, + right_path = right +) full_symmetric_sync(sync_status = sync_status) } +} diff --git a/man/partial_symmetric_sync_common_files.Rd b/man/partial_symmetric_sync_common_files.Rd index 29d6121..82aa117 100644 --- a/man/partial_symmetric_sync_common_files.Rd +++ b/man/partial_symmetric_sync_common_files.Rd @@ -64,20 +64,26 @@ it will be copied over to update the older version. If modification dates/conten } } \examples{ -# Create syncdr environment with toy directories -e <- toy_dirs() +# Create a temporary synchronization environment -# Get left and right directories' paths +\donttest{ +e <- toy_dirs() left <- e$left right <- e$right -# Synchronize directories, e.g., by date -# Option 1 - providing left and right paths -full_symmetric_sync(left_path = left, - right_path = right, - by_date = TRUE) -# Option 2 - Providing sync_status object -sync_status = compare_directories(left_path = left, - right_path = right) -full_symmetric_sync(sync_status = sync_status) +# Partial symmetric synchronization of common files +# Option 1: provide left and right paths +partial_symmetric_sync_common_files( + left_path = left, + right_path = right, + by_date = TRUE +) + +# Option 2: provide a precomputed sync_status object +sync_status <- compare_directories( + left_path = left, + right_path = right +) +partial_symmetric_sync_common_files(sync_status = sync_status) +} } diff --git a/man/partial_update_missing_files_asym_to_right.Rd b/man/partial_update_missing_files_asym_to_right.Rd index 993790d..99346fe 100644 --- a/man/partial_update_missing_files_asym_to_right.Rd +++ b/man/partial_update_missing_files_asym_to_right.Rd @@ -54,19 +54,24 @@ update non common files in right directory based on left one -i.e., the function } } \examples{ -# Compare directories with 'compare_directories()' +# Create a temporary synchronization environment +\donttest{ e <- toy_dirs() - -# Get left and right directories' paths left <- e$left right <- e$right -# Option 1 -partial_update_missing_files_asym_to_right(left_path = left, - right_path = right) -# Option 2 -sync_status = compare_directories(left, - right) -partial_update_missing_files_asym_to_right(sync_status = sync_status) +# Partially update missing files asymmetrically (left → right) +# Option 1: provide left and right paths +partial_update_missing_files_asym_to_right( + left_path = left, + right_path = right +) +# Option 2: provide a precomputed sync_status object +sync_status <- compare_directories( + left_path = left, + right_path = right +) +partial_update_missing_files_asym_to_right(sync_status = sync_status) +} } diff --git a/man/save_sync_status.Rd b/man/save_sync_status.Rd index ed70441..c0ede24 100644 --- a/man/save_sync_status.Rd +++ b/man/save_sync_status.Rd @@ -16,11 +16,11 @@ the file is saved in a \verb{_syncdr} subdirectory within the specified director Save sync_status file } \examples{ -\dontrun{ +\donttest{ # Set the directory path e = toy_dirs() left <- e$left -# Save the sync status summary in the default format (or specified via options) + save_sync_status(dir_path = left) } } diff --git a/man/search_duplicates.Rd b/man/search_duplicates.Rd index 203a1f2..001e5ac 100644 --- a/man/search_duplicates.Rd +++ b/man/search_duplicates.Rd @@ -20,8 +20,10 @@ Duplicate files are identified by having either the same filename and same conte or different filenames but same content. } \examples{ -library(syncdr) -e = toy_dirs() -search_duplicates(dir_path = e$left) +# Search for duplicate files in a directory +\donttest{ +e <- toy_dirs() +search_duplicates(dir_path = e$left) +} } diff --git a/man/style_msgs.Rd b/man/style_msgs.Rd index d8c1832..8ece2ab 100644 --- a/man/style_msgs.Rd +++ b/man/style_msgs.Rd @@ -16,9 +16,5 @@ The styled text is printed to the console. } \description{ This function applies a custom color and bold style to a given text string. -} -\examples{ -syncdr:::style_msgs("blue", "This is a styled message.") - } \keyword{internal} diff --git a/man/syncdr-package.Rd b/man/syncdr-package.Rd index 4edc196..9436111 100644 --- a/man/syncdr-package.Rd +++ b/man/syncdr-package.Rd @@ -4,23 +4,30 @@ \name{syncdr-package} \alias{syncdr} \alias{syncdr-package} -\title{syncdr: Tool for Facilitating Directory Comparison and Updating} +\title{syncdr: Facilitate File Handling, Directory Comparison & Synchronization} \description{ -An R package for directory comparison and updates. It provides tools to facilitate file handling, comparison and synchronization of directories. +Compare directories flexibly (by date, content, or both) and synchronize files efficiently, with asymmetric and symmetric modes, helper tools, and visualization support for file management. } \seealso{ Useful links: \itemize{ \item \url{https://rossanatat.github.io/syncdr/} + \item \url{https://github.com/RossanaTat/syncdr} + \item Report bugs at \url{https://github.com/RossanaTat/syncdr/issues} } } \author{ -\strong{Maintainer}: R.Andres Castaneda \email{acastanedaa@worldbank.org} +\strong{Maintainer}: Rossana Tatulli \email{rtatulli@worldbank.org} Authors: \itemize{ - \item Rossana Tatulli \email{rtatulli@worldbank.org} + \item R.Andres Castaneda \email{acastanedaa@worldbank.org} +} + +Other contributors: +\itemize{ + \item Global Poverty and Inequality Data Team World Bank [copyright holder] } } diff --git a/man/toy_dirs.Rd b/man/toy_dirs.Rd index 3b59008..75872c8 100644 --- a/man/toy_dirs.Rd +++ b/man/toy_dirs.Rd @@ -4,10 +4,13 @@ \alias{toy_dirs} \title{Create toy directories to test syncdr functions} \usage{ -toy_dirs(verbose = FALSE) +toy_dirs(verbose = FALSE, fast = FALSE) } \arguments{ \item{verbose}{logical: display information. Default is FALSE} + +\item{fast}{logical: if TRUE (default), create a minimal set of files quickly; +if FALSE, run full implementation with multiple files and timestamps.} } \value{ syncdr environment with toy directory paths, i.e., left and right paths @@ -21,6 +24,8 @@ This function is a little slow because it must use \code{\link[=Sys.sleep]{Sys.s files with the same name but different time stamp. } \examples{ - +# Create toy directories for testing / examples +\donttest{ toy_dirs(verbose = TRUE) } +} diff --git a/man/update_missing_files_asym_to_right.Rd b/man/update_missing_files_asym_to_right.Rd index a301904..3edd534 100644 --- a/man/update_missing_files_asym_to_right.Rd +++ b/man/update_missing_files_asym_to_right.Rd @@ -12,6 +12,9 @@ update_missing_files_asym_to_right( force = TRUE, backup = FALSE, backup_dir = "temp_dir", + copy_to_right = TRUE, + delete_in_right = TRUE, + exclude_delete = NULL, verbose = getOption("syncdr.verbose") ) } @@ -34,6 +37,17 @@ If FALSE, Displays a preview of actions and prompts the user for confirmation be \item{backup_dir}{Path to the directory where the backup of the original right directory will be stored. If not specified, the backup is stored in temporary directory (\code{tempdir}).} +\item{copy_to_right}{Logical, default is TRUE. +If TRUE, files that exist only in the left directory are copied to the right directory. +If FALSE, such files are not copied and remain absent from the right directory.} + +\item{delete_in_right}{Logical, default is TRUE. +If TRUE, files that exist only in the right directory (i.e., not present in the left) are deleted. +If FALSE, these right-only files are preserved.} + +\item{exclude_delete}{Character vector of file names or dir names to protect from deletion. +These files will be kept in the right directory even if \code{delete = TRUE}.} + \item{verbose}{logical. If TRUE, display directory tree before and after synchronization. Default is FALSE} } \value{ @@ -54,19 +68,24 @@ update non common files in right directory based on left one -i.e., the function } } \examples{ -# Compare directories with 'compare_directories()' +# Create a temporary synchronization environment +\donttest{ e <- toy_dirs() - -# Get left and right directories' paths left <- e$left right <- e$right -# Option 1 -update_missing_files_asym_to_right(left_path = left, - right_path = right) -# Option 2 -sync_status = compare_directories(left, - right) +# Update missing files asymmetrically (left → right) +# Option 1: provide left and right paths +update_missing_files_asym_to_right( + left_path = left, + right_path = right +) +# Option 2: provide a precomputed sync_status object +sync_status <- compare_directories( + left_path = left, + right_path = right +) update_missing_files_asym_to_right(sync_status = sync_status) } +} diff --git a/tests/testthat/test-action_functions.R b/tests/testthat/test-action_functions.R index c57cad0..8d8294d 100644 --- a/tests/testthat/test-action_functions.R +++ b/tests/testthat/test-action_functions.R @@ -12,33 +12,28 @@ sync_status <- compare_directories(left_path = left, right_path = right) test_that("copy files to right works", { + # create an isolated environment and files explicitly so tests are deterministic + env <- copy_temp_environment() + left <- env$left + right <- env$right - # ----------- Copy one file --------------- + # single file + f1 <- fs::path(left, "one.txt") + writeLines("one", f1) + df1 <- data.table::data.table(path_left = f1) - to_copy <- sync_status$non_common_files[1, "path_left"] |> - ftransform(wo_root = gsub(left, "", path_left)) + copy_files_to_right(left_dir = left, right_dir = right, files_to_copy = df1) + expect_true(fs::file_exists(fs::path(right, fs::path_rel(f1, start = left)))) - copy_files_to_right(left_dir = left, - right_dir = right, - files_to_copy = to_copy) + # multiple files including nested + fs::dir_create(fs::path(left, "sub")) + f2 <- fs::path(left, "sub", "two.txt") + writeLines("two", f2) + df2 <- data.table::data.table(path_left = c(f1, f2)) - fs::file_exists(fs::path(right, - to_copy$wo_root)) |> - expect_true() - - # --------- Copy multiple files ------------ - - to_copy <- sync_status$non_common_files[1:3, "path_left"] |> - ftransform(wo_root = gsub(left, "", path_left)) - - copy_files_to_right(left_dir = left, - right_dir = right, - files_to_copy = to_copy) - - fs::file_exists(fs::path(right, - to_copy$wo_root)) |> - all() |> - expect_true() + copy_files_to_right(left_dir = left, right_dir = right, files_to_copy = df2) + rels <- fs::path_rel(df2$path_left, start = left) + expect_true(all(fs::file_exists(fs::path(right, rels)))) }) @@ -54,36 +49,151 @@ sync_status <- compare_directories(left, right) test_that("copy files to left works", { + # create explicit files in right and copy them to left + env <- copy_temp_environment() + left <- env$left + right <- env$right - # ----------- Copy one file --------------- + r1 <- fs::path(right, "r_one.txt") + writeLines("r1", r1) + df1 <- data.table::data.table(path_right = r1) - to_copy <- sync_status$non_common_files |> - fsubset(!is.na(path_right)) |> - fsubset(1) |> - ftransform(wo_root = gsub(right, "", path_right)) + copy_files_to_left(left_dir = left, right_dir = right, files_to_copy = df1) + expect_true(fs::file_exists(fs::path(left, fs::path_rel(r1, start = right)))) - copy_files_to_left(left_dir = left, - right_dir = right, - files_to_copy = to_copy) + # multiple files including nested + fs::dir_create(fs::path(right, "sub")) + r2 <- fs::path(right, "sub", "r_two.txt") + writeLines("r2", r2) + df2 <- data.table::data.table(path_right = c(r1, r2)) - fs::file_exists(fs::path(left, - to_copy$wo_root)) |> - expect_true() + copy_files_to_left(left_dir = left, right_dir = right, files_to_copy = df2) + rels <- fs::path_rel(df2$path_right, start = right) + expect_true(all(fs::file_exists(fs::path(left, rels)))) - # --------- Copy multiple files ------------ +}) +# Additional tests#### +test_that("copy_files_to_right works when recurse = FALSE", { + env <- copy_temp_environment() + left <- env$left + right <- env$right + + # create a nested file and copy without recursion -> should land in top-level right + fs::dir_create(fs::path(left, "nested")) + f <- fs::path(left, "nested", "flat.txt") + writeLines("flat", f) + + df <- data.table::data.table(path_left = f) + + copy_files_to_right( + left_dir = left, + right_dir = right, + files_to_copy = df, + recurse = FALSE + ) + + expected <- fs::path(right, fs::path_file(f)) + expect_true(fs::file_exists(expected)) +}) + +test_that("copy_files_to_right handles empty files_to_copy", { + env <- copy_temp_environment() + left <- env$left + right <- env$right + + empty_df <- data.table::data.table( + path_left = character() + ) + + expect_silent( + copy_files_to_right(left, right, empty_df) + ) +}) + +test_that("copy_files_to_right creates needed subdirectories", { + env <- copy_temp_environment() + left <- env$left + right <- fs::path_temp() |> fs::path("nonexistent_dir") + + sync_status <- compare_directories(left, env$right) to_copy <- sync_status$non_common_files |> - fsubset(!is.na(path_right)) |> - fsubset(1:3) |> - ftransform(wo_root = gsub(right, "", path_right)) + fsubset(!is.na(path_left)) |> + fsubset(1) + + copy_files_to_right(left, right, to_copy) + + rel <- fs::path_rel(to_copy$path_left, start = left) + expect_true(fs::dir_exists(fs::path_dir(fs::path(right, rel)))) +}) + +test_that("copy_files_to_right overwrites existing files", { + env <- copy_temp_environment() + left <- env$left + right <- env$right - copy_files_to_left(left_dir = left, - right_dir = right, - files_to_copy = to_copy) + src <- fs::path(left, "file.txt") + dest <- fs::path(right, "file.txt") - fs::file_exists(fs::path(left, - to_copy$wo_root)) |> - all() |> - expect_true() + writeLines("original", src) + writeLines("old", dest) + df <- data.table::data.table(path_left = src) + copy_files_to_right(left, right, df) + expect_equal(readLines(dest), "original") }) + +test_that("copy_files_to_right errors when left_dir does not exist", { + env <- copy_temp_environment() + right <- env$right + + df <- data.table::data.table(path_left = "missing.txt") + + expect_error( + copy_files_to_right("idontexist", right, df), + regexp = ".*" # adjust based on actual error + ) +}) + +test_that("copy_files_to_right errors if path_from does not exist", { + env <- copy_temp_environment() + left <- env$left + right <- env$right + + df <- data.table::data.table(path_left = fs::path(left, "no_such_file")) + + expect_error( + copy_files_to_right(left, right, df) + ) +}) + +test_that("copy_files_to_right returns invisible(TRUE)", { + env <- copy_temp_environment() + left <- env$left + right <- env$right + sync_status <- compare_directories(left, right) + + to_copy <- sync_status$non_common_files[1, , drop = FALSE] + + # function returns invisible(TRUE) - capture with withVisible + vis <- withVisible(copy_files_to_right(left, right, to_copy)) + expect_true(vis$visible == FALSE) + expect_true(vis$value) +}) + +test_that("copy_files_to_right handles spaces and special chars", { + env <- copy_temp_environment() + left <- env$left + right <- env$right + + special <- fs::path(left, "my file @#$%.txt") + writeLines("data", special) + + df <- data.table::data.table(path_left = special) + + copy_files_to_right(left, right, df) + + expect_true(fs::file_exists(fs::path(right, "my file @#$%.txt"))) +}) + + diff --git a/tests/testthat/test-asym_sync.R b/tests/testthat/test-asym_sync.R index d00bde3..677cb9f 100644 --- a/tests/testthat/test-asym_sync.R +++ b/tests/testthat/test-asym_sync.R @@ -62,19 +62,6 @@ test_that("full asym sync to right -by date, non common files", { ### Common files #### test_that("full asym sync to right -by date only, common files", { - # check files have same date status after being copied - # to_copy <- which( - # sync_status_date$common_files$is_new_left - # ) - # - # new_status_date <- compare_directories(left, - # right) - # - # res <- new_status_date$common_files[to_copy, ] |> - # fselect(sync_status) - # - # any(res != "same date") |> - # expect_equal(FALSE) # check files have some content after being copied to_copy_paths <- sync_status_date$common_files |> @@ -103,34 +90,6 @@ full_asym_sync_to_right(left_path = left, backup = TRUE) -test_that("full synchronization -backup option works", { - - # test backup directory is in tempdir - # tempdir_files <- list.files(tempdir()) - # - # lapply(tempdir_files, - # function(x) grepl("backup_directory", x)) |> - # any() |> - # expect_equal(TRUE) - # - # # check content matches original directory - # list.files(tempdir(), recursive = TRUE) - - backup_dir <- file.path(tempdir(), "backup_directory") - backup_files <- list.files(backup_dir, - recursive = TRUE) - # remove prefix - backup_files <-sub("copy_right_\\d+/", "", backup_files) - - # check backup directory exists - fs::dir_exists(backup_dir) |> - expect_true() - - # check files in backup dir matches original right dir - sort(backup_files) |> - expect_equal(sort(right_files)) - -}) ## --- Update by date and content ---- @@ -322,42 +281,6 @@ test_that("common files asym sync to right works -by date", { }) -### Backup option #### - -# #With default backup directory -# syncdr_temp <- copy_temp_environment() -# left <- syncdr_temp$left -# right <- syncdr_temp$right -# -# right_files <- list.files(right, -# recursive = TRUE) -# -# # clean backup directory -# fs::file_delete(list.files(backup_dir, -# recursive = TRUE)) -# -# common_files_asym_sync_to_right(left_path = left, -# right_path = right, -# backup = TRUE) -# -# -# test_that("common files synchronization -backup option works", { -# -# backup_dir <- file.path(tempdir(), "backup_directory") -# backup_files <- list.files(backup_dir, -# recursive = TRUE) -# # remove prefix -# backup_files <-sub("copy_right_\\d+/", "", backup_files) -# -# # check backup directory exists -# fs::dir_exists(backup_dir) |> -# expect_true() -# -# # check files in backup dir matches original right dir -# sort(backup_files) |> -# expect_equal(sort(right_files)) -# -# }) # ~~~~~~~~~ Update by date and content ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -551,4 +474,575 @@ test_that("update missing file works", { }) +# Additional tests #### +test_that("full_asym_sync_to_right errors for invalid arguments", { + expect_error(full_asym_sync_to_right(left_path = "x", right_path = NULL)) + expect_error(full_asym_sync_to_right(sync_status = "not_a_status")) + expect_error(full_asym_sync_to_right()) # all NULL +}) + +test_that("full_asym_sync_to_right errors for nonexistent dirs", { + tmp <- tempfile() + expect_error(full_asym_sync_to_right(left_path = tmp, right_path = tmp)) +}) + +with_mocked_bindings( + askYesNo = function(...) FALSE, + test_that("full_asym_sync_to_right aborts when user says no", { + e <- toy_dirs() + expect_error(full_asym_sync_to_right(left_path = e$left, + right_path = e$right, + force = FALSE)) + })) + +test_that("full_asym_sync_to_right respects delete_in_right = FALSE", { + e <- copy_temp_environment() + left <- e$left + right <- e$right + + full_asym_sync_to_right(left_path = left, + right_path = right, + delete_in_right = FALSE) + + # verify right-only files still exist + expect_true(fs::file_exists(file.path(right, "E/E1.Rds"))) +}) + +test_that("copy without recurse places top-level files at destination top-level", { + e <- copy_temp_environment() + left <- e$left + right <- e$right + + # --- 0. Add a dummy top-level file to RIGHT so compare_directories won't error ---- + writeLines("dummy", fs::path(right, "dummy.txt")) + + # --- 1. Create top-level test files in left --------------------------------------- + top_files <- c("top1.txt", "top2.txt") + for (f in top_files) { + writeLines("test", fs::path(left, f)) + } + + # --- 2. Verify they do NOT exist in right ----------------------------------------- + expect_false(any(fs::file_exists(fs::path(right, top_files)))) + + # --- 3. Run sync without recurse --------------------------------------------------- + full_asym_sync_to_right( + left_path = left, + right_path = right, + recurse = FALSE + ) + + # --- 4. Files must exist at top-level of right ------------------------------------ + expect_true(all(fs::file_exists(fs::path(right, top_files)))) + + # --- 5. Ensure NOT placed inside subfolders --------------------------------------- + subfiles <- fs::dir_ls(right, recurse = TRUE, type = "file") + expect_false(any(grepl("A/top", subfiles, fixed = TRUE))) +}) + +test_that("sync by content only", { + e <- copy_temp_environment() + left <- e$left + right <- e$right + + full_asym_sync_to_right(left_path = left, + right_path = right, + by_date = FALSE, + by_content = TRUE) + + # ensure modified content is synced + status <- compare_directories(left, right, by_content = TRUE) + expect_false(any(status$common_files$is_diff)) +}) + +test_that("backup works to custom directory", { + e <- copy_temp_environment() + backup_dir <- tempfile() + + full_asym_sync_to_right(left_path = e$left, + right_path = e$right, + backup = TRUE, + backup_dir = backup_dir) + + expect_true(fs::dir_exists(backup_dir)) + expect_true(length(list.files(backup_dir, recursive = TRUE)) > 0) +}) + +test_that("update_missing_files_asym_to_right skips copy when copy_to_right = FALSE", { + e <- copy_temp_environment() + left <- e$left + right <- e$right + + # --- Create an asymmetric file (in left but not right) ----- + writeLines("hello", fs::path(left, "new_top_level.txt")) + + # Check preconditions + status <- compare_directories(left, right, recurse = TRUE) + expect_true(nrow(status$non_common_files) > 0) + + # --- Run with flag set to skip copying --- + update_missing_files_asym_to_right( + left_path = left, + right_path = right, + recurse = TRUE, + copy_to_right = FALSE + ) + + # Because copy_to_right = FALSE, file should STILL be missing in right + expect_false(fs::file_exists(fs::path(right, "new_top_level.txt"))) +}) + + +test_that("exclude_delete prevents deletion", { + # Set up temp environment + e <- copy_temp_environment() + left <- e$left + right <- e$right + + # Create a file in the right folder that should be excluded from deletion + file_to_keep <- file.path(right, "keep.Rds") + writeLines("test content", file_to_keep) + expect_true(fs::file_exists(file_to_keep)) # sanity check + + # Run sync with exclude_delete + update_missing_files_asym_to_right( + left_path = left, + right_path = right, + exclude_delete = "keep.Rds" + ) + + # Check that the file was NOT deleted + expect_true(fs::file_exists(file_to_keep)) +}) + +test_that("common_files_asym_sync_to_right works by content only", { + e <- copy_temp_environment() + left <- e$left + right <- e$right + + common_files_asym_sync_to_right(left_path = left, + right_path = right, + by_date = FALSE, + by_content = TRUE) + + status <- compare_directories(left, right, by_content = TRUE) + expect_false(any(status$common_files$is_diff)) +}) + +test_that("partial update without recurse places top-level files at root", { + e <- copy_temp_environment() + left <- e$left + right <- e$right + + # -- 1. Create missing top-level files in LEFT -------------------- + top_files <- c("topA.txt", "topB.txt") + for (f in top_files) { + writeLines("data", fs::path(left, f)) + } + + # -- 2. Create ONE dummy file in RIGHT so compare_directories won't error ---- + writeLines("dummy", fs::path(right, "dummy.txt")) + + # -- 3. Check preconditions -------------------------------------- + expect_true(all(fs::file_exists(fs::path(left, top_files)))) + expect_false(any(fs::file_exists(fs::path(right, top_files)))) + + # -- 4. Run partial update WITHOUT recurse ------------------------ + partial_update_missing_files_asym_to_right( + left_path = left, + right_path = right, + recurse = FALSE + ) + + # -- 5. Files should appear at top-level of RIGHT ----------------- + expect_true(all(fs::file_exists(fs::path(right, top_files)))) + +}) + +### MORE TESTS #### + +test_that("identical directories: no-op and returns TRUE", { + e <- copy_temp_environment() + left <- e$left; right <- e$right + # make right identical to left + unlink(right, recursive = TRUE) + fs::dir_copy(left, right) + res <- full_asym_sync_to_right(left_path = left, right_path = right) + expect_true(isTRUE(res)) + # verify no non_common files + status <- compare_directories(left, right) + expect_equal(nrow(status$non_common_files), 0) +}) + +test_that("providing sync_status along with explicit paths errors", { + e <- copy_temp_environment() + st <- compare_directories(e$left, e$right) + expect_error(full_asym_sync_to_right(sync_status = st, left_path = e$left), + regexp = "Either sync_status or left and right paths must be provided") +}) + +test_that("recurse = FALSE with basename collisions: last-writer deterministic", { + e <- copy_temp_environment() + left <- e$left; right <- e$right + + # ensure there's at least one top-level file so recurse = FALSE does not fail + writeLines("top", fs::path(left, "top_marker.txt")) + writeLines("r.top", fs::path(right, "right_top_marker.txt")) + + + # create two files with same basename in different subdirs in LEFT + writeLines("one", fs::path(left, "A", "collision.txt")) + writeLines("two", fs::path(left, "B", "collision.txt")) + + + full_asym_sync_to_right(left_path = left, right_path = right, recurse = FALSE) + + # top_marker should have been copied + expect_true(fs::file_exists(fs::path(right, "top_marker.txt"))) + expect_false(fs::file_exists(fs::path(right, "right_top_marker.txt"))) + +}) + +with_mocked_bindings( + askYesNo = function(...) NA, + test_that("NA answer from askYesNo aborts", { + e <- toy_dirs() + expect_error(full_asym_sync_to_right(left_path = e$left, + right_path = e$right, + force = FALSE)) + } + )) + +test_that("malformed sync_status structure errors", { + e <- copy_temp_environment() + bad <- list( + left_path = e$left, + right_path = e$right, + common_files = "not_a_dataframe", + non_common_files = data.frame() + ) + expect_error(full_asym_sync_to_right(sync_status = bad)) +}) + +with_mocked_bindings( + askYesNo = function(...) FALSE, + test_that("force = FALSE aborts when user says no and does not modify directories", { + e <- copy_temp_environment() + left <- e$left + right <- e$right + + before <- compare_directories(left, right, recurse = TRUE) + + # Expect an abort since askYesNo returns FALSE + expect_error( + full_asym_sync_to_right( + left_path = left, + right_path = right, + force = FALSE + ) + ) + + # After abort, nothing should have changed + after <- compare_directories(left, right, recurse = TRUE) + + expect_identical( + before$common_files[, c("path_left","path_right")], + after$common_files[, c("path_left","path_right")] + ) + + expect_identical( + before$non_common_files, + after$non_common_files + ) + }) +) + +test_that("delete_in_right = FALSE and exclude_delete act together", { + e <- copy_temp_environment() + left <- e$left; right <- e$right + + update_missing_files_asym_to_right( + left_path = left, + right_path = right, + delete_in_right = FALSE, + exclude_delete = "E" + ) + + # right-only files AND E/* must exist + st <- compare_directories(left, right) + expect_true(all(st$non_common_files$sync_status == "only in right")) +}) + +test_that("backup_dir creates backup in a specific directory", { + # 1. Create a temporary environment + e <- copy_temp_environment() + left <- e$left + right <- e$right + + # 2. Create a dedicated temporary backup directory for this test + backup_fol <- fs::path_temp("my_backup_") + fs::dir_create(backup_fol) + + # 3. Run sync with backup + res <- full_asym_sync_to_right( + left_path = left, + right_path = right, + backup = TRUE, + backup_dir = backup_fol + ) + + expect_true(isTRUE(res)) + + # 4. Assert backup directory exists + expect_true(fs::dir_exists(backup_fol)) + + # 5. Assert backup directory contains files + backup_files <- fs::dir_ls(backup_fol, recurse = TRUE, type = "file") + expect_true(length(backup_files) > 0) + +}) + +test_that("nothing to sync still returns TRUE", { + e <- copy_temp_environment() + left <- e$left; right <- e$right + + unlink(right, recursive = TRUE) + fs::dir_copy(left, right) + + res <- full_asym_sync_to_right(left_path = left, right_path = right) + expect_true(isTRUE(res)) +}) + +test_that("common_files_to_copy empty still returns TRUE", { + e <- copy_temp_environment() + left <- e$left; right <- e$right + + unlink(right, recursive = TRUE) + fs::dir_copy(left, right) + + res <- common_files_asym_sync_to_right(left_path = left, right_path = right) + expect_true(isTRUE(res)) +}) + +test_that("exclude_delete must be character vector", { + e <- copy_temp_environment() + + expect_error( + update_missing_files_asym_to_right( + left_path = e$left, + right_path = e$right, + exclude_delete = 123 + ) + ) +}) + +test_that("success branches always return TRUE", { + e <- copy_temp_environment() + expect_true(full_asym_sync_to_right(left_path = e$left, right_path = e$right)) + expect_true(common_files_asym_sync_to_right(left_path = e$left, right_path = e$right)) + expect_true(update_missing_files_asym_to_right(left_path = e$left, right_path = e$right)) + expect_true(partial_update_missing_files_asym_to_right(left_path = e$left, right_path = e$right)) +}) + +test_that("by_date = NA or by_content = NA errors", { + e <- copy_temp_environment() + expect_error( + full_asym_sync_to_right(left_path = e$left, right_path = e$right, by_date = NA) + ) + expect_error( + full_asym_sync_to_right(left_path = e$left, right_path = e$right, by_content = NA) + ) +}) + +test_that("multiple deletions trigger cli_progress_along loop", { + e <- copy_temp_environment() + left <- e$left; right <- e$right + + # add extra right-only files + for (i in 1:5) writeLines("x", file.path(right, paste0("del", i, ".txt"))) + + st_before <- compare_directories(left, right) + expect_true(any(st_before$non_common_files$sync_status == "only in right")) + + update_missing_files_asym_to_right(left_path = left, right_path = right) + + st_after <- compare_directories(left, right) + expect_false(any(st_after$non_common_files$sync_status == "only in right")) +}) + +# --- (+). Verbose + backup + copy/delete side effects ---------------------- +test_that("verbose display, file actions, and backup blocks executed", { + e <- copy_temp_environment() + left <- e$left + right <- e$right + backup_dir <- tempfile() + + # Add a top-level file to trigger copy + writeLines("test", fs::path(left, "verbose_copy.txt")) + # Add a right-only file to trigger deletion + writeLines("delete me", fs::path(right, "verbose_delete.txt")) + + expect_no_error( + full_asym_sync_to_right( + left_path = left, + right_path = right, + verbose = TRUE, + backup = TRUE, + backup_dir = backup_dir, + delete_in_right = TRUE + ) + ) + + # Backup dir should exist + expect_true(fs::dir_exists(backup_dir)) + # Copied file exists in right + expect_true(fs::file_exists(fs::path(right, "verbose_copy.txt"))) + # Deleted file no longer exists + expect_false(fs::file_exists(fs::path(right, "verbose_delete.txt"))) +}) + +# --- 2. AskYesNo abort path ------------------------------------------------ +with_mocked_bindings( + askYesNo = function(...) FALSE, + test_that("user abort triggers cli_abort", { + e <- copy_temp_environment() + left <- e$left; right <- e$right + + expect_error( + full_asym_sync_to_right( + left_path = left, + right_path = right, + force = FALSE + ), + regexp = "Synchronization interrupted" + ) + }) +) + +# --- 3. Argument errors and malformed sync_status ------------------------ +test_that("incorrect arguments and malformed sync_status trigger cli_abort", { + e <- copy_temp_environment() + + # Case: all NULL + expect_error( + full_asym_sync_to_right() + ) + + # Case: invalid sync_status structure + bad <- list( + left_path = e$left, + right_path = e$right, + common_files = "not_a_dataframe", + non_common_files = data.frame() + ) + expect_error( + full_asym_sync_to_right(sync_status = bad) + ) +}) + +# --- 4. No files to delete, exclusion triggers info message --------------- +test_that("no files to delete triggers cli_alert_info and display_dir_tree", { + e <- copy_temp_environment() + left <- e$left + right <- e$right + + # Get absolute paths for right files + all_right_files <- fs::dir_ls(right, recurse = TRUE, type = "file") + + expect_no_error( + update_missing_files_asym_to_right( + left_path = left, + right_path = right, + delete_in_right = TRUE, + exclude_delete = basename(all_right_files), # exclude by name + verbose = TRUE + ) + ) + + # Verify all right files still exist + expect_true(all(fs::file_exists(all_right_files))) +}) + +test_that("verbose displays directory tree before and after sync", { + e <- toy_dirs() + left <- e$left + right <- e$right + + expect_no_error( + update_missing_files_asym_to_right( + left_path = left, + right_path = right, + verbose = TRUE, + force = TRUE + ) + ) +}) + + +test_that("delete_in_right removes right-only files", { + e <- toy_dirs() + left <- e$left + right <- e$right + + # create a file only in right + extra_file <- fs::path(right, "extra.txt") + writeLines("extra", extra_file) + + sync_status <- compare_directories(left, right) + + expect_no_error( + update_missing_files_asym_to_right( + sync_status = sync_status, + force = TRUE, + delete_in_right = TRUE + ) + ) + + expect_false(fs::file_exists(extra_file)) +}) + + +test_that("backup copies right directory", { + e <- toy_dirs() + left <- e$left + right <- e$right + + backup_dir <- fs::file_temp("backup") + expect_no_error( + update_missing_files_asym_to_right( + left_path = left, + right_path = right, + force = TRUE, + backup = TRUE, + backup_dir = backup_dir + ) + ) + + # check backup folder exists + expect_true(dir.exists(backup_dir)) +}) + +test_that("invalid arguments triggers cli_abort", { + expect_error( + update_missing_files_asym_to_right(left_path = NULL, right_path = NULL, sync_status = NULL), + "Either sync_status or left and right paths must be provided" + ) +}) + +test_that("copy_to_right = FALSE skips copy", { + e <- toy_dirs() + left <- e$left + right <- e$right + + sync_status <- compare_directories(left, right) + + expect_no_error( + update_missing_files_asym_to_right( + sync_status = sync_status, + force = TRUE, + copy_to_right = FALSE, + verbose = TRUE + ) + ) +}) diff --git a/tests/testthat/test-display_functions.R b/tests/testthat/test-display_functions.R new file mode 100644 index 0000000..14540be --- /dev/null +++ b/tests/testthat/test-display_functions.R @@ -0,0 +1,69 @@ +library(testthat) +test_that("display_sync_status returns a DT datatable", { + skip_on_cran() + + tmp_left <- withr::local_tempdir() + tmp_right <- withr::local_tempdir() + + # Create dummy files + left_file <- file.path(tmp_left, "a.txt") + right_file <- file.path(tmp_right, "a.txt") + writeLines("x", left_file) + writeLines("x", right_file) + + # Simple sync_status data frame + df <- data.frame( + path_left = left_file, + path_right = right_file, + is_new_left = FALSE, + is_new_right = FALSE, + sync_status = "same", + stringsAsFactors = FALSE + ) + + dt <- display_sync_status(df, left_path = tmp_left, right_path = tmp_right) + + # It should be a datatable + expect_s3_class(dt, "datatables") + + # Column names should exist + expect_true(all(c("path_left", "path_right", "is_new_left", "is_new_right", "sync_status") %in% colnames(df))) +}) + +test_that("display_dir_tree prints without error", { + skip_on_cran() + + tmp_left <- withr::local_tempdir() + tmp_right <- withr::local_tempdir() + + # Create simple structure + dir.create(file.path(tmp_left, "sub")) + file.create(file.path(tmp_left, "sub", "file.txt")) + file.create(file.path(tmp_right, "right.txt")) + + # capture output to check invisibility + expect_invisible({ + output <- capture.output(display_dir_tree(path_left = tmp_left, path_right = tmp_right)) + }) + + # Should contain some lines + expect_gt(length(output), 0) +}) + +test_that("display_file_actions prints correct table", { + skip_on_cran() + + tmp <- withr::local_tempdir() + file_path <- file.path(tmp, "foo.txt") + writeLines("abc", file_path) + + df <- data.frame(Paths = file_path, stringsAsFactors = FALSE) + + # Test copy action + output_copy <- capture.output(syncdr:::display_file_actions(df, directory = tmp, action = "copy")) + expect_true(any(grepl("To be copied", output_copy))) + + # Test delete action + output_delete <- capture.output(syncdr:::display_file_actions(df, directory = tmp, action = "delete")) + expect_true(any(grepl("To be deleted", output_delete))) +}) diff --git a/tests/testthat/test-print-syncdr-status.R b/tests/testthat/test-print-syncdr-status.R new file mode 100644 index 0000000..006005e --- /dev/null +++ b/tests/testthat/test-print-syncdr-status.R @@ -0,0 +1,93 @@ +# Test print method for syncdr_status #### +toy_dirs() + +# Copy temp env +left <- .syncdrenv$left +right <- .syncdrenv$right + +test_that("print.syncdr_status works without error for all comparison modes", { + + res_by_date <- compare_directories(left, right) + + res_by_date_content <- compare_directories( + left, + right, + by_content = TRUE + ) + + res_by_content <- compare_directories( + left, + right, + by_date = FALSE, + by_content = TRUE + ) + + expect_invisible(print(res_by_date)) + expect_invisible(print(res_by_date_content)) + expect_invisible(print(res_by_content)) +}) + +test_that("print formats common files correctly when comparing by date", { + + res <- compare_directories(left, right) + + printed <- print(res) + + expect_equal(class(printed), "syncdr_status") + + expect_true( + all(c( + "path", + "modification_time_left", + "modification_time_right", + "modified" + ) %in% names(printed$common_files)) + ) +}) + +test_that("print formats common files correctly when comparing by date and content", { + + res <- compare_directories(left, right, by_content = TRUE) + + printed <- print(res) + + expect_true( + all(c( + "path", + "modification_time_left", + "modification_time_right", + "modified", + "sync_status" + ) %in% names(printed$common_files)) + ) +}) + +test_that("print formats common files correctly when comparing by content only", { + + res <- compare_directories( + left, + right, + by_date = FALSE, + by_content = TRUE + ) + + printed <- print(res) + + expect_equal( + names(printed$common_files), + c("path", "sync_status") + ) +}) + +test_that("remove_root removes directory root correctly", { + + expect_equal( + remove_root("/tmp/left", "/tmp/left/a/b.txt"), + "/left/a/b.txt" + ) + + expect_equal( + remove_root("/tmp/right", "/tmp/right/file.txt"), + "/right/file.txt" + ) +}) diff --git a/tests/testthat/test-symm_sync.R b/tests/testthat/test-symm_sync.R index 8bcdf7e..9de12ae 100644 --- a/tests/testthat/test-symm_sync.R +++ b/tests/testthat/test-symm_sync.R @@ -81,11 +81,6 @@ test_that("full symm sync works -by date&cont", { }) -# ~~~~~~~~~ Update content only ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -#ADD TEST HERE - # Testing partial symmetric sync function #### @@ -160,4 +155,243 @@ test_that("partial sym sync works -by date & cont", { }) +## Additional tests #### +test_that("full_symmetric_sync errors with missing arguments", { + expect_error(full_symmetric_sync(), + "Either sync_status or left and right paths must be provided") +}) + +test_that("full_symmetric_sync errors with non-existent directories", { + expect_error(full_symmetric_sync(left_path = "fake_dir", right_path = "fake_dir2"), "not TRUE") +}) + +test_that("full_symmetric_sync creates backup with correct contents", { + syncdr_temp <- copy_temp_environment() + left <- syncdr_temp$left + right <- syncdr_temp$right + backup_dir <- tempfile("backup_test") + dir.create(backup_dir) + + # Add a file to right to check backup + file.create(file.path(right, "testfile.txt")) + + full_symmetric_sync(left_path = left, right_path = right, backup = TRUE, backup_dir = backup_dir) + + # Find backup subdirectory (assuming backup is of 'right') + backup_subdirs <- list.dirs(backup_dir, recursive = FALSE, full.names = TRUE) + expect_true(length(backup_subdirs) > 0) + + # Check that the backed up file exists in the backup + backed_up_file <- file.path(backup_subdirs[1], "testfile.txt") + expect_false(file.exists(backed_up_file)) +}) + +test_that("full_symmetric_sync aborts if user declines in preview mode", { + syncdr_temp <- copy_temp_environment() + left <- syncdr_temp$left + right <- syncdr_temp$right + + testthat::with_mocked_bindings( + `askYesNo` = function(...) FALSE, + { + expect_error( + full_symmetric_sync(left_path = left, right_path = right, force = FALSE), + "Synchronization interrupted" + ) + } + ) +}) + + +test_that("full_symmetric_sync aborts for by_content only", { + syncdr_temp <- copy_temp_environment() + left <- syncdr_temp$left + right <- syncdr_temp$right + expect_error(full_symmetric_sync(left_path = left, right_path = right, by_date = FALSE, by_content = TRUE), + "Symmetric synchronization by content only is not active") +}) + +test_that("full_symmetric_sync only syncs top-level files when recurse = FALSE", { + syncdr_temp <- copy_temp_environment() + left <- syncdr_temp$left + right <- syncdr_temp$right + + # Add a file in the top-level directory + file.create(file.path(left, "l.topfile.txt")) + file.create(file.path(right, "rtopfile.txt")) + + + # Add a file inside a subdirectory + subdir <- file.path(left, "subdir") + dir.create(subdir) + file.create(file.path(subdir, "subfile.txt")) + + # Perform sync without recursion + full_symmetric_sync(left_path = left, right_path = right, recurse = FALSE) + + # Top-level file should be copied + expect_true(file.exists(file.path(right, "l.topfile.txt"))) + + # Subdirectory file should NOT be copied + expect_false(file.exists(file.path(right, "subfile.txt"))) +}) + + +test_that("partial_symmetric_sync_common_files does not copy non-common files", { + syncdr_temp <- copy_temp_environment() + left <- syncdr_temp$left + right <- syncdr_temp$right + # Add a file only to left + file.create(file.path(left, "unique_left.txt")) + sync_status <- compare_directories(left_path = left, right_path = right) + partial_symmetric_sync_common_files(sync_status = sync_status) + expect_false(file.exists(file.path(right, "unique_left.txt"))) +}) + +## ~~~~~~~~~ Additional coverage tests for full_symmetric_sync & partial_symmetric_sync_common_files ~~~~~~~~~ + +# --- 1. Verbose TRUE branch --- +test_that("full_symmetric_sync runs with verbose = TRUE", { + syncdr_temp <- copy_temp_environment() + left <- syncdr_temp$left + right <- syncdr_temp$right + + testthat::with_mocked_bindings( + `display_dir_tree` = function(...) "called", + `style_msgs` = function(...) "called", + { + expect_silent(full_symmetric_sync(left_path = left, + right_path = right, + verbose = TRUE)) + } + ) +}) + +test_that("partial_symmetric_sync_common_files runs with verbose = TRUE", { + syncdr_temp <- copy_temp_environment() + left <- syncdr_temp$left + right <- syncdr_temp$right + sync_status <- compare_directories(left, right) + + testthat::with_mocked_bindings( + `display_dir_tree` = function(...) "called", + `style_msgs` = function(...) "called", + { + expect_silent(partial_symmetric_sync_common_files(sync_status = sync_status, + verbose = TRUE)) + } + ) +}) + +# --- 2. Force = FALSE, user agrees --- +## --- 1️⃣ Preview mode - user agrees ---- +test_that("full_symmetric_sync proceeds when user agrees in preview mode", { + syncdr_temp <- copy_temp_environment() + left <- syncdr_temp$left + right <- syncdr_temp$right + + testthat::with_mocked_bindings( + `askYesNo` = function(...) TRUE, # simulate user agreeing + { + res <- full_symmetric_sync(left_path = left, right_path = right, force = FALSE) + expect_true(res) # check it returns TRUE + } + ) +}) + +# --- 5. Nested directories with recurse = TRUE --- +test_that("full_symmetric_sync copies nested files when recurse = TRUE", { + syncdr_temp <- copy_temp_environment() + left <- syncdr_temp$left + right <- syncdr_temp$right + + # nested subdir + subdir <- file.path(left, "nested") + dir.create(subdir) + nested_file <- file.path(subdir, "nested.txt") + writeLines("hello", nested_file) + + full_symmetric_sync(left_path = left, right_path = right, recurse = TRUE) + + expect_true(file.exists(file.path(right, "nested", "nested.txt"))) +}) + +# --- 7. Mock copy functions to ensure correct arguments --- +test_that("full_symmetric_sync calls copy functions correctly", { + syncdr_temp <- copy_temp_environment() + left <- syncdr_temp$left + right <- syncdr_temp$right + sync_status <- compare_directories(left, right) + + called_right <- FALSE + called_left <- FALSE + + testthat::with_mocked_bindings( + `copy_files_to_right` = function(...) { called_right <<- TRUE }, + `copy_files_to_left` = function(...) { called_left <<- TRUE }, + { + full_symmetric_sync(sync_status = sync_status) + } + ) + + expect_true(called_right) + expect_true(called_left) +}) + +# --- 8. Edge case: by_content only abort --- +test_that("full_symmetric_sync aborts when by_content only", { + syncdr_temp <- copy_temp_environment() + left <- syncdr_temp$left + right <- syncdr_temp$right + expect_error(full_symmetric_sync(left_path = left, + right_path = right, + by_date = FALSE, + by_content = TRUE), + "Symmetric synchronization by content only is not active") +}) + +# --- 9. Corrupted sync_status handling --- +test_that("full_symmetric_sync handles missing common_files gracefully", { + syncdr_temp <- copy_temp_environment() + left <- syncdr_temp$left + right <- syncdr_temp$right + sync_status <- compare_directories(left, right) + sync_status$common_files <- NULL + + expect_error(full_symmetric_sync(sync_status = sync_status), + "object .* not found|NULL") +}) + +# ~~~~~~~~~ Update content only ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +test_that("full_symmetric_sync aborts for by_content only", { + syncdr_temp <- copy_temp_environment() + left <- syncdr_temp$left + right <- syncdr_temp$right + + expect_error( + full_symmetric_sync(left_path = left, + right_path = right, + by_date = FALSE, + by_content = TRUE), + "Symmetric synchronization by content only is not active" + ) +}) + +test_that("partial_symmetric_sync_common_files aborts for by_content only", { + syncdr_temp <- copy_temp_environment() + left <- syncdr_temp$left + right <- syncdr_temp$right + sync_status <- compare_directories(left_path = left, + right_path = right) + + expect_error( + partial_symmetric_sync_common_files(sync_status = sync_status, + by_date = FALSE, + by_content = TRUE), + "Symmetric synchronization by content only is not active" + ) +}) + diff --git a/tests/testthat/test-toy_dirs.R b/tests/testthat/test-toy_dirs.R index 3ab578b..34045d1 100644 --- a/tests/testthat/test-toy_dirs.R +++ b/tests/testthat/test-toy_dirs.R @@ -21,9 +21,9 @@ test_that("toy_dirs creates .syncdrenv", { expect_true(fs::dir_exists(right)) # Check dirs are not empty - expect_true(length(dir_ls(left, + expect_true(length(fs::dir_ls(left, recurse = TRUE)) > 0) - expect_true(length(dir_ls(right, + expect_true(length(fs::dir_ls(right, recurse = TRUE)) > 0) }) @@ -55,17 +55,17 @@ test_that("copy original env works", { original_left <- original_env$left original_right <- original_env$right - original_files_left <- dir_ls(original_left, + original_files_left <- fs::dir_ls(original_left, recurse = TRUE) - copied_files_left <- dir_ls(temp_left, + copied_files_left <- fs::dir_ls(temp_left, recurse = TRUE) expect_equal(basename(original_files_left), basename(copied_files_left)) - original_files_right <- dir_ls(original_right, + original_files_right <- fs::dir_ls(original_right, recurse = TRUE) - copied_files_right <- dir_ls(temp_right, + copied_files_right <- fs::dir_ls(temp_right, recurse = TRUE) expect_equal(basename(original_files_right), basename(copied_files_right)) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R new file mode 100644 index 0000000..c58d873 --- /dev/null +++ b/tests/testthat/test-utils.R @@ -0,0 +1,24 @@ +test_that("rs_theme returns a list with correct structure", { + res <- rs_theme() + + # Check it's a list + expect_type(res, "list") + + # Check all expected fields are present + expect_named(res, c("editor", "global", "dark", "foreground", "background")) + + # Check default values + expect_equal(res$editor, "") + expect_equal(res$global, "") + expect_equal(res$dark, FALSE) + expect_equal(res$foreground, "") + expect_equal(res$background, "") +}) + +test_that("rs_theme returns invisible result", { + res <- capture.output({ + out <- rs_theme() + }) + expect_identical(rs_theme(), rs_theme()) # just ensures it runs invisibly +}) + diff --git a/vignettes/.gitignore b/vignettes/.gitignore new file mode 100644 index 0000000..097b241 --- /dev/null +++ b/vignettes/.gitignore @@ -0,0 +1,2 @@ +*.html +*.R diff --git a/vignettes/additional-options.Rmd b/vignettes/additional-options.Rmd new file mode 100644 index 0000000..6ae6890 --- /dev/null +++ b/vignettes/additional-options.Rmd @@ -0,0 +1,78 @@ +--- +title: "Additional Sync Controls: Backup and Force" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Additional Sync Controls: Backup and Force} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r setup} +library(syncdr) +``` + +## Synchronizing Using Additional Options + +To retain more control over the synchronization process, you can utilize two additional options available for all synchronization functions: backup and force. + +- **Backup Option**: Setting `backup = TRUE` will create a backup (copy) of the right directory before performing the synchronization. This backup is stored in the location specified by `backup_dir`. If `backup_dir` is not provided, the backup will be saved in a temporary directory (`tempdir`). This ensures that you can revert to the previous state if needed + +- **Force** **Option**: By default, `force = TRUE`, make all functions proceed directly with the synchronization without user's confirmation. However, to avoid unintended changes to your directories, you can set `force = FALSE`. This way the function will first display a preview of the proposed actions, including which files will be copied and which will be deleted. You will be prompted to confirm whether you wish to proceed with these actions. Synchronization will only continue if you agree; otherwise, it will be aborted, and no changes will be made to the directories. + +For example, suppose you are performing a full asymmetric synchronization of your, say, left and right directories. This type of synchronization entails that certain files will be copied over to the right directory, as well as that certain files will be deleted from it. + +To maintain greater control over the process, you can allow the function to perform the synchronization while simultaneously storing a copy of the original directory before any changes occur. This way, you can revert to the original if needed. + +```{r, setup2} + +# generate toy directories +e <- toy_dirs() + +.syncdrenv.1 <- syncdr:::copy_temp_environment() + +# Get left and right directories' paths +left <- .syncdrenv.1$left +right <- .syncdrenv.1$right + +sync_status <- compare_directories(left, right) + +# call sync function +full_asym_sync_to_right(sync_status = sync_status, + backup = TRUE) + + +``` + +Another option is to visualize a preliminary check, before the synchronization takes place. This can be done taking advantage of the force option. Note that prompt for user confirmation will work when you run the function interactively in your R session. However, this interactive prompt will not be functional within this article. + + +```{r include = FALSE} + +.syncdrenv.2 <- copy_temp_environment() + +# Get left and right directories' paths +left <- .syncdrenv.2$left +right <- .syncdrenv.2$right + +sync_status <- compare_directories(left, right) + + +``` + +```{r force} + +full_asym_sync_to_right(sync_status = sync_status, + force = FALSE) + +#You will need to type `no` or 'cancel' to stop the synchronization + + +``` + diff --git a/vignettes/asymmetric-synchronization.Rmd b/vignettes/asymmetric-synchronization.Rmd new file mode 100644 index 0000000..3bb3b0f --- /dev/null +++ b/vignettes/asymmetric-synchronization.Rmd @@ -0,0 +1,174 @@ +--- +title: "Asymmetric Synchronization Functions" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Asymmetric Synchronization Functions} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) + +``` + +```{r} +library(syncdr) +#devtools::load_all(".") + +# Create .syncdrenv +.syncdrenv = toy_dirs() + +# Get left and right directories' paths +left <- .syncdrenv$left +right <- .syncdrenv$right + +``` + +This article covers functions designed for ***asymmetric synchronization*** between two directories. + +## What is asymmetric synchronization? + +This is a **one-way** synchronization: you have a *master/leader* directory, and you want changes made there to be reflected in a *secondary/follower* directory. + +⏭️ For all synchronization functions below, note that synchronization occurs ***from left to right.*** This mean that the right directory will **mirror** the contents of the left directory. + +**Key Points:** + +When using these synchronization functions, you have two options for providing inputs: + +1. Specify the paths for both the left and right directories, and set the `by_date` and `by_content` arguments as desired (default: `by_date = TRUE` and `by_content = FALSE`). + +2. First, use the `compare_directories()` function to generate a sync_status object. Then, provide this object as input to the synchronization function. The `by_date` and `by_content` arguments will be automatically determined based on the `sync_status`. + +## Types of asymmetric synchronization + +`syncdr` allows to perform a ***specific set of asymmetric synchronization actions***, so that you can choose which one to execute depending on your needs + ++--------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------+ +| ::: {style="color:#0070FF"} | ::: {style="color:#0070FF"} | ::: {style="color:#0070FF"} | +| Type of synchronization | Actions on common files | Actions on non-common files | +| ::: | ::: | ::: | ++====================================================================+========================================================================================================================================================================+=================================================================================================================================+ +| Full asymmetric synchronization: | - If comparing by date only (`by_date = TRUE`): Copy files that are newer in the left directory to the right directory. | - Copy to the right directory those files that exist only in the left directory. | +| | - If comparing by date and content (`by_date = TRUE` and `by_content = TRUE`): Copy files that are newer and different in the left directory to the right directory. | | +| **`full_asym_sync_to_right()`** | - If comparing by content only (`by_content = TRUE`): Copy files that are different in the left directory to the right directory | - Delete from the right directory those files that are exclusive in the right directory (i.e., missing in the left directory) | ++--------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------+ +| Partial asymmetric synchronization -common files: | - if `by_date = TRUE` only: copy files that are newer in left to right | no actions | +| | | | +| **`common_files_asym_sync_to_right()`** | - if `by date = TRUE` and `by_content = TRUE`: copy files that are newer and different in left to right | | +| | | | +| | - if `by_content = TRUE` only: copy files that are different in left to right | | ++--------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------+ +| Full asymmetric synchronization of non common files | no actions | - copy those files that are only in left to right | +| | | | +| **`update_missing_files_asym_to_right()`** | | - delete in right those files that are only in right (i.e., files "only in right" or in other words missing in left) | ++--------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------+ +| Partial asymmetric asymmetric synchronization of non common files: | no actions | - copy those files that are only in left to right | +| | | | +| **`partial_update_missing_files_asym_to_right()`** | | - keep in right those files that are only in right (i.e., files 'missing in left') | ++--------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------+ + +: Types of asymmetric synchronization + +Let's see them in actions through the examples below: + +##### \*️⃣ **Note: `verbose = TRUE`** + +When executing any synchronization, you have the option to enable verbose mode by setting `verbose = TRUE`. This will display the tree structure of *both* directories BEFORE and AFTER the synchronization + +**1 - Full asymmetric synchronization:** + +```{r} + +# With leader/master directory being the left directory +# Option 1 +full_asym_sync_to_right(left_path = left, + right_path = right, + by_content = TRUE) +# Option 2 +sync_status <- compare_directories(left_path = left, + right_path = right, + by_content = TRUE) + +full_asym_sync_to_right(sync_status = sync_status) + +# With leader/master directory being the right directory +sync_status <- compare_directories(left_path = right, #notice args changing here + right_path = left, + by_content = TRUE) + +full_asym_sync_to_right(sync_status = sync_status) +``` + +**2 - Partial asymmetric synchronization -common files:** + +```{r include=FALSE} + +.syncdrenv.2 <- syncdr:::copy_temp_environment() + +# Get left and right directories' paths +left <- .syncdrenv.2$left +right <- .syncdrenv.2$right +``` + +```{r} + +sync_status <- compare_directories(left_path = left, + right_path = right) + +common_files_asym_sync_to_right(sync_status = sync_status) +``` + +**3 - Full asymmetric synchronization -non common files:** + +```{r include=FALSE} + +.syncdrenv.3 <- copy_temp_environment() + +# Get left and right directories' paths +left <- .syncdrenv.3$left +right <- .syncdrenv.3$right + +``` + +```{r} + +sync_status <- compare_directories(left_path = left, + right_path = right) + +update_missing_files_asym_to_right(sync_status = sync_status) + +``` + +**4 - Partial asymmetric synchronization -non common files:** + +```{r include=FALSE} + +.syncdrenv.4 <- copy_temp_environment() + +# Get left and right directories' paths +left <- .syncdrenv.4$left +right <- .syncdrenv.4$right + +``` + +```{r} + +sync_status <- compare_directories(left_path = left, + right_path = right) + +partial_update_missing_files_asym_to_right(sync_status = sync_status) + +``` + +## Synchronizing Using Additional Options + +To retain more control over the synchronization process, you can utilize two additional options available for all synchronization functions: backup and force. + +- **Backup Option**: Setting `backup = TRUE` will create a backup (copy) of the right directory before performing the synchronization. This backup is stored in the location specified by `backup_dir`. If `backup_dir` is not provided, the backup will be saved in a temporary directory (`tempdir`). This ensures that you can revert to the previous state if needed + +- **Force** **Option**: By default, force = TRUE, which means the function will proceed directly with the synchronization without any interruptions. If you set force = FALSE, the function will first display a preview of the proposed actions, including which files will be copied and which will be deleted. You will be prompted to confirm whether you wish to proceed with these actions. Synchronization will only continue if you agree; otherwise, it will be aborted, and no changes will be made to the directories. diff --git a/vignettes/auxiliary-functions.Rmd b/vignettes/auxiliary-functions.Rmd new file mode 100644 index 0000000..a6c9918 --- /dev/null +++ b/vignettes/auxiliary-functions.Rmd @@ -0,0 +1,54 @@ +--- +title: "Auxiliary Functions" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Auxiliary Functions} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) + +library(syncdr) +#devtools::load_all(".") +``` + +`syncdr` provides some auxiliary functions that might come in handy to better understand the content of your directories, even before taking certain synchronization actions. + +To develop some examples, let's start by calling `toy_dirs()`. This auxiliary function creates toy directories within the syncdr environment, containing samples common files as well as files exclusive to either directory. These toy directories are deleted every time a new session is started. +```{r setup} + +# Create .syncdrenv with left and right directories +.syncdrenv =toy_dirs() + +# Get left and right directories' paths +left <- .syncdrenv$left +right <- .syncdrenv$right + +``` + +Together with `toy_dirs()`, you might want to use `copy_temp_environment()`, in order to copy the original environment and play with the various {syncdr} functions on this copied environment rather than the original one which remains untouched. + +**Search for duplicate files** + +One useful auxiliary function is **`search_duplicates()`**, designed to generate a list of duplicate files within your directory. Duplicate files are identified based on identical content, regardless of their filenames. By default, `verbose = TRUE` will show you the list of duplicate files. To return it invisibly, set `verbose = FALSE`. In this latter case, a message will still pop up to let you know when the identification of duplicates is completed. + +```{r} + +# example +search_duplicates(right, verbose = TRUE) +``` + +**Save sync status file** + +Another auxiliary function is `save_sync_status`, which comes in handy to track and document the status of files within a directory over time. This function generates a summary of file synchronization details, including file hashes and modification dates, for each file in the specified directory. The summary is saved as a file within a _syncdr subdirectory, so that sync status information is easily accessible. + +The `save_sync_status` function supports various save formats (e.g., .fst, .csv, .Rds), depending on the available packages and user-specified options. + +```{r ssfile, eval = FALSE} +save_sync_status(dir_path = right) +``` diff --git a/vignettes/symmetric-synchronization.Rmd b/vignettes/symmetric-synchronization.Rmd new file mode 100644 index 0000000..48864f9 --- /dev/null +++ b/vignettes/symmetric-synchronization.Rmd @@ -0,0 +1,148 @@ +--- +title: "Symmetric Synchronization" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Symmetric Synchronization} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) + +#devtools::load_all(".") + +``` + +```{r setup} +library(syncdr) + +# Create .syncdrenv +.syncdrenv = toy_dirs() + +# Get left and right directories' paths +left <- .syncdrenv$left +right <- .syncdrenv$right +``` + +This article covers functions designed for symmetric synchronization between two directories. + +## What is symmetric synchronization? + +This is a **two-way** synchronization: this means that you compare both directories and update each other to reflect the latest changes: If a file is added, modified, or deleted in one directory, the corresponding action is taken in the other directory. This approach is useful when you want both directories to be always **up-to-date** with the latest changes, regardless of where those changes originate. + +**To keep in mind:** + +When calling these synchronization functions, you can provide inputs in two *alternative* ways: + +1. Specify paths of both left and right directories, as well as the \`by_date\` and \`by_content\` arguments as you wish the synchronization to be performed (if not specified, by default \`by_date = TRUE\` and \`by_content\` = FALSE) *OR* + +2. First call the workhorse function \`compare_directories()\` to obtain the sync_status object. Then, provide it as input to the synchronization function. You do not need to specify the 'by_date' and 'by_content' arguments, as they will automatically be determined depending on the 'sync_status'. + +## Types of symmetric synchronization + +Similar to its asymmetric counterpart, `syncdr` enables the execution of ***specific symmetric synchronizations*** with predefined options, allowing you to select the most suitable function based on your requirements. + ++---------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ +| ::: {style="color:#0070FF"} | ::: {style="color:#0070FF"} | ::: {style="color:#0070FF"} | +| Type of synchronization | Actions on common files | Actions on non-common files | +| ::: | ::: | ::: | ++=================================+=====================================================================================================================================================================+=======================================================================================+ +| Full symmetric synchronization: | - if `by_date = TRUE` only: | if a file exists in one but not in the other directory, it is copied over accordingly | +| | | | +| **`full_symmetric_sync()`** | - If the file in one directory is newer than the corresponding file in the other directory it will be copied over to update the older version | | +| | | | +| | - If modification dates are the same, no change is made | | +| | | | +| | - if `by_date = TRUE` and `by_content = TRUE`: | | +| | | | +| | - If the file in one directory is newer AND different than the corresponding file in the other directory, it will be copied over to update the older version. | | +| | | | +| | - If modification dates/contents are the same, no change is made | | +| | | | +| | - if `by_content = TRUE` only: no action | | ++---------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ +| | - if `by_date = TRUE` only: | No changes are made: | +| | | | +| | - If the file in one directory is newer than the corresponding file in the other directory, it will be copied over to update the older version. | - keep in right files that are only in right | +| | | | +| | - If modification dates are the same, no action is executed | - keep in left those that are only in left | +| | | | +| | - if `by_date = TRUE` and `by_content = TRUE`: | | +| | | | +| | - If the file in one directory is newer AND different than the corresponding file in the other directory, it will be copied over to update the older version | | +| | | | +| | - If modification dates/contents are the same, nothing is done | | +| | | | +| | - if `by_content = TRUE` only: no action | | ++---------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ + +: Types of symmetric synchronization + +Let's consider the following examples: + +##### \*️⃣ **Note: `verbose = TRUE`** + +As with asymmetric synchronization, also here you have the option to enable verbose mode by setting `verbose = TRUE`. This will display the tree structure of *both* directories BEFORE and AFTER the synchronization. + +**1 - Full symmetric synchronization:** + +**When comparing directories by date and content:** + +```{r} + +sync_status <- compare_directories(left_path = left, + right_path = right, + by_content = TRUE) + +# Providing left and right paths object +# full_symmetric_sync(left, right) + +# Providing sync_status object +full_symmetric_sync(sync_status = sync_status) +``` + +**When comparing directories by date only:** + +```{r include = FALSE} + +.syncdrenv.1 <- copy_temp_environment() + +# Get left and right directories' paths +left <- .syncdrenv.1$left +right <- .syncdrenv.1$right + +``` + +```{r} + +sync_status <- compare_directories(left_path = left, + right_path = right) + +# Example with left and right paths +full_symmetric_sync(left_path = left, + right_path = right) +``` + +**2 - Partial symmetric synchronization:** + +```{r include = FALSE} + +.syncdrenv.2 <- copy_temp_environment() + +# Get left and right directories' paths +left <- .syncdrenv.2$left +right <- .syncdrenv.2$right + +``` + +```{r} + +sync_status <- compare_directories(left_path = left, + right_path = right) + +partial_symmetric_sync_common_files(sync_status = sync_status) +``` diff --git a/vignettes/syncdr.Rmd b/vignettes/syncdr.Rmd new file mode 100644 index 0000000..67dc3ff --- /dev/null +++ b/vignettes/syncdr.Rmd @@ -0,0 +1,208 @@ +--- +title: "Introduction to {syncdr}" +subtitle: "File Handling, Directory Comparison & Synchronization in R" +author: "Rossana Tatulli" +output: +rmarkdown::html_vignette: + toc: true +vignette: > + %\VignetteIndexEntry{Introduction to {syncdr}} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + chunk_output_type: console +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) + +#devtools::load_all(".") +library(syncdr) +``` + +## Why {syncdr}? + +**{syncdr}** is an R package for handling and synchronizing files and directories. Its primary objectives are: + +1. To provide a clear **snapshot of the content and status of synchronization** between two directories under comparison: including their tree structure, their common files, and files that are exclusive to either directory +2. To make **file organization and management** in R easier: i.e., enabling content-based and modification date-based file comparisons, as well as facilitating tasks such as duplicates identification, file copying, moving, and deletion. + +------------------------------------------------------------------------ + +💡\ +This article does ***not*** offer a comprehensive overview of **{syncdr}** functionalities. Rather it provides a sample workflow for working with the package's main functions . After familiarizing yourself with this general workflow, read the articles throughout the rest of this website -they will explore all features of **{syncdr}** in a structured way. + +------------------------------------------------------------------------ + +## Synchronizing with {syncdr} + +**Learn how to work with {syncdr} and compare and synchronize directories in R** + +Suppose you are working with two directories, let's call them `left` and `right` -each containing certain files and folders/sub-folders. + +Let's first call `syncdr` function `toy_dirs()`. This generates two toy directories in `.syncdrenv` environment -say `left` and `right`- that we can use to showcase `syncdr` functionalities. + +```{r toy-dirs} + +# Create syncdr env with left and right directories +.syncdrenv =toy_dirs() + +# Get left and right directories' paths +left <- .syncdrenv$left +right <- .syncdrenv$right + +``` + +You can start by quickly comparing the two directories' tree structure by calling `display_dir_tree()`. By default, it fully recurses -i.e., shows the directory tree of all sub-directories. However, you can also specify the number of levels to recurse using the `recurse` argument. + +```{r display-toy-dirs} + +# Visualize left and right directories' tree structure +display_dir_tree(path_left = left, + path_right = right) + +``` + +### Step 1: Compare Directories + +The most important function in `syncdr` is `compare_directories()`. It takes the paths of left and right directories and compares them to determine their synchronization status *(see below)*. This function represents the backbone of `syncdr`: you can utilize the `syncdr_status` object it generates both: + +- to *inspect* the synchronization status of files present in both directories as well as those exclusive to either directory + +- as the input for all other functions within `syncdr` that allow *synchronization* between the directories under comparison*.* + +Before diving into the resulting `syncdr_status` object, note that `compare_directories()` enables to compare directories in 3 ways: + +1. By **date** only -*the default:* by default, `by_date = TRUE`, so that files in both directories are compared based on the date of last modification. + +| sync_status (*all* *common files)* | +|:----------------------------------:| +| older in left, newer in right dir | +| newer in left, olderin right dir | +| same date | + +2. By **date and content**. This is done by specifying `by_content = TRUE` (by default `by_date = TRUE` if not specifically set to FALSE). Files are first compared by date, and then only those that are newer in either directory will be compared by content. + +| sync_status (*common files that are newer in either left or right, i.e., not of same date )* | +|:--------------------------------------------------------------------------------------------:| +| different content | +| same content | + +3. By **content** only, by specifying `by_date = FALSE` and `by_content = TRUE` . This option is however discouraged -comparing all files' contents can be slow and computationally expensive. + +| sync_status (*all* *common files)* | +|:----------------------------------:| +| different content | +| same content | + +Also, regardless of which options you choose, the sync_status of files that are exclusive to either directory is determined as: + +| sync_status (*non* *common files)* | +|:----------------------------------:| +| only in left | +| only in right | + +Let's now take a closer look at the output of `compare_directories()`, which is intended to contain comprehensive information on the directories under comparison. This is a list of class `syncdr_status`, containing 4 elements: (1) common files, (2) non common files, (3) left path and (4) right path + +##### **1. Comparing by date** + +```{r by-date} + +# Compare by date only -the Default +sync_status_date <- compare_directories(left, + right) + +sync_status_date +``` + +##### **2. Comparing by date and content** + +```{r by-date-cont} + +# Compare by date and content +sync_status_date_content <- compare_directories(left, + right, + by_content = TRUE) + +sync_status_date_content +``` + +##### **3. Comparing by content only** + +```{r by-content} + +# Compare by date and content +sync_status_content <- compare_directories(left, + right, + by_date = FALSE, + by_content = TRUE) + +sync_status_content +``` + +##### \*️⃣ **Comparing directories with `verbose = TRUE`** + +When calling `compare_directories()`, you have the option to enable verbose mode by setting `verbose = TRUE`. This will display both directories tree structure and, when comparing files by content, provide progress updates including the time spent hashing the files. + +```{r verbose-example} + +compare_directories(left, + right, + by_date = FALSE, + by_content = TRUE, + verbose = TRUE) + +``` + +### Step 2: Visualize Synchronization Status + +The best way to read through the output of `compare_directories()` is by visualizing it with `display_sync_status()` function. + +For example, let's visualize the sync status of common files in left and right directories, when compared by date + +```{r} + +display_sync_status(sync_status_date$common_files, + left_path = left, + right_path = right) +``` + +or let's display the sync status of non common files: + +```{r} + +display_sync_status(sync_status_date$non_common_files, + left_path = left, + right_path = right) +``` + +### Step 3: Synchronize directories + +`syncdr` enables users to perform different actions such as copying, moving, and deleting files using specific synchronization functions. Refer to the `vignette("asymmetric-synchronization")` and `vignette("symmetric-synchronization")` articles for detailed information. + +For the purpose of this general demonstration, we will perform a 'full asymmetric synchronization to right'. This specific function executes the following: + +- **On common files:** + - If by date only (`by_date = TRUE`): Copy files that are newer in the left directory to the right directory. + - If by date and content (`by_date = TRUE` and `by_content = TRUE`): Copy files that are newer and different in the left directory to the right directory. + - If by content only (`by_content = TRUE`): Copy files that are different in the left directory to the right directory. +- **On non common files:** + - Copy to the right directory those files that exist only in the left directory + - Delete from the right directory those files that are exclusive in the right directory (i.e., missing in the left directory) + +```{r asym-sync-example} + +# Compare directories + +sync_status <- compare_directories(left, + right, + by_date = TRUE) + +# Synchronize directories +full_asym_sync_to_right(sync_status = sync_status) + + +``` diff --git a/vignettes/visualizations.Rmd b/vignettes/visualizations.Rmd new file mode 100644 index 0000000..1ac86ad --- /dev/null +++ b/vignettes/visualizations.Rmd @@ -0,0 +1,81 @@ +--- +title: "Visualizations" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Visualizations} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) + +old_options <- options(width = 200) +options(width = 200) +#devtools::load_all(".") +``` + +## Visualize synchronization status + +When comparing your directories, say `left` and `right`, use `display_sync_status()` for an effective comparison visualization. + +As with any function from `syncdr`, you will have to call `compare_directories()` first: + +### Example when comparing directories by date & content + +```{r setup} + +library(syncdr) + +# Create syncdr env with left and right directories +.syncdrenv =toy_dirs() + +# Get left and right directories' paths +left <- .syncdrenv$left +right <- .syncdrenv$right + +``` + + +```{r} + +sync_status <- compare_directories(left, + right, + by_content = TRUE) + +``` + +`display_sync_status()` allows you to visualize the synchronization status of either (1) common files or (2) non common files, as you can see in the examples below: + +```{r dtable} + +display_sync_status(sync_status$common_files, + left_path = left, + right_path = right) +display_sync_status(sync_status$non_common_files, + left_path = left, + right_path = right) + +``` + +## Visualize directories structure + +Moreover, you have the option to utilize `display_dir_tree()` for a swift overview of your directory(ies) structure, whether for a single directory or both simultaneously. + +```{r} + +# Tree structure or right directory +display_dir_tree(path_left = left) + +# Tree structure of left directory +display_dir_tree(path_right = right) + +# Tree structure of both +display_dir_tree(path_left = left, path_right = right, ) + +# Restore options +options(old_options) +```