File storage
Gebweb's storage abstraction sits between application code and
the actual persistence backend. Application code calls
gebweb.put, gebweb.get, gebweb.storageExists,
gebweb.storageDelete, and gebweb.storageUrl; the configured
backend handles the IO.
v1 ships three built-in backends:
| Backend | When | Behaviour |
|---|---|---|
gebweb.memoryStorage() |
Tests | Process-local dict; survives only as long as the app. |
gebweb.localStorage(dir) |
Dev / single-server prod | Files under dir; sub-directories auto-created. |
gebweb.s3Storage(opts) |
Prod | AWS S3 (and S3-compatible: MinIO, R2, B2) with sigv4. |
Other object stores (Azure Blob, GCS) plug in by implementing
the StorageContract interface against the SDK or REST API of
your choice.
Wiring
import gebweb;
let app = gebweb.app([UploadController()]);
if (env.get("ENV") == "test") {
gebweb.useStorage(app, gebweb.memoryStorage());
} else {
gebweb.useStorage(app, gebweb.localStorage("/srv/uploads")
.withUrlPrefix("/uploads"));
}
withUrlPrefix configures what storageUrl(app, name) returns
for LocalStorage - typically set this when you serve the
storage root through a static-file route at the matching path.
Reading and writing
gebweb.put(app, "avatars/123.jpg", uploaded.bytes());
let raw = gebweb.get(app, "avatars/123.jpg"); /* ?bytes */
gebweb.storageDelete(app, "avatars/123.jpg");
let isAvailable = gebweb.storageExists(app, "avatars/123.jpg");
let publicUrl = gebweb.storageUrl(app, "avatars/123.jpg");
put accepts string or bytes content. get returns
?bytes (null when the name doesn't exist).
UploadedFile.saveToStorage
The multipart-upload wrapper has a convenience helper:
class UploadController {
@Post("/avatar")
func upload(dict<string, gebweb.UploadedFile> files): dict<string, any> {
let avatar = files["avatar"];
avatar.saveToStorage(app, "avatars/" + currentUser.id + ".jpg");
return {"ok": true};
}
}
This is shorthand for gebweb.put(app, name, avatar.bytes()).
Path safety
Names are forward-slash-separated paths relative to the backend root. Both built-in backends reject:
- Empty names.
- Absolute names (
name.startsWith("/")). - Any segment equal to
..or..
These checks defend against accidental escapes; callers should
still sanitise user-provided components (e.g. the avatars/<userId>
example above relies on the caller controlling currentUser.id).
S3 and S3-compatible
gebweb.s3Storage(opts) returns a backend that talks to AWS S3
(or any sigv4-compatible service) using the same put/get/
exists/delete_ surface as the other backends.
gebweb.useStorage(app, gebweb.s3Storage({
"bucket": "acme-uploads",
"region": "us-east-1",
"accessKey": env.get("AWS_ACCESS_KEY_ID"),
"secretKey": env.get("AWS_SECRET_ACCESS_KEY")
}));
Options:
| Key | Required | Notes |
|---|---|---|
bucket |
yes | Bucket name; path-style URLs are used. |
region |
yes | Used for the sigv4 credential scope. |
accessKey |
yes | Read from env in production. |
secretKey |
yes | Read from env in production. |
endpoint |
no | Default https://s3.<region>.amazonaws.com. Set to point at MinIO / R2 / B2 / a custom test server. |
MinIO example:
gebweb.useStorage(app, gebweb.s3Storage({
"bucket": "uploads",
"region": "us-east-1",
"accessKey": "minioadmin",
"secretKey": "minioadmin",
"endpoint": "http://minio:9000"
}));
Cloudflare R2 example:
gebweb.useStorage(app, gebweb.s3Storage({
"bucket": "uploads",
"region": "auto",
"accessKey": env.get("R2_ACCESS_KEY_ID"),
"secretKey": env.get("R2_SECRET_ACCESS_KEY"),
"endpoint": "https://" + env.get("R2_ACCOUNT_ID") + ".r2.cloudflarestorage.com"
}));
The signing chain (storage.signingKey, storage.canonicalRequest,
storage.stringToSignForSigv4) is also exported as free
functions so application code can sign one-off requests
directly, or build pre-signed URLs.
Custom backends
Implement gebweb.StorageContract:
class S3Storage implements gebweb.StorageContract {
string bucket;
string region;
string accessKey;
string secretKey;
func S3Storage(string bucket, string region, string accessKey, string secretKey) {
this.bucket = bucket;
this.region = region;
this.accessKey = accessKey;
this.secretKey = secretKey;
}
func put(string name, any content): void { /* signed PUT */ }
func get(string name): ?bytes { /* signed GET */ }
func exists(string name): bool { /* signed HEAD */ }
func delete_(string name): void { /* signed DELETE */ }
func url(string name): string {
return "https://" + this.bucket + ".s3." + this.region + ".amazonaws.com/" + name;
}
}
gebweb.useStorage(app, S3Storage("acme-uploads", "us-east-1", key, secret));
Reference
| Helper | Purpose |
|---|---|
gebweb.StorageContract |
Backend interface: put, get, exists, delete_, url. |
gebweb.useStorage(app, backend) |
Attach a backend. |
gebweb.memoryStorage() |
In-process dict storage (tests). |
gebweb.localStorage(dir) |
Filesystem storage. .withUrlPrefix(prefix) for serving via static-file route. |
gebweb.s3Storage(opts) |
S3 / S3-compatible with sigv4. See "S3 and S3-compatible" above for opts. |
gebweb.put(app, name, content) |
Write (content is string or bytes). |
gebweb.get(app, name) |
Read (?bytes). |
gebweb.storageExists(app, name) |
Check. |
gebweb.storageDelete(app, name) |
Delete. |
gebweb.storageUrl(app, name) |
Public URL for serving. |
UploadedFile.saveToStorage(app, name) |
Convenience: write the upload's bytes. |