FEATURED · 精选文章

用业余时间造一台「私有照片保险箱」:picSync 的开发故事

发布时间 / 2026/8/18 1:40:02
来源 / 创域科博编辑部
栏目 / 资讯中心
用业余时间造一台「私有照片保险箱」:picSync 的开发故事 一台自托管的照片同步服务 一个 Android 客户端把手机相册安全地备份到自己的电脑或树莓派。本文聊聊它为什么诞生、怎么长成现在的样子以及它能帮你做什么。一、缘起为什么不直接用现成的云盘大概每个写代码的人手机里都堆着几千张照片——孩子的瞬间、旅行的风景、随手拍的文档。把它们交给公有云心里总有个疙瘩数据在别人手里、空间要续费、隐私条款看不懂。我也纠结过。直到有一天我想与其反复比较两个云盘的会员价不如自己写一个只属于我自己的照片同步工具。需求其实很朴素把手机相册按目录自动备份到一台我控制的机器同一张照片只存一份换手机、重复拷贝都不浪费空间有个简单的管理后台能在电脑上看缩略图、查占用全部跑在局域网里不出公网安心。于是就有了picSync——Photo照片 Sync同步。它没有颠覆性的技术却是一把能真正解决问题的螺丝刀。二、技术选型够用、好维护、能跑在我那台老机器上选型上我奉行能用就行不追新角色技术栈理由服务端Java 8 Spring Boot 2.7 JPA/H2 Layui本机只有 JDK 8Spring Boot 自带内嵌 Tomcat一个 jar 就能跑H2 零配置Layui 写后台页面快安卓端Kotlin Jetpack ComposeMaterial3 Retrofit/OkHttp WorkManager DataStore SAF现代 Android 标准栈Compose 写 UI 快WorkManager 做后台定时同步SAF 合规访问相册目录服务端所有静态资源管理后台直接打进 jar启动即可访问客户端是标准的 Gradle 工程编译出 APK 安装到手机。三、核心设计让去重成为第一公民整个系统最关键的抽象只有一句话每张照片用它的 MD5 值做全局唯一身份证。物理文件以MD5.扩展名命名落到originals/目录相同内容永远只存一份用户和照片之间通过一个user_photo关联表连接——你拥有一张照片不代表你独占它的字节同步时先算本地照片的 MD5再去问服务端这张我有没有把照片分成三类owned我已经有跳过global文件在服务端存在别人传过我只需认领不传任何字节秒传missing全新才真正上传。这套语义让多手机备份“重复照片”换机迁移都变得便宜。删除也只是摘掉你自己的关联当没有任何人引用这张照片时物理文件才会被真正删除——既节省空间又不误伤别人。四、开发路上踩过的几个坑最有意思的部分1. 手机本地扫图慢到假死最初一键同步时App 卡在正在扫描几分钟没反应。根因是 Android 的 SAF存储访问框架每次读目录、读文件属性都要跨进程 IPC慢得离谱。解法把目录树的遍历、MD5 计算全部丢到后台线程并且并行化——多个目录同时扫、8 路并发算哈希同时给 MD5 结果加了一层本地缓存文件名大小修改时间没变就不重算。改造后首屏出图快了非常多主线程也不再冻结。2. 缩略图在后台页面集体失踪管理页面里缩略图一格一格的破图。排查后发现Layui 表格里的img标签没法自定义 HTTP 请求头而服务端要求带 Bearer Token 鉴权结果每个缩略图请求都被返回 401。解法给鉴权拦截器加了一个兜底——允许通过?token查询参数携带令牌。前端生成缩略图地址时统一拼上 token浏览器img直接能加载。同样的处理也用在了原图预览上。3. 服务端启动后不翼而飞的端口有次服务端死活连不上查日志发现它监听在随机端口如 12368而不是 8080。顺着环境变量摸过去发现沙箱环境导出了SERVER__PORT0Spring Boot 把双下划线 relax 成server.port0随机端口。解法启动时显式追加--server.port8080命令行参数优先级高于环境变量端口从此稳定。这种环境变量悄悄改了配置的坑排起来很费神但记下来就再也不会被绊倒。4. 给服务端加导入照片能力有时候照片已经在电脑的某个目录里比如相机导出的 SD 卡、老电脑的备份没必要经手机再传一遍。于是新增了一个管理员导入接口在服务端后台填一个或多个本地目录服务端递归扫描里面的图片jpg/jpeg/png/gif/webp/bmp算 MD5 去重后入库并生成缩略图。默认对所有启用用户可见也可以只归某个用户所有。5. 看图不必返回再进无论是后台还是客户端最初点开一张图看完必须返回列表才能看下一张。后来都改成了在查看页内直接上一张 / 下一张切换后台还支持键盘左右方向键并显示x / n的进度。五、它现在能做什么自动 / 手动同步选好相册目录后App 在后台按计划把新照片增量备份上去也支持手动挑选本地照片立即同步。MD5 去重与秒传重复照片、跨设备相同照片只占用一份空间第二次上传实为秒传。断点续传大文件按 1MB 分片上传网络中断后只补传缺失分片不会从头再来。图库浏览客户端网格缩略图 详情页尺寸/大小/MD5/来源可上一张下一张、可下载原图。管理后台Layui概览统计用户数、照片数、占用空间、照片列表缩略图/预览/删除、用户管理增删改、启用禁用、导入照片。在线升级客户端打开会自动检测服务端发布的新版本一键下载安装。典型使用场景家庭里把一台旧电脑/树莓派跑起服务端全家人的手机都装上客户端相册自动回家摄影师把 SD 卡内容直接导入服务端统一管理或者纯粹当作一个完全私有的照片保险箱。六、怎么跑起来极简版服务端需要 JDK 8cdserver# 用本地 Maven 打包本机 mvn 脚本在 Git Bash 下加载类失败故用 plexus 启动器直调D:/program/jdk8/bin/java-cpD:/workbuddy/picSync/tools/apache-maven-3.9.9/boot/plexus-classworlds-2.8.0.jar\-Dclassworlds.confD:/workbuddy/picSync/tools/apache-maven-3.9.9/bin/m2.conf\-Dmaven.homeD:/workbuddy/picSync/tools/apache-maven-3.9.9\-Dmaven.multiModuleProjectDirectoryD:/workbuddy/picSync/server\org.codehaus.plexus.classworlds.launcher.Launcher package-Dmaven.test.skiptrue# 启动务必显式指定端口避免被 SERVER__PORT 环境变量覆盖成随机端口D:/program/jdk8/bin/java-jartarget/photo-sync-server-1.0.jar--server.port8080浏览器打开http://{服务端IP}:8080/默认管理员账号admin / admin123。客户端用 Android Studio 打开android/工程编译出 APK安装后填入服务端地址、登录选择要同步的相册目录即可。更完整的部署、配置项、API 与实现细节见同目录的《picSync 软件说明文档》。七、写在最后小工具大满足picSync 不算什么宏大项目但它解决了我自己的真实痛点而且数据完全在我手里。从手机扫图卡死到缩略图 401从随机端口失踪到上一张下一张每一个坑都让我对一个具体技术点理解更深了一层。如果你也想要一个不依赖公有云、能自己掌控的照片备份方案不妨把它跑起来试试。下一步我打算做共享相册多人协作一个相册、Web 端直接浏览、以及可选的服务端加密存储。—— 一个把周末花在造螺丝刀上的开发者
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