Skip to contents

A package that needs a four-gigabyte reference dataset cannot ship it, cannot download it during R CMD check, and cannot afford to fetch a different version on Tuesday than it fetched on Monday. getaca is the layer that makes those three constraints compatible.

There is one engine and many declarations. Packages say what they need; getaca gets it. The declaration is a small R object that ships inside the installed package, so retrieving a resource requires no registration call, no load hook, and no service to be up.

This vignette walks the whole cycle: declaring a resource, retrieving it, behaving during checks, reading its provenance, and knowing where it went. Each section links the companion article that goes deeper.

Declaring what you need

A resource record names exact bytes: one checksum, one version label, and the places those bytes live.

backbone <- resource(
  name    = "backbone",
  version = "2026-06",
  urls    = c("https://primary.invalid/backbone-2026-06.zip",
              "https://mirror.invalid/backbone-2026-06.zip"),
  sha256  = strrep("9f", 32),
  size    = 4.1e9,
  license = "CC-BY-4.0"
)

backbone
#> <getaca resource record>
#>   name      backbone
#>   version   2026-06
#>   sha256    9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f
#>   size      4.1e+09
#>   license   CC-BY-4.0
#>   urls      https://primary.invalid/backbone-2026-06.zip
#>              https://mirror.invalid/backbone-2026-06.zip

Each field earns its place. The sha256 is what makes the record a record rather than a bookmark: a truncated transfer, a proxy that served an HTML error page, and a publisher who quietly recut the file all produce a different digest, and all three are caught before the bytes reach your code. The size is a cheap early check, so a transfer that ends at 40% fails as a truncation rather than running a four-gigabyte hash to discover the same thing. Extra urls are tried in order, which turns one host’s outage into a slower first call. The license travels into provenance, so a result can say what terms the data came under.

Records live in a registry, which is scoped to the declaring package:

reg <- registry(package = "yourpkg", resources = list(backbone))
reg
#> <getaca registry> yourpkg  (policy "bundled")
#>   digest: sha256:d44183e77921
#>   - backbone@2026-06  9f9f9f9f9f9f  [CC-BY-4.0]

Scoping matters more than it looks. Identity here is the triple package / name / version, so two packages may both declare something called "backbone" and never collide, in the cache or anywhere else.

format(resource_id("yourpkg", "backbone", "2026-06"))
#> [1] "yourpkg/backbone@2026-06"

Ship the registry where getaca looks for it, and there is nothing else to wire up:

registry_write(reg, "inst/getaca/registry.rds")

Discovery is a file test across the library paths. getaca asks system.file() for getaca/registry.rds inside each installed package, which is why a declaring package needs no .onLoad() and no dependency on getaca being attached.

See vignette("declaring") for the full authoring guide: mirrors, derived artefacts, YAML authoring and the pre-ship checklist.

Getting it

path <- getaca("backbone", package = "yourpkg")

That path points to a complete file, verified against the declared checksum, at the resolved version, in a cache slot getaca owns. What happens on the way there depends on what is already true:

  • cached and intact, the path comes back after a size check

  • cached but past the re-verification interval, the bytes are re-hashed first

  • absent, the mirrors are tried in order into a temporary file, which is sized and hashed before it is moved into place

An interrupted transfer resumes rather than restarting, and can never be mistaken for a finished resource. The temporary file is named after the declared checksum and the mirror that produced it, so a resumed transfer only ever continues bytes from the same host. Sharing one partial file across mirrors would let a failed attempt at the first be resumed onto by the second, and the resulting corruption is indistinguishable from the publisher having changed the file.

Two sessions asking for the same resource at the same time take a per-resource directory lock. The second waits, then observes the first session’s success and returns the same path, so a four-gigabyte download happens once. vignette("cache") covers the locking protocol and what happens to a lock whose holder died.

Which version a bare name resolves to

getaca("backbone") asks for a name, and the registry says which record that name means. When a package offers several versions, it states the answer:

multi <- registry(
  package = "yourpkg",
  current = c(backbone = "2026-09"),
  resources = list(
    resource("backbone", "2026-06",
             urls = "https://primary.invalid/backbone-2026-06.zip",
             sha256 = strrep("9f", 32), license = "CC-BY-4.0"),
    resource("backbone", "2026-09",
             urls = "https://primary.invalid/backbone-2026-09.zip",
             sha256 = strrep("ab", 32), license = "CC-BY-4.0")
  )
)

