---
title: "Remote and Cloud Stores"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Remote and Cloud Stores}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  out.width = "100%"
)
library(pizzarr)

has_zarrs <- pizzarr:::.pizzarr_env$zarrs_available
has_s3 <- has_zarrs && ("s3" %in% pizzarr_compiled_features())
has_gcs <- has_zarrs && ("gcs" %in% pizzarr_compiled_features())
has_blosc <- requireNamespace("blosc", quietly = TRUE)

# Each gate probes the service its chunks actually use. Reaching one host says
# nothing about the other, and a gate that opens on the wrong probe fails the
# vignette build rather than skipping it.
reachable <- function(probe_url) {
  tryCatch({
    con <- url(probe_url)
    on.exit(try(close(con), silent = TRUE))
    length(readLines(con, warn = FALSE)) > 0
  }, error = function(e) FALSE, warning = function(w) FALSE)
}

# This vignette sets AWS_ENDPOINT below. Environment variables are
# process-global, and vignettes can share an R session, so stash the incoming
# value and restore it at the end rather than leaking the OSN endpoint into
# whatever builds next.
old_aws_endpoint <- Sys.getenv("AWS_ENDPOINT", unset = NA)

# Every chunk below that touches the network is also gated on reachability,
# so the vignette builds offline.
online <- reachable("https://usgs.osn.mghpcc.org/mdmf/gdp/gridMET.zarr/.zgroup")

# The GCS section reads a different service, so it gets its own probe against
# the same bucket over HTTPS.
gcs_online <- reachable(
  "https://storage.googleapis.com/pangeo-data/ECCO_basins.zarr/.zgroup"
)
```

Zarr was designed for data that sits in object storage or on a local
disk. This vignette covers
how pizzarr reaches them — over HTTPS, and over the S3 API, including
S3-compatible services that are not Amazon.

The worked example throughout is
[gridMET](https://www.climatologylab.org/gridmet.html), a daily gridded
meteorology dataset for the continental United States, republished by the USGS
on an [Open Storage Network](https://www.openstoragenetwork.org/) pod. Its
catalog entry advertises two addresses for the same data:

```
s3://mdmf/gdp/gridMET.zarr/    endpoint https://usgs.osn.mghpcc.org/
```

That pairing — a bucket URL plus a non-Amazon endpoint — is a case that requires
extra documentation, so it is illustrated in this vignette.

## Coming from xarray or fsspec

In Python you would hand that catalog entry to `xarray` and let `fsspec` sort out
the connection:

```python
import xarray as xr

ds = xr.open_dataset(
    "s3://mdmf/gdp/gridMET.zarr/",
    engine="zarr",
    backend_kwargs={"storage_options": {
        "anon": True,
        "client_kwargs": {"endpoint_url": "https://usgs.osn.mghpcc.org/"},
    }},
)
```

However, pizzarr has no `storage_options` argument. Connection settings come from
environment variables that the Rust `object_store` client reads when it first
opens a store. The reason is mechanical rather than principled: store handles are
cached by URL on the Rust side and shared across calls, so there is no per-call
place to pass options. The translation is:

| fsspec / xarray | pizzarr |
|---|---|
| `"anon": True` | the default — unsigned unless credentials are set |
| `"client_kwargs": {"endpoint_url": "https://host"}` | `AWS_ENDPOINT=https://host` |
| `"key"`, `"secret"` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` |
| `"token"` | `AWS_SESSION_TOKEN` |
| `"client_kwargs": {"region_name": "us-west-2"}` | `AWS_REGION=us-west-2` |
| `"use_ssl": False` | `AWS_ALLOW_HTTP=true` |
| `"config_kwargs": {"s3": {"addressing_style": "path"}}` | `AWS_VIRTUAL_HOSTED_STYLE_REQUEST=false` |
| `gcsfs` `"token"` | `GOOGLE_APPLICATION_CREDENTIALS` |
| `gcsfs` `"token": "anon"` | `GOOGLE_SKIP_SIGNATURE=true` |
| `gcsfs` `"endpoint_url"` | `GOOGLE_BASE_URL` |

Set them with `Sys.setenv()` for the session, or in `.Renviron` to persist them.
Because the handle is cached, changing a variable after a store is open does
nothing until you call `zarrs_close_store()` on that URL.

## Two routes to the same data

An S3 bucket read anonymously is, underneath, an HTTPS server that returns
objects at predictable paths. pizzarr can take either view of it, and which one
you want depends less on the data than on which build you installed (CRAN or r-universe).

The HTTPS route addresses objects directly. That is, an `s3://bucket/key` address served
by endpoint `https://host` is reachable at `https://host/bucket/key`, so gridMET
becomes:

