Достаточно часто возникает потребность проведения периодических вычислений и подготовки консолидированного отчета по самодостаточным данным. Т.е. по данным, которые хранятся в виде файлов. Это могут быть данные, набранные из открытых источников, различные документы и excel таблицы, выгрузки из корпоративных систем. Данные в сыром виде могут занимать как несколько мегабайт, так и несколько гигабайт. Данные могут быть обезличенными, либо содержать конфиденциальную информацию. В том случае, когда код вычислений помещается в репозиторий, а работа ведется более чем одним человеком более чем на одном компьютере, возникает проблема сохранения консистентности кода и данных. При этом необходимо еще обеспечить соблюдение разных прав доступа к коду и данным. Что делать?


Является продолжением предыдущих публикаций.


RStudio сейчас активно разрабатывают пакет pins для решения этой проблемы. К сожалению, применяемые бэкенд решения несколько непопулярны и дороговаты для применения на просторах нашей страны. AWS, Azure, Google cloud… за каждый чих надо платить, и за хранение и за трафик. Аутентификацию AWS4 pins пока не поддерживает, так что Yandex cloud пока тоже в стороне, хотя и он не бесплатен.


С другой стороны, команды аналитиков, работающих над конкретными задачами, как правило, невелики (не более 5-10 человек). Многие используют Google drive, One drive и пр., в платном или бесплатном формате. Почему бы не воспользоваться уже приобретенными ресурсами? Ниже предлагается один из возможных workflow.


Общий план


  1. Вычисления должны проводиться локально на машине, а значит на машине должна быть актуальная реплика всех необходимых для проведения вычислений данных.
  2. Код должен быть под системой контроля версий. Данные туда не должны никоим образом попадать (потенциальные объем и конфиденциальность). Будем хранить реплику данных либо в отдельной папке в проекте (включив ее в .gitignore), либо во внешней относительно проекта директории.
  3. Хранилищем мастер данных будет выступать google drive. Права на доступ к директориям развешиваем в нем же.

Осталось дело за малым. Необходимо реализовать функционал синхронизации локальной реплики данных с облаком. Авторизация и аутентификация обеспечивается google.


Ниже код.


library(googledrive)
library(memoise)
# синхронизация кэша с google disk
drive_user()
updateGdCache(here::here("data/"), cloud_folder = "XXX___jZnIW3jdkbdxK0iazx7t63Dc")

Функция для синхронизации кэша
updateGdCache <- function(local_folder, cloud_folder){
  # обновляем весь кэш одним махом
  cache_fname <- "gdrive_sig.Rds"
  # 0. Делаем memoise на загрузку данных из гугла
  getGdriveFolder <- memoise(function(gdrive_folder){
    drive_ls(as_id(gdrive_folder), recursive = FALSE)
  })

  # 1. Проверяем наличие и загружаем облачные идентификаторы указанной папки
  cloud_gdrive_sig <- purrr::possibly(getGdriveFolder, NULL)(cloud_folder)
  # Если в облаке ничего нет или связи с ним нет, то и делать дальше нечего
  if(is.null(cloud_gdrive_sig)) {
    message("Some Google Drive issues happened. Can't update cache")
    return()
  }
  # 2. Выцепляем директорию и имя файла из пути
  fdir <- if(fs::is_dir(local_folder)) local_folder else fs::path_dir(local_folder)

  # 3. Загружаем список локальных файлов на файловой системе
  local_files <- fs::dir_ls(fdir, recurse = FALSE) %>%
    fs::path_file()

  # 4. Проверяем наличие и загружаем локальный кэш облачных идентификаторов
  local_gdrive_sig <- purrr::possibly(readRDS, NULL, quiet = TRUE)(fs::path(fdir, cache_fname))
  if(is.null(local_gdrive_sig)){
    # Если локального кэша нет, то сверять нечего, просто загружаем все из облака
    # сохраняем структуру, удаляем все данные
    local_gdrive_sig <- cloud_gdrive_sig %>%
      dplyr::filter(row_number() == -1)
  }
  # актуализируем сигнатуры локальных файлов с учетом реально существующих файлов
  local_gdrive_sig <- local_gdrive_sig %>%
    dplyr::filter(name %in% local_files)

  # 5. Сверяем идентификаторы и времена последнего изменения, оставляем файл на обновление, если есть отличия
  # Облако первично, по нему формируем список файлов для загрузки
  reconcile_tbl <- cloud_gdrive_sig %>%
    dplyr::rename(drive_resource_cloud = drive_resource) %>%
    dplyr::left_join(local_gdrive_sig, by = c("name", "id")) %>%
    tidyr::hoist(drive_resource_cloud, cloud_modified_time = "modifiedTime") %>%
    tidyr::hoist(drive_resource, local_modified_time = "modifiedTime") %>%
    # TODO: надо сверять время, а тут времена изменения выступают как строки
    # для отсутствующих локальных файлов время модификации = NA
    dplyr::mutate(not_in_sync = is.na(local_modified_time) | cloud_modified_time != local_modified_time)

  # 6. Выгружаем файлы на диск
  syncFile <- function(fpath, id){
    res <- purrr::possibly(drive_download, otherwise = NULL)(as_id(id), path = fpath, overwrite = TRUE, verbose = TRUE)
    ifelse(is.null(res), FALSE, TRUE)
  }
  # пакетная загрузка, для сброса сигнатур в кэш оставляем только те, что успешно загрузились
  sync_gdrive_sig <- reconcile_tbl %>%
    dplyr::filter(not_in_sync == TRUE) %>%
    dplyr::mutate(fpath = fs::path(fdir, name)) %>%
    dplyr::mutate(sync_status = purrr::map2_lgl(fpath, id, syncFile)) %>%
    dplyr::select(name, id, sync_status)

  # 7. Сбрасываем в локальный кэш только файлы, находящиеся в синхронизации
  # Собираем все воедино
  cloud_gdrive_sig %>%
    # исключаем ошибочные файлы
    dplyr::anti_join(dplyr::filter(sync_gdrive_sig, sync_status == FALSE), by = c("name", "id")) %>%
    saveRDS(fs::path(fdir, cache_fname))
}

В качестве пути указываем идентификатор папки в google drive, можно взять его из адресной строки браузера. Идентификатор будет неизменным, даже если папка будет перемещаться в драйве.



Просто, компактно, удобно и бесплатно.


Пара замечаний


  1. Есть проблемы с кодировкой у gargle 0.4.0 версии. Надо грузить dev версию. Подробнее здесь.
  2. Есть проблемы с авторизацией на RStudio Server «Unable to authorize from RStudio Server #79 {Closed}», но идеи по обходному пути можно поглядеть здесь.

Предыдущая публикация — «Программирование и новогодняя елка, можно ли их совместить?».