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:
1 parent
469d3be735
commit
ff20f97522
66 files changed
+2497
-616
No files matched your search
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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/>
|
||||
@@ -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**.
|
||||
|
||||

|
||||
|
||||
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)
|
||||
@@ -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 |
|
||||
| :--------------: | :---: |
|
||||
|  |  |
|
||||
|
||||
| API | Stats & gallery |
|
||||
| :-: | :-------------: |
|
||||
|  |  |
|
||||
|
||||
| Storage | Settings |
|
||||
| :-----: | :------: |
|
||||
|  |  |
|
||||
|
||||
| Activity log |
|
||||
| :----------: |
|
||||
|  |
|
||||
|
||||
## 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.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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)
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user