第1章:综合实战——实现一个完整的文件备份工具
说实话,做C语言开发这么多年,我遇到过不少让人头疼的场景。有一次,客户的生产服务器突然宕机,数据丢了整整三天。原因是什么?备份脚本写得不够健壮,压缩和加密的流程出了岔子。从那以后,我就养成了一个习惯:备份工具必须自己写,而且要支持增量备份、压缩、加密、日志。
这一章,我们就来动手实现一个完整的文件备份工具。别小看它,麻雀虽小五脏俱全。你想想看,一个生产级的备份工具,核心就四个功能:
- 增量备份:只备份变化的部分,节省时间和空间
- 压缩:减少存储占用
- 加密:保证数据安全
- 日志:记录每一步操作,方便排查问题
我个人习惯把这类工具拆成模块来写。每个模块独立测试,最后再拼起来。这样出了问题,定位也快。
1.1 整体架构设计
我们先画一张图,看看这个工具的整体流程。嗯,这里要注意,架构设计决定了代码的扩展性。我见过太多人一上来就写main函数,写到一半发现逻辑乱成一团。
从图上可以看得很清楚:主控模块负责调度,四个子模块各司其职。数据从源文件流入,经过增量对比、压缩、加密,最后写入目标文件。每一步的操作都会被日志模块记录下来。
1.2 增量备份的核心逻辑
增量备份说白了就是:只备份那些发生变化的文件。怎么判断文件变了?我常用的方法是记录文件的最后修改时间(mtime)和文件大小。
我曾经踩过一个坑:直接用mtime做判断,结果系统时间被修改了,导致所有文件都被重新备份了一遍。后来我加上了文件内容的哈希校验,双重保险。
增量备份的判断依据:
- 文件不存在于备份目录 → 全量备份
- 文件存在,但mtime或大小不同 → 增量备份
- 文件存在,mtime和大小都相同 → 跳过
代码实现上,我们需要一个元数据文件来记录每个文件的备份状态。这个文件可以用简单的文本格式,也可以用二进制。我个人偏好二进制,解析速度快,而且不容易被误修改。
// 元数据结构体
typedef struct {
char filename[256]; // 文件名
time_t last_mtime; // 最后修改时间
off_t file_size; // 文件大小
unsigned char hash[16]; // MD5哈希(可选)
} FileMeta;
// 判断是否需要备份
int need_backup(const char *filepath, FileMeta *meta) {
struct stat st;
if (stat(filepath, &st) != 0) {
return -1; // 文件不存在
}
// 如果元数据为空,说明是首次备份
if (meta->last_mtime == 0) {
return 1;
}
// 比较mtime和大小
if (st.st_mtime != meta->last_mtime ||
st.st_size != meta->file_size) {
return 1; // 发生了变化
}
return 0; // 没有变化
}
小技巧:如果你要备份的文件数量很大(比如几十万个),建议用哈希表或者B树来存储元数据。线性查找会慢到你怀疑人生。我之前用线性查找处理10万个文件,花了将近3分钟。换成哈希表后,秒级完成。
1.3 压缩与加密的实现
压缩和加密这两个功能,我建议用现成的库。C语言标准库没有直接提供这些功能,但我们可以借助zlib和OpenSSL。
| 功能 | 推荐库 | 核心函数 |
|---|---|---|
| 压缩 | zlib | compress() / uncompress() |
| 加密 | OpenSSL | AES_encrypt() / AES_decrypt() |
压缩的流程很简单:读取源文件数据,调用compress()压缩,写入目标文件。加密则是在压缩之后进行,先压缩再加密,这样效率更高。
注意:加密密钥的管理是个大问题。千万不要把密钥硬编码在代码里。我建议从环境变量读取,或者使用密钥文件,并设置严格的权限(比如600)。我曾经见过有人把密钥写在代码里,结果代码上传到GitHub,整个公司的数据都暴露了。
// 压缩并加密一个文件
int compress_and_encrypt(const char *src, const char *dst,
const unsigned char *key) {
// 1. 读取源文件
FILE *fp_src = fopen(src, "rb");
if (!fp_src) return -1;
// 获取文件大小
fseek(fp_src, 0, SEEK_END);
long src_size = ftell(fp_src);
rewind(fp_src);
// 2. 分配缓冲区
unsigned char *src_data = malloc(src_size);
unsigned char *compressed = malloc(src_size + 64); // 压缩后可能略大
fread(src_data, 1, src_size, fp_src);
fclose(fp_src);
// 3. 压缩
uLong compressed_size = src_size + 64;
compress(compressed, &compressed_size, src_data, src_size);
// 4. 加密(使用AES-256-CBC)
unsigned char *encrypted = malloc(compressed_size + 16);
int encrypted_size = aes_encrypt(compressed, compressed_size,
encrypted, key);
// 5. 写入目标文件
FILE *fp_dst = fopen(dst, "wb");
fwrite(encrypted, 1, encrypted_size, fp_dst);
fclose(fp_dst);
// 6. 清理
free(src_data);
free(compressed);
free(encrypted);
return 0;
}
1.4 日志系统的设计
日志这东西,平时觉得没用,出问题的时候才知道它的好。我设计的日志系统包含三个级别:
- INFO:正常操作记录,比如「文件xxx备份成功」
- WARN:警告信息,比如「文件xxx跳过备份」
- ERROR:错误信息,比如「文件xxx打开失败」
日志格式我建议统一为:[时间] [级别] [模块] 消息。这样用grep过滤起来特别方便。
void log_message(const char *level, const char *module,
const char *format, ...) {
time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
char time_str[20];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info);
// 打开日志文件(追加模式)
FILE *log_fp = fopen("backup.log", "a");
if (!log_fp) return;
// 格式化消息
fprintf(log_fp, "[%s] [%s] [%s] ", time_str, level, module);
va_list args;
va_start(args, format);
vfprintf(log_fp, format, args);
va_end(args);
fprintf(log_fp, "\n");
fclose(log_fp);
}
建议:日志文件要定期轮转。我一般设置单个日志文件不超过10MB,超过就自动压缩归档。不然日志文件越来越大,最后磁盘满了,备份工具自己先挂了,那就尴尬了。
1.5 主控流程整合
好了,四个模块都准备好了。现在把它们串起来。主控流程大概是这样的:
- 读取配置文件,获取源目录、目标目录、密钥等信息
- 加载元数据文件,建立文件状态索引
- 遍历源目录,对每个文件调用need_backup()判断
- 如果需要备份,执行压缩+加密,写入目标目录
- 更新元数据文件
- 记录日志
嗯,这里要注意一点:遍历目录时要递归处理子目录。我习惯用nftw()函数,它比自己写递归要稳定得多。
int main(int argc, char *argv[]) {
// 初始化日志
log_message("INFO", "MAIN", "备份工具启动");
// 加载配置
Config cfg;
if (load_config("backup.conf", &cfg) != 0) {
log_message("ERROR", "MAIN", "加载配置文件失败");
return 1;
}
// 加载元数据
MetaDB *db = meta_db_open("backup.meta");
// 遍历源目录
nftw(cfg.src_dir, backup_callback, 20, FTW_PHYS);
// 保存元数据
meta_db_save(db);
meta_db_close(db);
log_message("INFO", "MAIN", "备份完成");
return 0;
}
说实话,这个工具写完之后,我用了好几年。每次遇到新的需求,比如支持远程备份、断点续传,我就在这个框架上扩展。模块化的好处就在这里——你不需要重写整个系统,只需要新增一个模块,然后改几行主控代码。
最后提醒一句:写完一定要测试。我建议准备一个测试目录,里面放各种奇奇怪怪的文件名(带空格、带中文、带特殊符号),看看你的工具能不能扛得住。我曾经就因为没处理文件名中的空格,导致备份脚本在线上跑崩了。从那以后,我对文件名处理格外小心。
公众号:蓝海资料掘金营,微信deep3321