LibrePhotos 2025 年 7—8 月开发进展:单容器统一部署、相册公开链接分享与文件夹导航
2026/9/16 19:19:51 网站建设 项目流程

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,其核心是:

  1. 在构建镜像的多阶段过程中,先用node:20-slim构建 React 前端(yarn run build),产物位于/frontend/dist
  2. 后端阶段将前端构建产物复制到/code/frontend_build,并通过 deploy/docker/unified/Dockerfile 安装whitenoise作为静态文件服务中间件;
  3. Django 用 WhiteNoise 高效地直接对外服务前端静态资源;
  4. URL 路由约定为:/api/*交给 Django API,其余路径交给 React 前端(SPA catch-all);
  5. 由于 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
  • 主机与 CORSALLOWED_HOSTS = ["*"]CORS_ALLOW_ALL_ORIGINS = True,因为容器前没有任何重写 Host 头的组件,且前后端同源后 CORS 不再构成实际限制;
  • CSRF:从CSRF_TRUSTED_ORIGINS环境变量(逗号分隔)读取运维者自己的域名;
  • 数据库:新增DB_BACKEND环境变量,默认sqlite,也可选postgresql(见下节)。

该模块还经历了一次重要的架构修正:早期版本是一份从生产配置复制出来的独立文件,由 entrypoint 在启动时覆盖真实设置,结果导致新增的MAP_TILE_PROVIDEROCR_MODELCONSTANCE_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.sqlite3cache.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 按顺序完成以下工作:

  1. 设置MPLCONFIGDIR/protected_media/matplotlib,避免 matplotlib 每次启动都重建字体缓存;
  2. 导出BASE_LOGS(默认/logs)与LOG_LEVEL(默认INFO),并强制创建日志目录(写 secret.key 需要);
  3. SERVE_FRONTENDtrue/1/yes/on(不区分大小写),则导出DJANGO_SETTINGS_MODULE=librephotos.settings.production_noproxy并执行python manage.py collectstatic --noinput
  4. 依据DB_BACKEND执行python manage.py migrate
  5. 若设置了ADMIN_USERNAMEADMIN_PASSWORD,则自动创建或更新管理员账号(api.models.User,可用ADMIN_EMAIL指定邮箱);
  6. 依次启动各类服务:start_service allstart_cleaning_servicestart_job_cleanup_serviceclear_cachebuild_similarity_index,后台运行qcluster
  7. 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_BACKENDsqlitepostgresqlsqlite
CSRF_TRUSTED_ORIGINS逗号分隔的受信任源(生产必设)
SECRET_KEYDjango 密钥自动生成
DB_NAME/DB_USER/DB_PASS/DB_HOST/DB_PORTPostgreSQL 连接信息仅 postgresql 模式使用
ADMIN_USERNAME/ADMIN_PASSWORD/ADMIN_EMAIL自动创建管理员不创建
WEB_CONCURRENCYgunicorn 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_locationshare_camera_infoshare_timestampsshare_captionsshare_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/makepublicSetUserAlbumPublic开启/关闭分享,设置 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=Truein_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_pathparent_pathsubfolders列表以及分页元数据(pagepage_sizetotal_folderstotal_pageshas_nexthas_previous);
  • 安全校验:管理员仅能访问DATA_ROOT之内的路径;普通用户仅能访问自己scan_directory之内的路径(startswith前缀校验),从根源上防止目录穿越;路径不存在或不是目录时分别返回 400。

配套测试 api/tests/albums/test_folder_navigation_subfolders.py 验证了:排序(alphaBetaGamma按忽略大小写排序)、空照片计数文件夹不显示、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_KEYCSRF_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询