Private media, searchable metadata, recoverable imports, and storage channels under one bright control surface.
Cloudflare Pages Functions | D1/KV Hybrid | Telegram Sync | R2/S3/Discord/Hugging Face
简体中文 | Open-source origin | Experience | Screenshots | Storage Model | Quick Start | Operator Notes
SUNDOWNER is an independent, long-term open-source second development based on MarSeventh/CloudFlare-ImgBed. That upstream project is itself a remake of cf-pages/Telegraph-Image. SUNDOWNER retains the upstream MIT license and attribution, but is maintained independently and is not an official upstream release.
This is more than a renamed deployment copy. The public codebase has expanded the original image-bed foundation into a private media library with D1/KV hybrid metadata, multi-channel storage, Telegram recovery and migration tooling, WebDAV, security hardening, and a dedicated dashboard. Source code, documentation, deployment scripts, regression tests, and issue tracking remain open in this repository.
Source code | Issues | Pull requests | MIT License
Building on that image-bed foundation, SUNDOWNER has grown into a private media cockpit: upload, catalog, search, serve, recover, and migrate media across storage backends from one admin surface.
| Surface | What it is for |
|---|---|
| Library cockpit | Browse photos, videos, audio, documents, albums, films, moments, and notes from one admin UI. |
| Upload routing | Send files to Telegram, Discord, Cloudflare R2, S3-compatible storage, or Hugging Face channels. |
| Recovery tooling | Repair Telegram imports, recover file_id values, scan orphan records, and migrate KV metadata into D1. |
| Operations | Manage auth, API tokens, WebDAV, public browsing, random APIs, quotas, cache behavior, and channel config. |
The screenshots below are captured from this repository running locally with Wrangler Pages dev, D1/KV/R2 bindings, temporary README-only credentials, and privacy-safe fixture media generated by npm run capture:readme. They are not old upstream screenshots and not user-provided images.
| Library with local fixture media | Theme/style controls |
|---|---|
![]() |
![]() |
| Films | Moments |
|---|---|
![]() |
![]() |
On Windows, refresh the captures with one command:
npm run capture:readme:localThe wrapper starts Wrangler with an isolated local persistence directory, runs the screenshot capture script, and stops the local server when it finishes.
If you need to run the steps manually, start the local app first:
npx wrangler pages dev ./ --kv img_url --d1 img_d1 --r2 img_r2 \
--binding BASIC_USER=readme-admin \
--binding BASIC_PASS=readme-password \
--binding AUTH_CODE=readme-upload-code \
--ip 127.0.0.1 --port 8787 \
--persist-to D:/Codex/tmp_toDel/_sundowner-readme-dataThen, in another terminal, run:
npm run capture:readme- D1 is the preferred metadata/query path, so normal library listing and search do not depend on expensive KV
list()scans. - Hybrid mode keeps compatibility with KV-backed deployments while moving queryable metadata into SQL.
- Telegram sync is treated as a first-class import pipeline, including the
file_idvsfile_unique_idrecovery problem. - Sensitive channel credentials are kept out of per-file metadata by design.
- The frontend is evolving from upload panel to media library: richer search, albums, films, moments, notes, and file management live together.
- Security hardening covers constant-time comparisons, fail-closed config handling, SSRF host allowlists, direct-file access checks, token response shaping, proxy header stripping, and generic 5xx responses.
Cloudflare binding names are part of the project contract:
| Binding | Type | Purpose |
|---|---|---|
img_url |
KV namespace | Legacy metadata, settings, index chunks, and fallback state |
img_d1 |
D1 database | Preferred metadata, settings, and query database |
img_r2 |
R2 bucket | Cloudflare R2 file channel |
When both img_url and img_d1 exist, functions/utils/databaseAdapter.js runs in Hybrid mode. D1 handles queryable metadata; KV remains available for compatibility and selected mirrored state.
Do not store tokens in file metadata. Non-manage@ keys are sanitized before write, stripping sensitive fields such as Telegram, Discord, S3, and Hugging Face credentials. Store channel credentials in upload config (manage@sysConfig@upload) or environment variables, then attach files through ChannelName.
.
+-- functions/ Cloudflare Pages Functions routes
| +-- api/ public and management APIs
| +-- file/ direct media serving
| +-- upload/ upload and chunk-merge flows
| +-- dav/ WebDAV endpoint
| +-- utils/ database, auth, storage, sync, cache helpers
+-- js/media-library/ admin media-library frontend modules
+-- css/ compiled and override styles
+-- database/ local SQLite/D1 bootstrap SQL and migrations
+-- server/ local Node runtime that emulates Pages Functions
+-- test/ Mocha regression tests
+-- static/brand/ SUNDOWNER brand artwork
+-- static/icons/ favicon and PWA icon assets
+-- static/fonts/ web font assets
+-- static/legacy/img/ legacy bundle image assets
+-- static/readme/ README screenshots
+-- static/tools/ standalone operator tools
Key files:
| File | Role |
|---|---|
functions/utils/databaseAdapter.js |
Selects KV, D1, or Hybrid mode and protects metadata writes. |
functions/utils/d1Database.js |
Owns D1 schema repair, indexed file queries, and settings operations. |
functions/utils/indexManager.js |
Manages legacy chunked indexes and index operation compatibility. |
functions/utils/mediaSecurity.js |
Resolves channel credentials and strips sensitive file metadata. |
functions/api/manage/sysConfig/upload.js |
Stores upload-channel configuration. |
functions/api/manage/migrate/kv-to-d1.js |
Migrates legacy KV metadata/settings into D1. |
scripts/capture-readme-screenshots.mjs |
Seeds neutral local demo data and captures the current README screenshots. |
scripts/capture-readme-screenshots.ps1 |
Starts an isolated local Wrangler session, runs the screenshot capture, and cleans up the server. |
Requirements:
- Node.js 22.x
- npm
- Cloudflare Wrangler for Pages local development
- Cloudflare resources for deployed use: KV
img_url, D1img_d1, and optionally R2img_r2
Install dependencies:
npm installInstall function dependencies for Cloudflare Pages builds:
npm run installRun the Cloudflare Pages development server:
npm startnpm start writes Wrangler local state under ./.local/data, keeping generated development data out of the repository root.
Wrangler serves the app at:
http://localhost:8787
Run the local Node/Docker-compatible runtime:
npm run start:dockerOr run the container:
docker compose up -dDocker maps the service to:
http://localhost:7658
This repo does not depend on a checked-in wrangler.toml. Configure bindings in Cloudflare Pages project settings or your deployment pipeline.
Recommended production bindings:
- KV namespace:
img_url - D1 database:
img_d1 - R2 bucket:
img_r2when using Cloudflare R2
Common optional settings:
TG_BOT_TOKEN: fallback Telegram bot token when upload config does not provide one.FETCH_RES_ALLOWED_HOSTS: explicit host allowlist for/api/fetchRes; leave unset to keep that proxy disabled.ALLOW_BEARERLESS_FILE_ACCESS/access.allowBearerlessFileAccess: legacy compatibility opt-out for/file/*; leave unset orfalseso no-Refererdirect file requests requireauthCode.- Admin/user auth, upload channels, WebDAV, public browsing, random API, API tokens, quotas, and page options are managed through system config APIs/UI and stored under
manage@sysConfig@....
After binding D1 to an existing KV-backed deployment, migrate metadata in batches:
POST /api/manage/migrate/kv-to-d1
The migration endpoint requires both img_url and img_d1. It skips internal chunk/index keys, can include settings, and records migration state under manage@sysConfig@kvToD1Migration.
Run the Mocha suite:
npm testRun integration-style tests with the local dev server:
npm run ci-testRun Docker-style local server tests:
npm run ci-test:dockerRun the public documentation guard after README, screenshot, or branding changes:
npm run test:docsFor quick syntax checks on touched files:
node --check scripts/capture-readme-screenshots.mjs
npm run test:docs
node --check functions/utils/databaseAdapter.js
node --check js/media-library/app.jsSome local environments can fail the full suite because the native better-sqlite3 binding is unavailable for the active Node runtime. Treat that as an environment baseline only after focused tests and failure titles confirm there are no new regressions.
- Avoid new code paths that depend on broad KV
list()scans. Prefer D1 SQL queries or chunk-basedkv.get()reads. - Telegram
file_idandfile_unique_idare not interchangeable. Only the realfile_idcan be used with TelegramgetFile. - Keep credentials in upload config or environment variables, not in file metadata.
/file/*direct access is not bearerless by default: use same-origin/allowed-domainReferer, validauthCode, or an explicit legacy opt-out.- If a route proxies third-party media, do not forward inbound
Authorization,Cookie, orauthCodeheaders. - Keep cache-busted frontend module versions in
index.html,js/entry-loader.js, and related tests synchronized. - Refresh
static/readme/current-*.pngfrom a local run before a public-facing release.
This project keeps the upstream MIT license. See LICENSE.


