LibrePhotos 移动端使用与实现解析:服务器连接、本地图片同步与分块上传机制
2026/9/16 16:14:01 网站建设 项目流程

LibrePhotos 移动端使用与实现解析:服务器连接、本地图片同步与分块上传机制

【免费下载链接】librephotosA self-hosted open source photo management service.项目地址: https://gitcode.com/GitHub_Trending/li/librephotos

本文以 LibrePhotos 官方文档 Mobile Apps 为主体,系统讲解 LibrePhotos 生态中的两款移动端客户端(UhuruPhotos 与官方的 LibrePhotos Mobile),并结合仓库中的移动端源码(React Native 实现)与服务端上传接口实现,深入剖析首次连接服务器的校验流程、本地图片的增量扫描与同步状态模型、按 1MB 分块的上传协议,以及服务端分块上传接口的去重、落盘与后台任务链路。读完后你将能够完整走通「连接服务器 → 登录 → 本地照片同步 → 分块上传 → 全量备份/清理」的全流程,并理解每个环节在 apps/mobile/src 与 apps/backend/api/views/upload.py 中的具体实现。

两款移动端客户端概览

LibrePhotos 官方文档指出,目前存在两款可供使用的移动端方案,定位与成熟度完全不同:

UhuruPhotos:功能最全的第三方原生客户端

UhuruPhotos 是由社区作者开发的原生 Android 客户端,采用最新的 Android 技术栈编写,是 LibrePhotos 生态中功能最完整的 App。它定位为完整的照片相册替代品,目标特性包括离线支持、备份与同步等。该应用目前以开放测试(open beta)形式发布在 Google Play 上,作者通过 Gitter 与 Discord 社区提供交流渠道。

LibrePhotos Mobile:官方概念验证应用

LibrePhotos Mobile 是官方应用,目前处于概念验证(proof-of-concept)阶段,使用 React Native 编写。官方文档明确说明,未来它会与 Web 前端共享更多代码——这一点在当前仓库结构中已有体现:移动端目录下存在一份与前端 apps/frontend/src/api_client 结构对齐的 apps/mobile/src/api_client,按 auth、photos、albums、jobs、upload 等模块组织,各自包含 hooks 与 types,是后续代码共享的雏形。

官方提供了可直接下载的 APK 安装包。从源码结构看,工程保留了完整的 iOS 目录(apps/mobile/ios)与 Android 构建配置(apps/mobile/android),因此技术上可以编译出 iOS 版本,但官方文档说明目前尚无人接手完成 iOS 构建。

LibrePhotos Mobile 的工程结构

移动端源码位于 apps/mobile/src,从目录结构可以看出其分层:

目录职责
src/Containers页面级容器:登录(Login)、相册(Albums)、图库(Gallery)、搜索(Search)、设置(Settings)、启动(Startup)等
src/ComponentsUI 组件:图片网格(ImageGrid)、时间线列表(TimelineList)、灯箱(LightBox)、上传按钮(UploadButton)、下载按钮(DownloadButton)等
src/api_client自动生成的 API 客户端,按后端模块拆分为 auth、photos、albums、jobs、upload、user 等,upload 模块包含 useUploadExistsMutation.ts、useUploadMutation.ts、useUploadFinishedMutation.ts 三个 hooks,分别对应服务端「查重、分块上传、完成上传」三步
src/stores基于 Zustand 的全局状态:authStore、configStore、localImagesStore、uploadStore,本地图片状态通过 AsyncStorage 持久化
src/Services/Config服务器连通性检测逻辑

这种「api_client + stores + 页面容器」的组织方式与 Web 前端一致,印证了官方文档中「未来共享更多代码」的规划方向。

连接服务器:输入地址、自动探测与登录

首次打开应用时,需要在Server Name字段中输入 LibrePhotos 服务器地址——使用与浏览器访问 LibrePhotos 时相同的地址,例如photos.example.com192.168.1.10:3000。协议是可选的:应用会先尝试http://,失败后回退到https://;同时它会对你输入的地址做小写化并去除尾部斜杠的归一化处理。

输入过程中应用会实时校验服务器连通性:出现绿色对勾表示服务器可达;出现红色警告三角和 "Unable to connect to the server" 提示则说明地址无法访问。由于探测存在较短的超时时间,慢速或远距离的服务器可能需要等待片刻或再试一次。校验通过(绿色对勾)后,使用你的 LibrePhotos 用户名和密码登录即可。

这一「边输入边探测」的行为在源码中有精确对应,见 CheckServer.js:

