惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Jina AI
Jina AI
Google DeepMind News
Google DeepMind News
C
Cybersecurity and Infrastructure Security Agency CISA
T
Tenable Blog
T
The Exploit Database - CXSecurity.com
Latest news
Latest news
G
GRAHAM CLULEY
Project Zero
Project Zero
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Webroot Blog
Webroot Blog
Help Net Security
Help Net Security
TaoSecurity Blog
TaoSecurity Blog
Hacker News: Ask HN
Hacker News: Ask HN
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
N
News and Events Feed by Topic
Cisco Talos Blog
Cisco Talos Blog
T
Tor Project blog
The Hacker News
The Hacker News
The Last Watchdog
The Last Watchdog
C
CXSECURITY Database RSS Feed - CXSecurity.com
V2EX - 技术
V2EX - 技术
S
Secure Thoughts
AWS News Blog
AWS News Blog
W
WeLiveSecurity
云风的 BLOG
云风的 BLOG
V
V2EX
Last Week in AI
Last Week in AI
雷峰网
雷峰网
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
Google Developers Blog
P
Palo Alto Networks Blog
A
Arctic Wolf
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
M
MIT News - Artificial intelligence
V
Visual Studio Blog
C
CERT Recently Published Vulnerability Notes
WordPress大学
WordPress大学
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Threatpost
Simon Willison's Weblog
Simon Willison's Weblog
PCI Perspectives
PCI Perspectives
量子位
K
Kaspersky official blog
腾讯CDC
Schneier on Security
Schneier on Security
F
Full Disclosure
S
Schneier on Security

少数派

