Several established approaches solve neighbouring problems, and for a good number of packages one of them is the better fit. This article describes what each is built around and where the boundaries fall, so the choice can be made on the shape of the problem rather than on which package someone met first.
The question getaca is built around: a package needs a
file it cannot ship, and every machine running that package should get
the same bytes.
Bundling the data in the package
The first thing to rule in or out. A dataset in data/
needs no cache, no network, no checksum and no policy, and every one of
those is a moving part that can fail.
CRAN’s size guidance makes this a question of megabytes rather than
gigabytes, and the release cadence question usually settles it before
the size does: data in data/ release when the package
releases. For a reference table that changes yearly and a package that
releases yearly, coupling them is a feature.
Bundle when the data are small, static relative to your release cycle, and redistributable. Look further when any of the three fails.
A companion data package
The classic R answer to data that are too large for the main package
but still manageable: ship them as their own package, list it in
Suggests, and put it on CRAN, Bioconductor or
r-universe.
| companion data package | getaca |
|
|---|---|---|
| Size | fits a repository | too large to bundle |
| Release cadence | coupled to code releases | independent of them |
| Shape | naturally R objects | any file, any format |
| Granularity | all of it, always | users take what they need |
| License | redistribution permitted | download permitted, redistribution discouraged |
| Installation | the usual package machinery | first use, or an explicit prefetch |
| Offline | works once installed | works once cached |
The licence row is the one that decides it most often. A companion package redistributes the data, which needs terms that permit redistribution. Plenty of scientific datasets permit download and discourage or forbid redistribution, and for those a companion package is not available whatever its other merits.
Granularity is the second. A package needing one of fifteen reference
files, at a gigabyte each, would install fifteen through a companion
package. Under getaca a user takes what they use.
A companion package can itself use getaca, which is
occasionally the right structure when the companion owns expensive build
logic. It is rarely the first recommendation, since it moves the
complexity rather than removing it.
pins
pins publishes “data sets, models, and other R objects, making it easy to share them across projects and with your colleagues”, across boards including local folders, Posit Connect and AWS S3.
The unit is a pin on a board, and the person at the centre of the design is the one publishing an artefact for others to read. Boards abstract over where that artefact lives, which is what makes the same code work against a network drive during development and Posit Connect in production.
Reach for pins when the artefact is yours, the audience
is a team or an organisation, the storage backend matters, and writing
is part of the workflow. It covers a range of backends
getaca has no interest in growing.
Reach for getaca when the artefact is a third party’s,
the audience is everyone who installs your package, and the declaration
has to travel inside the package so that R CMD check and an
offline user both behave.
The two coexist. A package can declare its public reference data
through getaca and its team’s internal model artefacts
through pins without either knowing about the other.
BiocFileCache
BiocFileCache “creates a persistent on-disk cache of files that the user can add, update, and retrieve”, for resources that are costly or difficult to create and for web resources used across sessions, backed by an SQLite metadata database.
It is the established answer inside Bioconductor, and a Bioconductor package already carrying that dependency stack has little reason to add another. The model is a cache the calling package manages: your code decides what to add, when a cached copy needs updating, and what identity a resource has.
getaca makes those decisions from a declaration instead.
Identity is package / name / version, resolution runs
through a policy, and the retention sweeps are the package’s rather than
the caller’s. That is a narrower contract, and it exists because the
same behaviour then holds for every declaring package rather than being
reimplemented per package.
Reach for BiocFileCache when you are in the Bioconductor
ecosystem, when the resources are ones your code creates rather than
ones a publisher versions, or when you want the cache under your own
control.
pooch
The nearest equivalent outside R, and the one a reader arriving from Python will already know. pooch, “a friend to fetch your data files”, is where a Python package puts this problem: a registry of file names and hashes, a cache folder under the OS convention, downloaders and processors around them.
The overlap is the middle of the problem, fetch and hash and cache. The two ends differ, and both differences are about where the constraints come from.
At the front, a pooch registry is a registry.txt shipped
as package data, and version is documented as “the version
string for your project”, naming the subfolder the cache uses. Data
identity is the declaring project’s identity, so a repaired mirror or a
fresh upstream cut reaches users when the code does. A
getaca declaration versions the data separately, holds
several versions at once, names the head, and can resolve through a
remote registry the author keeps, which is what lets
2026-09 reach an installed copy between releases.
urls in pooch sets one URL per file; a getaca
record takes a list of mirrors and walks it until one answers. pooch
also accepts a doi: URL and asks figshare, Zenodo or
Dataverse for the download location as it fetches. getaca
asks the same three archives in registry_draft(), when the
registry is written, and a record then carries the DOI as what the bytes
are cited as and the locations as themselves.
At the back, the constraints are CRAN’s. R CMD check
runs the tests, examples and vignettes of every package on machines with
no network, and tools::R_user_dir() is permitted on
condition its contents are actively managed. The check clamp, the three
access helpers and the retention sweeps exist for those two
sentences.
A downloader and a cache directory
The common alternative in practice: download.file() or
curl, a directory under tools::R_user_dir(),
and a file.exists() check. It is a few dozen lines and it
works.
Two things cost more work than they look. The first is what happens when the bytes at the URL differ from the bytes you expected, which covers a truncated transfer, a proxy serving an HTML error page, and a publisher recutting a file. Each of those reaches your parser as a confusing error some distance from its cause.
The second is CRAN’s requirement to fail gracefully with an informative message when a resource is unavailable, across tests, examples and vignettes, on a machine with no network. That is the part that turns a few dozen lines into a few hundred, and it has to be written again in every package that depends on external data.
getaca is that layer written once. The declaring package
supplies a registry.
Targets and the workflow tools
targets and drake manage a computational
pipeline, tracking which steps need to re-run when inputs change. That
is a different question from where a file comes from and whether it is
the right file.
They compose. A targets pipeline can have a target whose
command is getaca("backbone", package = "yourpkg"), which
gives the pipeline a path, and the path a provenance record:
library(targets)
list(
tar_target(backbone_path, getaca::getaca("backbone", package = "yourpkg"),
format = "file"),
tar_target(backbone, read_backbone(backbone_path)),
tar_target(summary, summarise_backbone(backbone))
)format = "file" makes targets watch the
returned path, so the pipeline invalidates downstream targets when the
resolved version changes. That is the combination worth reaching for:
getaca decides which bytes, targets decides
what to recompute.
renv
renv records which package versions an analysis used. A
getaca pin records which data versions those packages
resolved to. Neither subsumes the other: a package version does not
determine a data version once the package is on the current
policy, and a data version says nothing about the code that read it.
renv::snapshot()
getaca_pin(c("yourpkg", "otherpkg"))Commit both files. Restoring is renv::restore() followed
by getaca_prefetch() on a connected machine, after which
the analysis runs offline. See vignette("policies") for
what a pin holds.
Decision summary
| If | then |
|---|---|
| the data are small, static and redistributable | put them in data/
|
| the data are moderate, redistributable and release with your code | a companion data package |
| you are publishing your own artefacts to a team | pins |
| you are inside Bioconductor, or caching things your code creates | BiocFileCache |
| you are managing which steps re-run |
targets, with getaca supplying the
path |
| your package declares a third party’s versioned file it cannot ship | getaca |
What getaca deliberately does not do
Naming the boundary is part of choosing. None of these is planned:
-
Credential storage. A declaration names the
environment variable a host requires, and getaca reads it at the moment
of the request. It holds no credentials, reads no
.netrcand talks to no keyring. Seevignette("declaring"). - Cloud storage abstractions. No S3, no Azure, no board concept. A URL is a URL.
-
DOI resolution at fetch time. A
doion a record is what the bytes are cited as, and it routes nothing. Resolving one through a repository API on every fetch would put a second host, which is not itself mirrorable, in front of every retrieval, and would break the mirror loop’s assumption that the locations it walks are independent sources. The archives are read when the registry is written instead:registry_draft()covers Zenodo, figshare and Dataverse, and emits plain locations. -
Reading data.
getacareturns a path and knows nothing about file formats. A processor can unpack an archive; nothing reads its contents. - Publishing. Uploading the data, minting the version, computing the checksum in the first place: all outside.
- Semantic version solving. Version strings are labels, and the registry names the head rather than ranking them.
-
Workflow orchestration. Composing with
targetsis the answer, rather than growing a dependency graph.
The list is short on purpose. Each item is a place where a package
could grow into something else, and the dependency footprint that makes
getaca inexpensive to depend on is the thing that would go
first.
packageDescription("getaca")$Imports
#> [1] "curl (>= 5.0.0), stats, tools, utils"Where to go next
vignette("quickstart")for the whole cycle in one passvignette("declaring")for what a declaring package writesvignette("policies")for reproducibility across machines and time