```
https://usgs.osn.mghpcc.org/mdmf/gdp/gridMET.zarr
```

This https route works on both distribution tiers — the pure-R CRAN build and the
r-universe build with the zarrs backend. It needs no credentials and no
configuration, and it is the only route the CRAN build has, so it is the one to
reach for unless something stops you.

The S3 route uses the `s3://` URL and the real S3 API by way of `object_store`.
It requires the r-universe build with the `s3` feature compiled in, and it is
what you need for buckets that are not anonymously readable over plain HTTPS,
or that require signed requests. Check what you have:

```{r}
pizzarr_compiled_features()
```

## Reading over HTTPS

`HttpStore` takes the URL, and `zarr_open()` gives back a group:

```{r, eval = online}
url <- "https://usgs.osn.mghpcc.org/mdmf/gdp/gridMET.zarr"

store <- HttpStore$new(url)
g <- zarr_open(store)
g
```

HTTP servers generally cannot be listed the way a directory can, so a remote
store is only browsable if it publishes consolidated metadata — a `.zmetadata`
key holding every array's metadata in one document. gridMET does, and `listdir()`
reports its contents:

```{r, eval = online}
store$listdir()
```

Without consolidated metadata you have to know the array names in advance. This
is why so many published Zarr stores are consolidated, and why the flag appears
in the gridMET catalog entry alongside the connection settings.

gridMET's arrays are Blosc-compressed, which the pure-R build handles only if the
`blosc` package is installed. The chunks below need it:

```{r, eval = online && has_blosc}
lat <- g$get_item("lat")
lat

lat$get_item(list(slice(1, 5)))$data
```

Reaching the data variables is the same call. Their shape is worth a look before
reading anything:

```{r, eval = online && has_blosc}
tmmx <- g$get_item("max_air_temperature")
tmmx
```

That is 17369 days by 585 rows by 1386 columns, in chunks of 2190 by 150 by 150.
A chunk is the smallest unit the store will return, so asking for a single value
still transfers the roughly 98 MB block that contains it — compressed in flight,
but decompressed in full. Slicing a remote array cheaply means slicing along
chunk boundaries and keeping the request small in the dimensions that are chunked
finely.

## Reading over the S3 API

The `s3://` route needs the endpoint, and nothing else for a public bucket:

```{r, eval = has_s3 && online}
Sys.setenv(AWS_ENDPOINT = "https://usgs.osn.mghpcc.org")

s3_url <- "s3://mdmf/gdp/gridMET.zarr"

zarrs_open_array_metadata(s3_url, "lat")
```

Reads go through `zarrs_get_subset()`, which takes zero-based, stop-exclusive
ranges — one per dimension — and returns the data along with its shape:

```{r, eval = has_s3 && online}
zarrs_get_subset(s3_url, "lat", list(c(0L, 5L)), NULL)
```

Blosc decompression happens in Rust here, so this path does not need the `blosc`
R package. When you are done with a store, drop its cached handle:

```{r, eval = has_s3 && online}
zarrs_close_store(s3_url)
```