const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), 500) await fetch(serverName + '/api/auth/token/obtain/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'test', password: 'test' }), signal: controller.signal, }) // Any response means the server is reachable (200 or 401 etc.) return true

可以推断出几个实现细节:

  1. 探测端点选用登录接口:向/api/auth/token/obtain/发送一次伪造凭据的 POST 请求。只要收到任何 HTTP 响应(200、401 等)就判定服务器可达,网络错误或 500ms 超时则判定不可达。选择该端点是因为它对未认证请求返回标准错误而非连接成功,能区分「服务器存活」与「路径存在」。
  2. 500ms 的硬超时:这就是文档中提示「慢服务器可能需要再试一次」的来源——探测窗口只有半秒。
  3. 登录凭据使用 JWT:登录成功后由 authStore.ts 持有 access token,上传请求通过fetchClient(api_client/api.ts)携带认证信息访问后端。

本地图片模型:三种同步状态与增量扫描

官方文档 Local Images 定义了每张图片的三种同步状态:

  • Synced(已同步):图片已同步到服务器,且本地手机上也存在;
  • Local(仅本地):图片尚未同步到服务器,只存在于手机上;
  • ☁️Remote(仅远程):图片只在服务器上,手机本地没有。

增量扫描机制

每次打开时间线("With Timestamp" 选项卡)时,应用都会扫描相机胶卷(camera roll)中比上一次扫描更新的照片,并将它们与服务器上的照片合并展示。由于扫描是增量的,那些携带较旧日期的图片——恢复的备份、从电脑复制的文件、保留原始 EXIF 日期的导入——不会被重新拾取。如果因此漏掉了照片,可以在设置中点击 "Reset Local Images" 清空本地索引并强制全量重扫。

这一状态管理由 localImagesStore.ts 实现,核心状态为:

type LocalImagesState = { images: LocalImages // 本地图片列表,每张带 syncStatus lastFetch: number | undefined // 上次成功获取的时间戳(秒),驱动增量扫描 isLoading: boolean }

从源码结构看,lastFetch只在addImages收到非空新增列表时才更新,这正是「比上一次扫描更新」这一增量语义的落点;而整个 store 通过 Zustand 的persist中间件序列化到 AsyncStorage(存储键为localImages-storage),保证应用重启后本地索引不丢失。状态变更由markSynced(置为 SYNCED)、markNotSynced(置为 LOCAL,且仅当原状态为 LOCAL 之外时才改写,避免覆盖其他状态)和removeImages完成——「删除已备份照片」功能最终就是调用removeImages将本地记录移除。

Android 权限要求

应用首次启动时会申请以下权限:

  • Read external storage(Android 12 及以下,API ≤ 32)
  • Write external storage(Android 10 及以下,API ≤ 29)
  • Read media images(Android 13+)
  • Read media video(Android 13+)
  • Manage external storage(Android 11+)

其中大部分权限用于读取手机上的图片。需要注意的是,"Manage external storage" 权限需要用户手动到系统应用设置页授予——这是应用能够从手机上删除图片的前提。这些权限声明与 apps/mobile/android/app/src/main/AndroidManifest.xml 中的声明一一对应。

上传机制:从客户端分块到服务端落盘

官方文档 Upload 描述了完整的上传行为,以下结合两端源码展开。

上传前置条件(两者都由管理员配置)

移动端上传前必须满足两个条件:

  1. 上传功能必须开启——管理后台的Allow uploads开关处于开启状态(见下文「开启/关闭上传功能」);
  2. 你的账户必须配置了扫描目录(Scan Directory)——管理员需要为你的用户设置扫描目录,且该目录必须在服务器容器内真实存在。

如果扫描目录缺失或不存在,上传会被拒绝。文档特别指出:移动端目前不会把这个错误显式呈现给用户,照片会一直停留在未同步状态。因此如果上传始终无法完成,应与管理员核对扫描目录配置(参见 How to Change Your Scan Directory)。

支持的文件类型

所有 MIME 类型为 image 的文件均可上传。

客户端:1MB 分块上传

文档描述上传流程为:先比较md5 + user_id组成的哈希判断文件是否已在服务器上,存在则跳过,不存在则上传;文件按 1MB 分块发送;单张照片上传完成后即标记为已同步。

客户端实现见 uploadActions.ts,关键细节包括:

const chunkSize = 1000000 // < 1MB chunks
  • 先查重后传输:对每个文件先请求GET /exists/{hash}/file.idmd5 + user_id的组合),服务端返回{ exists: boolean };已存在则直接markSynced,不产生任何传输;
  • 分块循环:用file.slice()将文件切成若干 1MB Blob,逐块 POST 到/upload/,携带upload_idoffsetuser以及Content-Range: bytes start-end/total头;服务端每块响应新的offsetupload_id,客户端据此推进(第一块不传upload_id,由服务端创建分块上传会话);
  • 进度上报:uploadStore.ts 维护total(全部文件总字节数)与current(已上传字节数),设置界面据此显示 "Uploading X%" 进度;
  • 容错:某张照片被服务器拒绝(例如未配置扫描目录触发的 HTTP 400)只把该照片标记为FAILED,不影响批次中其余文件;
  • 完成确认:全部分块发完后调用POST /upload/complete/,提交upload_idmd5userfilename,服务端收到后才真正合并文件并入库,此时客户端执行markSynced

服务端:分块合并、去重与后台任务链

服务端接口在 upload.py 中实现,基于 chunked_upload 应用(ChunkedUploadView/ChunkedUploadCompleteView)扩展:

  1. 权限与认证UploadPhotosChunked.check_permissions首先检查site_config.ALLOW_UPLOAD,未开启即返回 403 "Uploading is not allowed";随后authenticate_upload_request从 Cookie 中的jwt解析出用户(见 upload.py#L47-L58)。
  2. 文件类型校验on_completion中调用is_valid_media(来自 directory_watcher),非法媒体类型直接删除已上传的分块并返回 400 "File type not allowed"。
  3. 扫描目录校验validate_scan_directory(upload.py#L61-L69)检查用户是否配置了scan_directory且目录在容器内真实存在,否则抛出文档中提到的那个 400 错误——这正是移动端把照片标记为失败/未同步的来源。
  4. 哈希去重:完成上传时服务端重新计算内容哈希(calculate_hash_b64),并在target_path中判断:若数据库已存在相同image_hash的照片,或uploads/web下同名文件的哈希与之一致,则不再落盘,直接返回 "Photo duplicated. No new import performed.";若同名文件已存在但内容不同,则写入文件名_哈希.扩展名以避免覆盖。
  5. 落盘位置:新文件写入scan folder + /uploads/web(即你扫描目录下的uploads/web子目录,代码中device当前固定为"web",见 upload.py#L161-L167),与文档「所有分块上传完成后合并为单个文件放入scan folder + /uploads/web」完全一致。
  6. 后台任务链:文件落盘后import_photo立即构建一条 Django Q 的Chain(upload.py#L140-L148):
chain.append(handle_new_image, user, photo_path, image_hash, photo) # 缩略图、元数据等 chain.append(generate_captions_wrapper, photo, True) # AI 描述/标签 chain.append(photo._geolocate) # 地理编码 chain.append(photo._add_location_to_album_dates) # 日期相册归集 chain.append(photo._extract_faces) # 人脸检测 chain.run()

这印证了文档中「为这张照片入队一个后台作业(缩略图、元数据、描述、地理位置、日期相册与人脸检测),不会触发独立的文件夹扫描」的说法——上传路径复用了与目录扫描完全相同的单图处理管线handle_new_image

备份全部照片与删除已同步照片

  • 备份全部照片:在设置界面点击Sync all images按钮,即可把手机上所有未同步的图片一次性上传,进度以 "Uploading X%" 展示(对应下图中设置页的Sync all images选项):

  • 删除已同步照片:点击Remove backed up images按钮,应用会逐张检查照片的同步状态,已同步的即从手机上删除(对应下图中设置页的Remove backed up images选项):

这两项操作都依赖前文所述的localImagesStore:同步状态既是 UI 标记,也是「能否安全删除」的判定依据。

开启/关闭上传功能(管理端)

管理员可以在管理后台(admin area)点击Allow uploads开关来启用或禁用上传。需要理解这个开关与环境变量的关系:

  • 这个开关是权威设置
  • ALLOW_UPLOAD环境变量(Docker 部署时在librephotos.env中通过allowUpload设置,见 Environment variables)只提供初始默认值:一旦有值被写入数据库——无论是通过拨动开关还是首次设置向导——存储的设置即生效,环境变量将被忽略。

这与源码一致:服务端每次检查的都是site_config.ALLOW_UPLOAD(constance 管理的站点配置,存于数据库),而非环境变量本身。

当前局限与后续方向

结合官方文档与源码结构,可以归纳出 LibrePhotos Mobile 目前的边界:

  1. 官方定位为 proof-of-concept,核心链路(登录、时间线浏览、本地扫描、分块上传、相册/搜索/设置骨架)已可用,但功能完整度仍不及 UhuruPhotos;
  2. 上传被拒绝时(如扫描目录未配置)没有明确的错误提示,只能靠管理员侧排查;
  3. iOS 构建虽技术上可行,但尚无社区维护;
  4. 增量扫描基于「比上次扫描更新」的语义,对旧日期图片需要手动 "Reset Local Images"。

若你需要更完整的移动端体验(离线支持、更成熟的备份同步),文档推荐优先使用 UhuruPhotos;LibrePhotos Mobile 则适合作为跟随主干开发、未来与前端深度共享代码的官方入口,其源码位于 apps/mobile,相关文档见 LibrePhotos Mobile、Local Images 与 Upload。

【免费下载链接】librephotosA self-hosted open source photo management service.项目地址: https://gitcode.com/GitHub_Trending/li/librephotos

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询