From 62fa547d6b3962a849c62a9551ae8912345b3b7d Mon Sep 17 00:00:00 2001 From: Anatoly Tsyplenkov Date: Tue, 20 Oct 2026 13:14:29 +0300 Subject: [PATCH] fix(index): download the catalogue without a 60 second cap download.file() stops at options(timeout), which defaults to 60 seconds. curl writes the index to a temporary file and moves it into the cache only when the size matches Content-Length. --- DESCRIPTION | 4 +-- R/index.R | 53 ++++++++++++++++++++++++++++++++----- tests/testthat/test-index.R | 37 ++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 185e943..495cdce 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: alphaearth Title: Search embeddings available in the AlphaEarth Foundations Satellite Embeddings dataset -Version: 0.1.0 +Version: 0.1.1 Authors@R: person("Felipe", "Carlos", email = "efelipecarlos@gmail.com", role = c("aut", "cre", "cph")) @@ -13,6 +13,7 @@ Depends: R (>= 4.1.0) Imports: cli, + curl, DBI, duckspatial, fs, @@ -23,7 +24,6 @@ Imports: sf, tibble Suggests: - curl, knitr, pkgload, rmarkdown, diff --git a/R/index.R b/R/index.R index 6302375..ec6927f 100644 --- a/R/index.R +++ b/R/index.R @@ -73,24 +73,65 @@ index <- function(overwrite = FALSE) { #' #' @noRd .index_download <- function(overwrite = FALSE) { - # get the index path gpkg <- .config_gpkg_path() - # check if the index already exists and if we should overwrite it if (fs::file_exists(gpkg) && !overwrite) { return(gpkg) } - # update user cli::cli_alert_info("Downloading the AlphaEarth index (this happens once)...") - # download index file - utils::download.file(.config_index_url(), gpkg, mode = "wb", quiet = TRUE) + staged <- fs::path_temp("aef_index.gpkg") + on.exit(if (fs::file_exists(staged)) fs::file_delete(staged), add = TRUE) + + # download index file (curl has no 60 second cap, unlike download.file()) + curl::curl_download(.config_index_url(), staged, quiet = TRUE) + + .index_check_size(staged, .config_index_url()) + fs::file_move(staged, gpkg) - # return! gpkg } +#' Check the downloaded index against the size reported by the server. +#' +#' @description Check the downloaded index against the size reported by the +#' server. +#' +#' @param path [fs::path()] with the path to the downloaded file. +#' @param url `character` with the URL the file was downloaded from. +#' +#' @return `TRUE`, invisibly. Errors when the sizes differ. +#' +#' @noRd +.index_check_size <- function(path, url) { + handle <- curl::new_handle(nobody = TRUE) + headers <- curl::parse_headers_list( + rawToChar(curl::curl_fetch_memory(url, handle = handle)$headers) + ) + + content_length <- headers[["content-length"]] + expected <- suppressWarnings(as.numeric(content_length)) + + # as.numeric(NULL) has length 0, so test the header before is.na(). + if (is.null(content_length) || length(expected) != 1L || is.na(expected)) { + return(invisible(TRUE)) + } + + actual <- fs::file_size(path) + + if (actual != expected) { + cli::cli_abort(c( + "Downloading the AlphaEarth index failed.", + "x" = "Downloaded {.val {actual}} bytes, but the server reported + {.val {expected}}.", + "i" = "Please try again. The index was not written to the cache." + )) + } + + invisible(TRUE) +} + #' Open a DuckDB connection #' #' @description Open a DuckDB connection with the spatial extension loaded. diff --git a/tests/testthat/test-index.R b/tests/testthat/test-index.R index 312f205..ae5dfac 100644 --- a/tests/testthat/test-index.R +++ b/tests/testthat/test-index.R @@ -29,3 +29,40 @@ test_that("index - reuses the cache without re-downloading", { # expect search resolves the fixture tiles expect_equal(search(fid = 3L)$tile, "24S-0-0") }) + +test_that("index - a missing content length does not fail the size check", { + path <- withr::local_tempfile() + writeBin(raw(4L), path) + + local_mocked_bindings( + curl_fetch_memory = function(url, handle) { + list( + headers = charToRaw( + "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\n\r\n" + ) + ) + }, + .package = "curl" + ) + + expect_true(.index_check_size(path, "https://example.invalid/aef_index.gpkg")) +}) + +test_that("index - a short download fails the size check", { + path <- withr::local_tempfile() + writeBin(raw(4L), path) + + local_mocked_bindings( + curl_fetch_memory = function(url, handle) { + list( + headers = charToRaw("HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\n") + ) + }, + .package = "curl" + ) + + expect_error( + .index_check_size(path, "https://example.invalid/aef_index.gpkg"), + "not written to the cache" + ) +})