Fotos Guaviare

# 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)

Configuración de rutas y opciones

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

Configuración de directorios y funciones auxiliares

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)}"))
}

Extraer archivos de los ZIP

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)

Detección de duplicados dentro de NUEVA

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)."))

Eliminar duplicados dentro de la carpeta nueva

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)
}

Detección de duplicados entre NUEVA y VIEJA

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)
}

Archivos NUEVOS listos para subir

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

Resumen final

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