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

+129
View File
@@ -0,0 +1,129 @@
import { defineConfig } from 'vitepress'
const docsBase = 'https://o96u.github.io/PicHost'
const zhSidebar = [
{
text: '开始',
items: [
{ text: '文档首页', link: '/guide/' },
{ text: '快速开始', link: '/guide/getting-started' },
{ text: '本地开发', link: '/guide/local-dev' }
]
},
{
text: '部署与配置',
items: [
{ text: '环境变量', link: '/guide/configuration' },
{ text: '反向代理', link: '/guide/reverse-proxy' },
{ text: '双域名分离', link: '/guide/domain-separation' }
]
},
{
text: '使用',
items: [
{ text: '存储', link: '/guide/storage' },
{ text: '用户与权限', link: '/guide/users-and-permissions' },
{ text: 'API', link: '/guide/api' },
{ text: 'Twikoo', link: '/guide/twikoo' }
]
},
{
text: '升级',
items: [
{ text: 'v1.2 迁移', link: '/guide/migration' },
{ text: '常见问题', link: '/guide/faq' }
]
}
]
const enSidebar = [
{
text: 'Getting started',
items: [
{ text: 'Overview', link: '/en/guide/' },
{ text: 'Quick start', link: '/en/guide/getting-started' },
{ text: 'Local development', link: '/en/guide/local-dev' }
]
},
{
text: 'Deploy & configure',
items: [
{ text: 'Environment variables', link: '/en/guide/configuration' },
{ text: 'Reverse proxy', link: '/en/guide/reverse-proxy' },
{ text: 'Dual-domain separation', link: '/en/guide/domain-separation' }
]
},
{
text: 'Usage',
items: [
{ text: 'Storage', link: '/en/guide/storage' },
{ text: 'Users & permissions', link: '/en/guide/users-and-permissions' },
{ text: 'API', link: '/en/guide/api' },
{ text: 'Twikoo', link: '/en/guide/twikoo' }
]
},
{
text: 'Upgrade',
items: [
{ text: 'v1.2 migration', link: '/en/guide/migration' },
{ text: 'FAQ', link: '/en/guide/faq' }
]
}
]
export default defineConfig({
base: '/PicHost/',
title: 'PicHost',
description: '自托管图床文档 · Self-hosted image hosting docs',
head: [['link', { rel: 'icon', href: '/PicHost/favicon.ico' }]],
locales: {
root: {
label: '简体中文',
lang: 'zh-CN',
themeConfig: {
nav: [
{ text: '指南', link: '/guide/getting-started' },
{ text: 'API', link: '/guide/api' },
{ text: 'GitHub', link: 'https://github.com/O96u/PicHost' }
],
sidebar: zhSidebar,
docFooter: { prev: '上一页', next: '下一页' },
outline: { label: '本页目录' },
lastUpdated: { text: '最后更新' },
returnToTopLabel: '回到顶部',
sidebarMenuLabel: '菜单',
darkModeSwitchLabel: '主题',
langMenuLabel: '语言'
}
},
en: {
label: 'English',
lang: 'en-US',
link: '/en/',
themeConfig: {
nav: [
{ text: 'Guide', link: '/en/guide/getting-started' },
{ text: 'API', link: '/en/guide/api' },
{ text: 'GitHub', link: 'https://github.com/O96u/PicHost' }
],
sidebar: enSidebar,
docFooter: { prev: 'Previous', next: 'Next' },
outline: { label: 'On this page' },
lastUpdated: { text: 'Last updated' },
returnToTopLabel: 'Back to top',
sidebarMenuLabel: 'Menu',
darkModeSwitchLabel: 'Appearance',
langMenuLabel: 'Language'
}
}
},
themeConfig: {
socialLinks: [{ icon: 'github', link: 'https://github.com/O96u/PicHost' }],
search: { provider: 'local' },
editLink: {
pattern: `${docsBase}/edit/main/docs-site/:path`,
text: '在 GitHub 上编辑此页'
}
}
})
+6
View File
@@ -0,0 +1,6 @@
:root {
--vp-c-brand-1: #22c55e;
--vp-c-brand-2: #16a34a;
--vp-c-brand-3: #15803d;
--vp-c-brand-soft: rgba(34, 197, 94, 0.14);
}
+4
View File
@@ -0,0 +1,4 @@
import DefaultTheme from 'vitepress/theme'
import './custom.css'
export default DefaultTheme
+16
View File
@@ -0,0 +1,16 @@
import { cpSync, existsSync, mkdirSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
const src = join(root, 'docs', 'screenshots')
const dest = join(root, 'docs-site', 'public', 'screenshots')
if (!existsSync(src)) {
console.error(`Screenshot source missing: ${src}`)
process.exit(1)
}
mkdirSync(dest, { recursive: true })
cpSync(src, dest, { recursive: true })
console.log(`Copied screenshots to ${dest}`)
+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)
+32
View File
@@ -0,0 +1,32 @@
---
layout: home
hero:
name: PicHost
text: Lightweight image hosting
tagline: Self-hosted · Multi-user · Docker · API · Twikoo · Local disk or object storage
actions:
- theme: brand
text: Quick start
link: /en/guide/getting-started
- theme: alt
text: API reference
link: /en/guide/api
- theme: alt
text: GitHub
link: https://github.com/O96u/PicHost
features:
- title: Drag, click, paste
details: Server-side WebP compression and Referer hotlink protection.
- title: Multi-backend storage
details: Local disk plus S3-compatible backends (R2 / COS / OSS / AWS); proxy or public URLs.
- title: Users & RBAC
details: Admin and regular users; global and per-user API tokens; gallery scoped by owner.
- title: Zero-config Docker
details: First-run web wizard; reset passwords with `docker exec pichost reset-password`.
- title: Dual-domain separation
details: One instance, two hostnames, full reverse proxy; middleware enforces isolation.
- title: Twikoo compatible
details: "POST /api/index.php follows EasyImage 2.0 for comment systems."
---
+99
View File
@@ -0,0 +1,99 @@
# API
PicHost 提供 REST 接口与 Twikoo 兼容端点。部署后登录 **API** 页可查看当前 Token 与可复制 cURL 示例。
## 鉴权
所有 REST 接口使用请求头:
```http
Auth-Token: YOUR_TOKEN
```
| Token 类型 | 获取方式 | 说明 |
| ---------- | -------- | ---- |
| **全局 Token** | 管理员 **API** 页生成;或 `API_UPLOAD_TOKEN` 环境变量 | 环境变量优先且锁定后台重新生成 |
| **个人 Token** | 各用户 **API** 页 | 仅用于该用户脚本,上传归属本人 |
表单上传仍可使用字段 `token`(Twikoo 协议)。
## 接口列表
### 1. 上传图片
`POST /api/images/upload`
上传单张或多张图片。字段 `image` 传图片;仍兼容 `file`、`files`。存入 `images/`。
```bash
curl -X POST "https://admin.example.com/api/images/upload" \
-H "Auth-Token: YOUR_TOKEN" \
-F "image=@./demo.png"
```
### 2. 获取图片列表
`GET /api/images`
分页列出图库。`limit` 默认 20、最大 100。
```bash
curl "https://admin.example.com/api/images?limit=20&page=1" \
-H "Auth-Token: YOUR_TOKEN"
```
### 3. 搜索图片
`GET /api/images/search`
按文件名或路径关键词搜索,参数 `q` 必填。
```bash
curl "https://admin.example.com/api/images/search?q=demo&limit=20&page=1" \
-H "Auth-Token: YOUR_TOKEN"
```
### 4. 删除图片
`DELETE /api/images`
通过 `key` 删除单张,`key` 为存储路径(如 `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. 批量删除
`POST /api/images/batch-delete`
请求体 JSON:`{"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"]}'
```
## 错误格式
REST 接口返回 JSON,含 `code` 与 `message`(如 `UNAUTHORIZED`、`FORBIDDEN`、`INVALID_REQUEST`)。Twikoo 端点使用 EasyImage 兼容 JSON 格式。
## 权限与可见范围
- 普通用户 Token:列表/搜索/删除仅作用于自己的图片
- 管理员 Token:可访问全部图片
- 全局 Token 上传的图片归属管理员
详见 [用户与权限](./users-and-permissions.md)。
## Twikoo
`POST /api/index.php` 见 [Twikoo](./twikoo.md)。
## 相关
- [环境变量](./configuration.md) — `API_UPLOAD_TOKEN`
- [反向代理](./reverse-proxy.md) — 公网 URL 与 HTTPS
+101
View File
@@ -0,0 +1,101 @@
# 环境变量
PicHost 支持通过环境变量(Docker / `.env`)与后台 **设置** 页配置。多数项 **SQLite 优先**;部分项环境变量可覆盖或锁定后台编辑。
## 优先级总览
| 配置项 | 优先级(高 → 低) | 环境变量覆盖后台 |
| ------ | ----------------- | ---------------- |
| API 上传 Token | 环境变量 → SQLite | **是**(设了则后台无法重新生成) |
| WebP 质量 | SQLite → 环境变量 → 默认 80 | 否(未在 DB 存时读 env) |
| 防盗链白名单 | SQLite → 环境变量 | 否 |
| 网站 / 图片域名 | SQLite → 环境变量 → 当前请求 | 否 |
| 隐藏 `images/` 前缀 | SQLite → 环境变量 → 默认 false | 否 |
| 按年/月分组存储 | SQLite → 环境变量 → 默认 true | 否 |
| 全局自动删除天数 | SQLite → 环境变量 → 默认 0 | 否 |
## 变量说明
### `API_UPLOAD_TOKEN`
外部脚本上传 Token(Twikoo / 油猴 / 博客)。请求头:`Auth-Token: <token>`;表单上传仍可用 `token` 字段。
- 未配置时可在后台 **API** 页生成
- **环境变量一旦设置,优先级最高**,后台显示「由环境变量覆盖」且无法重新生成
- 别名:`NUXT_API_UPLOAD_TOKEN`
### `ALLOWED_REFERER_HOSTS`
防盗链白名单,逗号分隔域名。自身域名自动放行。可选;不填可在设置页配置。若环境变量有值且 SQLite 未存,则使用环境变量。
### `SITE_BASE_URL` / `IMAGE_BASE_URL`
- **网站域名**:管理后台、API、Twikoo 上传入口
- **图片域名**:复制链接与直链使用的公网地址
双域名分离需两者主机名不同,详见 [双域名分离](./domain-separation.md)。填写后须与反代 `server_name` 一致;**勿**通过 `localhost`、IP 或未配置的域名访问生产后台。
### `HIDE_FOLDER_IN_URL`
`true` 时直链隐藏 `images/` 前缀(如 `images/2026/08/a.webp` → `/2026/08/a.webp` 或仅文件名,取决于路径模式)。
### `STORAGE_USE_DATE_PATH` / `STORAGE_LAYOUT`
- `STORAGE_USE_DATE_PATH=true`(默认)或 `STORAGE_LAYOUT=date`:新图保存为 `images/年/月/随机ID.webp`
- `false` 或 `STORAGE_LAYOUT=flat`:保存为 `images/随机ID.webp`
也可在设置页切换。
### `DATA_DIR`
图片与 SQLite 根目录。默认 `./data`,容器内为 `/data`。
### `WEBP_QUALITY`
服务端 WebP 压缩质量 1–100,默认 **80**。
### `AUTO_DELETE_DAYS`
全局自动删除超过 N 天的图片,`0` 表示关闭。启用后仅影响**之后新上传**的图片(与按用户策略一致)。也可在设置页配置。
### `DEV_BYPASS_ACCESS`
仅本地开发:`true` 绕过登录验证。生产环境勿用。
### 存储后端(可选)
管理员通常在 **存储** 页添加后端;也可用环境变量引导(如 `STORAGE_BACKEND=s3` 与 `S3_*`)。详见 [存储](./storage.md)。
## Docker Compose 示例
```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"
```
修改环境变量后需 **重启容器** 才能生效(Token、质量等读取在启动时或请求时按实现读取)。
## 与后台设置对照
| 后台设置项 | 对应环境变量 |
| ---------- | ------------ |
| API Token | `API_UPLOAD_TOKEN` |
| 防盗链 | `ALLOWED_REFERER_HOSTS` |
| 网站域名 | `SITE_BASE_URL` |
| 图片域名 | `IMAGE_BASE_URL` |
| 隐藏目录前缀 | `HIDE_FOLDER_IN_URL` |
| 按年/月分组 | `STORAGE_USE_DATE_PATH` / `STORAGE_LAYOUT` |
| WebP 质量 | `WEBP_QUALITY` |
| 自动删除 | `AUTO_DELETE_DAYS` |
完整模板见仓库根目录 [`.env.example`](https://github.com/O96u/PicHost/blob/main/.env.example)。
+172
View File
@@ -0,0 +1,172 @@
# 双域名分离 · 推荐部署架构
启用「后台与图片域名分离」后,管理后台、API 与图片直链使用不同域名。PicHost 通过 **Host 中间件** 双向隔离:
- **网站域**:后台、API、上传;**禁止**直链出图(图片路径返回 404)
- **图片域**:仅放行图片路径;其余请求返回 404
## 架构说明
**单 Docker 实例 + 两个域名**,均可 **全量反代** 到 `6892`(隔离由 PicHost 中间件负责,无需在 Nginx 层拆分路径)。
| 域名 | 用途 |
| ---- | ---- |
| `admin.example.com` | 管理后台、API、Twikoo 上传 |
| `pic.example.com` | 图片直链(中间件拦截非图片路径) |
初始化时在 `/setup` 或后台 **设置** 中填写:
- **网站域名**:`https://admin.example.com`
- **图片域名**:`https://pic.example.com`
## Nginx 示例
```nginx
# admin.example.com — 后台 + 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 — 同样全量反代;PicHost 中间件会拦截非图片路径
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;
}
}
# 默认 server:拒绝裸 IP、未配置域名等其它 Host(生产环境强烈建议)
server {
listen 443 ssl default_server;
listen 80 default_server;
server_name _;
return 444;
}
```
> Caddy、NPM(Nginx Proxy Manager)等工具同理:两个域名均反代到 `http://<主机>:6892`,并保留 `Host` 与 `X-Forwarded-Proto` 头。
## 安全与注意事项
### 中间件只隔离「已配置的两个域名」
PicHost 根据请求 **Host**(反代会转发为 `X-Forwarded-Host`,应用会一并识别)判断当前是网站域还是图片域:
- **图片域**:非图片路径(如 `/`、`/settings`)→ 404
- **网站域**:图片直链路径(如 `/images/...`)→ 404
若 Host **既不是网站域也不是图片域**(例如 `localhost`、服务器 **公网 IP**、Cloudflare **Pages** 的 `*.pages.dev`、**Workers** 的 `*.workers.dev`、或其它未在设置里填写的域名),**当前版本不会拦截**,后台与 API 仍可能被访问。这与域名是否带 `-`、长短无关,而是多了一条未纳入双域名配置的入口。
**生产环境建议:**
1. 反代层仅为网站域、图片域配置 `server_name`,并增加 **default server** 拒绝其它 Host 与裸 IP(见上方 Nginx 示例)
2. 使用 Cloudflare **橙云** 时,源站防火墙 **仅放行 [Cloudflare IP 段](https://www.cloudflare.com/ips/)**,避免绕过 CDN 直连源站
3. **仅使用** 设置中的网站域名登录后台,不要用 `localhost`、IP 或未配置的域名混用
4. **不要** 用 Cloudflare Pages / Workers 对 PicHost **整站反代**(会多出 `pages.dev` / `workers.dev` 等第三入口,且易出现上述绕过)
5. PicHost **不能** 作为 Node 应用直接部署到 Pages/Workers(依赖 `node-server`、SQLite、`sharp` 与本地 `data/`);应用请继续 Docker/VPS 部署,CF 侧用橙云 DNS + 可选 R2 存储即可
### Cloudflare 橙云(含 DNS 优选)
**推荐架构:** `admin.example.com` 与 `pic.example.com` 均 **DNS Proxied(橙云)** → 源站 Nginx / 1Panel 反代 `6892`,SSL 模式 **Full (strict)**(源站可用 Cloudflare Origin Certificate)。
| 能力 | 说明 |
| ---- | ---- |
| 橙云 CDN | 与 PicHost 双域名兼容;回源须保留 `Host`、`X-Forwarded-Proto` |
| DNS 优选 | 仅改变用户连到 CF 边缘的 IP,Host 仍为正式域名,无额外配置 |
| R2 对象存储 | 在 **存储** 页添加 R2 后端即可,与是否橙云无关 |
| `cloudflare` Git 分支 | R2 专用部署线,仍是 Docker 运行,**不是** Workers 托管 |
**勿混淆:** CF 控制台「创建 Worker」并连接本仓库 **无法** 直接部署 PicHost;`npm run build` + `wrangler deploy` 与当前 `node-server` 预设不兼容。
反代与源站加固详见 [反向代理](./reverse-proxy.md#cloudflare-橙云)。
## 飞牛 NAS · Lucky 反向代理
在 [Lucky](https://github.com/gdy666/lucky) 中为两个域名各建一条 **反向代理** 规则,**前端地址** 填公网域名,**后端地址** 填 PicHost 所在内网 IP 与端口(如 `http://192.168.8.3:6892`)。
**网站域名(管理后台)**
| 项 | 示例值 |
| -- | ------ |
| 服务类型 | 反向代理 |
| 前端地址 | `admin.pichost.com` |
| 后端地址 | `http://192.168.8.3:6892` |
**图片域名**
| 项 | 示例值 |
| -- | ------ |
| 服务类型 | 反向代理 |
| 前端地址 | `image.pichost.com` |
| 后端地址 | `http://192.168.8.3:6892` |
两条规则的后端地址相同即可;域名分离与路径隔离由 PicHost 根据请求 `Host` 自动处理。
## 环境变量(可选)
除 Web 设置外,也可在 `.env` 或 Docker 环境中配置:
```env
SITE_BASE_URL=https://admin.example.com
IMAGE_BASE_URL=https://pic.example.com
```
详见 [环境变量](./configuration.md)。
## 本地测试(Windows / macOS)
### 1. 修改 hosts
```text
127.0.0.1 admin.pichost.test
127.0.0.1 pic.pichost.test
```
建议使用 `.test` 后缀,避免 `.local` 在部分系统上与 mDNS 冲突。
### 2. 启动开发服务
```bash
npm run dev
```
项目已默认绑定 `127.0.0.1:3000`(与 hosts 的 IPv4 一致)。若曾出现 `ping` 通但浏览器连不上,多半是 dev 只监听了 IPv6 `::1`。
### 3. 填写双域名
| 项 | 示例 |
| -- | ---- |
| 网站域名 | `http://admin.pichost.test:3000` |
| 图片域名 | `http://pic.pichost.test:3000` |
**后台请始终用网站域名打开**,不要用 `localhost`(Cookie 与 Host 不一致)。
### 4. 预期结果
| 地址 | 预期 |
| ---- | ---- |
| `http://admin.pichost.test:3000/` | 正常进入后台 |
| `http://admin.pichost.test:3000/images/...` 或 `/2026/08/xxx.webp` | **404**(请用图片域直链) |
| `http://pic.pichost.test:3000/` | **404**(正常,图片域不提供后台) |
| `http://pic.pichost.test:3000/images/...` | 能出图 |
| `http://localhost:3000/` 或 `http://127.0.0.1:3000/` | **能打开后台**(第三 Host,隔离不生效;仅开发可接受,生产须用反代封禁) |
上传一张图后,复制链接应指向 `pic.pichost.test` 域名;在后台图库中缩略图也应能正常显示。
## 相关说明
- 两个域名 **主机名不能相同**(可同机、不同子域)
- 设置里的网站域 / 图片域须与反代 `server_name` **完全一致**(含是否带 `www`)
- 图片域无需单独部署静态目录;与 EasyImages 等不同,PicHost 在应用内完成隔离
- 单域名部署可不启用分离,仅配置 `IMAGE_BASE_URL` 或使用默认当前访问域名
- 配置了 Referer 防盗链时,网站域与图片域会自动加入白名单,无需手写两个域名
单域名反代要点见 [反向代理](./reverse-proxy.md)。英文版:[Dual-domain separation](/en/guide/domain-separation)
+65
View File
@@ -0,0 +1,65 @@
# 常见问题
## 忘记管理员密码怎么办?
能执行 `docker exec` 时:
```bash
docker exec pichost reset-password
# 或指定用户名
docker exec pichost reset-password 用户名
```
本地:`npm run reset-password`。详见 [快速开始](./getting-started.md#忘记密码)。
## 为什么后台无法重新生成 API Token?
若设置了环境变量 `API_UPLOAD_TOKEN`,Token 由环境变量 **锁定**,需修改 env 并 **重启服务**。见 [环境变量](./configuration.md#api-upload-token)。
## 图片外链显示「盗链」或无法加载?
检查 **防盗链白名单**(`ALLOWED_REFERER_HOSTS` 或设置页)。引用图片的站点域名需在白名单中;PicHost 自身域名与双域名场景下的网站域、图片域会自动放行。
## 双域名下为什么用 pages.dev / workers.dev / IP 也能进后台?
PicHost 双域名隔离**只对已配置的网站域、图片域做路径分流**;其它 Host(`localhost`、公网 IP、Cloudflare Pages 的 `*.pages.dev`、Workers 的 `*.workers.dev` 等)**当前不会拦截**,因此可能出现「奇怪地址也能开后台」。
常见原因:在橙云之外又用 **Pages/Workers 整站反代** 到源站,或源站未限制 `server_name` / 允许裸 IP 访问。
处理:仅用正式两个域名访问;反代加 default server;橙云时源站只放行 CF IP。详见 [双域名分离 · 安全与注意事项](./domain-separation.md#安全与注意事项)。
## 能把 PicHost 部署到 Cloudflare Workers / Pages 吗?
**不能(现状)。** 项目使用 `node-server`、SQLite、`sharp` 与本地 `data/`,需 Docker / VPS。CF 控制台「从 Git 创建 Worker」连本仓库会部署失败。
推荐:源站跑 PicHost,CF 用 **橙云 DNS**(含优选)+ 可选 **R2 存储**;不要用 Pages/Workers 反代整站。
## 双域名下后台能打开但缩略图裂图?
- 确认 **图片域名** 配置正确且反代到同一实例
- 后台请用 **网站域名** 打开,不要用 `localhost` 混用
- 见 [双域名分离](./domain-separation.md)
## 升级 v1.2 后图库有删不掉的记录?
可能是 **孤儿索引**(有记录无文件)。重启后启动同步会自动清理;也可手动删除仅索引项。见 [v1.2 迁移](./migration.md#第二步启动时自动同步索引)。
## 遗留 `data/blog/` 目录还要吗?
执行 `migrate --apply` 后文件已迁入 `data/images/blog/`。若原目录只剩空壳或非图片文件,可自行备份后删除;迁移脚本不会删除仍含非图片文件的目录。
## 只用对象存储需要跑 migrate 吗?
不需要。图片在桶内时跳过 CLI;启动同步主要面向本地 `data/images/` 磁盘扫描。
## 如何替换博客里的旧图片 URL?
使用 `data/mapping.json`(`migrate` 预览时生成)批量替换旧 key。注意映射的是 **存储 key**,不是完整旧站 URL。见 [迁移指南](./migration.md#外链与直链)。
## 开发时如何跳过登录?
`DEV_BYPASS_ACCESS=true`(仅本地,勿用于生产)。正常流程请访问 `/setup` 创建管理员。
## 文档与仓库 README 哪个为准?
**本 VitePress 文档站** 为完整用户指南;README 保留摘要与快速开始。在线地址:<https://o96u.github.io/PicHost/>
+75
View File
@@ -0,0 +1,75 @@
# 快速开始
PicHost 适合在 NAS、家用服务器或 VPS 上自托管图床。推荐用 Docker 部署,首次访问通过 Web 向导创建管理员。
## 在线体验
| 分支 | 说明 |
| ---- | ---- |
| **main** | 无公网演示;产品界面见 [文档首页](./index.md#截图预览) 或仓库 README |
| **cloudflare** | R2 专用线在线演示:[pic.roven.cc](https://pic.roven.cc) |
## Docker(推荐)
```bash
docker run -d \
--name pichost \
-p 6892:6892 \
-v ./data:/data \
--restart unless-stopped \
muxui/pichost:latest
```
默认端口 **6892**。浏览器打开 `http://<主机IP>:6892`,按引导创建管理员即可。
已克隆仓库时也可用:
```bash
docker compose up -d
```
见仓库内 `docker-compose.yml`。
## 首次设置
1. 访问实例 URL(Docker 默认 `http://<主机>:6892`)。
2. 在 `/setup` 填写管理员用户名与密码。
3. (可选)配置网站域名、图片域名、防盗链等 — 详见 [环境变量](./configuration.md) 与 [双域名分离](./domain-separation.md)。
4. 登录后可在首页上传图片;管理员可进入 **存储**、**设置**、**API** 页。
## 登录
访问上传、统计、设置等管理页面前需登录。在登录卡片中填写用户名与密码,**拖动滑块对齐缺口**完成验证后点击 **登录**。
![登录页](/screenshots/login.png)
若管理员开启了开放注册,可前往注册页创建账号。遗留部署仍可能使用密钥登录(见界面提示)。
## 忘记密码
需能执行 `docker exec`(相当于服务器权限):
```bash
# 无参数:重置管理员(仅当系统中只有一个管理员时)
docker exec pichost reset-password
# 指定用户名:可重置管理员或普通用户
docker exec pichost reset-password 用户名
```
终端会打印随机新密码;用户不存在时会报错。登录后请到「修改密码」更换。
本地开发:`npm run reset-password` 或 `npm run reset-password -- 用户名`
## 升级到 v1.2.0
若 `data/` 下存在与 `images` 并列的遗留目录(如 `blog/`、`twikoo/`),升级前请阅读 [v1.2 迁移指南](./migration.md)。
仅对象存储、或图片已在 `data/images/` 下时,拉取新镜像并重启即可。
## 下一步
- [本地开发](./local-dev.md) — `npm run dev` 与 `.env`
- [环境变量](./configuration.md) — 生产配置
- [存储](./storage.md) — 多后端与直链
- [API](./api.md) — Token 与 REST 接口
+39
View File
@@ -0,0 +1,39 @@
# PicHost 文档
欢迎使用 PicHost 用户文档。本站为完整指南的单一事实来源;仓库 README 保留特性摘要与快速开始。
## 推荐阅读顺序
1. [快速开始](./getting-started.md) — Docker 部署与首次设置
2. [环境变量](./configuration.md) — 生产环境配置与后台设置对照
3. [存储](./storage.md) — 多后端与直链模式
4. [API](./api.md) — REST 接口与 Token
## 截图预览
**main** 分支暂无公网演示,以下为 v1.2.0 产品界面。
| 首次设置 | 登录 |
| :------: | :--: |
| ![首次设置](/screenshots/setup.png) | ![登录](/screenshots/login.png) |
| API | 统计与图库 |
| :-: | :--------: |
| ![API](/screenshots/api.png) | ![统计与图库](/screenshots/stats.png) |
| 存储管理 | 系统设置 |
| :------: | :------: |
| ![存储管理](/screenshots/storage.png) | ![系统设置](/screenshots/settings.png) |
| 操作日志 |
| :------: |
| ![操作日志](/screenshots/logs.png) |
## 分支说明
| 分支 | 说明 |
| ---- | ---- |
| [**main**](https://github.com/O96u/PicHost/tree/main) | 主线:多后端、双域名、统一 `images/` 存储;界面见上表 |
| [**cloudflare**](https://github.com/O96u/PicHost/tree/cloudflare) | 仅 Cloudflare R2;在线演示 [pic.roven.cc](https://pic.roven.cc) |
需要多种云存储或完整管理界面请用 **main**;图床只接 R2 可选用 **cloudflare** 分支。
+54
View File
@@ -0,0 +1,54 @@
# 本地开发
在仓库根目录进行 Nuxt 4 全栈开发,数据默认写入 `./data`。
## 环境要求
- Node.js **22**
- npm **11**(与 `package.json` 中 `packageManager` 一致)
## 启动
```bash
npm install
cp .env.example .env # 可选
npm run dev
```
访问 `http://localhost:3000/setup` 创建管理员。数据目录默认 `./data`(`DATA_DIR` 可改)。
## 常用命令
| 命令 | 说明 |
| ---- | ---- |
| `npm run dev` | 开发服务器 |
| `npm run build` | 生产构建 |
| `npm run start` | 运行 `.output` |
| `npm run lint` | ESLint |
| `npm run typecheck` | 类型检查 |
| `npm test` | 单元测试 |
| `npm run reset-password` | 重置密码(可选用户名) |
| `npm run migrate` | 预览遗留目录迁移(加 `-- --apply` 执行) |
## 开发专用环境变量
| 变量 | 说明 |
| ---- | ---- |
| `DEV_BYPASS_ACCESS` | 设为 `true` 可绕过登录(跳过 `/setup`,一般无需开启) |
| `DATA_DIR` | 数据目录,默认 `./data` |
完整变量列表见 [环境变量](./configuration.md)。
## 双域名本地测试
修改系统 `hosts` 后可用两个测试域名验证分离逻辑,步骤见 [双域名分离](./domain-separation.md#本地测试-windows-macos)。
## 文档站
```bash
npm run docs:dev # VitePress 开发
npm run docs:build # 构建静态站点
npm run docs:preview # 预览构建结果
```
文档源码在 `docs-site/`;截图源文件在 `docs/screenshots/`(含 `login.png` 登录页),构建前会复制到 `docs-site/public/screenshots/`。
+178
View File
@@ -0,0 +1,178 @@
# v1.2.0 本地磁盘迁移指南
v1.2.0 起,**所有本地图片文件**统一放在 `data/images/` 下;SQLite `images` 表中的 `key` 也以 `images/` 开头。旧版在 `data/blog/`、`data/twikoo/` 等与 `images` **并列的顶层目录**不再符合新模型。
迁移分 **两步**:先用手动 CLI **搬文件**,再由服务 **启动时自动同步索引**。
---
## 从其它图床迁入(EasyImage / 兰空 / Chevereto 等)
**没有**单独的「一键从外链拉取」或「对接某某图床 API」工具。从 **PicHost 旧版** 或其它自托管图床迁入,走的是同一套 **本地文件迁移**:
1. 在旧图床服务器上找到图片**物理目录**(整目录拷贝,不要只拷数据库)。
2. 把该目录放到 PicHost 的 `data/` 下,作为与 `images` **并列的顶层文件夹**(文件夹名任意,会保留为 `images/{文件夹名}/...`)。
3. 执行 `migrate` / `migrate --apply`,再重启 PicHost。
### 常见来源示例
| 来源 | 典型图片目录(在旧服务器上) | 放入 PicHost 后示例 |
| ---- | ---------------------------- | ------------------- |
| PicHost v1.1.x | `data/blog/`、`data/twikoo/` | 已在 `data/` 下,直接 `migrate` |
| EasyImage / EasyImages | `i/` 或安装目录下的 `upload/` | 拷到 `data/easyimage/` 再 `migrate` |
| 兰空图床 (Lsky) | `public/uploads/` 等 | 拷到 `data/lsky/` 再 `migrate` |
| Chevereto 等 | `images/` 或 `content/` | 拷到 `data/chevereto/` 再 `migrate` |
| 任意自定义目录 | 你的顶层文件夹名 | 拷到 `data/你的目录名/` 再 `migrate` |
脚本会扫描 `data/` 下**除 `images` 外所有顶层目录**里的图片,不限于 `blog` / `twikoo`。子目录结构会原样保留在 `images/{顶层名}/` 下。
### 操作示例(Docker)
```bash
# 在宿主机:把从旧图床打包的目录放进挂载卷(名称自定)
# 例如:./data/easyimage/2026/01/photo.jpg
docker exec pichost migrate # 预览
docker exec pichost migrate --apply
# 重启容器 → 启动日志会补全 SQLite 索引
```
### 说明与限制
- **只迁移文件**:旧图床的账号、Token、外链 URL **不会**自动导入;`mapping.json` 里的 key 是「旧磁盘相对 key → 新 key」,**不是**旧站完整 URL,替换外链需自行对照。
- **对象存储**:若旧图床图片在 S3/R2 桶里,请用桶工具同步到本地 `data/某目录/`,或直接在 PicHost **存储**页配置同一桶(不经过本 CLI)。
- **已直接在 `data/images/` 下**:无需 `migrate`,重启后启动同步会自动建索引。
---
## 存储结构对比
### 迁移前(v1.1.x 常见)
```
/data
├── pichost.db
├── blog/
│ └── 2026/08/TeKJ2fD6cB6j.webp
├── twikoo/
│ └── xxx.webp
└── images/ # 可能已有部分新图
└── ...
```
SQLite 中 `key` 可能是 `blog/2026/08/TeKJ2fD6cB6j.webp`(与磁盘顶层目录一致)。
### 迁移后(v1.2.0)
```
/data
├── pichost.db
├── mapping.json # CLI 生成:旧 key → 新 key(供替换外链)
└── images/
├── blog/
│ └── 2026/08/TeKJ2fD6cB6j.webp
├── twikoo/
│ └── xxx.webp
└── ... # 新上传:images/随机ID.webp 或 images/年/月/随机ID.webp
```
对应 `key`:`images/blog/2026/08/TeKJ2fD6cB6j.webp`。
---
## 第一步:CLI 迁移磁盘文件
在 **升级镜像之前或之后**均可执行;建议在升级前完成,并先备份整个 `data/` 目录。
### Docker
```bash
# 预览:列出待迁移文件,写入 data/mapping.json(旧 key → 新 key)
docker exec pichost migrate
# 确认无误后执行文件移动
docker exec pichost migrate --apply
```
### 本地开发
```bash
npm run migrate # 预览
npm run migrate -- --apply # 执行
```
### 脚本行为
| 项目 | 说明 |
| ---- | ---- |
| 扫描范围 | `data/` 下**除 `images` 外所有顶层目录**(不限于 blog/twikoo) |
| 迁移目标 | `data/images/{原顶层目录名}/{相对路径}` |
| 图片类型 | `.jpg` `.jpeg` `.png` `.webp` `.gif` `.svg` `.ico` |
| 伴随文件 | 若存在同名的 `.meta.json` 会一并移动 |
| 非图片文件 | 不移动;若目录内仍有其它文件,**保留原目录**不删除 |
| 映射表 | `data/mapping.json`,例如 `"blog/foo.webp": "images/blog/foo.webp"` |
脚本路径:`server/cli/migrate-to-single-images.mjs`。
---
## 第二步:启动时自动同步索引
每次 PicHost 启动(`npm run dev`、Docker 容器启动)会自动执行,**无需手动命令**。日志示例:
```text
[INFO] 开始同步图片索引:扫描本地磁盘 {"path":".../data/images/"}
[INFO] 磁盘扫描完成:已补全缺失索引 {"inserted":3}
[INFO] 开始归一化遗留图片路径(如 blog/ → images/blog/)
[INFO] 遗留路径归一化完成:无需调整
[INFO] 开始校验索引与磁盘一致性,清理无文件的孤儿记录
[INFO] 孤儿索引清理完成:未发现孤儿记录
[INFO] 开始修复存量 Content-Type 元数据
[INFO] Content-Type 修复完成:无需修复
[INFO] 图片索引同步结束
```
| 步骤 | 作用 |
| ---- | ---- |
| 扫描 `data/images/` | 磁盘上有、索引里没有 → **补全** `images` 表 |
| 归一化遗留 key | 将 `blog/...` 等旧 key 更新为 `images/blog/...`(文件已在正确路径时) |
| 清理孤儿索引 | 索引里有记录、磁盘无文件 → **删除**幽灵记录(避免图库出现删不掉的项) |
| 修复 Content-Type | 修正错误的 `application/octet-stream` |
CLI 只负责**搬文件**;索引对齐、遗留 key、孤儿记录由启动同步处理。执行 `migrate --apply` 后**重启一次**服务即可。
---
## 推荐升级流程
1. **备份** `data/`(含 `pichost.db` 与各目录)。
2. `docker exec pichost migrate` 预览,检查输出与 `mapping.json`。
3. `docker exec pichost migrate --apply` 执行迁移。
4. 拉取 / 部署 **v1.2.0** 镜像并重启容器。
5. 查看启动日志,确认「图片索引同步结束」无报错。
6. 用 `mapping.json` 批量替换博客、Twikoo 等外链中的旧路径(若需要)。
仅使用对象存储、或图片早已全部在 `data/images/` 下时,**跳过第一步**;第二步仍会在启动时运行,通常几秒内完成且无变更日志。
---
## 外链与直链
| 场景 | 说明 |
| ---- | ---- |
| 旧 key | `blog/TeKJ2fD6cB6j.webp` |
| 新 key | `images/blog/TeKJ2fD6cB6j.webp` |
| 隐藏目录模式 | 若开启「隐藏 images 前缀」,短链可能为 `/TeKJ2fD6cB6j.webp`;服务按文件名反查索引 |
| 替换建议 | 以 `mapping.json` 为准;或重新在图库复制新链接 |
---
## 相关命令
| 命令 | 说明 |
| ---- | ---- |
| `docker exec pichost migrate` | 预览磁盘迁移 |
| `docker exec pichost migrate --apply` | 执行磁盘迁移 |
| `docker exec pichost reset-password` | 重置密码(与迁移无关) |
英文版:[v1.2 migration guide](/en/guide/migration)
+85
View File
@@ -0,0 +1,85 @@
# 反向代理
生产环境建议通过 Nginx、Caddy、NPM(Nginx Proxy Manager)或 NAS 自带反代,将 HTTPS 流量转发到 PicHost 容器 **6892** 端口。
## 基本要求
- 反代目标:`http://127.0.0.1:6892`(或容器所在主机 IP)
- 保留请求头:`Host`、`X-Forwarded-Proto`(及常见的 `X-Forwarded-For`)
- 上传大小:Nginx 建议 `client_max_body_size 12m` 或更大
- 公网访问请配置 **HTTPS**
## 单域名示例(Nginx)
管理、API、出图同一域名:
```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;
}
}
```
环境变量(可选):
```env
IMAGE_BASE_URL=https://pic.example.com
```
未设置 `IMAGE_BASE_URL` 时,PicHost 会根据反代转发的 `Host` 与 `X-Forwarded-Proto` 推断公网地址。
## 双域名
若后台与图片使用不同域名,两个 `server` 块均可 **全量反代** 到同一端口;隔离由 PicHost 中间件完成。
详见专章:[双域名分离](./domain-separation.md)。
## Cloudflare 橙云
PicHost 应用运行在 **自有服务器 / Docker**(端口 `6892`),Cloudflare 侧推荐:
| 做法 | 说明 |
| ---- | ---- |
| **DNS 橙云** | `admin` / `pic` 两个子域 Proxied → 源站反代 `6892`,SSL **Full (strict)** |
| **DNS 优选** | 与 PicHost 兼容;Host 仍为正式域名 |
| **R2** | 在存储页作对象后端,非 Pages 托管应用 |
| **源站防火墙** | 仅放行 [Cloudflare IP](https://www.cloudflare.com/ips/),禁止公网直连 `6892` |
**不建议:**
- 用 **Cloudflare Pages**(`*.pages.dev`)或 **Workers**(`*.workers.dev`)对 PicHost **整站反代** — 会多出第三 Host,且可能绕过双域名隔离(见 [双域名分离 · 安全与注意事项](./domain-separation.md#安全与注意事项))
- 在 CF 控制台「从 Git 创建 Worker」部署本仓库 — 当前为 `node-server` 预设,无法直接 `wrangler deploy`
## 源站安全(双域名推荐)
除 PicHost 中间件外,建议在反代 / 防火墙层加固:
1. **`server_name` 白名单** — 仅网站域与图片域;`default_server` 拒绝裸 IP 与其它 Host
2. **禁止公网暴露 6892** — 仅本机或内网反代访问;公网只开 80/443
3. **橙云时限制源站来源 IP** — 仅 Cloudflare 回源 IP 段
双域名场景下 PicHost **不会** 拒绝未配置的 Host;上述措施由运维层补齐。
## Caddy 示例
```caddy
pic.example.com {
reverse_proxy 127.0.0.1:6892
}
```
双域名时添加第二个站点块,后端地址相同即可。
## 缓存注意
图片 404/403 与域名隔离 404 响应带 **禁止缓存** 头,避免 CDN 长时间缓存错误页面。若在前端再加 CDN,请对动态 API 路径谨慎缓存。
## 相关
- [环境变量](./configuration.md) — `SITE_BASE_URL`、`IMAGE_BASE_URL`
- [快速开始](./getting-started.md) — Docker 端口
+72
View File
@@ -0,0 +1,72 @@
# 存储
PicHost 支持 **本地磁盘** 与 **S3 兼容对象存储**(Cloudflare R2、腾讯云 COS、阿里云 OSS、AWS S3 等)。管理员在 **存储**(`/storage`)页管理后端;新上传写入 **默认后端**。
![存储管理](/screenshots/storage.png)
## 存储结构(本地)
```
/data
├── pichost.db # SQLite:用户、会话、设置、storage_backends、images 索引
└── images/ # 本地后端时的全部图片文件
```
- 新上传:`images/随机ID.webp` 或 `images/年/月/随机ID.webp`(可在设置中切换扁平模式)
- 迁移遗留:`images/blog/...` 等子路径
- 云存储时文件在对应桶内,索引仍在 SQLite `images` 表
所有图片 **key** 以 `images/` 开头。上传 API **不再接受** `folder` 参数。
## 添加存储后端
1. 以管理员登录,打开 **存储** → **添加存储**。
2. 选择类型:**本地磁盘** 或 **S3 兼容**(含 R2 / COS / OSS 预设)。
3. 填写连接信息(桶名、Endpoint、Access Key 等)。
4. 保存后可 **设默认**,新上传将写入该后端。
### Cloudflare R2 要点
| 项 | 值 |
| -- | -- |
| Endpoint | `https://<account_id>.r2.cloudflarestorage.com` |
| Region | `auto` |
| 桶 | 你的 R2 桶名 |
| 密钥 | R2 API Token 的 Access Key / Secret Key |
若仓库 **cloudflare** 分支专用于「仅 R2」部署,也可用环境变量预设,见 [文档首页](./index.md) 分支说明。
### 环境变量(可选)
部分部署用环境变量初始化默认 S3 后端(如 `STORAGE_BACKEND=s3`、`S3_ENDPOINT`、`S3_BUCKET`、`S3_ACCESS_KEY`、`S3_SECRET_KEY`、`S3_REGION`)。**UI 中配置的后端信息存于 SQLite**,与环境变量并存时以管理界面为准管理多后端。
## 直链模式
每个后端可配置公开 URL 行为:
| 模式 | 说明 |
| ---- | ---- |
| **proxy** | 通过 PicHost 同源代理出图(`GET /images/...`) |
| **public** | 302 重定向到桶/CDN 公网地址 |
复制链接时使用 **图片域名**(`IMAGE_BASE_URL`)拼接路径。开启「隐藏 images 前缀」后 URL 可能更短,服务按文件名或路径反查索引。
## 用量与筛选
- 存储页展示各后端用量(本地扫描目录,对象存储查询 API)
- 图库可按存储后端筛选(管理员)
## 备份
请同时备份:
- `data/images/`(本地后端)
- `data/pichost.db`
- 各云桶中的对象
从 v1.1.x 升级且存在 `data/blog/` 等并列目录时,见 [v1.2 迁移](./migration.md)。
## 相关
- [环境变量](./configuration.md) — `STORAGE_USE_DATE_PATH`、`HIDE_FOLDER_IN_URL`
- [用户与权限](./users-and-permissions.md) — 谁可以管理存储
+53
View File
@@ -0,0 +1,53 @@
# Twikoo
PicHost 兼容 **Twikoo** 与 **EasyImage 2.0** 图片上传协议,评论系统可像接 EasyImage 一样配置。
## Twikoo 配置
在 Twikoo 环境变量或配置中设置:
| 配置项 | 值 |
| ------ | -- |
| `IMAGE_CDN` | `easyimage` |
| `IMAGE_CDN_URL` | `https://你的域名/api/index.php` |
| `IMAGE_CDN_TOKEN` | 与全局 `API_UPLOAD_TOKEN` 相同 |
- 使用 **网站域名**(管理后台域)作为 `IMAGE_CDN_URL` 的主机名
- Token 需在 PicHost **API** 页生成,或通过 `API_UPLOAD_TOKEN` 环境变量设置
## 上传接口
`POST /api/index.php`
表单字段:
| 字段 | 说明 |
| ---- | ---- |
| `image` | 图片文件 |
| `token` | 全局 API Token(与 `Auth-Token` 等效) |
```bash
curl -X POST "https://admin.example.com/api/index.php" \
-F "token=YOUR_TOKEN" \
-F "image=@./demo.png"
```
上传的图片存入 `images/` 目录,归属与 **全局 Token** 相同(管理员)。
## 与 REST API 的区别
| 项目 | REST `/api/images/upload` | Twikoo `/api/index.php` |
| ---- | ------------------------- | ------------------------ |
| 鉴权 | `Auth-Token` 请求头 | 表单 `token` |
| 响应格式 | PicHost JSON | EasyImage 兼容 JSON |
| Token | 全局或个人 | 仅全局 Token |
## 双域名部署
Twikoo 上传应走 **网站域名**(`SITE_BASE_URL`),图片展示走 **图片域名**(`IMAGE_BASE_URL`)。详见 [双域名分离](./domain-separation.md)。
## 相关
- [API](./api.md)
- [用户与权限](./users-and-permissions.md)
- [环境变量](./configuration.md)
+58
View File
@@ -0,0 +1,58 @@
# 用户与权限
PicHost 使用基于角色的访问控制(RBAC):**管理员(admin)** 与 **普通用户(user)**。权限在服务端强制,不可仅靠前端绕过。
## 角色能力
| 能力 | 管理员 | 普通用户 |
| ---- | :----: | :------: |
| 上传图片 | ✓ | ✓ |
| 查看自己的图库 | ✓ | ✓ |
| 查看全部图库 / 上传者标签 | ✓ | — |
| 存储后端管理(`/storage`) | ✓ | — |
| 系统设置(`/settings`) | ✓ | — |
| 开放注册开关 | ✓ | — |
| 全局 API Token 管理 | ✓ | — |
| 个人 API Token | ✓ | ✓ |
| 统计页(含注册用户数) | ✓ | 部分 |
| 操作日志 | ✓ | — |
普通用户顶栏:**API**、**统计**;用户菜单:**修改密码**、**退出登录**。
## 鉴权场景
| 场景 | 鉴权方式 | 图片归属 / 可见范围 |
| ---- | -------- | ------------------- |
| 网页上传 | Session Cookie | 当前登录用户 |
| API 上传 + **个人 Token** | `Auth-Token` 头 | Token 所属用户 |
| API 上传 + **全局 Token** | `Auth-Token` 头 | 管理员(`userId` 为空) |
| Twikoo `POST /api/index.php` | 表单 `token` | 与全局 Token 相同 |
| 图库列表 / 搜索 / 删除 | Session 或 Token | 普通用户仅自己;管理员全部 |
| 图片直链 `GET /images/...` | 无(Referer 防盗链) | 知道 URL 即可访问 |
## 注册与账号
- 默认仅管理员可创建用户;管理员可在设置中 **开放注册**
- 密码使用 scrypt 哈希存储
- Web 登录需先完成滑动拼图验证(拖动滑块对齐缺口)
- 登录失败有速率限制
## 上传偏好与自动删除
- **上传偏好**(首页卡片翻转):客户端预压缩、自动复制链接等,按用户保存
- **自动删除**:管理员可设全局策略;用户可设个人策略。启用后仅影响 **之后新上传** 的图片
## API Token 说明
| 类型 | 谁管理 | 上传归属 | 典型用途 |
| ---- | ------ | -------- | -------- |
| **全局 Token** | 管理员(API 页) | 管理员账号 | Twikoo、站点级脚本 |
| **个人 Token** | 各用户(API 页) | 该用户 | 个人博客、私有脚本 |
若设置了环境变量 `API_UPLOAD_TOKEN`,则全局 Token 被锁定,见 [环境变量](./configuration.md)。
## 相关
- [API](./api.md)
- [Twikoo](./twikoo.md)
- [存储](./storage.md)
+32
View File
@@ -0,0 +1,32 @@
---
layout: home
hero:
name: PicHost
text: 个人轻量图床
tagline: 自托管 · 多用户 · Docker · API · Twikoo · 本地磁盘或对象存储
actions:
- theme: brand
text: 快速开始
link: /guide/getting-started
- theme: alt
text: 查看 API
link: /guide/api
- theme: alt
text: GitHub
link: https://github.com/O96u/PicHost
features:
- title: 拖拽 / 粘贴上传
details: 支持点击、拖拽与 Ctrl+V 粘贴;服务端 WebP 压缩与 Referer 防盗链。
- title: 多后端存储
details: 本地磁盘 + S3 兼容(R2 / COS / OSS / AWS);proxy 或 public 直链模式。
- title: 多用户与权限
details: 管理员与普通用户 RBAC;全局 Token 与个人 Token;图库按归属隔离。
- title: Docker 零配置
details: 首次访问 Web 引导创建管理员;`docker exec pichost reset-password` 重置密码。
- title: 双域名分离
details: 单实例 + 两个域名全量反代;应用内中间件隔离后台与图片域。
- title: Twikoo 兼容
details: "POST /api/index.php 兼容 EasyImage 2.0 协议,评论系统一键接入。"
---