派早报:Google 发布 Fitbit Air 等 - 少数派 「新人报到」確認需求,再開始 - 少数派 从 SOLO 独立开发者社区,我看到了越来越多开发者开始做自己的产品 - 少数派 我怎么管理那些"不常做,但总会忘"的生活事项 - 少数派 人形机器人量产元年,数据才是具身智能的“生死线” - 少数派 BuhoLaunchpad 高度还原 Mac 启动台:开发历程与思考 - 少数派 五年陪伴依然不舍,DIY 换壳后让罗技 MX Master 3 继续服役 - 少数派 新玩意 240|少数派的编辑们最近买了啥? - 少数派 一日一技|为什么你应该关闭 iOS 的键盘声音 - 少数派 我做了个插件和 Skills,一键提取任何网站的设计规范 Design.md - 少数派 住在三四线城市的你,该开始录播客了 - 少数派 甘南秘境,大白高国 - 少数派 AI的审美:谁让把我变成川内倫子 - 少数派 返工怎能不烦恼,打工人片单总有一部是你的「嘴替」 - 少数派 为了让「上厕所」更健康,我做了一个小工具 - 少数派 AI + Skill,能够让生成的文章去除 AI 味吗? - 少数派 新玩意|韶音OpenDots ONE 耳夹式耳机 - 少数派 《美满》| 在每一个春天的晚上相爱(362) - 少数派 新玩意|优篮子 PS01 MagSnap 磁吸支架 - 少数派 自我整合手记 | 我开始早睡了:用稳定规则,为自由托底 - 少数派 用龙虾(OpenClaw)两个多月,我最深的12个体会 - 少数派 听歌时间到,12 张你可能错过的 2025 华语乐坛好专辑 - 少数派 承诺能追吗 - 少数派 macOS 26启动台没了? 我做了个不一样的App启动器 - Keboard - 少数派 《四海为家的人》| INTJ对话INTJ(361) - 少数派 你发过的那些黑历史,是时候一次清干净了 - 少数派 新玩意:安安静静玩,越玩越专注:计客密码机 - 少数派 iPad 用户首次体验 Android 平板:vivo Pad6 Pro - 少数派 数据逻辑强 - 少数派 极北行+ | 一路向北,探访日本至北之地 | 001 - 少数派 万字剖析:千问App深度体验报告(2026) - 少数派 在2026年,如何真正防止别人抄袭你的作品 - 少数派 怎么用 50 块搭个 AI 语音助手?我踩了 3 天坑 - 少数派 YeeroAI:让 AI 对话真正成为知识管理的一部分 - 少数派 爬泰山 - 少数派 「旅图显影」 App 更新:这次,我们补上了一点「手感」 - 少数派 假期出门太折磨?我的 23 条经验帮你规划惬意旅行 - 少数派 工作流会变吗 - 少数派 Claude Opus 4.6 怎么用最省钱?我测了 5 种方案 - 少数派 GPT Image 2 让图文并茂不再稀罕 - 少数派 用户侧出发——什么是AI,我要不要学习? - 少数派 找片、转存、整理、播放一条龙!让你的付费网盘值回票价 - 少数派 欢迎试用!日课一问2.0插件 - 少数派 自己做的MDeditor,原本想购买 Typora 试了两次支付不成功,干脆自己做一个 - 少数派 vibe coding了一个 3MB 的小工具,让 ~/Downloads 彻底告别混乱 - 少数派 因为受不了 Mac 的风扇策略,我做了一个风扇控制工具 - 少数派 别只怪模型 - 少数派 Warp 终端的 AI 功能怎么用?我测了一周的体验 - 少数派 AI 写代码老是出 bug?这 5 个配置我后悔没早知道 - 少数派 「新玩意」苹果出相机可能就这样:Sigma BF + 45mm F2.8 DG Contemporary - 少数派 一个面向2030年的AI操作系统是什么样子的:浅谈cola这款有灵魂的Agent - 少数派 别只看写代码 - 少数派 每天解决10个问题,还是一口气攻坚解决400个? - 少数派 AI 交易机器人怎么搭?我用 Claude 跑了一周实盘 - 少数派 Maptoposter Online:把你爱的城市画成艺术海报 - 少数派 Function Calling 怎么用?我测了 3 个模型发现差距真大 - 少数派 Legend Talk:我做了个 AI 圆桌,让 160 位思想家围着你的问题转 - 少数派 如何找到自己的蓝方?在小县城寻找压力测试 - 少数派 语音输入与软件接口|2026年聊AI时,我们都聊些什么(上) - 少数派 混动已经卖爆,纯电又来补刀——钛7闪充版简直“不讲武德” - 少数派 本月玩什么|朋友收藏、识质存在、沙罗周期 - 少数派 为什么要每天坚持输出? - 少数派 Claude API 挂了好几个小时,你的项目有备用方案吗? - 少数派 Function Calling 没你想的复杂——我用它做了个有点用的工具 - 少数派 登录系统立即播放视频或者图片音乐的软件 - 少数派 我为什么创建 FlipHTML5 下载工具 - 少数派 残局没电?多品牌外设电量统一管理软件EasyBluetooth已支持RTSS游戏内显示以及AIDA64 - 少数派 前往通义路的路 - 少数派 太好看了,媲美Sun的个人导航页,NAS部署星云门户 - 少数派 乌黑嘴唇“一键检测”上线了 - 少数派 派早报:Claude AI 接入多个创意软件生态、FILCO 生产方接手品牌等 - 少数派 【更新】BearCLI、Claude 连接器与 MCP 服务器 - 少数派 记了上千条流水,还是看不懂财务?我做了一个让 AI 读懂账本的工作台 - 少数派 MINI R56 升级原厂 Sport 模式 - 少数派 新玩意 | 一棵柠檬树(仿真版) - 少数派 Momenta的“物理AI”野望,需迈过“含摩量”这道关 - 少数派 网页直接投屏控制手机!NAS一键部署PandaScrcpy,流畅丝滑可远程。 - 少数派 众测|邀你一同探索随身 AI 硬件入口 YoooClaw C·ONE - 少数派 2050大会:分享时间是真诚 参会记 - 少数派 iPad 赋能电影创作:国内首部宣纸手绘长片《燃比娃》的幕后故事 - 少数派 AI的审美:我用 8 个大模型给 100 张旅行照片打分 - 少数派 普通人如何破圈?去参加一个本地协会 - 少数派 把极空间的图标全换了,主题DIY全攻略打造你的专属NAS桌面 - 少数派 电子便签墙,帮你实现便签自由 - 少数派 我如何用三个 CLI 工具取代文档创建需求 - 少数派 原来真的有人可以玩一辈子 - 少数派 社区速递 139 | 派友热议三月买了啥、复古单反尼康 Df 体验 - 少数派 06 作品的赏析与评价 - 少数派 TDS REVIEW|索尼 WF-1000XM6 降噪真无线耳机体验 - 少数派 35.98万起售的第二代腾势D9,我看重的不是堆料,而是不凑合 - 少数派 鼠须管 Squirrel 皮肤配置指北 - 少数派 从watch ultra2换到redmi watch6 - 少数派 派早报:阿里巴巴发布视频生成模型 HappyHorse 1.0 等 - 少数派 别迷信1M - 少数派 家人们天塌了!网盘“大封杀”,多个渠道多条路,NAS部署PanHub - 少数派 AI与人勾心斗角!NAS一键部署AI狼人杀,假日休闲必备。 - 少数派 电商必备!Comfyui工作流批量生图插件,一次生成12张!支持Nano banana pro模型 - 少数派 Comfyui工作流配置Gpt-image-2模型教程,0.03/张 - 少数派 OpenClaw第三方APi怎么配置?可使用Gpt-image-2模型 - 少数派 会员社区话题精选 Ep. 103 - 少数派
用 Termux + Syncthing 自动定时备份 Android 媒体文件 - 少数派
2025-09-08 · via 少数派