resolve_resource("backbone", registry = multi)$id
#> <getaca resource> yourpkg/backbone@2026-09

The older record stays resolvable by asking for it:

resolve_resource("backbone", registry = multi, version = "2026-06")$id
#> <getaca resource> yourpkg/backbone@2026-06

Version strings here are labels rather than semantic versions. source-2026-06_build-3 is a perfectly good label and has no defensible ordering, so getaca never tries to rank them. A registry that declares two versions of a name and states no head is refused:

registry(
  package = "yourpkg",
  resources = list(
    resource("backbone", "2026-09",
             urls = "https://primary.invalid/a",
             sha256 = strrep("ab", 32)),
    resource("backbone", "2026-03",
             urls = "https://primary.invalid/b",
             sha256 = strrep("cd", 32))
  )
)
#> Error:
#> ! Invalid getaca registry for package 'yourpkg'.
#>   - resource 'backbone' declares 2 versions (2026-09, 2026-03) but the registry names no current one; add current = c("backbone" = "2026-03")
#> 
#> Fix: the declaring package needs a correction. Report it to its maintainer.

The error lands on the author’s machine, at the moment the registry is built, rather than on every user of the package.

Surviving R CMD check

Resolution collapses to offline under check, whatever policy is set, so a check run never reaches the network. Three helpers cover the three places that matters, and they answer three different questions.

In tests, skip cleanly and say what is missing:

test_that("the backbone parses", {
  getaca_skip_if_unavailable("backbone", package = "yourpkg")
  expect_s3_class(read_backbone(getaca("backbone", package = "yourpkg")), "backbone")
})

In examples and vignettes, degrade to a message rather than an error. This vignette is running with the network switched off, so the call below takes exactly the path a CRAN check machine would take:

path <- getaca_optional("backbone", registry = reg)
#> getaca: 'backbone' is not available here, so this output is abbreviated.
#> yourpkg/backbone@2026-06 is not cached and cannot be downloaded (offline mode is in effect).
#> 
#> Action: on a connected machine run getaca_prefetch("backbone", package = "yourpkg"),
#> or point GETACA_CACHE at a cache that already holds it.
#> 
#> Fix: this is expected during checks. Use getaca_skip_if_unavailable()
#> in tests and getaca_optional() in examples.
is.null(path)
#> [1] TRUE

And where a plain logical reads better:

getaca_available("backbone", registry = reg)
#> [1] FALSE

getaca_available() never touches the network. It asks whether a cached copy exists and passes its cheap integrity check, which is what makes it safe to call in a condition that gates expensive work.

To prepare a machine that will later be offline, or a CI job that should find everything already present:

getaca_prefetch("backbone", package = "yourpkg")
getaca_prefetch(package = "yourpkg")   # everything the package declares

Setting GETACA_CACHE points any session at a cache that has already been seeded, which is how a CI job restores a cached directory and finds the resources waiting. vignette("checks") has the workflow files.

When it does not work

Every failure is classed, and carries an actor field naming who can act on it. Here is the one a check run produces, in full:

err <- tryCatch(getaca("backbone", registry = reg), getaca_error = function(e) e)
class(err)
#> [1] "getaca_error_offline"     "getaca_error_unavailable"
#> [3] "getaca_error"             "error"                   
#> [5] "condition"
cat(conditionMessage(err))
#> yourpkg/backbone@2026-06 is not cached and cannot be downloaded (offline mode is in effect).
#> 
#> Action: on a connected machine run getaca_prefetch("backbone", package = "yourpkg"),
#> or point GETACA_CACHE at a cache that already holds it.
#> 
#> Fix: this is expected during checks. Use getaca_skip_if_unavailable()
#> in tests and getaca_optional() in examples.
err$actor
#> [1] "user"

getaca_error_offline is a subclass of getaca_error_unavailable, so a handler for the general case catches it and a narrower handler can separate the two. A declaring package usually catches the ones its users will meet and answers in its own vocabulary:

install_backbone <- function(name = "backbone") {
  path <- tryCatch(
    getaca(name, package = "yourpkg"),
    getaca_error_unavailable = function(e) {
      stop("The backbone is not installed and no network is available.\n",
           "Connect, then run: yourpkg::install_backbone(\"backbone\")", call. = FALSE)
    }
  )
  open_backbone(path)
}

