最近每天打开电脑都被提醒:『您的磁盘几乎已满』。我知道,那些过去下载的杂乱文件、传递消息的截图录屏、保存即看过的各种文档,叒叒一次要撑爆我 256g 的磁盘了。但白天太忙,我实在懒得去整理他们,所以习惯去 mute 它。第二天,打开电脑继续被提醒:『您的磁盘几乎已满』。。。
好难受,还是处理下吧,写个程序自动处理下吧,如果你也遇到这种问题,我们一起来看下吧。
确定整理文件的策略
在动手之前我们需要分析一下机器的文件目录(比如我的这台工作电脑),制定一些处理策略。显然我需要处理那些体积大的目录,但常用的工作目录、应用程序、系统文件目录,现在不该去碰它们,应该等到空余时间多的时候专门处理。
除去这些目录之后,体积最大的 『下载 download』 就是这台工作电脑上我们可以动的首要目录,此外 『文稿document』 和 『桌面 desktop』 也是我们应该去清理的常用目录。我们姑且确定就清理这三个目录。
对于这三个目录里的各种文件,应该有几种处理:
- 删除。比如图片、视频(我清楚的知道他们都来自日常工作消息传递中留存的,现在和未来不会再用,这台工作电脑本地无需留存,你也可以自己决定)、下载包、压缩包等。
- 保留。有用的文档、资料、或者从程序角度目前无法判断的文件。
所以我决定先检测一下这三个目录下到底有哪些文件。新建一个 js 文件,我们定义好需要检测的目录
checkingDirectories
, 主函数是一个 checkFolders
,他会遍历 checkingDirectories
下的每一个目录。
接下来我们需要写 checkDir
检查被检测目录下的所有文件并标记文件类型。如果这个文件是一个目录,我会标记成 dir
,如果是文件则标记文件类型,如果最后依然识别不出,就标记成 unknown
。
那么我们如何标记文件类型呢?最简单方法当然是通过后缀名,但这也是最粗糙的方法,毕竟后缀名可以随便改。
更好的方法也是我们这篇文章涉及的一个知识点,通过 Magic Number 判断文件类型。对于某一些类型的文件,起始的几个字节内容都是固定的,会有一个数字标识文件类型,这个数字可以是任意值、也可以占据任何字节(通常是4或8个字节)。计算机可以根据这几个字节的内容就可以判断文件类型,这些特定字节也被称为魔数 Magic Number。 通过 文件的 Magic Number 列表,我可以查到几乎所有文件的 Magic Number,注意是几乎所有,因为这个表也在动态更新,也可能有没覆盖到的特殊格式。
This is a dynamic list and may never be able to satisfy particular standards for completeness.
但,我想应该可以覆盖我这个工作电脑上的文件了,所以我们先找一个基于 Magic Number 的文件类型判断库,比如 file-type。所以 checkDir
实现如下:
最后我们用 console.table
展示一下统计数据:
根据这个数据,我又更新了一次处理思路:
- 毫不犹豫删除:比如图片、视频、下载包(通常已经被解压成文件夹)、压缩包,这些我确定需要删除的文件。
- 判断访问时间再删除:比如一年多我都没有碰过的文件,大概率我已经忘了他,不再需要,可以删除。
- 保留并且分类:比如会把 pptx、pdf 之类的文件移到 doc 目录下,把 psd、stl 等文件移到 workfile 目录下。
- 跳过不处理:文件夹(大部分文件夹是有用的)和未识别类型文件(保险起见)。
因此,我们增加一些常量配置来定义要做的处理:
按策略处理文件
首先改造一下主函数 checkFolders
可以看到我们增加一个 makeFileTypeActions
用来把配置常量转换成方便处理的 map 结构(这里我期望这个 map 的 key 是文件类型,value 是要做的操作),同时保证每个文件类型只有一种操作(即使之前有重复的操作配置,或者配置错了)。所以我们会按顺序遍历 SKIP_FILE_TYPES
、DELETE_FILE_TYPES
、MOVE_FILE_TYPES
:
我们还增加了一个 actionLog
用来记录日志,会把日志存到 /logs
目录下并用时间戳作为文件名:
接着我们需要改造一下 checkDir
。为了判断一个文件是否很久没被访问,我们从 fs.stat
中取出文件的上一次被访问时间 atimeMs
,并且增加一个 doFileActions
操作方法。
doFileActions
的实现也很简单,即实现三种操作。其中我们增加了一个可以接受的最久的访问天数
THRESHOLD_DAY
(期望他是可配置的),并将他转换成距离现在的毫秒数 THRESHOLD
用来跟文件的
atimeMs
作比较,超出也会删除文件:
看起来为了保险起见我们先不执行真正的文件操作,而是仅记录日志。
定时自动整理
到这里我们的程序看起来基本写完了,但你可能不想每次都手动执行,希望它可以每周或者每个月跑一次,所以接下来我们再增加一个定时任务,让我们可以配置执行周期。
为了更容易配置,首先我们把所有配置抽成一个文件并完善一下它 .cf-config.js
,这里的每个配置项你都可以为不填置空。注意我们增加了一个 scheduleCron
支持配置 Cron 格式的定时任务配置(比如这里设置成了每个月20号2点触发 0 0 2 20 * ?
):
接着把主代码改成一个模块 lib/cleanfiles.js
,方便读取配置和调用:
同时为了让我们可以定时调用,增加了一个 resetActionState
方法在每次定时任务执行时重置一些状态。
之后我们就可以新建一个入口 index.js
定时调用 CleanFiles
模块了:
最后 npm init
一下并配置到 package.json
, 让它看起来更像一个可以用的模块包:
而我们整个项目也变成了这样:
自定义文件类型判断
看起来我们前面已经做完了所有事,只需要把 doFileActions
中真实的文件操作注释放开就可以。
但在这之前,我又检查了一遍日志,发现了一些文件判断有问题,比如:
key
文件因为被识别成了 zip
而被标记成删除,dmg
和 geojson
文件因为被识别成了 unknown
而被标记成跳过。显然是因为之前提到的现有的 Magic Number 列表无法覆盖所文件格式。
所以我们去看下使用的 file-type 是如何判断的,如果有误判我们得自己补充。比如对 zip 的判断,可以看到知检测了前四个字节:
我们再简单加一端代码测试一下 key
文件的头部二进制:
可以看到头部二进制前十个字节都是一样的,也就是说我们应该提取前十个字节 0x50 0x4b 0x03 0x04 0x14 0x00 0x00 0x00 0x00 0x00
作为 Magic Number:
同样的方法我们可以得到 dmg
文件的 Magic Number 是 0x78 0xda 0x63 0x60 0x18 0x05 0x43 0x18 0xfc 0xfb 0xff 0xff 0x1d 0x10 0x33 0x02 0x99
这类无法识别的文件未来可能还会有,为了以后可以灵活补充 Magic Number,我们应该支持通过配置文件添加 Magic Number :
然后我们增加一个 CustomFileChecker
类专门用来做读取配置的 Magic Number 做自定义文件检测处理:
我们再把 customFileType
改造成调用 customFileChecker.checkeFileType
。注意我们发现的特殊 Magic Number 长度都是不固定的,不再是 4 或 8个字节,所以需要比较的文件头长度也不固定。但如果直接拿整个文件去比较显然也没有必要,所以我们暂定截取前 20 个字节作为文件头(如果以后不够长再补充吧)。
随后我们检测文件类型时增加一步,先执行自定义的判断 customFileType
, 如果没有匹配上,再调用原来的逻辑 用 file-type 库判断:
更多优化
到此,我们基本完成了目前能想到的优化处理。打开处理文件的注释,执行了一遍任务,相信我短期内不会再看到『您的磁盘几乎已满』的推送了。
总结一下:
我们通过 Magic Number 判断文件类型、通过文件上一次被访问时间找出了长期不访问的文件、通过 nodejs fs api 完成了各种文件查找、遍历、移动和删除处理,并从零到一开发了一个可以灵活配置的定时整理文件的工具。
我把这个简陋的模块叫做 clean-files,如果你感兴趣,欢迎查看源码: https://github.com/AlbertAZ1992/clean-files
事实上这个模块还有很多优化可以做,比如怎样把这个模块改造成一个 cli,添加进程守护常驻后台;怎样让文件头的 Magic Number 检查效率更高;等等。如果有时间,后续会持续优化。