README.md

# ReqGCS

A [Req](https://hexdocs.pm/req) plugin for [Google Cloud Storage](https://cloud.google.com/storage).

Provides an ergonomic API for bucket and object operations, with flexible
authentication via [Goth](https://hexdocs.pm/goth).

## Installation

Add `req_gcs` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:req_gcs, "~> 0.1.0"}
  ]
end
```

## Authentication

ReqGCS supports three ways to authenticate, checked in this order:

### 1. Named Goth process (recommended for production)

Start a [Goth](https://hexdocs.pm/goth) process in your application's supervision
tree. Tokens are cached in ETS and auto-refreshed before expiry.

```elixir
# In your Application.start/2:
credentials = "service-account.json" |> File.read!() |> Jason.decode!()

children = [
  {Goth, name: MyApp.Goth, source: {:service_account, credentials}}
]

Supervisor.start_link(children, strategy: :one_for_one)
```

Then pass the process name when attaching:

```elixir
req = Req.new() |> ReqGCS.attach(gcs_goth: MyApp.Goth, gcs_project: "my-project")
```

### 2. Inline credentials (per-request)

Pass a parsed service account JSON map directly. Useful when credentials are
stored in a database or vary per tenant. ReqGCS automatically starts a managed
Goth process for each unique credential set, so tokens are cached in ETS and
auto-refreshed — no per-request OAuth round-trips.

```elixir
credentials = Jason.decode!(stored_json_key)
req = Req.new() |> ReqGCS.attach(gcs_credentials: credentials, gcs_project: "my-project")
```

Managed Goth processes that haven't been used in over an hour are automatically
stopped by a background sweeper to prevent unbounded memory growth.

### 3. Application config (fallback)

Set credentials in your app config:

```elixir
config :req_gcs, credentials: Jason.decode!(File.read!("service-account.json"))
```

Then attach without explicit credentials:

```elixir
req = Req.new() |> ReqGCS.attach(gcs_project: "my-project")
```

This path also benefits from automatic token caching (same as inline credentials).

## Usage

All convenience functions return `{:ok, %Req.Response{}}` or `{:error, exception}`.
Every function accepts trailing opts that are passed through to `Req.request/2`,
so you can use any Req option (`:headers`, `:params`, etc.).

### Setup

```elixir
req = Req.new() |> ReqGCS.attach(gcs_goth: MyApp.Goth, gcs_project: "my-project")
```

You can also use the `plugins` option:

```elixir
req = Req.new(plugins: [ReqGCS], gcs_goth: MyApp.Goth, gcs_project: "my-project")
```

### Buckets

```elixir
# List buckets
{:ok, resp} = ReqGCS.list_buckets(req)

# Get bucket metadata
{:ok, resp} = ReqGCS.get_bucket(req, "my-bucket")

# Create a bucket
{:ok, resp} = ReqGCS.create_bucket(req, %{"name" => "my-new-bucket"})

# Update bucket metadata
{:ok, resp} = ReqGCS.update_bucket(req, "my-bucket", %{"versioning" => %{"enabled" => true}})

# Delete a bucket
{:ok, resp} = ReqGCS.delete_bucket(req, "my-bucket")
```

### Objects

```elixir
# List objects
{:ok, resp} = ReqGCS.list_objects(req, "my-bucket")

# List with prefix/delimiter for "directory" listing
{:ok, resp} = ReqGCS.list_objects(req, "my-bucket", prefix: "logs/", delimiter: "/")

# Pagination
{:ok, resp} = ReqGCS.list_objects(req, "my-bucket", max_results: 100, page_token: token)

# Get object metadata
{:ok, resp} = ReqGCS.get_object(req, "my-bucket", "path/to/file.txt")

# Download object content
{:ok, resp} = ReqGCS.download_object(req, "my-bucket", "path/to/file.txt")
resp.body  # => raw bytes

# Upload an object
{:ok, resp} = ReqGCS.upload_object(req, "my-bucket", "hello.txt", "Hello, world!",
  content_type: "text/plain"
)

# Replace an object (upload with the same name overwrites)
{:ok, resp} = ReqGCS.upload_object(req, "my-bucket", "hello.txt", "Updated content")

# Delete an object
{:ok, resp} = ReqGCS.delete_object(req, "my-bucket", "hello.txt")

# Copy an object
{:ok, resp} = ReqGCS.copy_object(req, "src-bucket", "src.txt", "dest-bucket", "dest.txt")

# Compose multiple objects into one
{:ok, resp} = ReqGCS.compose_objects(req, "my-bucket", "combined.txt", [
  %{"name" => "part1.txt"},
  %{"name" => "part2.txt"}
])
```

## Testing

The auth step is skipped when `:auth` is already set on the request, so you can
use `Req.Test` stubs without real credentials:

```elixir
Req.Test.stub(MyStub, fn conn ->
  Req.Test.json(conn, %{"kind" => "storage#objects", "items" => []})
end)

req =
  Req.new(plug: {Req.Test, MyStub})
  |> ReqGCS.attach(gcs_project: "test-project")

{:ok, resp} = ReqGCS.list_objects(req, "my-bucket", auth: {:bearer, "fake-token"})
```

## Configuration

Optional settings with sensible defaults:

```elixir
config :req_gcs,
  sweep_interval: 300_000,   # how often to check for idle processes (default: 5 min)
  max_idle: 3_600_000        # idle time before a managed Goth process is stopped (default: 1 hour)
```

## License

See [LICENSE](LICENSE) for details.