diff --git a/src/garageS3.jl b/src/garageS3.jl
index 044efbd..a00f490 100644
--- a/src/garageS3.jl
+++ b/src/garageS3.jl
@@ -277,179 +277,50 @@ end
Generate a pre-signed URL for an object in the Garage S3 bucket.
-A pre-signed URL is a signed URL that grants temporary access to an S3 object without
-requiring AWS credentials. The URL expires after the specified number of seconds.
-The signature is computed using AWS Signature Version 4 (HMAC-SHA256).
-
-# Why use pre-signed URLs?
-
-Pre-signed URLs let you grant temporary, scoped access to objects without sharing
-your API credentials. Typical use cases:
-
-- **Direct browser downloads**: Return the URL to a frontend so users can download
- files without going through your server.
-- **Direct browser uploads**: Generate a PUT pre-signed URL, pass it to the frontend,
- and let the user upload a file directly to S3 with a `curl` or `fetch` call.
-- **Temporary sharing**: Send an expiring link to someone who should not have
- permanent access to the object.
-- **Server-side download without round-trip**: Instead of downloading the file to
- your server and re-uploading it elsewhere, pass the pre-signed URL to another
- service.
-
-# How it works
-
-The function computes an HMAC-SHA256 signature over a canonical request that
-includes the HTTP verb, the object path, the query parameters, and the host header.
-The signature is appended to the URL as `X-Amz-Signature`. Garage (and any S3
-compatible service) verifies the signature before serving the object.
-
-# URL format
-https://s3-api.your-garage-instance.com/bucket/key?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Date=...&X-Amz-Expires=...&X-Amz-SignedHeaders=...&X-Amz-Signature=...
+A pre-signed URL grants temporary access without AWS credentials. Signature is computed
+using AWS Signature Version 4 (HMAC-SHA256).
# Arguments
-- `storage::GarageStorage`
- A storage client created with `GarageStorage(endpoint, key, secret, bucket)`.
-- `key::String`
- The object key (file name) in the bucket. Supports nested paths like
- `"folder/subfolder/file.txt"`.
-- `seconds::Int`
- How long the URL remains valid, in seconds. Default: 3600 (1 hour).
+- `storage::GarageStorage`: Storage client from `GarageStorage(endpoint, key, secret, bucket)`.
+- `key::String`: Object key. Supports nested paths like `"folder/subfolder/file.txt"`.
+- `seconds::Int`: URL validity in seconds. Default: 3600.
# Keyword Arguments
-- `verb::String = "GET"`
- The HTTP method the URL permits. Use `"GET"` for downloads (the default) or
- `"PUT"` for uploads. Other values like `"DELETE"` are also supported.
-- `content_type::String = "application/octet-stream"`
- The `Content-Type` header value that must match when the URL is used.
- For PUT uploads, set this to the actual MIME type of the file you plan to
- upload (e.g. `"text/plain"`, `"application/json"`, `"image/png"`).
-- `signature_version::String = "v4"`
- Signature algorithm version. Always use `"v4"` — version 2 is deprecated
- and not supported by many S3-compatible services.
+- `verb::String = "GET"`: HTTP method (`"GET"` downloads, `"PUT"` uploads).
+- `content_type::String = "application/octet-stream"`: Required `Content-Type` header.
+ For PUT, set to the file's MIME type (`"image/png"`, `"application/json"`, etc.).
+- `signature_version::String = "v4"`: Always use `"v4"` (v2 is deprecated).
# Return
-- `String`: The full pre-signed URL, ready to use with `curl`, a browser, or any
- HTTP client.
+`String`: The pre-signed URL.
# Examples
-### Download link (GET — default)
```julia
-storage = GarageStorage("https://s3-api.example.com", "mykey", "mysecret", "mybucket")
+storage = GarageStorage("https://s3-api.yiem.cc", "key", "secret", "testbucket")
-# 1-hour download link
-url = s3_sign_url(storage, "documents/report.pdf", 3600)
-println("Download link: ", url)
+# Download — GET (default)
+url = s3_sign_url(storage, "docs/report.pdf", 3600)
-# Use in a browser — opens the file directly
-# > open(url)
-
-# Use with curl from the terminal
-# > curl -L "url" -o report.pdf
-```
-
-### Upload link (PUT)
-```julia
-# Generate a PUT URL valid for 10 minutes, accepting text/plain uploads
-url = s3_sign_url(storage, "uploads/user-photo.png", 600;
+# Upload — PUT
+url = s3_sign_url(storage, "avatars/user123.png", 300;
verb="PUT", content_type="image/png")
-# The user (or your frontend) uploads a file like this:
-# > curl -X PUT -H "Content-Type: image/png" --data @photo.png "url"
-```
+# Multiple uploads (chat app)
+urls = [s3_sign_url(storage, "chat/GeneralUtils.uuid4snakecase()", 300;
+ verb="PUT", content_type="image/png")
+ for f in filenames]
-### Embedding in HTML
-```julia
-url = s3_sign_url(storage, "shared/shared-file.zip", 86400) # 24 hours
-# Use the URL in HTML: Download shared-file.zip (valid 24 hours)
-```
-
-### Verification — signature differs by verb and content-type
-```julia
+# Signature differs by verb and content_type
url_get = s3_sign_url(storage, "file.txt", 3600; verb="GET")
url_put = s3_sign_url(storage, "file.txt", 3600; verb="PUT", content_type="text/plain")
-# url_get and url_put have different X-Amz-Signature values because the
-# canonical request (and thus the HMAC) is different.
```
# Notes
-- The pre-signed URL is **not encrypted** — anyone with the URL can access the
- object until it expires. Treat the URL like a temporary password.
-- The signature includes the current UTC time (`X-Amz-Date`), so both the
- generating machine and the Garage server should have reasonably synchronized
- clocks (± a few minutes tolerance).
-- Only `v4` signature is implemented. Garage supports v4 natively.
-- For cross-origin (CORS) uploads from a browser, you also need the Garage bucket
- to have CORS rules that allow the `Origin` and `Method` headers.
-
-# s3_sign_url vs get_file — when to use which
-
-| Scenario | Use |
-|---|---|
-| Your server needs to read/write an object | `get_file(storage, key)`, `put_file(storage, key, data)` |
-| A browser or external client needs to download an object | `s3_sign_url(storage, key, 3600; verb="GET")` |
-| A browser or external client needs to upload a file | `s3_sign_url(storage, key, 600; verb="PUT", content_type="image/png")` |
-| You need to pass a download link to a third party | `s3_sign_url(storage, key, 86400)` — no credentials shared |
-
-`get_file` / `put_file` use your API credentials on every call.
-`s3_sign_url` embeds a time-limited signature in the URL — no credentials needed.
-
-# Complete upload workflow example
-
-```julia
-# 1. Generate a PUT pre-signed URL on your server
-url = s3_sign_url(storage, "avatars/user-12345.png", 300;
- verb="PUT", content_type="image/png")
-
-# 2. Send `url` to your frontend (JSON response, React state, etc.)
-
-# 3. Frontend uploads directly to Garage:
-# fetch(url, { method: "PUT", headers: {"Content-Type": "image/png"}, body: fileBlob })
-# or:
-# curl -X PUT -H "Content-Type: image/png" --data @photo.png url
-
-# 4. Optional — verify the upload succeeded
-# data = get_file(storage, "avatars/user-12345.png")
-```
-
-# Complete download workflow example
-
-```julia
-# 1. Generate a GET pre-signed URL
-url = s3_sign_url(storage, "reports/quarterly.pdf", 7200) # 2 hours
-
-# 2a. Redirect user to the URL (S3 returns 302)
-# return redirect(url)
-
-# 2b. Embed in HTML link
-# Download quarterly.pdf
-
-# 2c. Use with curl
-# curl -L url -o quarterly.pdf
-```
-
-# Query parameters in the generated URL
-
-| Parameter | Purpose |
-|---|---|
-| `X-Amz-Algorithm` | Must be `AWS4-HMAC-SHA256` |
-| `X-Amz-Credential` | Your access key ID + date + region + service + request token |
-| `X-Amz-Date` | Timestamp when the URL was generated (UTC, ISO 8601 format) |
-| `X-Amz-Expires` | Number of seconds until expiration |
-| `X-Amz-Security-Token` | Optional session token (empty if not provided) |
-| `X-Amz-SignedHeaders` | Semicolon-separated list of signed headers (currently only `host`) |
-| `X-Amz-Signature` | The HMAC-SHA256 signature — the core authorization token |
-
-# Common pitfalls
-
-- **Clock skew**: If the server and Garage are more than ~5 minutes out of sync,
- the signature will be rejected.
-- **Content-Type mismatch**: When using a PUT URL, the `Content-Type` sent by the
- client must match the `content_type` you passed to `s3_sign_url`.
-- **URL encoding**: The key is URL-encoded in the path. Keys with spaces or special
- characters work correctly, but do not double-encode the returned URL.
-- **Single use**: Pre-signed URLs can be used multiple times until expiration;
- they are not one-time tokens.
+- URLs are valid until `seconds` expires; they can be used **multiple times** (not one-time).
+- Server clocks must be within ~5 minutes of the Garage server (UTC time in signature).
+- PUT uploads require the client's `Content-Type` to match `content_type`.
+- For browser CORS uploads, configure Garage bucket CORS rules for `Origin` and `Method`.
"""
function s3_sign_url(
storage::GarageStorage,