Matrix 首页推荐 

Matrix 是少数派的写作社区,我们主张分享真实的产品体验,有实用价值的经验与思考。我们会不定期挑选 Matrix 最优质的文章,展示来自用户的最真实的体验和观点。 
文章代表作者个人观点,少数派仅对标题和排版略作修改。


背景

我使用 Syncthing 在我的多个设备间(安卓手机、笔记本、台式机和 NAS)进行媒体文件的同步。选择 Syncthing 的原因有两点。相比 Nextcloud 这类功能全面的私有云,在除去掉不必要的多用户管理、文件分享和在线协作等功能后,Syncthing 更纯粹和轻量,专注于文件同步。而 Immich 这类自托管的媒体服务虽然提供了安卓客户端的备份功能,但它的文件存储结构由应用管理,不适合我这种希望保留原始文件夹结构的用户。

因此,我的最终方案是,使用 Syncthing 进行同步,并配合 Immich 的外部图库功能来进行管理。

然而,Syncthing 同步方案在安卓设备上存在一个问题:安卓的媒体文件(照片和视频、系统截图以及应用的图片等)散落在 DCIM、Pictures、Movies、Download 多个不同的目录下。固然可以为这些顶层目录各建立一个同步任务,但这样就会同步很多不必要的文件(例如放入回收站的媒体文件和应用临时产生的缩略图或缓存文件)。而如果为每一个需要同步的子文件,都手动建立一个同步任务,管理起来又过于繁琐了。自然而然地,一个想法产生了:创建一个统一的中转文件夹,然后定期将散落的媒体文件移动到这个中转站中,之后只需要让这个中转站保持同步即可。

方案

首先是脚本的执行方案。要在安卓手机上运行这个脚本,我立即想到了通过 Termux 来执行定时任务。折腾过安卓的人多少可能都听说过这个软件。简单的说,这是一个终端模拟器,无需 Root 和额外设置,只需要安装完软件就可以为安卓手机提供一个 Linux 环境。再通过一些设置和扩展,Termux 就可以访问设备的存储目录并且实现和系统的良好集成。用它来执行这个脚本再合适不过了。

然后是脚本的实现方式。固然可以编写一个纯粹的 Shell 脚本,通过后缀名来判断文件类型并执行移动操作,但这种识别方式并不可靠。更准确的方式是,通过读取文件的魔数(Magic Number),即文件开头的一串固定字节序列,来判断文件的类型,类 UNIX 系统中的 file 命令就是通过它来工作的。我选择了 Python 来实现脚本。Python 的 python-magic 库可以轻松地读取文件的魔数判断类型。在复杂的处理逻辑时(如排除特定的目录、忽略隐藏文件),Python 的代码结构和可读性也远优于 Shell 脚本。

实现

准备同步文件夹

首先,在安卓设备上创建一个单独的文件夹,作为 Syncthing 同步媒体文件的中转站,例如 /storage/emulated/0/Syncthing/pictures(即安卓存储目录下的 Syncthing 目录中的 pictures 文件夹,Syncthing 文件夹与 Download、DCIM 等文件夹处于同一级)。然后在 Syncthing 应用中,把这个文件夹设置为需要监控的媒体目录,也是唯一需要监控的媒体目录。安卓上的 Syncthing 应用可以使用这个

编写工具脚本

脚本工具的逻辑并不复杂:通过递归扫描指定路径,然后排除忽略的文件和目录并识别出需要处理的媒体文件,最后再执行移动到目标目录的操作即可。

以下是一个完整的 Python 实现示例代码:

