Storage Providers

@afilmory/builder reads photos through a storage-provider abstraction. Jacky's Photography currently uses the local provider against a private photo-repository checkout during builds, then syncs the published photo tree to Cloudflare R2 for public delivery.

Current Setup

The active builder.config.ts is:

import { defineBuilderConfig } from '@afilmory/builder'

export default defineBuilderConfig(() => ({
  storage: {
    provider: 'local',
    basePath: './photos',
    baseUrl: 'https://photos3.jackyw.cn/photos/',
    excludeRegex: '^incoming($|/.*)',
  },
  plugins: [new URL('plugins/builder/photo-descriptions.ts', import.meta.url).href],
}))

In local development, ./photos is a checkout of Jackyhq/Photography-Photos. In GitHub Actions, the workflow checks out the same private repository into ./photos, rejects symlinks, standardizes incoming files, builds the manifest, and syncs published files to Cloudflare R2.

excludeRegex keeps photos/incoming/ out of the public manifest until files have been standardized or moved into a real category folder.

Manifest And Thumbnails

The builder writes generated data to the web app:

apps/web/src/data/photos-manifest.json
apps/web/public/thumbnails/

packages/data/src/photos-manifest.json is a symlink to the generated manifest. The symlink is tracked so @afilmory/data can expose the manifest path without copying a large JSON file into two packages.

Generated data is not source code. Do not hand-edit it; regenerate it with:

pnpm run build:manifest

When thumbnail fields or image variants change:

pnpm run build:manifest -- --force-thumbnails --force-manifest

Cloudflare R2 Layout

The production R2 sync uses one bucket plus a prefix:

s3://<CLOUDFLARE_R2_BUCKET>/photos/

The public URLs in the manifest use:

https://photos3.jackyw.cn/photos/

Keep these values separate:

  • CLOUDFLARE_R2_BUCKET is the bucket name only.
  • R2_PHOTOS_PREFIX=photos is set in the workflow.
  • CLOUDFLARE_R2_ENDPOINT is the S3 API endpoint, not the public custom domain.
  • baseUrl in builder.config.ts is the browser-facing public URL.

The workflow uses aws s3 sync --size-only --delete. This avoids re-uploading unchanged photos after a fresh checkout changes local file timestamps. If a file is replaced with different bytes but the exact same byte size, delete the object from R2 or change the file name before redeploying.

Supported Providers

Local

Use local storage for this project, local development, and self-hosted workflows.

export default defineBuilderConfig(() => ({
  storage: {
    provider: 'local',
    basePath: './photos',
    baseUrl: 'https://cdn.example.com/photos/',
    excludeRegex: '^incoming($|/.*)',
  },
}))

You can also copy originals into the frontend static directory:

export default defineBuilderConfig(() => ({
  storage: {
    provider: 'local',
    basePath: './photos',
    distPath: './apps/web/public/originals',
    baseUrl: '/originals/',
  },
}))

S3-Compatible

Use S3-compatible storage when the builder should read directly from object storage.

export default defineBuilderConfig(() => ({
  storage: {
    provider: 's3',
    bucket: 'your-photos-bucket',
    region: 'us-east-1',
    endpoint: process.env.S3_ENDPOINT,
    accessKeyId: process.env.S3_ACCESS_KEY_ID,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
    prefix: 'photos/',
    customDomain: 'cdn.example.com',
  },
}))

Store credentials in environment variables, never in committed config.

GitHub

Use GitHub storage for small public galleries or demos where repository limits and API rate limits are acceptable.

export default defineBuilderConfig(() => ({
  storage: {
    provider: 'github',
    owner: 'your-username',
    repo: 'photo-storage',
    branch: 'main',
    token: process.env.GIT_TOKEN,
    path: 'photos',
    useRawUrl: true,
  },
}))

For private repositories, use a fine-grained token with the minimum required Contents permissions.

Eagle

Use Eagle storage when publishing selected assets from an Eagle 4 desktop library.

export default defineBuilderConfig(() => ({
  storage: {
    provider: 'eagle',
    libraryPath: '/Users/alice/Pictures/Eagle.library',
    distPath: '/Users/alice/workspaces/gallery/apps/web/public/originals',
    baseUrl: '/originals/',
    include: [{ type: 'folder', name: 'Published', includeSubfolder: true }],
    exclude: [{ type: 'tag', name: 'Private' }],
    folderAsTag: true,
    omitTagNamesInMetadata: ['Temp'],
  },
}))

Processing Pipeline

For each supported image, the builder performs:

  1. storage scan and path normalization
  2. format detection
  3. EXIF/GPS/camera/lens metadata extraction
  4. Live Photo and Motion Photo detection
  5. browser-friendly image preparation when needed
  6. thumbnail and Thumbhash generation
  7. tone analysis
  8. manifest merge through configured plugins
  9. manifest save and stale-thumbnail cleanup

Provider Comparison

ProviderBest forNotes
LocalThis repository, local builds, private photo checkoutFast and simple; public delivery is handled separately by R2 sync
S3Direct object-storage sourceRequires credentials and network access during manifest build
GitHubSmall public galleries or demosRepository size and API limits apply
EaglePublishing curated desktop library assetsRequires an Eagle 4 library path and URL/static-output alignment

Security Notes

  • Never commit storage tokens or private photo files.
  • Reject symlinks before publishing local photo trees; the GitHub workflow already does this.
  • Keep incoming/ excluded from the manifest and R2 sync.
  • Scope GitHub and S3/R2 tokens to the minimum required repositories or buckets.
Created At
Last Modified