# Instalar/cargar paquetes necesarios
pkgs <- c("fs","digest","dplyr","readr","stringr","glue","purrr","tibble","tools","zip")
to_install <- pkgs[!sapply(pkgs, requireNamespace, quietly = TRUE)]
if(length(to_install)) install.packages(to_install)
invisible(lapply(pkgs, library, character.only = TRUE))
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
## Warning: package 'zip' was built under R version 4.4.3
##
## Attaching package: 'zip'
## The following objects are masked from 'package:utils':
##
## unzip, zip
options(stringsAsFactors = FALSE)
Qué hace: define las rutas de los ZIP (nueva y vieja), el
directorio de trabajo, las extensiones a procesar y los flags de
ejecución. Funciones clave: (ninguna función aún; solo
variables y flags). Lógica: con dry_run decides si
solo simulas (no mueve/borra) o ejecutas movimientos reales; con
really_delete decides si borrar definitivamente o mover a
“cuarentena”. Mantener really_delete = FALSE es más
seguro.
# RUTAS DE TUS ARCHIVOS ZIP (usa barras / para evitar escapes de Windows)
new_zip <- "C:/Users/tomas/Downloads/drive-download-20250819T034808Z-1-001.zip"
old_zip <- "C:/Users/tomas/Downloads/drive-download-20250818T234745Z-1-001.zip"
# Carpeta de trabajo temporal donde se extraerá todo
work_base <- "C:/Users/tomas/Downloads/photo_dedup_work"
# Extensiones a considerar
extensions <- c("HEIC","heic","MOV","mov")
# Modo simulación (no mueve/borra nada). Pon FALSE cuando verifiques resultados.
dry_run <- FALSE
# Si pones TRUE y dry_run == FALSE, los archivos se BORRAN en lugar de moverse.
# Recomendado: dejar en FALSE para mover a carpetas "cuarentena".
really_delete <- FALSE
Qué hace: crea carpetas de trabajo y define utilidades para descomprimir, listar archivos, hashearlos, mover/borrar y guardar logs.
Funciones clave:
safe_unzip()descomprime con limpieza previa.
list_media() lista .HEIC/.MOV de forma recursiva.
hash_file() calcula SHA-256 (para detectar duplicados
por contenido).
quarantine_or_delete() mueve a “cuarentena” o borra
según flags.
save_csv() guarda reportes en logs/.
Lógica: preparar un “pipeline” seguro que no mezcle ejecuciones anteriores y que deje trazabilidad en CSV.
# Directorios de trabajo
dir_new <- fs::path(work_base, "new_extracted")
dir_old <- fs::path(work_base, "old_extracted")
dir_out_keep <- fs::path(work_base, "to_upload")
dir_out_dups_new <- fs::path(work_base, "dups_within_new")
dir_out_in_old <- fs::path(work_base, "already_in_old")
dir_logs <- fs::path(work_base, "logs")
fs::dir_create(work_base)
fs::dir_create(dir_logs)
# Función para descomprimir con sobreescritura segura
safe_unzip <- function(zip_path, out_dir){
fs::dir_create(out_dir)
# Borra contenido previo del dir (no el dir) para evitar mezclas
if(length(fs::dir_ls(out_dir, all = TRUE)) > 0){
fs::dir_delete(out_dir)
fs::dir_create(out_dir)
}
utils::unzip(zipfile = zip_path, exdir = out_dir, overwrite = TRUE)
}
# Hash de archivo (sha256)
hash_file <- function(f){
tryCatch(
digest::digest(file = f, algo = "sha256"),
error = function(e) NA_character_
)
}
# Listar archivos con extensiones objetivo (recursivo)
list_media <- function(path, exts){
if(!fs::dir_exists(path)) return(tibble::tibble(path=character(), size=integer(), ext=character()))
files <- fs::dir_ls(path, recurse = TRUE, type = "file", fail = FALSE)
if(length(files) == 0) return(tibble::tibble(path=character(), size=integer(), ext=character()))
df <- tibble::tibble(
path = files,
ext = tools::file_ext(files)
) |>
dplyr::filter(ext %in% exts) |>
dplyr::mutate(size = as.numeric(fs::file_info(path)$size))
df
}
# Mover o borrar según flags
quarantine_or_delete <- function(paths, target_dir){
if(length(paths) == 0) return(invisible(NULL))
if(dry_run){
message(glue::glue("[dry_run] → {length(paths)} archivos IRÍAN a: {target_dir}"))
return(invisible(NULL))
}
if(really_delete){
message(glue::glue("BORRANDO {length(paths)} archivos"))
fs::file_delete(paths)
} else {
fs::dir_create(target_dir)
# Preservar estructura de nombres básica
base_names <- fs::path_file(paths)
# Resolver colisiones de nombre
uniq_names <- make.unique(base_names, sep = "_")
dests <- fs::path(target_dir, uniq_names)
fs::file_move(paths, dests)
}
}
# Guardar CSV de apoyo
save_csv <- function(df, name){
readr::write_csv(df, fs::path(dir_logs, name))
message(glue::glue("Guardado log: {fs::path(dir_logs, name)}"))
}
Qué hace: verifica que existan los ZIP y los descomprime a
carpetas limpias; luego lista los archivos de interés. Funciones
clave: safe_unzip(), list_media().
Lógica: trabajar con copias extraídas y un índice (data frame)
de archivos sobre el que se harán todas las comparaciones.
stopifnot(fs::file_exists(new_zip), fs::file_exists(old_zip))
message("Descomprimiendo carpeta NUEVA…")
## Descomprimiendo carpeta NUEVA…
safe_unzip(new_zip, dir_new)
message("Descomprimiendo carpeta VIEJA…")
## Descomprimiendo carpeta VIEJA…
safe_unzip(old_zip, dir_old)
message("Listando archivos de interés…")
## Listando archivos de interés…
new_df <- list_media(dir_new, extensions)
old_df <- list_media(dir_old, extensions)
cat(glue::glue("NUEVA: {nrow(new_df)} archivos (.HEIC/.MOV)\n"))
## NUEVA: 336 archivos (.HEIC/.MOV)
cat(glue::glue("VIEJA: {nrow(old_df)} archivos (.HEIC/.MOV)\n"))
## VIEJA: 57 archivos (.HEIC/.MOV)
Qué hace: calcula un hash SHA-256 por archivo (contenido) y
guarda índices base en logs/. Funciones clave:
hash_file() + purrr::map_chr(), save_csv().
Lógica: el hash permite identificar duplicados reales aunque
cambie el nombre; los CSV sirven para auditoría y verificación
posterior.
message("Calculando hashes sha256 (esto puede tardar un poco)…")
## Calculando hashes sha256 (esto puede tardar un poco)…
# Hash para NUEVA
new_df$sha256 <- purrr::map_chr(new_df$path, hash_file)
# Hash para VIEJA
old_df$sha256 <- purrr::map_chr(old_df$path, hash_file)
# Guardar logs
save_csv(new_df, "new_before_dedup.csv")
## Guardado log: C:/Users/tomas/Downloads/photo_dedup_work/logs/new_before_dedup.csv
save_csv(old_df, "old_index.csv")
## Guardado log: C:/Users/tomas/Downloads/photo_dedup_work/logs/old_index.csv
sum(is.na(new_df$sha256)) -> na_new
sum(is.na(old_df$sha256)) -> na_old
if(na_new > 0) warning(glue::glue("No se pudo hashear {na_new} archivos en NEW (ver log)."))
if(na_old > 0) warning(glue::glue("No se pudo hashear {na_old} archivos en OLD (ver log)."))
Qué hace: detecta duplicados dentro de NEW comparando hashes y conserva una sola copia. Funciones clave: dplyr::group_by(sha256), arrange(), mutate(keep = row_number() == 1), quarantine_or_delete(). Lógica: agrupa por contenido idéntico; mantiene la primera (menor tamaño como proxy) y mueve el resto a dups_within_new/ (o borra si lo activas).
message("Detectando duplicados dentro de la carpeta NUEVA…")
## Detectando duplicados dentro de la carpeta NUEVA…
dups_within <- new_df |>
dplyr::filter(!is.na(sha256)) |>
dplyr::group_by(sha256) |>
dplyr::arrange(size, .by_group = TRUE) |>
dplyr::mutate(keep = dplyr::row_number() == 1) |>
dplyr::ungroup()
to_remove_within <- dups_within |>
dplyr::filter(!keep) |>
dplyr::pull(path)
save_csv(dups_within, "new_within_groups.csv")
## Guardado log: C:/Users/tomas/Downloads/photo_dedup_work/logs/new_within_groups.csv
cat(glue::glue("Duplicados dentro de NEW: {length(to_remove_within)}\n"))
## Duplicados dentro de NEW: 49
# Mover/borrar duplicados internos
quarantine_or_delete(to_remove_within, dir_out_dups_new)
# Actualizar new_df quitando los movidos/borrados
if(!dry_run && !really_delete && length(to_remove_within) > 0){
new_df <- new_df |> dplyr::filter(!path %in% to_remove_within)
} else if(!dry_run && really_delete && length(to_remove_within) > 0){
new_df <- new_df |> dplyr::filter(!path %in% to_remove_within)
}
Qué hace: compara NEW contra OLD por hash para identificar archivos ya existentes en la carpeta vieja. Funciones clave: comparación de vectores con %in%, quarantine_or_delete(), save_csv(). Lógica: cualquier hash de NEW que esté en old_hashes no es nuevo, así que se mueve a already_in_old/ (o se borra si lo activas).
message("Comparando NEW contra OLD por hash…")
## Comparando NEW contra OLD por hash…
old_hashes <- unique(old_df$sha256[!is.na(old_df$sha256)])
already_in_old <- new_df |>
dplyr::filter(!is.na(sha256), sha256 %in% old_hashes) |>
dplyr::pull(path)
save_csv(tibble::tibble(path = already_in_old), "already_in_old.csv")
## Guardado log: C:/Users/tomas/Downloads/photo_dedup_work/logs/already_in_old.csv
cat(glue::glue("Archivos en NEW que ya están en OLD: {length(already_in_old)}\n"))
## Archivos en NEW que ya están en OLD: 46
# Mover/borrar coincidencias con OLD
quarantine_or_delete(already_in_old, dir_out_in_old)
# Actualizar new_df quitando los movidos/borrados
if(!dry_run && length(already_in_old) > 0){
new_df <- new_df |> dplyr::filter(!path %in% already_in_old)
}
Qué hace: calcula el conjunto final de archivos verdaderamente nuevos y los mueve a to_upload/ (y opcionalmente crea un ZIP). Funciones clave: setdiff() para definir “lo que queda”, fs::file_move() para mover y zip::zip() para comprimir. Lógica: quita de NEW todo lo marcado como duplicado interno o ya existente en OLD; lo restante es lo que subes a Google Photos.
# En simulación o no, calcula qué QUEDARÍA si se moviera/borra lo marcado:
remove_candidates <- union(to_remove_within, already_in_old)
remaining_paths <- setdiff(new_df$path, remove_candidates)
remaining <- new_df |>
dplyr::filter(path %in% remaining_paths, !is.na(sha256))
# (Opcional) nombres de archivos que quedarían
save_csv(
tibble::tibble(path = remaining$path, file = fs::path_file(remaining$path)),
"to_upload_preview.csv"
)
## Guardado log: C:/Users/tomas/Downloads/photo_dedup_work/logs/to_upload_preview.csv
cat(glue::glue("Archivos NUEVOS listos para subir: {nrow(remaining)}\n"))
## Archivos NUEVOS listos para subir: 241
save_csv(remaining, "to_upload_list.csv")
## Guardado log: C:/Users/tomas/Downloads/photo_dedup_work/logs/to_upload_list.csv
if(dry_run){
message("[dry_run] No se moverán los archivos finales. Revisa los conteos y los logs en /logs.")
} else {
fs::dir_create(dir_out_keep)
# Mover lo que queda a to_upload/
if(nrow(remaining) > 0){
base_names <- fs::path_file(remaining$path)
uniq_names <- make.unique(base_names, sep = "_")
dests <- fs::path(dir_out_keep, uniq_names)
fs::file_move(remaining$path, dests)
}
# (Opcional) crear un ZIP con lo que quedó
out_zip <- fs::path(work_base, "to_upload.zip")
if(fs::dir_exists(dir_out_keep) && length(fs::dir_ls(dir_out_keep)) > 0){
zip::zip(zipfile = out_zip, files = fs::dir_ls(dir_out_keep, recurse = TRUE))
message(glue::glue("ZIP creado: {out_zip}"))
}
}
## Warning in warn_for_colon(data$key): Some paths include a `:` character, this
## might cause issues when uncompressing the zip file on Windows.
## ZIP creado: C:/Users/tomas/Downloads/photo_dedup_work/to_upload.zip
Qué hace: imprime métricas globales y, si no estás en dry_run, cuantifica lo que quedó en cada carpeta de salida. Funciones clave: lectura de CSV de logs/ con readr::read_csv(). Lógica: validar que los conteos coincidan con tu expectativa antes de subir.
cat("\n--- RESUMEN ---\n")
##
## --- RESUMEN ---
cat(glue::glue("Total NEW (inicial): {nrow(readr::read_csv(fs::path(dir_logs,'new_before_dedup.csv'), show_col_types = FALSE))}\n"))
## Total NEW (inicial): 336
cat(glue::glue("Total OLD: {nrow(readr::read_csv(fs::path(dir_logs,'old_index.csv'), show_col_types = FALSE))}\n"))
## Total OLD: 57
within_tbl <- readr::read_csv(fs::path(dir_logs,"new_within_groups.csv"), show_col_types = FALSE)
cat(glue::glue("Duplicados internos detectados en NEW (moved/borrados): {sum(!within_tbl$keep)}\n"))
## Duplicados internos detectados en NEW (moved/borrados): 49
already_tbl <- readr::read_csv(fs::path(dir_logs,"already_in_old.csv"), show_col_types = FALSE)
cat(glue::glue("Coincidencias NEW vs OLD (moved/borrados): {nrow(already_tbl)}\n"))
## Coincidencias NEW vs OLD (moved/borrados): 46
if(!dry_run){
if(fs::dir_exists(dir_out_keep)) {
cat(glue::glue("Archivos finales en to_upload/: {length(fs::dir_ls(dir_out_keep))}\n"))
}
if(fs::dir_exists(dir_out_dups_new)) {
cat(glue::glue("En dups_within_new/: {length(fs::dir_ls(dir_out_dups_new))}\n"))
}
if(fs::dir_exists(dir_out_in_old)) {
cat(glue::glue("En already_in_old/: {length(fs::dir_ls(dir_out_in_old))}\n"))
}
} else {
cat("[dry_run] No se movió/borro nada. Ajusta 'dry_run = FALSE' para ejecutar los cambios.\n")
}
## Archivos finales en to_upload/: 241En dups_within_new/: 49En already_in_old/: 46