Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 避免将本地无关文件加入构建上下文
.venv
venv
__pycache__
*.pyc
.git
.github
node_modules
frontend/node_modules
data
*.md
.dockerignore
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
myenv
__pycache__
app/data
notice.txt
notice.txt

.venv
data/
43 changes: 34 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
# 基于redis镜像构建
FROM redis:7-alpine
# 工作目录
# 使用 python-slim 作为基础镜像(不再使用 redis 镜像)
FROM python:3.12-slim

WORKDIR /opt/zurl
# 把当前目录下的所有文件拷贝到工作目录

# 安装 Node.js 用于在容器内构建前端(构建完成后会卸载)
RUN apt-get update && apt-get install -y --no-install-recommends \
nodejs npm \
&& rm -rf /var/lib/apt/lists/*

RUN npm install -g pnpm

# 复制项目文件(含前端源码,用于构建)
COPY . .
# 执行安装脚本
RUN sh install.sh
# 暴露端口和目录

# 修正脚本换行符与权限
RUN sed -i 's/\r$//' install.sh run.sh && chmod +x install.sh run.sh

# 创建数据目录
RUN mkdir -p /opt/zurl/app/data/db

# 安装 Python 依赖
RUN python3 -m venv myenv && . myenv/bin/activate && pip install --no-cache-dir -r app/requirements.txt

# 构建前端:BASE_URL 仅在构建时生效,用于设置 VITE_BASE_URL(与运行时后端 BASE_URL 一致)
ARG BASE_URL=
ENV VITE_BASE_URL=${BASE_URL}
RUN cd frontend && pnpm install && pnpm build

# 构建完成后删除前端源码和 Node,缩小镜像
RUN rm -rf frontend && \
apt-get purge -y nodejs npm && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/* /root/.npm

EXPOSE 3080
VOLUME /opt/zurl/app/data
# 启动命令
CMD ["sh", "run.sh"]
CMD ["sh", "run.sh"]
61 changes: 51 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,42 +22,83 @@ Zurl is a simple and practical short URL system that can quickly generate short
* [x] Custom site information
* [x] API Token management
* [x] Bilingual support (Chinese and English)
* [x] Subpath deployment (BASE_URL) for reverse proxy
* [ ] Advanced analytics
* [ ] Login session management

## Installing Zurl

> Currently only Docker installation is supported. Please ensure you have Docker and Docker Compose installed.
> Docker deployment is supported. Please ensure you have Docker and Docker Compose installed. Zurl requires Redis for delayed click counting; the project provides a `docker-compose.yaml` that runs both Zurl and Redis.

Create a new `docker-compose.yaml` file with the following content:
**Clone the repo and run with Docker Compose (recommended):**

```yaml
version: '3.8'
```bash
git clone https://github.com/helloxz/zurl.git && cd zurl
```

Use the included `docker-compose.yaml`. Optional: set subpath via env (e.g. `BASE_URL=/s` for `http://IP:3080/s`):

```yaml
# docker-compose.yaml (in project root)
services:
redis:
image: redis:7-alpine
container_name: zurl-redis
restart: always
command: redis-server --requirepass zurl
volumes:
- ./redis/data:/data

zurl:
container_name: zurl
build:
context: .
args:
- BASE_URL=${BASE_URL:-}
image: helloz/zurl
ports:
- "3080:3080"
restart: always
environment:
- BASE_URL=${BASE_URL:-}
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_DB=0
- REDIS_PASSWORD=zurl
volumes:
- ./data:/opt/zurl/app/data
depends_on:
- redis
```

Run `docker-compose up -d` to start, then visit `http://IP:3080` and follow the prompts to complete initialization!
Then run:

```bash
docker compose up -d --build
```

Visit `http://IP:3080` (or `http://IP:3080/<BASE_URL>` if you set `BASE_URL`) and follow the prompts to complete initialization.

**Using pre-built image only:**
Use the same `zurl` service block but set `build` to use image `helloz/zurl`, and either run Redis via the same compose or set `REDIS_HOST` / `REDIS_PORT` / `REDIS_PASSWORD` to point to an existing Redis instance.

**Upgrade**

1. Backup the data in the current mounted directory
2. Stop and remove the current container: `docker-compose down`
3. Pull the latest image: `docker-compose pull`
4. Recreate and start the container: `docker-compose up -d`
1. Backup the data in the current mounted directory (and Redis data if needed).
2. Stop and remove the current containers: `docker compose down`
3. Pull the latest image or rebuild: `docker compose pull` or `docker compose build --pull`
4. Recreate and start: `docker compose up -d`

> Note: Please be sure to backup your data before upgrading. You are responsible for any data risks caused by upgrades!
> Note: Please backup your data before upgrading. You are responsible for any data risks caused by upgrades!

## Configuration

**Redis**
Redis is required. In Docker, it is configured via environment variables: `REDIS_HOST`, `REDIS_PORT`, `REDIS_DB`, `REDIS_PASSWORD`. You can also set these in `config.toml` under the `[redis]` section when not using env vars.

**Subpath (BASE_URL)**
For deployment under a subpath (e.g. `/zurl`), set `BASE_URL` in the environment or in `config.toml` under `app.BASE_URL` (e.g. `BASE_URL = "/zurl"`). Rebuild the frontend with the same base if you build from source.

**UA Blocking**

You can find `config.toml` in the mounted directory and add User-Agents to block in `app.DENY_UA`. Default blocks:
Expand Down
63 changes: 52 additions & 11 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,48 +22,89 @@ Zurl 是一款简单且实用的短链接系统,可以快速生成短链接,
* [x] 自定义站点信息
* [x] API Token管理
* [x] 中英文双语支持
* [x] 子路径部署(BASE_URL),便于反向代理
* [ ] 高级分析
* [ ] 登录会话管理

## 安装Zurl

> 目前仅支持Docker安装,请确保您已经安装Docker和Docker Compose
> 支持 Docker 部署,请确保已安装 Docker 与 Docker Compose。Zurl 依赖 Redis 做延迟计数,项目提供的 `docker-compose.yaml` 会同时启动 Zurl 与 Redis。

新建`docker-compose.yaml`文件,内容如下:
**克隆仓库并使用 Docker Compose 运行(推荐):**

```yaml
version: '3.8'
```bash
git clone https://github.com/helloxz/zurl.git && cd zurl
```

使用项目自带的 `docker-compose.yaml`。如需子路径部署,可设置环境变量(如 `BASE_URL=/s` 则访问地址为 `http://IP:3080/s`):

```yaml
# 项目根目录下的 docker-compose.yaml
services:
redis:
image: redis:7-alpine
container_name: zurl-redis
restart: always
command: redis-server --requirepass zurl
volumes:
- ./redis/data:/data

zurl:
container_name: zurl
build:
context: .
args:
- BASE_URL=${BASE_URL:-}
image: helloz/zurl
ports:
- "3080:3080"
restart: always
environment:
- BASE_URL=${BASE_URL:-}
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_DB=0
- REDIS_PASSWORD=zurl
volumes:
- ./data:/opt/zurl/app/data
depends_on:
- redis
```

输入`docker-compose up -d`启动,然后访问`http://IP:3080` 根据提示完成初始化!
然后执行:

```bash
docker compose up -d --build
```

访问 `http://IP:3080`(若设置了 `BASE_URL` 则为 `http://IP:3080/<BASE_URL>`),按提示完成初始化。

**仅使用预构建镜像:**
保留上述 `zurl` 服务配置,可不写 `build` 仅用镜像 `helloz/zurl`,并单独启动 Redis 或在环境中配置 `REDIS_HOST`、`REDIS_PORT`、`REDIS_PASSWORD` 指向已有 Redis。

**升级**

1. 备份当前挂载目录的数据
2. 停止并删除当前容器:`docker-compose down`
3. 拉取最新镜像:`docker-compose pull`
4. 重新创建并启动容器:`docker-compose up -d`
1. 备份当前挂载目录的数据(如有 Redis 数据也请备份)。
2. 停止并删除当前容器:`docker compose down`
3. 拉取最新镜像或重新构建:`docker compose pull` 或 `docker compose build --pull`
4. 重新创建并启动:`docker compose up -d`

> 注意:升级前请务必备份数据,升级造成的数据风险由您自行承担!

## 设置

**Redis**
Zurl 依赖 Redis。在 Docker 中通过环境变量配置:`REDIS_HOST`、`REDIS_PORT`、`REDIS_DB`、`REDIS_PASSWORD`。未使用环境变量时,可在挂载目录下的 `config.toml` 中配置 `[redis]` 段。

**子路径(BASE_URL)**
若部署在子路径(如 `/zurl`),请设置环境变量 `BASE_URL` 或在 `config.toml` 的 `app.BASE_URL` 中填写(如 `BASE_URL = "/zurl"`)。从源码构建时需使用相同 base 重新构建前端。

**UA屏蔽**

可以在挂载目录下找到`config.toml`中的`app.DENY_UA`添加需要屏蔽的User-Agent,默认屏蔽:

* *
* *Q
* *微信
* *QQ

> 注意:修改配置后需要重启容器!

Expand Down
25 changes: 25 additions & 0 deletions alembic/versions/a1b2c3d4e5f6_set_urls_is_active_default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""set urls is_active default for legacy data

Revision ID: a1b2c3d4e5f6
Revises: 94b0cb3b951f
Create Date: 2025-02-26

旧版数据迁移:将 is_active 为 NULL 的记录设为 1(启用),保证启用/禁用功能与旧数据兼容。
"""
from typing import Sequence, Union

from alembic import op


revision: str = "a1b2c3d4e5f6"
down_revision: Union[str, Sequence[str], None] = "94b0cb3b951f"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.execute("UPDATE zurl_urls SET is_active = 1 WHERE is_active IS NULL")


def downgrade() -> None:
pass
32 changes: 27 additions & 5 deletions app/api/index.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
from app.api.sys import *
from app.config import templates
from app.config import templates, get_config
from app.middleware.deny import deny_uas
from app.models.options import Options
import json
import os


def _get_base_path():
"""与 main 中保持一致:优先环境变量 BASE_URL,否则配置文件。供模板注入前端使用。"""
base = os.environ.get("BASE_URL", "").strip() or ""
if not base:
try:
base = (get_config().get("app") or {}).get("BASE_URL") or ""
except Exception:
pass
base = (base or "").strip().rstrip("/")
return base if base else ""

class IndexAPI:
async def index(self, request: Request):
Expand All @@ -15,13 +28,14 @@ async def index(self, request: Request):
"version": VERSION,
"version_date": VERSION_DATE
}
# 默认站点信息,确保变量始终已定义
# 默认站点信息,确保变量始终已定义(allow_guest_shorten 默认 True,与站点设置一致)
site_info = {
"title": "Zurl",
"keywords": "zurl,短链服务,短链接",
"description": "Zurl是一款轻量级短链服务,使用FastAPI开发。",
"header": "",
"footer": ""
"footer": "",
"allow_guest_shorten": True,
}
# 获取站点信息
site_str = Options.get_option("site_info")
Expand All @@ -36,5 +50,13 @@ async def index(self, request: Request):
# 解析失败保持默认
pass

# 渲染index.html模板
return templates.TemplateResponse("index.html", {"request": request, "versionInfo": versionInfo, "site_info": site_info})
base_path = _get_base_path()
return templates.TemplateResponse(
"index.html",
{
"request": request,
"versionInfo": versionInfo,
"site_info": site_info,
"base_path": base_path or "",
},
)
4 changes: 3 additions & 1 deletion app/api/option.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ async def get_site_info(self):
site_info = json.loads(site_info)
except (TypeError, ValueError):
return show_json(500, "Site info invalid JSON")
# 如果已经是 dict/list 等结构,直接返回
# 确保返回的站点信息包含 allow_guest_shorten,缺失时默认为 True
if isinstance(site_info, dict) and "allow_guest_shorten" not in site_info:
site_info = {**site_info, "allow_guest_shorten": True}
return show_json(200, "success", site_info)

Loading