```python
import os
import shutil
import datetime
import pathlib  # 使用 pathlib 来处理路径
import magic  # 使用 python-magic 识别媒体文件
import subprocess

# --- 配置 ---
HOME_DIR = pathlib.Path.home()

# 日志
LOG_DIR = HOME_DIR / "logs"
LOG_FILE = LOG_DIR / "scan_and_move_media.log"

# 安卓存储目录在 Termux 中的映射
SHARED_DIR = HOME_DIR / "storage/shared"
# 目标目录
TARGET_DIR = SHARED_DIR / "Syncthing/pictures"
# 扫描的源目录
INCLUDED_DIRS_CANDIDATES = ["DCIM", "Pictures", "Movies", "Download"]
# 排除的目录
EXCLUDED_SUBDIRS = {".thumbnails", "cache"}
# 媒体文件类型
MEDIA_MIME_PREFIXES = ("image/", "video/")

def log(message: str, level: str = "INFO", indent: int = 0):
    """
    结构化的日志记录函数。
    - level: 日志级别 (INFO, WARN, ERROR, DEBUG)
    - indent: 日志缩进层级,用于美化输出
    """
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    indent_str = "    " * indent
    log_message = f"{timestamp} [{level.upper():<5}] {indent_str}{message}\n"

    try:
        # 确保日志目录存在
        LOG_DIR.mkdir(parents=True, exist_ok=True)
        with open(LOG_FILE, "a", encoding="utf-8") as f:
            f.write(log_message)
    except Exception as e:
        print(f"CRITICAL: Failed to write to log file {LOG_FILE}: {e}")
        print(log_message)

def find_media_to_move() -> list:
    """
    扫描文件系统,找出所有需要移动的媒体文件。
    此函数只读,不修改任何文件。
    返回一个包含 (源路径,目标路径) 元组的列表。
    """
    log("Scan phase started.", level="INFO")
    move_operations = []

    for dir_name in INCLUDED_DIRS_CANDIDATES:
        scan_dir = SHARED_DIR / dir_name

        if not scan_dir.is_dir():
            log(f"Skipping non-existent directory: {scan_dir}", level="WARN")
            continue

        log(f"Scanning directory: {scan_dir}", indent=1)

        for root, dirs, files in os.walk(scan_dir, topdown=True):
            current_root = pathlib.Path(root)
            # 跳过 .nomedia 文件目录处理
            if ".nomedia" in files:
                log(
                    f"Skipping due to .nomedia: {current_root}", level="DEBUG", indent=2
                )
                dirs[:] = []
                continue
            # 跳过排除目录
            dirs[:] = [d for d in dirs if d not in EXCLUDED_SUBDIRS]

            for name in files:
                # 忽略隐藏文件
                if name.startswith("."):
                    continue

                file_path = current_root / name
                try:
                    mime_type = magic.from_file(str(file_path), mime=True)

                    if mime_type.startswith(MEDIA_MIME_PREFIXES):
                        # 计算相对路径以保持目录结构
                        rel_path = file_path.relative_to(scan_dir)
                        dest_path = TARGET_DIR / dir_name / rel_path

                        # 将操作添加到列表
                        move_operations.append((file_path, dest_path))
                        log(
                            f"Found media ({mime_type}): {file_path}",
                            level="DEBUG",
                            indent=2,
                        )

                except (IOError, OSError) as e:
                    log(f"Could not access {file_path}: {e}", level="ERROR", indent=2)
                except Exception as e:
                    if "0-byte file" in str(e):
                        pass
                    else:
                        log(
                            f"Unexpected error with {file_path}: {e}",
                            level="ERROR",
                            indent=2,
                        )

    log(
        f"Scan phase finished. Found {len(move_operations)} files to move.",
        level="INFO",
    )
    return move_operations

def execute_move_operations(operations: list):
    """
    执行文件移动操作
    """
    total_ops = len(operations)
    if total_ops == 0:
        log("No files to move.", level="INFO")
        return

    log(f"Execution phase started. Moving {total_ops} files...", level="INFO")

    success_count = 0
    failure_count = 0

    for i, (source_path, dest_path) in enumerate(operations, 1):
        log(f"Processing [{i}/{total_ops}]: {source_path}", indent=1)
        try:
            # 确保目标目录存在
            dest_path.parent.mkdir(parents=True, exist_ok=True)

            # 移动文件
            shutil.move(str(source_path), str(dest_path))
            log(f"Moved to: {dest_path}", level="DEBUG", indent=2)

            # 通知 Android MediaStore 扫描新文件,以便图库更新
            # 需要安装 Termux:API 插件
            log("Triggering media scan for the new file...", level="DEBUG", indent=2)
            # 使用 subprocess.run 调用 termux-media-scan
            result = subprocess.run(
                ["termux-media-scan", str(dest_path)],
                capture_output=True,
                text=True,
                check=False,
            )
            if result.returncode == 0:
                log("Media scan successful.", level="DEBUG", indent=2)
            else:
                # 记录扫描失败的错误,但继续执行
                log(
                    f"Media scan failed: {result.stderr.strip()}",
                    level="WARN",
                    indent=2,
                )
            # 如无需即时更新图库,可以注释掉 termux-media-scan 相关代码

            success_count += 1

        except Exception as e:
            log(f"Failed to move file: {e}", level="ERROR", indent=2)
            failure_count += 1

    log(
        f"Execution phase finished. Moved: {success_count}, Failed: {failure_count}.",
        level="INFO",
    )

def main():
    log("=========================================")
    log("Scan and Move script started.")
    try:
        operations_to_perform = find_media_to_move()
        execute_move_operations(operations_to_perform)
    except Exception as e:
        log(f"A critical error occurred in main execution: {e}", level="ERROR")
    finally:
        log("Script finished.")
        log("=========================================\n")

if __name__ == "__main__":
    main()

```

