LibrePhotos 2025 年 7—8 月开发进展:单容器统一部署、相册公开链接分享与文件夹导航
【免费下载链接】librephotosA self-hosted open source photo management service.项目地址: https://gitcode.com/GitHub_Trending/li/librephotos
LibrePhotos 是一个自托管的开源照片管理服务,2025 年第 35 周发布的开发日志(2025-08-28-2025w35.md)集中展示了 7—8 月间三个重量级新特性:将 API 与前端合并进单个容器的 "librephotos-unified" 统一镜像、通过链接对外公开分享相册、以及带分页和子文件夹照片计数的文件夹导航视图。本文以该日志为骨架,结合仓库中的 Dockerfile、settings 模块、ViewSet 与模型源码逐项展开,帮助你了解这些特性的实现原理、正确配置方式与适用前提。
一、单容器统一部署:librephotos-unified 镜像
此前 LibrePhotos 的标准部署方式是"后端 + 前端 + nginx 反代"的拆分架构。7—8 月的一大进展是:Docker 现在支持用librephotos-unified:latest单个容器启动整个 LibrePhotos,由 Django 直接同时承担 API 与 React 前端(静态文件)的对外服务,从而完全去掉 nginx 代理这一层。
1.1 工作原理
统一镜像的实现思路记录在 deploy/docker/unified/README.md,其核心是:
- 在构建镜像的多阶段过程中,先用
node:20-slim构建 React 前端(yarn run build),产物位于/frontend/dist; - 后端阶段将前端构建产物复制到
/code/frontend_build,并通过 deploy/docker/unified/Dockerfile 安装whitenoise作为静态文件服务中间件; - Django 用 WhiteNoise 高效地直接对外服务前端静态资源;
- URL 路由约定为:
/api/*交给 Django API,其余路径交给 React 前端(SPA catch-all); - 由于 API 与前端同源,CORS 配置被大幅简化。
1.2 无代理的 Django 设置模块
统一镜像对应专门的设置模块 production_noproxy.py。该模块通过from .production import *继承生产配置,只覆盖"去掉代理后必须改变"的部分:
SERVE_FRONTEND = True:强制开启前端服务(librephotos/urls.py依据它挂载 SPA catch-all 路由,api/views/views.py依据它决定/是否仍属于 DRF 的 API 根路径);- 静态文件:
STATICFILES_DIRS指向/code/frontend_build,存储后端采用whitenoise.storage.CompressedManifestStaticFilesStorage; - 中间件:紧跟在
SecurityMiddleware之后插入whitenoise.middleware.WhiteNoiseMiddleware; - 主机与 CORS:
ALLOWED_HOSTS = ["*"]、CORS_ALLOW_ALL_ORIGINS = True,因为容器前没有任何重写 Host 头的组件,且前后端同源后 CORS 不再构成实际限制; - CSRF:从
CSRF_TRUSTED_ORIGINS环境变量(逗号分隔)读取运维者自己的域名; - 数据库:新增
DB_BACKEND环境变量,默认sqlite,也可选postgresql(见下节)。
该模块还经历了一次重要的架构修正:早期版本是一份从生产配置复制出来的独立文件,由 entrypoint 在启动时覆盖真实设置,结果导致新增的MAP_TILE_PROVIDER、OCR_MODEL等CONSTANCE_CONFIG配置键永远到不了这份副本,GET /api/sitesettings直接返回 500、整个 UI 无法加载。改为继承覆盖后,这类"复制漂移"问题从根上被消除。这是理解该模块设计的关键背景,也是它写成"import production 后只改差异"的根本原因。
1.3 快速启动方式
方式一:Docker Compose(推荐),使用仓库中的无代理编排文件:
docker-compose -f docker-compose.no-proxy.yml up -d该文件位于 deploy/docker/unified/docker-compose.no-proxy.yml,包含一个 PostgreSQL 数据库服务(pgautoupgrade/pgautoupgrade,带健康检查)和一个 backend 服务,backend 直接使用reallibrephotos/librephotos-unified:${tag}镜像,端口映射${httpPort:-3000}:8001,且已在环境变量中预设SERVE_FRONTEND=true。
方式二:直接 docker run(SQLite 单机模式):
# 创建数据目录结构 mkdir -p ./librephotos-data/{db,internal_media,logs} # 启动容器 docker run -d \ --name librephotos \ -p 3000:8001 \ -v ./librephotos-data/db:/db \ -v ./librephotos-data/internal_media:/protected_media \ -v ./librephotos-data/logs:/logs \ -v /path/to/your/photos:/data \ -e SERVE_FRONTEND=true \ -e DB_BACKEND=sqlite \ reallibrephotos/librephotos-unified:latest把/path/to/your/photos替换为你的真实照片目录。目录结构含义:
./librephotos-data/db/:SQLite 数据库(librephotos.sqlite3、cache.sqlite3);./librephotos-data/protected_media/:处理后的图片、缩略图与模型数据;./librephotos-data/logs/:应用日志与 Secret Key。
1.4 SQLite 的生产化调优
日志中提到"为了让 SQLite 正常工作做了多处修复"。在 production_noproxy.py 中可以看到具体的生产级 SQLite 配置:
DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(db_dir, "librephotos.sqlite3"), "OPTIONS": { "transaction_mode": "IMMEDIATE", "timeout": 5, "init_command": """ PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA mmap_size=134217728; PRAGMA journal_size_limit=27103364; PRAGMA cache_size=2000; """, }, }, }要点包括:WAL 日志模式、synchronous=NORMAL、128 MB mmap、以及transaction_mode=IMMEDIATE避免写冲突时的忙等待。同时,当数据库不是 PostgreSQL 时,django.contrib.postgres会从INSTALLED_APPS中剔除,因为该应用注册的是仅 PostgreSQL 的查询与操作(如SearchVector等全文检索表达式,api/models/photo_search.py及 OCR 全文索引迁移会用到)。若DB_BACKEND既不是sqlite也不是postgresql,会抛出ImproperlyConfigured异常并提示合法取值。
1.5 入口脚本做了什么
entrypoint.sh 按顺序完成以下工作:
- 设置
MPLCONFIGDIR到/protected_media/matplotlib,避免 matplotlib 每次启动都重建字体缓存; - 导出
BASE_LOGS(默认/logs)与LOG_LEVEL(默认INFO),并强制创建日志目录(写 secret.key 需要); - 若
SERVE_FRONTEND为true/1/yes/on(不区分大小写),则导出DJANGO_SETTINGS_MODULE=librephotos.settings.production_noproxy并执行python manage.py collectstatic --noinput; - 依据
DB_BACKEND执行python manage.py migrate; - 若设置了
ADMIN_USERNAME与ADMIN_PASSWORD,则自动创建或更新管理员账号(api.models.User,可用ADMIN_EMAIL指定邮箱); - 依次启动各类服务:
start_service all、start_cleaning_service、start_job_cleanup_service、clear_cache、build_similarity_index,后台运行qcluster; DEBUG=1时用runserver 0.0.0.0:8001,否则用 gunicorn(默认 4 个 worker,WEB_CONCURRENCY可覆盖,timeout 3600,--max-requests 2000)。
1.6 环境变量速查
统一镜像可用的核心环境变量(来自 unified README 与 compose 文件):
| 变量 | 说明 | 默认值 |
|---|---|---|
SERVE_FRONTEND | 是否由 Django 直接服务前端 | false(标准代理模式) |
DB_BACKEND | sqlite或postgresql | sqlite |
CSRF_TRUSTED_ORIGINS | 逗号分隔的受信任源(生产必设) | 空 |
SECRET_KEY | Django 密钥 | 自动生成 |
DB_NAME/DB_USER/DB_PASS/DB_HOST/DB_PORT | PostgreSQL 连接信息 | 仅 postgresql 模式使用 |
ADMIN_USERNAME/ADMIN_PASSWORD/ADMIN_EMAIL | 自动创建管理员 | 不创建 |
WEB_CONCURRENCY | gunicorn worker 数 | 4 |
BASE_LOGS/LOG_LEVEL | 日志目录与级别 | /logs/INFO |
MAPBOX_API_KEY | 地图服务密钥 | 空 |
SKIP_PATTERNS | 扫描忽略模式 | 空 |
ALLOW_UPLOAD | 是否允许上传 | false |
NEXTCLOUD_ENABLED | 是否启用 Nextcloud 扫描 | false |
FEATURE_VIDEO/FEATURE_FACE_DETECTION/FEATURE_FACE_CLUSTER/FEATURE_IMAGE_CAPTIONING/FEATURE_REVERSE_GEOCODING/FEATURE_SCENE_CLASSIFICATION | 功能开关 | true |
从标准拆分部署迁移到统一部署的步骤:备份数据库与照片 → 切换到docker-compose.no-proxy.yml→ 设置SERVE_FRONTEND=true→ 按你的域名更新CSRF_TRUSTED_ORIGINS→ 重新up -d。官方说明该方案与标准代理部署完全向后兼容,两种方式可任选。
二、相册公开链接分享:Public Sharing via link
日志的第二项大特性是通过链接公开分享相册,即不要求对方登录即可查看指定用户相册。它由模型AlbumUserShare、序列化器AlbumUserPublicSerializer与三个 API 视图共同实现。
2.1 数据模型:AlbumUserShare
api/models/album_user_share.py 定义了分享实体:
album:与AlbumUser一对一关联;enabled:是否启用分享(带索引);slug:公开访问标识,唯一、最长 64 字符,未设置时由ensure_slug()自动生成(uuid4().hex[:12],冲突时追加-N后缀);expires_at:过期时间(可空,空表示永不过期);- 五个分享选项字段(
share_location、share_camera_info、share_timestamps、share_captions、share_faces):None表示继承用户默认值,True/False表示相册级覆盖。
is_active()方法判定分享是否有效:未启用返回 False;有expires_at且已过期返回 False,否则为 True。get_effective_sharing_settings()按"相册覆盖 > 用户默认值 > 系统默认值(全 False)"的优先级解析最终生效的分享选项,系统默认值来自api.models.user.get_default_public_sharing_settings,用户默认值来自owner.public_sharing_defaults(相关字段与迁移见 api/models/user.py 与 0119_add_public_sharing_options.py)。
2.2 API 视图与路由
实现位于 api/views/public_albums.py,路由注册于 librephotos/urls.py:
| 方法 | 路径 | 视图 | 说明 |
|---|---|---|---|
| POST | /api/useralbum/makepublic | SetUserAlbumPublic | 开启/关闭分享,设置 slug、过期时间与分享选项;校验相册归属(非拥有者返回 403) |
| GET | /api/public/albums/s/<slug>/ | PublicAlbumBySlug | 无需认证(AllowAny),按 slug 返回公开相册;仅当enabled=True且未过期时命中 |
| GET | /api/public/albums/s/<slug>/photos/<photo_id>/ | PublicPhotoDetailBySlug | 无需认证,获取相册内单张照片详情,photo_id同时支持 UUID(36 字符含 4 个连字符)与 32 位十六进制image_hash两种格式 |
PublicPhotoDetailBySlug还会过滤掉hidden=True或in_trashcan=True的照片,并把解析出的sharing_settings传入PublicPhotoDetailSerializer的 context,从而在序列化层面依据分享选项决定是否输出位置、相机信息、时间戳、标题与人物标签等字段。公开相册通过/media/.../public-album/...路径服务缩略图(见 api/tests/media_serving/test_unified_media_access_view.py)。
2.3 分享选项的实际效果
公开分享不只是"给个链接",而是可细粒度控制对外暴露哪些元数据。SHARING_OPTION_FIELDS常量定义了五个开关,分别控制:
share_location:地理位置(EXIF GPS 与逆地理编码地名);share_camera_info:相机型号、镜头、焦距等设备信息;share_timestamps:拍摄时间等时间戳;share_captions:照片标题/描述;share_faces:人物识别结果与人物标签。
默认全部为 False,即链接访客只能看到最基础的照片内容,不会泄露隐私元数据;相册所有者可在开启分享时逐项覆盖,或统一配置用户级默认值。
三、文件夹导航视图:分页与子文件夹照片计数
第三个新特性是文件夹导航视图 + 分页 + 子文件夹聚合照片计数,配套前端新增了详情页面包屑导航与子文件夹无限滚动。
3.1 后端实现
核心是 api/views/album_folder.py 中的FolderNavigationViewSet,路由注册为/api/folders/subfolders/(librephotos/urls.py)。该接口接收两个查询参数:
path:要列出子文件夹的目录路径,缺省时管理员取settings.DATA_ROOT,普通用户取自己的scan_directory;page:页码,默认 1。
实现要点:
- 性能优化:
PAGE_SIZE = 100,每页最多返回 100 个子文件夹,且只对当前页的子文件夹批量计算照片数(一次Photo.objects.owned_by(user).aggregate(...)聚合查询,通过files__path__startswith前缀匹配统计),而不是扫描整个目录树; - 排序与过滤:子文件夹按名称忽略大小写排序,跳过以
.开头的隐藏目录,photo_count为 0 的文件夹不返回; - 响应结构:包含
current_path、parent_path、subfolders列表以及分页元数据(page、page_size、total_folders、total_pages、has_next、has_previous); - 安全校验:管理员仅能访问
DATA_ROOT之内的路径;普通用户仅能访问自己scan_directory之内的路径(startswith前缀校验),从根源上防止目录穿越;路径不存在或不是目录时分别返回 400。
配套测试 api/tests/albums/test_folder_navigation_subfolders.py 验证了:排序(alpha、Beta、Gamma按忽略大小写排序)、空照片计数文件夹不显示、photo_count聚合正确、100 条分页边界、越界页码返回空列表、以及权限/路径校验等行为。
3.2 前端联动
日志中提到前端配套改动包括:详情视图的面包屑路径(Breadcrumb Path)与子文件夹的无限滚动(infinite scroll)。这意味着在前端可以沿着current_path/parent_path逐级向上回溯形成面包屑,并利用has_next分页元数据在滚动到底时自动请求下一页,形成流畅的目录浏览体验。相关实现可参考 apps/frontend/src/api_client/folders/ 下的 API 客户端封装。
四、其他性能与稳定性修复
日志还列出多项性能与稳定性改进,可在源码中找到对应证据:
- 减少多处视图的查询次数(perf pass):例如删除操作修复 n+1 查询、
AlbumUserShare查询使用select_related("share", "owner")预取关联对象; - 迁移稳定性:修复缺失图片路径时的迁移稳定性(相关迁移见 0114_add_file_path_unique.py 与 0115_cleanup_duplicate_photos.py);
- 前端修复:查看器中的 GIF 动图、无时间戳照片的报错、删除确认对话框改进、登录标题居中;
- 其他:Docker 使用原生 runner 而非模拟器构建(AMD64/ARM64 多架构支持),依赖与社区翻译字符串更新。
五、总结与适用建议
- 想极简起步:用
librephotos-unified镜像 +DB_BACKEND=sqlite,一条docker run即可跑通(deploy/docker/unified/README.md); - 生产环境:推荐 compose 方案(deploy/docker/unified/docker-compose.no-proxy.yml),用 PostgreSQL、设置
SECRET_KEY与CSRF_TRUSTED_ORIGINS,并置于 Traefik/Caddy 等反向代理或云负载均衡之后(该方案对自动 HTTPS 友好); - 分享相册:通过"公开链接"功能生成 slug 链接,并用五个分享选项控制对外暴露的元数据粒度;
- 浏览大目录:使用文件夹导航视图逐层浏览,利用分页与子文件夹照片计数避免一次性加载海量目录。
这些功能均已在 2025-08-28 的开发日志 中宣布,本文结合仓库源码补充了底层实现细节,供你在部署、排障与二次开发时参考。
【免费下载链接】librephotosA self-hosted open source photo management service.项目地址: https://gitcode.com/GitHub_Trending/li/librephotos
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考