release: v1.2.1 — login captcha, docs site, and bug fixes

Add slider captcha for web login, VitePress documentation on GitHub Pages,
and fixes for gallery delete refresh, pagination jump, and copy-format persistence.
This commit is contained in:
O96u committed 2026-08-29 11:42:18 +08:00
1 parent 469d3be735
commit ff20f97522
66 files changed
+2497 -616

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
# API
PicHost exposes REST endpoints and a Twikoo-compatible upload. After deployment, open the in-app **API** page for your token and copyable cURL snippets.
## Authentication
All REST calls use:
```http
Auth-Token: YOUR_TOKEN
```
| Token type | How to obtain | Notes |
| ---------- | ------------- | ----- |
| **Global** | Admin **API** page or `API_UPLOAD_TOKEN` env | Env wins and blocks UI regenerate |
| **Personal** | Each user’s **API** page | Uploads belong to that user |
Form uploads may use field `token` (Twikoo protocol).
## Endpoints
### 1. Upload images
`POST /api/images/upload`
Upload one or more images via `image`; also accepts `file`, `files`. Stored under `images/`.
```bash
curl -X POST "https://admin.example.com/api/images/upload" \
-H "Auth-Token: YOUR_TOKEN" \
-F "image=@./demo.png"
```
### 2. List images
`GET /api/images`
Paginated gallery. `limit` default 20, max 100.
```bash
curl "https://admin.example.com/api/images?limit=20&page=1" \
-H "Auth-Token: YOUR_TOKEN"
```
### 3. Search images
`GET /api/images/search`
Search by filename or path; `q` is required.
```bash
curl "https://admin.example.com/api/images/search?q=demo&limit=20&page=1" \
-H "Auth-Token: YOUR_TOKEN"
```
### 4. Delete image
`DELETE /api/images`
Delete by `key` (storage path, e.g. `images/2026/08/xxx.webp`).
```bash
curl -X DELETE "https://admin.example.com/api/images?key=images/2026/08/xxxx.webp" \
-H "Auth-Token: YOUR_TOKEN"
```
### 5. Batch delete
`POST /api/images/batch-delete`
JSON body: `{"keys":["images/2026/08/a.webp","images/2026/08/b.webp"]}`
```bash
curl -X POST "https://admin.example.com/api/images/batch-delete" \
-H "Auth-Token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"keys":["images/2026/08/a.webp","images/2026/08/b.webp"]}'
```
## Errors
REST responses are JSON with `code` and `message` (`UNAUTHORIZED`, `FORBIDDEN`, `INVALID_REQUEST`, etc.). Twikoo uses EasyImage-compatible JSON.
## Visibility
- User token: list/search/delete own images only
- Admin token: all images
- Global token uploads belong to admin
See [Users & permissions](./users-and-permissions.md).
## Twikoo
`POST /api/index.php` — [Twikoo](./twikoo.md).
## See also
- [Environment variables](./configuration.md)
- [Reverse proxy](./reverse-proxy.md)
+101
View File
@@ -0,0 +1,101 @@
# Environment variables
Configure PicHost via environment variables (Docker / `.env`) or the in-app **Settings** page. Most values prefer **SQLite** when saved; some env vars override or lock UI editing.
## Priority overview
| Setting | Priority (high → low) | Env overrides UI |
| ------- | --------------------- | ---------------- |
| API upload token | Env → SQLite | **Yes** (cannot regenerate in UI) |
| WebP quality | SQLite → env → default 80 | No |
| Referer whitelist | SQLite → env | No |
| Site / image base URL | SQLite → env → request origin | No |
| Hide `images/` prefix | SQLite → env → default false | No |
| Date-based paths | SQLite → env → default true | No |
| Global auto-delete days | SQLite → env → default 0 | No |
## Variables
### `API_UPLOAD_TOKEN`
Token for external uploads (Twikoo, userscripts, blogs). Header: `Auth-Token: <token>`; form field `token` still works.
- Generate in **API** page when unset
- **When set in env, it wins** — UI shows “overridden by environment” and cannot regenerate
- Alias: `NUXT_API_UPLOAD_TOKEN`
### `ALLOWED_REFERER_HOSTS`
Comma-separated Referer whitelist. Own hostnames are allowed automatically. Optional; configurable in Settings.
### `SITE_BASE_URL` / `IMAGE_BASE_URL`
- **Site URL**: admin UI, API, Twikoo upload
- **Image URL**: copied links and public image URLs
Single-domain: set `IMAGE_BASE_URL` only or leave empty (request origin). Dual-domain: hostnames must differ — see [Dual-domain separation](./domain-separation.md). Values must match proxy `server_name`; do not use `localhost`, IP, or unlisted hostnames for production admin access.
### `HIDE_FOLDER_IN_URL`
`true` hides the `images/` prefix in public URLs.
### `STORAGE_USE_DATE_PATH` / `STORAGE_LAYOUT`
- `true` or `STORAGE_LAYOUT=date` (default): `images/YYYY/MM/id.webp`
- `false` or `STORAGE_LAYOUT=flat`: `images/id.webp`
Also toggled in Settings.
### `DATA_DIR`
Root for images and SQLite. Default `./data`; `/data` in container.
### `WEBP_QUALITY`
Server WebP quality 1–100, default **80**.
### `AUTO_DELETE_DAYS`
Delete images older than N days globally; `0` disables. Affects **new uploads after enable** only. Also in Settings.
### `DEV_BYPASS_ACCESS`
Local dev only: `true` bypasses login. Never use in production.
### Storage backends (optional)
Admins usually add backends in **Storage** UI; env vars such as `STORAGE_BACKEND=s3` and `S3_*` are also supported. See [Storage](./storage.md).
## Docker Compose example
```yaml
services:
pichost:
image: muxui/pichost:latest
ports:
- "6892:6892"
volumes:
- ./data:/data
environment:
SITE_BASE_URL: https://admin.example.com
IMAGE_BASE_URL: https://pic.example.com
API_UPLOAD_TOKEN: your-secret-token
WEBP_QUALITY: "85"
```
Restart the container after changing env vars.
## Settings mapping
| In-app setting | Environment variable |
| -------------- | ---------------------- |
| API token | `API_UPLOAD_TOKEN` |
| Referer protection | `ALLOWED_REFERER_HOSTS` |
| Site URL | `SITE_BASE_URL` |
| Image URL | `IMAGE_BASE_URL` |
| Hide folder prefix | `HIDE_FOLDER_IN_URL` |
| Date-based paths | `STORAGE_USE_DATE_PATH` / `STORAGE_LAYOUT` |
| WebP quality | `WEBP_QUALITY` |
| Auto-delete | `AUTO_DELETE_DAYS` |
Template: [`.env.example`](https://github.com/O96u/PicHost/blob/main/.env.example) in the repo root.
+167
View File
@@ -0,0 +1,167 @@
# Dual-domain separation · Recommended deployment
When admin and image domains are split, the admin UI, API, and public image links use different hostnames. PicHost enforces isolation in **application middleware**: the image host only serves image paths; other requests return 404.
## Architecture
**One Docker instance + two domains**, both **fully reverse-proxied** to port `6892` (isolation is handled by PicHost middleware; no path splitting in Nginx).
| Domain | Purpose |
| ------ | ------- |
| `admin.example.com` | Admin UI, API, Twikoo upload |
| `pic.example.com` | Public image links (non-image paths blocked) |
Set during `/setup` or in **Settings**:
- **Site URL**: `https://admin.example.com`
- **Image URL**: `https://pic.example.com`
## Nginx example
```nginx
# admin.example.com — admin + API
server {
server_name admin.example.com;
location / {
proxy_pass http://127.0.0.1:6892;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 12m;
}
}
# pic.example.com — same full proxy; PicHost middleware blocks non-image paths
server {
server_name pic.example.com;
location / {
proxy_pass http://127.0.0.1:6892;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Default server: reject bare IP and unknown Host values (strongly recommended in production)
server {
listen 443 ssl default_server;
listen 80 default_server;
server_name _;
return 444;
}
```
> Caddy, NPM, and similar tools work the same: proxy both hostnames to `http://<host>:6892` and forward `Host` and `X-Forwarded-Proto`.
## Security notes
### Middleware only splits the two configured hostnames
PicHost uses the request **Host** (and `X-Forwarded-Host` when proxied) to decide site vs image domain:
- **Image host**: non-image paths (e.g. `/`, `/settings`) → 404
- **Site host**: image URL paths (e.g. `/images/...`) → 404
If the Host is **neither** the configured site nor image hostname (e.g. `localhost`, the server **public IP**, Cloudflare **Pages** `*.pages.dev`, **Workers** `*.workers.dev`, or any other unlisted name), **the app does not block the request today** — admin and API may still be reachable. This is not about hyphenated or long domain names; it is an extra entry point outside your dual-domain pair.
**Production recommendations:**
1. Configure `server_name` only for the site and image hosts; add a **default server** to reject other Host values and bare IP access (see Nginx example above)
2. With Cloudflare **orange-cloud** DNS, restrict the origin firewall to **[Cloudflare IP ranges](https://www.cloudflare.com/ips/)** so traffic cannot bypass the CDN
3. Sign in only on the **site hostname** from Settings; do not mix `localhost`, IP, or unlisted domains
4. Do **not** put Cloudflare Pages / Workers in front of PicHost as a **full-site reverse proxy** (extra `pages.dev` / `workers.dev` entry points worsen this issue)
5. PicHost **cannot** run as-is on Pages/Workers (needs `node-server`, SQLite, `sharp`, local `data/`). Keep Docker/VPS; use orange-cloud DNS and optional R2 storage on CF
### Cloudflare orange cloud (incl. preferred edge IPs)
**Recommended:** both `admin.example.com` and `pic.example.com` **DNS Proxied** → origin Nginx / 1Panel → port `6892`, SSL **Full (strict)** (origin cert or Cloudflare Origin Certificate).
| Feature | Notes |
| ------- | ----- |
| Orange-cloud CDN | Compatible with dual-domain; preserve `Host` and `X-Forwarded-Proto` on origin |
| DNS “preferred IP” setups | Only changes which CF edge IP clients hit; Host stays your real domain |
| R2 storage | Add an R2 backend under **Storage**; independent of orange cloud |
| `cloudflare` Git branch | R2-focused deployment line; still Docker, **not** Workers hosting |
**Do not confuse** “Create Worker” in the CF dashboard (Git + `wrangler deploy`) with deploying PicHost — the repo uses `nitro.preset: 'node-server'` and is not Workers-ready.
See [Reverse proxy](./reverse-proxy.md#cloudflare-orange-cloud) for origin hardening.
## FN NAS · Lucky reverse proxy
In [Lucky](https://github.com/gdy666/lucky), create one **reverse proxy** rule per domain. **Frontend** is the public hostname; **backend** is PicHost on your LAN (e.g. `http://192.168.8.3:6892`).
**Site domain (admin)**
| Field | Example |
| ----- | ------- |
| Service type | Reverse proxy |
| Frontend | `admin.pichost.com` |
| Backend | `http://192.168.8.3:6892` |
**Image domain**
| Field | Example |
| ----- | ------- |
| Service type | Reverse proxy |
| Frontend | `image.pichost.com` |
| Backend | `http://192.168.8.3:6892` |
Both rules can point to the same backend; PicHost applies host-based isolation automatically.
## Environment variables (optional)
```env
SITE_BASE_URL=https://admin.example.com
IMAGE_BASE_URL=https://pic.example.com
```
See [Environment variables](./configuration.md).
## Local testing (Windows / macOS)
### 1. Edit hosts
```text
127.0.0.1 admin.pichost.test
127.0.0.1 pic.pichost.test
```
Prefer the `.test` TLD; avoid `.local` where it conflicts with mDNS.
### 2. Start dev server
```bash
npm run dev
```
Dev binds `127.0.0.1:3000` by default. If the hostname resolves but the browser cannot connect, check whether dev is listening on IPv6 `::1` only.
### 3. Configure dual domains
| Field | Example |
| ----- | ------- |
| Site URL | `http://admin.pichost.test:3000` |
| Image URL | `http://pic.pichost.test:3000` |
Always open admin on the **site hostname**, not `localhost` (cookies and Host must match).
### 4. Expected behavior
| URL | Expected |
| --- | -------- |
| `http://admin.pichost.test:3000/` | Admin UI works |
| `http://admin.pichost.test:3000/images/...` or `/2026/08/xxx.webp` | **404** (use image host) |
| `http://pic.pichost.test:3000/` | **404** (image host has no admin) |
| `http://pic.pichost.test:3000/images/...` | Image served |
| `http://localhost:3000/` or `http://127.0.0.1:3000/` | **Admin still loads** (third Host — isolation skipped; OK for dev only; block at proxy in production) |
After upload, copied links should use `pic.pichost.test`; gallery thumbnails should load.
## Notes
- Hostnames must **differ** (same machine, different subdomains is fine)
- Site / image URLs in Settings must **exactly match** proxy `server_name` (including `www` or not)
- No separate static root on the image domain; unlike EasyImages, PicHost isolates in-app
- Single-domain setups can skip separation and use `IMAGE_BASE_URL` or the request host
- When Referer protection is enabled, site and image hosts are whitelisted automatically
For single-domain proxy basics see [Reverse proxy](./reverse-proxy.md). Chinese version: [双域名分离](/guide/domain-separation)
+64
View File
@@ -0,0 +1,64 @@
# FAQ
## Forgot the admin password?
With `docker exec` access:
```bash
docker exec pichost reset-password
docker exec pichost reset-password <username>
```
Local: `npm run reset-password`. See [Quick start](./getting-started.md#forgot-password).
## Why can’t I regenerate the API token in the UI?
`API_UPLOAD_TOKEN` in the environment **locks** the token. Change env and **restart**. See [Environment variables](./configuration.md#api-upload-token).
## Hotlink / Referer errors?
Check the **Referer whitelist** (`ALLOWED_REFERER_HOSTS` or Settings). Referring sites must be listed; PicHost’s own hosts and dual-domain pair are allowed automatically.
## Why can admin load on pages.dev / workers.dev / IP with dual-domain?
Dual-domain isolation only splits paths on the **configured site and image hostnames**. Other Host values (`localhost`, public IP, Cloudflare **Pages** `*.pages.dev`, **Workers** `*.workers.dev`, etc.) are **not blocked today**, so admin may appear on unexpected URLs.
Common cause: **full-site reverse proxy** via Pages/Workers on top of orange-cloud DNS, or an origin that accepts any Host / bare IP.
Fix: use only the two configured domains; add a default `server_name` block; with orange cloud, allow only CF IPs on the origin. See [Security notes](./domain-separation.md#security-notes).
## Can I deploy PicHost on Cloudflare Workers / Pages?
**Not with the current codebase.** PicHost needs `node-server`, SQLite, `sharp`, and local `data/` — use Docker/VPS. Connecting the repo in “Create Worker” will not deploy successfully.
Recommended: run PicHost on origin; use **proxied DNS** (and optional **R2** storage). Do not full-proxy the app through Pages/Workers.
## Dual-domain: admin works but thumbnails break?
- Verify **IMAGE_BASE_URL** and proxy to the same instance
- Always open admin on the **site hostname**, not mixed with `localhost`
- See [Dual-domain separation](./domain-separation.md)
## Undeletable gallery items after v1.2 upgrade?
Likely **orphan index** rows (no file on disk). Startup sync removes them on restart. See [v1.2 migration](./migration.md#step-2-startup-index-sync-automatic).
## Do I still need `data/blog/` after migration?
After `migrate --apply`, files live under `data/images/blog/`. Remove leftover empty dirs after backup if you like; dirs with non-image files are kept by the CLI.
## Object storage only — run migrate?
No. Skip the CLI; startup sync focuses on local `data/images/` scanning.
## Replace old image URLs in posts?
Use `data/mapping.json` from `migrate` preview. Keys are **storage paths**, not full legacy URLs. See [Migration](./migration.md#urls).
## Skip login during development?
`DEV_BYPASS_ACCESS=true` (local only). Use `/setup` for normal flows.
## Docs vs README?
This **VitePress site** is the full guide; README is a short overview. Online: <https://o96u.github.io/PicHost/>
+72
View File
@@ -0,0 +1,72 @@
# Quick start
PicHost is a self-hosted image host for NAS, home servers, or VPS. Docker is recommended; the first visit runs a web wizard to create the admin account.
## Try it online
| Branch | Notes |
| ------ | ----- |
| **main** | No public demo — see [screenshots](./index.md#screenshots) or the repository README |
| **cloudflare** | R2-only live demo: [pic.roven.cc](https://pic.roven.cc) |
## Docker (recommended)
```bash
docker run -d \
--name pichost \
-p 6892:6892 \
-v ./data:/data \
--restart unless-stopped \
muxui/pichost:latest
```
Default port: **6892**. Open `http://<host>:6892` and complete setup.
If you cloned the repo:
```bash
docker compose up -d
```
See `docker-compose.yml` in the repository.
## First-time setup
1. Open your instance URL (Docker default `http://<host>:6892`).
2. Complete `/setup` with admin username and password.
3. (Optional) Configure site URL, image URL, Referer rules — see [Environment variables](./configuration.md) and [Dual-domain separation](./domain-separation.md).
4. Upload from the home page; admins can open **Storage**, **Settings**, and **API**.
## Sign in
Management pages (upload, stats, settings, etc.) require login. Enter username and password, **drag the slider to align with the gap**, then click **Sign in**.
![Login](/screenshots/login.png)
If registration is enabled, create an account from the register page. Legacy deployments may still use a secret key (see the on-screen hint).
## Forgot password
Requires `docker exec` (server access):
```bash
docker exec pichost reset-password # admin only when there is exactly one
docker exec pichost reset-password <username> # admin or regular user
```
A random password is printed. Change it after login.
Local dev: `npm run reset-password` or `npm run reset-password -- <username>`
## Upgrading to v1.2.0
If `data/` has legacy top-level folders next to `images` (e.g. `blog/`, `twikoo/`), read the [v1.2 migration guide](./migration.md) first.
Object-storage-only or files already under `data/images/` — pull the new image and restart.
## Next steps
- [Local development](./local-dev.md)
- [Environment variables](./configuration.md)
- [Storage](./storage.md)
- [API](./api.md)
+39
View File
@@ -0,0 +1,39 @@
# PicHost documentation
Welcome to the PicHost user guide. This site is the single source of truth; the repository README keeps a short overview and quick start.
## Suggested reading order
1. [Quick start](./getting-started.md) — Docker and first-time setup
2. [Environment variables](./configuration.md) — Production config vs in-app settings
3. [Storage](./storage.md) — Backends and URL modes
4. [API](./api.md) — REST endpoints and tokens
## Screenshots
**main** has no public demo. Below are v1.2.0 product screenshots.
| First-time setup | Login |
| :--------------: | :---: |
| ![Setup](/screenshots/setup.png) | ![Login](/screenshots/login.png) |
| API | Stats & gallery |
| :-: | :-------------: |
| ![API](/screenshots/api.png) | ![Stats & gallery](/screenshots/stats.png) |
| Storage | Settings |
| :-----: | :------: |
| ![Storage](/screenshots/storage.png) | ![Settings](/screenshots/settings-en.png) |
| Activity log |
| :----------: |
| ![Activity log](/screenshots/logs.png) |
## Branches
| Branch | Notes |
| ------ | ----- |
| [**main**](https://github.com/O96u/PicHost/tree/main) | Full product; screenshots above |
| [**cloudflare**](https://github.com/O96u/PicHost/tree/cloudflare) | R2-only; live demo [pic.roven.cc](https://pic.roven.cc) |
Use **main** for multiple cloud backends; use **cloudflare** if you only deploy with Cloudflare R2.
+54
View File
@@ -0,0 +1,54 @@
# Local development
Full-stack Nuxt 4 development at the repository root. Data defaults to `./data`.
## Requirements
- Node.js **22**
- npm **11** (see `packageManager` in `package.json`)
## Start
```bash
npm install
cp .env.example .env # optional
npm run dev
```
Open `http://localhost:3000/setup` to create the admin account. Data directory defaults to `./data` (`DATA_DIR` overrides).
## Common commands
| Command | Description |
| ------- | ----------- |
| `npm run dev` | Dev server |
| `npm run build` | Production build |
| `npm run start` | Run `.output` |
| `npm run lint` | ESLint |
| `npm run typecheck` | Type check |
| `npm test` | Unit tests |
| `npm run reset-password` | Reset password (optional username) |
| `npm run migrate` | Preview legacy dir migration (`-- --apply` to run) |
## Dev-only variables
| Variable | Description |
| -------- | ----------- |
| `DEV_BYPASS_ACCESS` | `true` bypasses login (skips `/setup`; rarely needed) |
| `DATA_DIR` | Data directory, default `./data` |
Full list: [Environment variables](./configuration.md).
## Dual-domain local test
Edit `hosts` and follow [Dual-domain separation](./domain-separation.md) for local verification.
## Documentation site
```bash
npm run docs:dev
npm run docs:build
npm run docs:preview
```
Sources live in `docs-site/`; screenshot sources are in `docs/screenshots/` (including `login.png`), copied to `docs-site/public/screenshots/` before build.
+129
View File
@@ -0,0 +1,129 @@
# v1.2.0 local disk migration guide
From v1.2.0, **all local image files** live under `data/images/`; SQLite `keys` also start with `images/`. Legacy top-level folders next to `images` (e.g. `data/blog/`, `data/twikoo/`) are no longer used.
Migration is **two steps**: run the **CLI** to move files on disk, then let **startup sync** align the SQLite index.
---
## Migrating from other image hosts (EasyImage, Lsky, Chevereto, …)
There is **no** dedicated “pull from URLs” or “import via vendor API” tool. Moving from **legacy PicHost** or **another self-hosted** install uses the same **local file** workflow:
1. Locate the **image files** on the old server (copy the tree, not just the database).
2. Place them under PicHost `data/` as a **top-level folder** next to `images` (any name — it becomes `images/{name}/...`).
3. Run `migrate` / `migrate --apply`, then restart PicHost.
| Source | Typical path on old server | After copy into PicHost |
| ------ | -------------------------- | ----------------------- |
| PicHost v1.1.x | `data/blog/`, `data/twikoo/` | Already under `data/` → run `migrate` |
| EasyImage(s) | `i/` or `upload/` | e.g. `data/easyimage/` → `migrate` |
| Lsky | `public/uploads/` etc. | e.g. `data/lsky/` → `migrate` |
| Chevereto etc. | `images/` or `content/` | e.g. `data/chevereto/` → `migrate` |
| Custom | your folder name | `data/your-folder/` → `migrate` |
The CLI scans **every top-level directory under `data/` except `images`**, not only `blog` / `twikoo`.
**Limits:** files only — accounts, tokens, and old public URLs are not imported. `mapping.json` maps old **storage keys**, not full old-site URLs. For S3/R2-only legacy hosts, sync blobs to `data/…` or attach the bucket in PicHost **Storage** settings.
---
## Layout before and after
### Before (typical v1.1.x)
```
/data
├── pichost.db
├── blog/
│ └── 2026/08/TeKJ2fD6cB6j.webp
├── twikoo/
└── images/
```
Index keys might look like `blog/2026/08/TeKJ2fD6cB6j.webp`.
### After (v1.2.0)
```
/data
├── pichost.db
├── mapping.json
└── images/
├── blog/
│ └── 2026/08/TeKJ2fD6cB6j.webp
└── twikoo/
```
Index key: `images/blog/2026/08/TeKJ2fD6cB6j.webp`.
---
## Step 1: CLI file migration
Run **before or after** upgrading the image; backup `data/` first.
```bash
docker exec pichost migrate # preview + mapping.json
docker exec pichost migrate --apply # move files
```
Local: `npm run migrate` / `npm run migrate -- --apply`.
| Item | Behavior |
| ---- | -------- |
| Scan | Every top-level directory under `data/` **except `images`** |
| Target | `data/images/{original-top-dir}/{relative-path}` |
| Extensions | jpg, jpeg, png, webp, gif, svg, ico |
| Sidecar | `.meta.json` next to an image is moved if present |
| Other files | Not moved; parent directory kept if anything remains |
| Mapping | `data/mapping.json` — old key → new key |
Script: `server/cli/migrate-to-single-images.mjs`.
---
## Step 2: Startup index sync (automatic)
Runs on every start (`npm run dev`, Docker). No manual command.
```text
[INFO] 开始同步图片索引:扫描本地磁盘 ...
[INFO] 磁盘扫描完成:...
[INFO] 开始归一化遗留图片路径 ...
[INFO] 开始校验索引与磁盘一致性,清理无文件的孤儿记录
[INFO] 图片索引同步结束
```
| Step | Purpose |
| ---- | ------- |
| Scan `data/images/` | Insert missing index rows for files on disk |
| Normalize legacy keys | `blog/...` → `images/blog/...` when files are in place |
| Purge orphans | Remove index rows with no file on disk |
| Repair Content-Type | Fix `application/octet-stream` entries |
The CLI only **moves files**. Index alignment runs at startup — **restart** after `migrate --apply`.
---
## Recommended upgrade flow
1. Backup `data/`.
2. `docker exec pichost migrate` — review output and `mapping.json`.
3. `docker exec pichost migrate --apply`.
4. Deploy v1.2.0 and restart.
5. Check logs for successful index sync.
6. Use `mapping.json` to update external URLs if needed.
Skip step 1 if you only use object storage or everything is already under `data/images/`.
---
## URLs
| Old key | `blog/TeKJ2fD6cB6j.webp` |
| New key | `images/blog/TeKJ2fD6cB6j.webp` |
With “hide folder” mode, short URLs like `/TeKJ2fD6cB6j.webp` may still resolve by filename. Use `mapping.json` for bulk replacements.
Chinese version: [v1.2 迁移指南](/guide/migration)
+83
View File
@@ -0,0 +1,83 @@
# Reverse proxy
In production, terminate HTTPS with Nginx, Caddy, NPM (Nginx Proxy Manager), or your NAS proxy and forward to PicHost on port **6892**.
## Basics
- Upstream: `http://127.0.0.1:6892` (or host IP)
- Forward: `Host`, `X-Forwarded-Proto` (and `X-Forwarded-For` as needed)
- Body size: Nginx `client_max_body_size 12m` or larger
- Use **HTTPS** on the public edge
## Single domain (Nginx)
Admin, API, and images on one hostname:
```nginx
server {
server_name pic.example.com;
location / {
proxy_pass http://127.0.0.1:6892;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 12m;
}
}
```
Optional:
```env
IMAGE_BASE_URL=https://pic.example.com
```
Without `IMAGE_BASE_URL`, PicHost infers the public URL from forwarded headers.
## Dual domains
Both hostnames can fully proxy to the same port; isolation is enforced in PicHost middleware.
See [Dual-domain separation](./domain-separation.md).
## Cloudflare orange cloud
Run PicHost on **your server / Docker** (port `6892`). On Cloudflare:
| Practice | Notes |
| -------- | ----- |
| **Proxied DNS** | Both admin and image hostnames → origin proxy to `6892`, SSL **Full (strict)** |
| Preferred edge IPs | Compatible; Host remains your real domain |
| **R2** | Object storage backend in **Storage**; not the same as hosting the app on Pages |
| Origin firewall | Allow only [Cloudflare IPs](https://www.cloudflare.com/ips/); do not expose `6892` publicly |
**Avoid:**
- **Cloudflare Pages** (`*.pages.dev`) or **Workers** (`*.workers.dev`) as a **full-site reverse proxy** to PicHost — extra hostnames can bypass dual-domain isolation ([Security notes](./domain-separation.md#security-notes))
- “Create Worker” from Git on this repo — `node-server` preset is not Workers-ready
## Origin hardening (dual-domain)
PicHost middleware does **not** reject unlisted Host values. Complement with:
1. **`server_name` allowlist** — site + image hosts only; `default_server` blocks bare IP and unknown Host
2. **Do not expose 6892 on the public Internet** — only local reverse proxy; public 80/443 only
3. **Orange cloud** — origin firewall limited to Cloudflare IP ranges
## Caddy
```caddy
pic.example.com {
reverse_proxy 127.0.0.1:6892
}
```
Add a second site block for the second hostname with the same upstream.
## Caching
Image 404/403 and domain-isolation 404 responses are **no-cache** to avoid sticky CDN errors. Be careful caching API paths behind a CDN.
## See also
- [Environment variables](./configuration.md)
- [Quick start](./getting-started.md)
+66
View File
@@ -0,0 +1,66 @@
# Storage
PicHost supports **local disk** and **S3-compatible object storage** (Cloudflare R2, Tencent COS, Alibaba OSS, AWS S3, etc.). Admins manage backends at **Storage** (`/storage`); new uploads go to the **default** backend.
![Storage management](/screenshots/storage.png)
## Layout (local)
```
/data
├── pichost.db # SQLite: users, sessions, settings, storage_backends, images index
└── images/ # All files when using local backend
```
- New uploads: `images/randomId.webp` or `images/YYYY/MM/randomId.webp` (flat mode in Settings)
- Migrated legacy: `images/blog/...` and similar
- With cloud backends, blobs live in the bucket; index stays in SQLite `images`
All **keys** start with `images/`. The upload API does **not** accept a `folder` parameter.
## Adding a backend
1. Sign in as admin → **Storage** → **Add backend**.
2. Choose **local disk** or **S3-compatible** (R2 / COS / OSS presets).
3. Enter connection details (bucket, endpoint, keys).
4. **Set as default** so new uploads use it.
### Cloudflare R2
| Field | Value |
| ----- | ----- |
| Endpoint | `https://<account_id>.r2.cloudflarestorage.com` |
| Region | `auto` |
| Bucket | Your R2 bucket name |
| Keys | R2 API token access / secret |
The **cloudflare** branch streamlines R2-only deploys; see branch notes on the [docs overview](./index.md).
### Environment variables (optional)
Some installs seed a default S3 backend via `STORAGE_BACKEND=s3`, `S3_ENDPOINT`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `S3_REGION`. **UI-configured backends live in SQLite** alongside env-based defaults.
## URL modes
| Mode | Behavior |
| ---- | -------- |
| **proxy** | Serve via PicHost (`GET /images/...`) |
| **public** | 302 redirect to bucket/CDN public URL |
Copied links use **IMAGE_BASE_URL**. “Hide folder prefix” may shorten URLs; the server resolves by path or basename.
## Usage & gallery filter
- Per-backend usage on the Storage page
- Gallery filter by backend (admin)
## Backup
Back up `data/images/`, `data/pichost.db`, and bucket objects.
Upgrading from v1.1.x with parallel folders: [v1.2 migration](./migration.md).
## See also
- [Environment variables](./configuration.md)
- [Users & permissions](./users-and-permissions.md)
+49
View File
@@ -0,0 +1,49 @@
# Twikoo
PicHost supports **Twikoo** and **EasyImage 2.0** image upload — configure your comment system like EasyImage.
## Twikoo settings
| Setting | Value |
| ------- | ----- |
| `IMAGE_CDN` | `easyimage` |
| `IMAGE_CDN_URL` | `https://your-domain/api/index.php` |
| `IMAGE_CDN_TOKEN` | Same as global `API_UPLOAD_TOKEN` |
- Use your **site hostname** (admin domain) in `IMAGE_CDN_URL`
- Generate the token in PicHost **API** or set `API_UPLOAD_TOKEN`
## Upload endpoint
`POST /api/index.php`
| Field | Description |
| ----- | ----------- |
| `image` | Image file |
| `token` | Global API token (same as `Auth-Token`) |
```bash
curl -X POST "https://admin.example.com/api/index.php" \
-F "token=YOUR_TOKEN" \
-F "image=@./demo.png"
```
Files are stored under `images/` and owned by the **global token** account (admin).
## REST vs Twikoo
| Aspect | REST `/api/images/upload` | Twikoo `/api/index.php` |
| ------ | ------------------------- | ------------------------ |
| Auth | `Auth-Token` header | form `token` |
| Response | PicHost JSON | EasyImage-compatible JSON |
| Token | Global or personal | Global only |
## Dual-domain
Point Twikoo uploads at **SITE_BASE_URL**; public images use **IMAGE_BASE_URL**. See [Dual-domain separation](./domain-separation.md).
## See also
- [API](./api.md)
- [Users & permissions](./users-and-permissions.md)
- [Environment variables](./configuration.md)
@@ -0,0 +1,58 @@
# Users & permissions
PicHost uses role-based access control (RBAC): **admin** and **user**. Enforcement is server-side.
## Role capabilities
| Capability | Admin | User |
| ---------- | :---: | :--: |
| Upload images | ✓ | ✓ |
| Own gallery | ✓ | ✓ |
| All galleries / uploader badges | ✓ | — |
| Storage management (`/storage`) | ✓ | — |
| System settings (`/settings`) | ✓ | — |
| Allow registration | ✓ | — |
| Global API token | ✓ | — |
| Personal API token | ✓ | ✓ |
| Stats (incl. user count) | ✓ | partial |
| Activity log | ✓ | — |
Regular users see **API**, **Stats** in the nav; user menu: **Change password**, **Sign out**.
## Auth scenarios
| Scenario | Auth | Ownership / visibility |
| -------- | ---- | ---------------------- |
| Web upload | Session cookie | Current user |
| API upload + **user token** | `Auth-Token` header | Token owner |
| API upload + **global token** | `Auth-Token` header | Admin (`userId` null) |
| Twikoo `POST /api/index.php` | form `token` | Same as global token |
| Gallery list / search / delete | Session or token | Users: own only; admin: all |
| Direct link `GET /images/...` | None (Referer rules) | Public if URL is known |
## Accounts
- By default only admins create users; **allow registration** is an admin setting
- Passwords hashed with scrypt
- Web login requires a slider captcha (align the thumb with the gap)
- Login rate limiting
## Upload preferences & auto-delete
- **Upload preferences** (home card flip): client compression, auto-copy links, per user
- **Auto-delete**: global (admin) and per-user policies; affects **new uploads after enable** only
## API tokens
| Type | Managed by | Upload owner | Typical use |
| ---- | ---------- | ------------ | ----------- |
| **Global** | Admin (API page) | Admin account | Twikoo, site-wide scripts |
| **Personal** | Each user (API page) | That user | Personal blog, private scripts |
`API_UPLOAD_TOKEN` in env locks the global token — see [Environment variables](./configuration.md).
## See also
- [API](./api.md)
- [Twikoo](./twikoo.md)
- [Storage](./storage.md)