这个脚本可以直接复制保存为 scan_and_move_media.py 来使用。

设置自动化执行

最后就是脚本的自动化执行了。安装完 Termux 后,直接打开 APP 就可以启动终端进行配置。

Termux 基本配置

# 可选,但建议更换为国内的镜像源
# termux-change-repo

# 设置安卓存储目录在 Termux 环境中的软链接,需要给 Termux 进行授权
# 成功后可以通过 ls ~/storage/shared 查看手机存储目录
termux-setup-storage

# 安装必要的软件包
# file 提供 libmagic,这是 python-magic 必须的。cronie 提供定时任务。
pkg update && pkg upgrade
pkg install python file cronie

# 如果需要使用远程连接
# pkg install sshd
# sshd	

创建 Python 环境

python -m venv ~/.venv
source ~/.venv/bin/activate
pip install python-magic pathlib

设置定时任务

crontab -e 编辑定时任务后运行 crond 启动定时任务即可。我的定时任务如下:

    ~ $ crontab -l
    0 * * * * /data/data/com.termux/files/usr/bin/bash /data/data/com.termux/files/home/scripts/scan_and_move_media.sh
    * * * * * echo "$(date) - $(whoami)" >> ~/temp.logs   

第一行为实际执行的脚本,配置为 1 小时一次。第二行是用来监控 crond 是否在正常执行的。因为使用了虚拟环境的原因,我没有直接执行 Python 脚本,而是通过一个 Shell 脚本来执行的。并且为了统一管理,这个 Shell 脚本和第二步的 Python 脚本我都统一放在 `$HOME/scripts` 路径下。

#!/data/data/com.termux/files/usr/bin/bash

# 设置 trap:无论脚本是正常退出 (EXIT)、被中断 (INT) 还是被终止 (TERM) 都会释放唤醒锁
trap termux-wake-unlock EXIT INT TERM

# 获取 Termux 唤醒锁
termux-wake-lock

# 激活虚拟环境并执行 Python 脚本
source "$HOME/.venv/bin/activate"
python "$HOME/scripts/scan_and_move_media.py"

# 脚本正常退出,执行释放唤醒锁的命令
exit 0

最后别忘了给脚本可执行权限:

# 直接给 scripts 目录可执行权限

# ~ $ chmod +x -R scripts/

# 或者单独设置两个脚本的可执行权限

~ $ chmod +x scripts/scan_and_move_media.py

~ $ chmod +x scripts/scan_and_move_media.sh

完成配置后,让 Termux 在后台保持运行即可。一段时间后,可以通过检查日志文件,确定定时任务的执行情况。

优化

  • 开机启动:可以安装 Termux:Boot 应用,配置 crond 命令在安卓设备开机时自动启动。
  • 后台保活:让 Termux 获取唤醒锁或者将 Termux 加入电池优化白名单,避免定时任务执行失败。
  • 媒体库刷新:需要执行 pkg install termux-api,并在手机上安装 Termux:API应用。然后配合 Python 脚本中执行的 termux-media-scan 命令即可。

> 关注 少数派小红书,感受精彩数字生活 🍃

> 实用、好用的 正版软件,少数派为你呈现 🚀