hidekuma/flask-s3-viewer
GitHub: hidekuma/flask-s3-viewer
Flask S3 查看器是一个 Flask 扩展,用于在 Web 应用中便捷地浏览、上传和管理 Amazon S3 存储桶。
Stars: 13 | Forks: 4

# Flask S3 查看器
从任何 Flask 应用程序浏览、上传和管理 Amazon S3 存储桶。
[](https://badge.fury.io/py/flask-s3-viewer)
[](https://github.com/hidekuma/flask-s3-viewer/actions/workflows/ci.yml)
[](https://www.python.org/)
[](LICENSE)
## 主要特性
- **现代 UI** — Tailwind CSS,HTMX 驱动的局部更新,明/暗模式,内联 heroicons。标题栏中的上级文件夹(`..`)行和已登录用户小部件。终端用户无需构建流程(CSS 已预构建)。
- **可选认证** — 钩子框架(`auth_callback` + `permission_callback`,包含按操作定义的常量),外加通过 `[auth]` 附加组件提供的内置 Google OAuth。认证路由(`/auth/{login,callback,logout}`)位于命名空间前缀之外,因此一个重定向 URI 即可覆盖应用中的所有查看器。
- **智能搜索** — 大小写不敏感的子字符串匹配,Unicode 安全(韩文/日文/带音调的拉丁文),NFC 规范化以确保 macOS 上传与浏览器 IME 匹配,搜索范围限定在当前文件夹内,并会显示匹配的子文件夹行。
- **默认安全** — 在每个前缀边界拒绝路径遍历令牌(`..`、`.`、`//`、`\`)。缓存目录通过 `realpath` 包容性检查阻止逃逸,缓存文件以 JSON 格式存储并设置严格的文件权限。
- **Flask 扩展模式** — `FlaskS3Viewer(app, namespace=...)` 自动注册。通过 `add_new_one(...)` 支持每个应用多个存储桶。支持 `init_app(app)` 进行延迟绑定。
- **多存储桶** — 独立的命名空间,可选的每个存储桶 CloudFront / 外部 `object_hostname`。
- **预签名上传** — 适用于大文件的多文件预签名 POST 流程;也支持默认的表单上传。
- **缓存** — 文件系统 JSON 缓存,带 TTL;写入时自动失效(搜索绕过缓存,已认证的列表按用户隔离)。
- **经过测试** — 203 个 pytest 用例,ruff + mypy 检查通过,基于 moto 的 S3 模拟。
## 安装说明
```
pip install flask_s3_viewer
```
需要 Python 3.10+,Flask 3.0+,boto3 1.34+。
## 快速开始
```
from flask import Flask
from flask_s3_viewer import FlaskS3Viewer
from flask_s3_viewer.aws.ref import Region
app = Flask(__name__)
# 自动注册,无需调用 `register()`。
FlaskS3Viewer(
app,
namespace="my-bucket",
object_hostname="https://cdn.example.com", # optional CloudFront host
config={
"profile_name": "default",
"region_name": Region.SEOUL.value,
"bucket_name": "my-bucket",
"cache_dir": "/tmp/flask_s3_viewer",
"use_cache": True,
"ttl": 86400,
},
)
@app.route("/")
def index():
return "App index"
if __name__ == "__main__":
app.run(debug=True, port=3000)
```
访问 `http://localhost:3000/my-bucket/files` 以浏览存储桶。
### 品牌标识(标题 + 标志)
```
FlaskS3Viewer(
app,
namespace="my-bucket",
title="ACME File Vault",
logo_path="/opt/acme/assets/logo.svg", # local file, auto inlined as a data: URI
# or: logo_url="https://cdn.acme.io/logo.svg",
logo_link_url="https://intranet.acme.io/dashboard", # optional (v1.3+)
config={...},
)
```
`logo_link_url` (v1.3+) 覆盖标题栏标志 + 标题锚点的点击目标。设置后,该锚点渲染为指向配置 URL 的普通 ``,默认的 HTMX 列表重置将被禁用——当品牌标识应将用户引导回外部仪表板/主页时很有用。省略则保留 v1.2 的就地 HTMX 交换。使用 `add_new_one` 时,省略以继承父级值,传递 `None` 以移除子命名空间上的父级覆盖,或传递不同的字符串以按命名空间覆盖。
### 自定义模板(`template_folder`)
使用 CLI 脚手架生成可写的预置模板副本,编辑后,将查看器指向该文件夹:
```
# 仅使用模板(默认 — 涵盖大多数主题需求)
flask_s3_viewer -p ./fsv-templates
# 或者,fork 整个 UI 模板包(templates + static/css/app.css + htmx + core.js)
flask_s3_viewer -p ./fsv-templates --with-static
```
```
FlaskS3Viewer(
app,
namespace="my-bucket",
template_folder="./fsv-templates", # files here win over bundled defaults
config={...},
)
```
实际上,扩展通过 `ChoiceLoader` 将一个 `FileSystemLoader(template_folder)` 预置到应用的 Jinja 加载器中,因此任何未被覆盖的模板(例如当你只编辑了 `files.html` 时,`error.html`)仍会从预置模板解析。其他蓝图的模板解析不受影响。
### 多存储桶
```
viewer = FlaskS3Viewer(app, namespace="primary", config={...})
viewer.add_new_one(namespace="backups", config={...})
```
每个命名空间都有自己的 URL 前缀和自己的配置。
### 延迟初始化
```
viewer = FlaskS3Viewer(namespace="my-bucket", config={...})
def create_app():
app = Flask(__name__)
viewer.init_app(app)
return app
```
### 访问底层 boto3 客户端
```
from flask import current_app
from flask_s3_viewer import FlaskS3Viewer
# 在请求内部:
client = current_app.extensions["flask_s3_viewer"]["my-bucket"]._s3
# 或通过辅助函数:
client = FlaskS3Viewer.get_boto_client(app, "my-bucket")
session = FlaskS3Viewer.get_boto_session(app, "my-bucket")
```
## 配置
所有 `config` 键都会转发给底层的 S3 客户端:
| 键 | 类型 | 默认值 | 说明 |
|------------------|------------------|---------|------------------------------------------------|
| `bucket_name` | str | — | 必需。 |
| `profile_name` | str \| None | None | 如果为 None,则使用 boto3 默认凭证链。 |
| `region_name` | str \| None | None | 例如 `ap-northeast-2`。 |
| `endpoint_url` | str \| None | None | 自定义 S3 端点(MinIO 等)。 |
| `access_key` | str \| None | None | 优先使用配置文件/IAM 角色。 |
| `secret_key` | str \| None | None | |
| `session_token` | str \| None | None | |
| `verify` | bool \| str | False | TLS 验证(或 CA 证书包路径)。 |
| `base_path` | str | `""` | 此查看器的对象键前缀范围。 |
| `use_cache` | bool | False | 文件系统 JSON 缓存。 |
| `cache_dir` | str \| None | None | 当 `use_cache=True` 时必需。 |
| `ttl` | int (秒) | 300 | 缓存生存时间。 |
| `timezone` | str \| None | None | 用于“修改时间”显示的 IANA 时区,例如 `Asia/Seoul`。如果为 None,则显示 boto3 的原始时间戳字符串。 |
| `role_arn` | str \| None | None | 如果设置,包装器将在基础凭证之上运行 STS `AssumeRole`,并使用返回的临时密钥(跨账户、多租户)。 |
| `role_session_name` | str \| None | `"flask-s3-viewer"` | 已承担会话的标识符。 |
| `external_id` | str \| None | None | 转发给 STS 用于需要此 ID 的跨账户角色。 |
| `duration_seconds` | int \| None | None | 已承担凭证的有效期(秒)(15 分钟 – 12 小时)。|
| `mfa_serial` | str \| None | None | 用于 STS `AssumeRole` 的 MFA 设备 ARN/序列号。|
| `token_code` | str \| None | None | 一次性 MFA 代码(与 `mfa_serial` 配对)。 |
| `token_code_callback` | callable | None | `token_code` 的替代方案——调用一次以提示用户输入。|
构造函数选项:
| 选项 | 说明 |
|----------------------|----------------------------------------------------------------|
| `app` | Flask 应用实例(可选;稍后通过 `init_app(app)` 传递)。 |
| `namespace` | 每个应用唯一。将成为 URL 前缀。 |
| `object_hostname` | 外部链接前缀(例如 CloudFront)。 |
| `allowed_extensions` | `set[str] \| None` — 只允许这些扩展名的文件上传。 |
| `upload_type` | `"default"`(多部分表单上传)或 `"presign"`(预签名)。 |
| `title` | 标题 + 浏览器标签页标题文本。默认为 `"Flask S3 Viewer"`。 |
| `logo_url` | 自定义标志图像的 URL(绝对路径、`url_for(...)` 或 `/static/...`)。|
| `logo_path` | 标志图像的本地文件系统路径——自动内联为 `data:` URI。优先于 `logo_url`。|
| `logo_link_url` | (v1.3+) 覆盖标题栏标志 + 标题锚点的点击目标。设置后,用标准导航替换默认的 HTMX 列表重置。|
| `template_folder` | 其 Jinja 文件覆盖预置模板的目录(Flask `ChoiceLoader` 模式)。通过 CLI 脚手架生成。|
## AWS 身份验证
`flask-s3-viewer` 遵循 boto3 的默认凭证链,因此以下方式开箱即用:
- 静态密钥(`access_key` / `secret_key` / `session_token`)
- 命名配置文件(`profile_name='my-profile'`)——包括在 `~/.aws/config` 中设置了 `role_arn` + `source_profile` 的配置文件(boto3 自动处理 AssumeRole)
- `AWS_*` 环境变量
- EC2 IMDS / ECS 任务角色 / AWS SSO 缓存 / EKS IRSA(Web Identity OIDC)——当没有其他设置时自动获取。
```
FlaskS3Viewer(
app,
namespace="cross-account",
config={
"bucket_name": "target-bucket",
"region_name": "us-east-1",
# Base credentials come from the default chain (profile/env/IRSA).
"role_arn": "arn:aws:iam::123456789012:role/AppRole",
"external_id": "shared-secret", # optional
"role_session_name": "my-app", # default: "flask-s3-viewer"
"duration_seconds": 3600, # 15 min – 12 h
},
)
```
对于受 MFA 保护的角色,提供一个令牌(或用于交互式提示的回调):
```
FlaskS3Viewer(
app,
namespace="mfa-account",
config={
"bucket_name": "secure-bucket",
"region_name": "us-east-1",
"role_arn": "arn:aws:iam::123456789012:role/AdminRole",
"mfa_serial": "arn:aws:iam::123456789012:mfa/alice",
"token_code_callback": lambda: input("MFA code: ").strip(),
},
)
```
## 认证与权限
`flask-s3-viewer` 附带两个可选层。**在没有任何认证连接的情况下,该包的行为与以前完全相同**——两者默认设置为“允许所有人”。
### 第 1 层:钩子框架(无额外依赖)
使用两个可调用对象插入您现有的登录系统:
```
from flask_s3_viewer.auth import ACTION_LIST, ACTION_UPLOAD, ACTION_DELETE
def who_is_asking(request):
"""Return the user's email (or any opaque id) — None means anonymous."""
return request.headers.get("X-Forwarded-Email")
def can_they(email, action, namespace, key):
"""Authorize a single action. action is one of the ACTION_* constants."""
if action == ACTION_DELETE:
return email.endswith("@admin.example.com")
return True
FlaskS3Viewer(
app, namespace="bucket",
auth_callback=who_is_asking,
permission_callback=can_they,
config={...},
)
```
五个操作常量是 `ACTION_LIST`、`ACTION_DOWNLOAD`、`ACTION_UPLOAD`、`ACTION_DELETE`、`ACTION_PRESIGN`。
### RBAC 存储桶切换器
对于多存储桶应用,在 `permission_callback` 中进行硬编码授权,并使用
`visible_namespaces_callback(email, registry)` 来控制哪些存储桶出现在
标题栏切换器中:
```
RBAC = {
"alice@example.com": {"assets", "private"},
"bob@example.com": {"assets"},
}
def visible_buckets(email, registry):
return RBAC.get(email, set())
def can_they(email, action, namespace, key):
return namespace in RBAC.get(email, set())
viewer = FlaskS3Viewer(
app,
namespace="assets",
title="Assets",
auth_callback=who_is_asking,
permission_callback=can_they,
visible_namespaces_callback=visible_buckets,
config={...},
)
viewer.add_new_one(
namespace="private",
title="Private",
config={...},
)
```
切换器仅从 UI 中隐藏无法访问的命名空间。直接的 URL 访问
仍然由 `permission_callback` 检查,因此 RBAC 保持在服务器端。
### 第 2 层:内置 Google OAuth(可选的 `[auth]` 附加组件)
```
pip install "flask_s3_viewer[auth]"
```
```
app.secret_key = "..." # required — signs the session cookie
FlaskS3Viewer(
app, namespace="bucket",
google_client_id="...apps.googleusercontent.com",
google_client_secret="...",
allowed_emails=["alice@example.com"],
allowed_domains=["example.com"],
config={...},
)
```
将 `/auth/login`、`/auth/callback`、`/auth/logout` 安装为应用级路由(位于 FlaskS3Viewer 命名空间前缀之外)。在 Google Cloud Console 中将重定向 URI 配置为 `https:///auth/callback`——即使挂载了多个命名空间,每个应用也只需一个 URI。匿名浏览器访问受保护页面时会自动重定向进行 Google 登录。
混合使用:即使启用了 Google,也可以传递您自己的 `auth_callback` / `permission_callback`,或者在非 Google 部署中使用 `email_allowlist()` 作为权限构建器。
## 安全
- **路径遍历加固** — 每个用户提供的 `prefix` 都经过验证。令牌 `..`、`.`、空段和 `\` 将被拒绝并返回 HTTP 400。
- **深度防御** — 缓存层额外执行 `realpath` 包容性检查,阻止任何解析到 `cache_dir` 之外的路径。
- **子资源完整性** — 预置的 `htmx.min.js` 引用了其 `sha384` 哈希值;Tailwind 输出是预构建的,并由该包签名。
- **凭证** — 绝不记录凭证。优先使用命名配置文件或实例角色,而非硬编码密钥。
## 开发
前端资源已预构建并提交到仓库中。编辑模板后重新构建:
```
cd frontend
npm install
npm run build # writes flask_s3_viewer/blueprints/static/css/app.css
```
CI 验证 CSS 是最新的(`git diff --exit-code`)。
测试:
```
pip install -e ".[dev]"
ruff check flask_s3_viewer/ tests/
mypy flask_s3_viewer/
pytest tests/ --cov=flask_s3_viewer
```
## 从 0.x 版本迁移
请参阅 [`MIGRATION.md`](MIGRATION.md) 获取完整指南。主要变更:
- 移除 `s3viewer.register()` — 构造函数现在会自动注册。
- `FlaskS3Viewer.get_instance(ns)` → `FlaskS3Viewer.get_instance(app, ns)`(`get_boto_client`、`get_boto_session` 同理)。
- 重复的命名空间注册现在会引发 `ValueError`,而不是静默重用。
- 未知的命名空间现在返回 HTTP 404,而不是 500。
- 单一模板命名空间 — `template_namespace="base"|"mdl"` 已被忽略并带有弃用警告。
- CLI `--template` 选项已移除。
- `prefix` 中的路径遍历令牌现在返回 HTTP 400。
- 需要 Flask 3.0+ 和 boto3 1.34+。
## 许可证
[MIT](LICENSE) © Hoiwoong Jung
标签:AWS S3, Flask扩展, Flask框架, HTMX, OAuth认证, Python开发, REST API, S3集成, Tailwind CSS, 云存储管理, 云服务, 前端组件, 后端开发, 多存储桶, 安全性, 搜索功能, 文件上传工具, 文件处理, 文件浏览器, 无构建流程, 现代界面, 用户认证, 缓存管理, 路径安全, 逆向工具, 预签名URL