
详解 LibrePhotos Web 上传分块上传、去重跳过与上传后后台处理链路【免费下载链接】librephotosA self-hosted open source photo management service.项目地址: https://gitcode.com/GitHub_Trending/li/librephotos本文围绕 LibrePhotos 用户上传Upload功能展开先讲清楚网页端上传按钮的使用方式与文件类型限制再剖析“1MB 分块上传 md5 去重 扫描目录落盘”的完整工作流程最后覆盖Allow uploads开关、allowUpload/ALLOW_UPLOAD环境变量、每用户扫描目录的配置方法以及各类上传故障的排查手段。读完本文你可以独立完成 Compose 部署下的上传功能启用并理解上传请求从前端切片到后端入库、触发后台任务链的全部调用关系。一、如何使用上传功能在 LibrePhotos 网页界面右上角有一个上传按钮。点击它会打开文件选择器可以多选文件一次性上传。前端该按钮由 ChunkedUploadButton 组件实现它的可见性与可用性受两个条件共同控制全局开关组件会读取站点设置中的allow_upload若为假则直接不渲染上传按钮if (!settings?.allow_upload) return null。该设置对应管理后台的Allow uploads开关在后端由 site_settings 模式 中的allow_upload布尔字段描述并经 views.py 的 settings 接口读写。每用户扫描目录组件从userSelfDetails.scan_directory判断当前账号是否配置了扫描目录。未配置时按钮保持可见但置灰鼠标悬停显示提示 “Scan directory not configured - contact administrator”且拖放/选择文件不会发起上传。支持哪些文件类型官方文档说明LibrePhotos 接受所有 MIME 类型为 image 或 video 的文件。这与前后端两处实现一致前端 ChunkedUploadButton 使用 react-dropzone 的accept限制为image/*与video/*因此文件选择器只会列出图片与视频后端在完成上传时调用is_valid_media(uploaded_file.file.path, user)做最终校验非法类型会删除已接收的临时分块文件并返回 HTTP 400 “File type not allowed”见 upload.py 的on_completion方法。二、上传是如何工作的分块上传与去重机制官方文档对上传流程的概括是先按“hash md5 user_id”检查文件是否已存在于服务器上——已存在则跳过不存在则上传文件按 1MB 分块发送每个文件上传完成后后端注册这张照片并为它排入一条后台任务链元数据与缩略图、字幕、地理定位、相册日期、人脸提取不会触发目录扫描。下面结合源码逐点验证。2.1 前端1MB 分块与 MD5 计算前端分块逻辑位于 chunkedUpload.ts// 1MB chunks, because of the nginx default client_max_body_size export const CHUNK_SIZE 1000000;注意两个细节块大小取1000000字节略小于 1 MiB源码注释明确说明这是为了兼容 nginx 默认的client_max_body_size1MB限制避免单块被代理层拒绝calculateChunks()用file.slice把文件切成若干BlobcalculateMD5()则以 25 MiB 的步长分片读取文件并增量计算 MD5避免一次性把大文件读入内存。上传队列 useUploadQueue.ts 逐块调用上传 mutationuseUploadMutation.ts 在请求头中携带Content-Range: bytes offset-end/total告诉服务端当前分块在文件中的位置。2.2 后端分块接收、续传校验与 MD5 校验后端分块接收由 Django 视图 ChunkedUploadView 实现LibrePhotos 在 upload.py 中将其子类化为UploadPhotosChunked。关键机制定位分块位置content_range_pattern解析bytes start-end/total格式的Content-Range头若客户端未提供该头则按“整块即整个文件”处理与 jquery.file.upload 的行为保持一致顺序与大小校验check_chunk()会校验三件事——总大小不超过CHUNKED_UPLOAD_MAX_BYTESsettings.py 中默认None即不限制、分块起始偏移必须等于已接收字节数chunked_upload.offset ! start时返回 400 “Offsets do not match”、分块实际大小与头声明一致可续传每次成功接收分块后响应中返回upload_id、offset与expires。客户端凭upload_id可从中断处继续上传分块在服务器上按 settings.py 的DEFAULT_UPLOAD_PATHchunked_uploads/%Y/%m/%d暂存为.part文件默认 1 天后过期EXPIRATION_DELTA一天过期后服务端返回 410 “Upload has expired”完成请求必须带 MD5ChunkedUploadCompleteView 要求提交upload_id与整个文件的md5服务端会比对已落盘内容的 MD5 与客户端声明值不一致返回 400 “md5 checksum does not match”。这就是文档所说“按 hash 校验”的服务端落点。每次分块请求还会经过check_permissions它先检查站点级ALLOW_UPLOAD开关关闭时返回 403 “Uploading is not allowed”再通过authenticate_upload_request()解析 Cookie 中的 JWT 并定位用户——upload.py 中的该函数只接受 Cookie 内的jwt凭证缺失或无效都返回 403。2.3 去重为什么同一张照片只存一次分块合并完成后UploadPhotosChunkedComplete.on_completionupload.py执行如下步骤重新鉴权并调用validate_scan_directory(user)未配置扫描目录、或目录在服务器上不存在时直接抛出 400 错误错误文案见下文第三节is_valid_media()校验文件类型用get_valid_filename()净化文件名并把设备来源固定为device web计算整个文件的image_hashcalculate_hash_b64(user, ...)以用户为参数参与哈希对应文档中“md5 user_id”的表述调用target_path()决定落盘位置见 2.4删除临时分块文件若判定为重复则直接返回 200 与{detail: Photo duplicated. No new import performed.}不再写入磁盘、也不再入库。target_path()的去重判定共有三层upload.py数据库中已存在Photo.image_hash image_hash的记录 → 返回空路径视为重复目标文件{scan_directory}/uploads/web/{filename}在磁盘上已存在且其重新计算的哈希与新文件相同 → 同样视为重复文件已存在但哈希不同 → 改为写入文件名_image_hash.扩展名的新路径避免覆盖不同的文件。2.4 落盘位置{scan_directory}/uploads/web/{filename}target_path()中upload_dir os.path.join(user.scan_directory, uploads, device)device 恒为web即上传文件最终保存在{scan_directory}/uploads/web/{filename}。若该目录尚未存在on_completion会依次创建uploads与uploads/web两级目录后再写入。在 Docker Compose 部署中扫描目录由宿主机目录绑定挂载进容器docker-compose.yml 中 backend 服务有- ${scanDirectory}:/data一行示例 librephotos.env 中scanDirectory./librephotos/pictures。因此只要scanDirectory指向宿主机上真实存在的持久化路径上传文件就写在挂载卷内容器重建不会丢失——这正是文档所说“文件存放在挂载的主机目录中是持久的”。三、扫描目录上传行为的前置条件上传行为完全取决于该用户是否配置了扫描目录源码中的校验函数validate_scan_directory()upload.py只有一段逻辑def validate_scan_directory(user): if not user.scan_directory or user.scan_directory.strip() : raise _bad_request( Upload failed: No scan directory configured. ... ) if not os.path.exists(user.scan_directory): raise _bad_request( fUpload failed: Scan directory {user.scan_directory} does not exist. ... )由此得到文档中的两种行为场景表现扫描目录已正确配置文件保存到{scan_directory}/uploads/web/{filename}这是正常且预期的行为文件落在挂载的主机目录中、具有持久性账号未配置扫描目录前端上传按钮置灰并显示 “Scan directory not configured - contact administrator”即使请求直接打到后端on_completion阶段也返回 HTTP 400 “Upload failed: No scan directory configured…” 不写任何文件已配置但路径在服务器上不存在按钮保持可用但最终分块提交时被拒绝返回 HTTP 400 “Upload failed: Scan directory does not exist…”不写任何文件如何为每个用户配置扫描目录仅管理员可操作只有管理员能为用户设置扫描目录进入管理后台点击右上角头像 →Admin Area设置 Scan Directory为每个用户手动填写Scan Directory验证路径目录必须存在并且容器能访问到它在 Compose 部署中即位于/data数据根之内。四、启用 / 停用上传功能Allow uploads 开关与环境变量的优先级上传功能有两条前置配置缺一不可Upload feature enabled在管理后台打开Allow uploads。Docker Compose 用户可以在首次启动前向.env中加入allowUploadtrue来预启用——Compose 会将其传给后端环境变量ALLOW_UPLOAD。注意.env里的变量名是allowUpload而 Compose 注入后端时才是ALLOW_UPLOAD见 docker-compose.yml 中- ALLOW_UPLOAD${allowUpload:-false}Scan directory configured每个用户都必须由管理员配置好扫描目录。关于优先级文档与源码结论一致管理后台的开关是权威设置环境变量只供给初始默认值。后端在 production.py 中注册 Constance 配置ALLOW_UPLOAD: ( os.environ.get(ALLOW_UPLOAD, True) not in (false, False, 0, f), ..., )即环境变量仅作为 Constance 站点设置的默认值。一旦Allow uploads开关或首次运行设置向导把值保存进了数据库存储的设置优先之后再修改ALLOW_UPLOAD环境变量不再生效。管理端通过 views.py 的 settings 接口site_config.ALLOW_UPLOAD request.data[allow_upload]写库读取时同样返回site_config.ALLOW_UPLOAD。五、上传之后的处理链路文档指出上传完成后处理已上传照片有两条路径自动处理上传流程会自动为上传的照片触发处理。对应 upload.py 中import_photo()方法先create_new_image(user, photo_path)把照片注册入库然后构建并运行一条 django-q 任务链chain Chain() photo create_new_image(user, photo_path) chain.append(handle_new_image, user, photo_path, image_hash, photo) # 元数据 缩略图 chain.append(generate_captions_wrapper, photo, True) # 字幕 chain.append(photo._geolocate) # 地理定位 chain.append(photo._add_location_to_album_dates) # 按日期/地点归档 chain.append(photo._extract_faces) # 人脸提取 chain.run()该链路不会触发目录扫描directory scan——上传路径独立于 directory_watcher 的扫描作业只对刚上传的这一张照片工作手动扫描前往 Library 页面点击扫描按钮可以手动扫描所有照片包括未处理的存量文件。相关行为可通过 apps/backend/api/tests/uploads/ 下的测试验证例如 test_chunked_upload_permissions.py 覆盖ALLOW_UPLOAD开/关时的 403 分支test_chunked_upload_completion.py 覆盖完成阶段的鉴权与校验顺序。六、故障排查Troubleshooting上传按钮置灰原因该账号未配置扫描目录。悬停按钮显示 “Scan directory not configured - contact administrator”。前端 ChunkedUploadButton 在scan_directory为空时把 dropzone 设为disabled并包裹 Tooltip。解决请管理员按上文“如何为每个用户配置扫描目录”一节设置扫描目录。上传报 “Scan directory does not exist”原因配置的扫描目录路径在后端容器内不存在。后端validate_scan_directory()通过os.path.exists(user.scan_directory)判定路径在容器视角下缺失即触发 400。解决检查 Compose 文件中${scanDirectory}:/data绑定挂载行并确认宿主机上该目录真实存在、且扫描目录值指向数据根默认/data内的子路径。容器重启后上传的文件消失原因数据根没有宿主机目录支撑。扫描目录必须位于后端数据根默认/data之内——放在之外的路径会被 “Scan directory must be inside the data root.” 拒绝。此时上传本身成功但若/data没有绑定挂载写入内容只存在于容器文件系统容器重建即丢失。解决确认 Compose 文件的backend服务挂载了宿主机目录到/data即${scanDirectory}:/data行且.env中scanDirectory指向宿主机上真实、持久化的路径。上传报权限错误原因容器对扫描目录没有写权限。解决检查目录权限确保容器进程能向挂载目录写入。上传按钮不可见原因上传功能被整体停用。前端组件在settings.allow_upload为假时直接返回null。解决在管理后台打开Allow uploads。再次强调allowUpload环境变量只提供默认值一旦该设置从管理后台或首次运行向导被保存进数据库修改环境变量就没有效果必须以数据库中的值为准。小结LibrePhotos 的 Web 上传是一条“前端 1MB 分块 → 服务端顺序/MD5 双重校验 → 按用户哈希去重 → 落盘到{scan_directory}/uploads/web/→ django-q 任务链自动处理”的完整链路其可用性由站点级Allow uploads开关数据库值优先于ALLOW_UPLOAD环境变量与每用户扫描目录共同决定。排障时抓住两个核心事实即可定位绝大多数问题扫描目录必须真实存在于容器内的数据根下且文件持久性依赖 Compose 对/data的绑定挂载。【免费下载链接】librephotosA self-hosted open source photo management service.项目地址: https://gitcode.com/GitHub_Trending/li/librephotos创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考