`S3Store` and `GcsStore` exist to mark a URL as cloud-backed so that dispatch
picks the right backend. They are not full stores — they carry a URL and nothing
else, so calling `get_item()` or `listdir()` on one raises an error naming
`zarrs_get_subset()`, `HttpStore`, and this vignette. The functions above are the
working entry point for `s3://` data.

## Alternate endpoints

The endpoint variable is the whole trick for MinIO, Ceph, Wasabi, Cloudflare R2,
and Open Storage Network pods. A self-hosted MinIO over plain HTTP with
path-style addressing needs three settings:

```r
Sys.setenv(
  AWS_ENDPOINT = "http://localhost:9000",
  AWS_ALLOW_HTTP = "true",
  AWS_VIRTUAL_HOSTED_STYLE_REQUEST = "false",
  AWS_ACCESS_KEY_ID = "minioadmin",
  AWS_SECRET_ACCESS_KEY = "minioadmin"
)

zarrs_get_subset("s3://my-bucket/data.zarr", "temperature",
                 list(c(0L, 10L)), NULL)
```

Most managed services need only `AWS_ENDPOINT`; region is often unnecessary
outside AWS itself. Requests are signed as soon as any credential variable is
present and unsigned when none are, which is what makes public buckets work with
no setup. `AWS_SKIP_SIGNATURE` overrides that decision in either direction — set
it to `"true"` to force anonymous access when stale credentials are lying around
in your environment, or `"false"` to insist on signing.

## Google Cloud Storage

GCS works the same way, with `GOOGLE_` variables in place of `AWS_` ones, and it
needs the `gcs` feature compiled in. The one difference that matters: GCS does
not infer anonymous access from the absence of credentials the way S3 does. A
`gs://` read against a world-readable bucket still tries to authenticate — via
`GOOGLE_APPLICATION_CREDENTIALS`, `GOOGLE_SERVICE_ACCOUNT`, or the GCE metadata
server — and fails if none are available. Ask for anonymous access explicitly:

```{r, eval = has_gcs && gcs_online}
Sys.setenv(GOOGLE_SKIP_SIGNATURE = "true")

gs_url <- "gs://pangeo-data/ECCO_basins.zarr"

zarrs_open_array_metadata(gs_url, "basin_mask")$shape
```

```{r, include = FALSE}
if (has_gcs && gcs_online) {
  try(zarrs_close_store("gs://pangeo-data/ECCO_basins.zarr"), silent = TRUE)
}
Sys.unsetenv("GOOGLE_SKIP_SIGNATURE")
```

`GOOGLE_BASE_URL` overrides the endpoint, which is what you want for a local
emulator such as [fake-gcs-server](https://github.com/fsouza/fake-gcs-server).

The same data is also reachable over plain HTTPS at
`https://storage.googleapis.com/bucket/path`, which needs no configuration and
works on the CRAN tier as well:

```r
z <- zarr_open(HttpStore$new(
  "https://storage.googleapis.com/pangeo-data/ECCO_basins.zarr"
))
```

## What is not supported

Cloud stores are read-only. Writes to `s3://` and `gs://` fall back to the
R-native path, which has no cloud implementation, so they fail — build locally
and upload with another tool. Azure (`az://`) is not implemented at all.
Requester-pays buckets have no way to signal the requester, and there is no
per-store credential object: configuration is process-global, so two buckets
needing different credentials in one session have to be read in sequence with
`zarrs_close_store()` and an environment change between them.

```{r, include = FALSE}
if (online) try(zarrs_close_store("https://usgs.osn.mghpcc.org/mdmf/gdp/gridMET.zarr"),
                silent = TRUE)

# Restore the endpoint setting; see the note at the top of this vignette.
if (is.na(old_aws_endpoint)) {
  Sys.unsetenv("AWS_ENDPOINT")
} else {
  Sys.setenv(AWS_ENDPOINT = old_aws_endpoint)
}
```