Six conditions cover the failure surface, and they distinguish causes a plain downloader reports identically. vignette("failures") works through each one, including the case where several independent mirrors agree with each other and disagree with the registry.

Knowing where a file came from

getaca_info("backbone", package = "yourpkg")
#> <getaca cache entry> yourpkg/backbone@2026-06
#>   path        ~/.cache/R/getaca/yourpkg/backbone/2026-06/raw/backbone-2026-06.zip
#>   sha256      9f9f9f...
#>   size        4,100,000,000 bytes
#>   license     CC-BY-4.0
#>   resolved by bundled registry sha256:1c4d7a90f2be (published 2026-07-20)
#>   source url  https://primary.invalid/backbone-2026-06.zip
#>   getaca      0.0.0.9000
#>   fetched     2026-07-26 11:02:13
#>   verified    2026-07-26 11:09:44 (full re-hash)
#>   checked     2026-07-26 15:31:02 (size and mtime)

The registry digest names the exact declaration that chose these bytes, so “which version of yourpkg’s declaration was this” has an answer years later, without anyone having kept a revision number in step by hand.

Four timestamps, kept apart on purpose. “Verified” means the bytes were re-hashed then. “Checked” means size and modification time were compared against the entry, which is the cheap test run on ordinary access. “Accessed” is the clock the retention sweeps read. Collapsing them into one “last checked” field would make a resource verified in January look verified today because someone opened it this morning.

An uncached resource gives NULL from getaca_info(), which keeps the call usable in a report covering a machine that holds some of the set:

is.null(getaca_info("backbone", registry = reg))
#> [1] TRUE

getaca_catalogue() widens that to a data frame covering both halves: every resource the installed packages declare, and every copy the cache holds.

getaca_catalogue(registry = multi)[, c("name", "version", "current",
                                       "declared", "cached")]
#>       name version current declared cached
#> 1 backbone 2026-06   FALSE     TRUE  FALSE
#> 2 backbone 2026-09    TRUE     TRUE  FALSE

The three logical columns answer three different questions. current marks the version a bare request resolves to. declared says whether the registry in force names that version at all. cached says whether a local copy is recorded. A row with declared = TRUE, cached = FALSE is work still to do on this machine. A row with declared = FALSE is a copy of a version nothing asks for any more, which is what the retention sweeps reclaim first.

Where things are stored

getaca_cache_dir()
#> [1] "<tempdir>/getaca-quickstart"

That is the temporary directory this vignette is sandboxed in. The default is tools::R_user_dir("getaca", "cache"), which is what CRAN policy permits, on the condition that contents are actively managed. getaca treats that as a retention policy rather than a function users might find, and sweeps after every successful retrieval. The getaca.cache option and the GETACA_CACHE environment variable override it, in that order.

The layout is scoped the same way identity is:

<cache>/
  .locks/                        per-resource locks
  .tmp/                          in-flight downloads, never visible as cache
  <package>/
    index.rds                    provenance for this package only
    <name>/<version>/
      raw/<file>                 verified bytes as served
      proc-<processor-id>/       processed result, own provenance

Nothing about it is private. It is an ordinary directory tree, which is what makes GETACA_CACHE and a CI cache key sufficient for seeding.

To see what a sweep would remove without removing it:

getaca_clean(dry_run = TRUE)
#> [1] package  resource reason   bytes    path    
#> <0 rows> (or 0-length row.names)

An empty result on a fresh cache. On a working one, each row names the resource, the reason, and the bytes it would reclaim. vignette("cache") covers the four sweeps, the clocks they read, and what is never touched.

Choosing a policy

The registry declares a default, and a session or a single call can override it:

getaca_policy()
#> [1] "offline"
getaca_policy("current")                                     # for this session
getaca("backbone", package = "yourpkg", policy = "bundled")  # for this call

bundled is the default because a dependency that resolves differently on different days is not a dependency. current consults an author-controlled remote registry, which lets a dead mirror be repaired without a CRAN release. pinned resolves through a frozen local snapshot, so an analysis keeps resolving what it was written against. offline never reaches for the network at all.

getaca_pin(c("yourpkg", "otherpkg"))   # writes getaca.pins.rds in the project

vignette("policies") covers what a remote channel is allowed to change, why redefining a published version is refused, and how pinning interacts with renv.

Where to go next