第三十讲:综合实战——实现一个简易文件备份工具

终于到了最后一讲。说实话,前面二十九讲我们啃了很多硬骨头——从文件指针到二进制读写,从缓冲区到错误处理。今天,我们把所有东西串起来,做一个真正能用的东西。

一个文件备份工具。听起来简单?嗯,但我们要做的是:支持增量备份、压缩、校验。这三个功能加在一起,就不是玩具了。我在项目中做过类似的东西,当时是为了给嵌入式设备做固件备份,踩了不少坑。今天我把经验都倒给你。

整体架构设计

先别急着写代码。你想想看,一个备份工具需要哪些模块?

  • 文件扫描模块:遍历源目录,找出所有文件
  • 差异检测模块:判断哪些文件需要备份(增量备份的核心)
  • 压缩模块:对文件进行压缩,节省空间
  • 校验模块:计算哈希值,确保数据完整性
  • 备份执行模块:实际执行复制/压缩/校验操作

我习惯先把模块图画出来,再动手。这样写代码时心里有底。

简易文件备份工具架构图 主控模块 文件扫描模块 差异检测模块 压缩模块 校验模块 备份执行模块 备份存储(文件/目录) 增量备份 + 压缩 + 校验 = 可靠备份

增量备份的实现思路

增量备份说白了就是:只备份变化的部分。第一次全量备份,后面只备份新增或修改的文件。

怎么判断文件变了?我常用的方法是:比较文件的修改时间戳和大小。当然,更严谨的做法是计算文件内容的哈希值,但那样性能开销大。对于大多数场景,时间戳+大小已经够用了。

我曾经遇到过一个坑:有些编辑器保存文件时,只修改内容不更新修改时间。结果增量备份漏掉了文件。后来我加了一个选项,允许用户选择「强制全量校验」模式。

核心数据结构:用一个清单文件记录上次备份的状态

// 备份清单结构
typedef struct {
    char filename[256];      // 文件名
    long last_modified;      // 最后修改时间戳
    long file_size;          // 文件大小
    unsigned int checksum;   // 校验和(CRC32)
    int is_compressed;       // 是否已压缩
} BackupEntry;

// 清单文件格式:二进制存储,便于快速读取
// 文件头 + 条目数组

压缩模块——用zlib做简易压缩

压缩不是必须的,但能省不少空间。我建议用zlib库,它轻量、稳定,嵌入式系统也能用。

压缩的流程很简单:

  1. 读取源文件内容到内存
  2. 调用zlib的compress()函数压缩
  3. 将压缩后的数据写入备份文件

解压时反过来:用uncompress()。

#include <zlib.h>

int compress_file(const char *src, const char *dst) {
    FILE *fin = fopen(src, "rb");
    if (!fin) return -1;
    
    // 获取文件大小
    fseek(fin, 0, SEEK_END);
    long src_len = ftell(fin);
    rewind(fin);
    
    // 读取源文件
    unsigned char *src_buf = malloc(src_len);
    fread(src_buf, 1, src_len, fin);
    fclose(fin);
    
    // 压缩(zlib的compress函数)
    unsigned long dst_len = compressBound(src_len);
    unsigned char *dst_buf = malloc(dst_len);
    compress(dst_buf, &dst_len, src_buf, src_len);
    
    // 写入压缩后的数据
    FILE *fout = fopen(dst, "wb");
    fwrite(dst_buf, 1, dst_len, fout);
    fclose(fout);
    
    free(src_buf);
    free(dst_buf);
    return 0;
}

小技巧:压缩级别不是越高越好。我一般用Z_BEST_SPEED,速度快,压缩率也还行。对于文本文件,压缩率能达到50%以上;对于已经压缩过的文件(如jpg、mp4),再压缩基本没效果,可以跳过。

校验模块——CRC32还是MD5?

校验是为了确保备份的数据没有损坏。我推荐用CRC32,原因有三:

  • 计算速度快,适合大文件
  • 能检测出常见的传输错误
  • zlib库自带CRC32函数,不用额外引入依赖

当然,如果你对安全性要求极高(比如备份加密数据),可以用SHA256。但一般场景下,CRC32足够了。

// 计算文件的CRC32校验值
unsigned int calc_file_crc32(const char *filename) {
    FILE *fp = fopen(filename, "rb");
    if (!fp) return 0;
    
    unsigned int crc = crc32(0L, Z_NULL, 0);
    unsigned char buf[4096];
    size_t bytes;
    
    while ((bytes = fread(buf, 1, sizeof(buf), fp)) > 0) {
        crc = crc32(crc, buf, bytes);
    }
    
    fclose(fp);
    return crc;
}

注意:校验值要存储在备份清单中,而不是备份文件本身。否则文件损坏时,你连校验值都读不到。我曾经犯过这个错,恢复数据时才发现校验值也跟着坏了……从那以后,我把校验清单单独存一份。

主控流程——把一切串起来

好了,模块都有了,现在把它们组装起来。主控流程大概是这样的:

  1. 读取上次的备份清单
  2. 扫描源目录,获取当前文件列表
  3. 对比清单,找出新增或修改的文件
  4. 对需要备份的文件进行压缩
  5. 计算压缩后的CRC32校验值
  6. 将压缩文件复制到备份目录
  7. 更新备份清单

代码实现上,我习惯用函数指针数组来做模块调度,这样扩展性更好。但今天为了清晰,我们直接用顺序调用。

int do_backup(const char *src_dir, const char *backup_dir) {
    // 1. 加载上次的备份清单
    BackupEntry *old_list = NULL;
    int old_count = load_backup_list(backup_dir, &old_list);
    
    // 2. 扫描源目录
    BackupEntry *new_list = NULL;
    int new_count = scan_directory(src_dir, &new_list);
    
    // 3. 差异检测
    int need_backup[new_count];
    detect_changes(old_list, old_count, new_list, new_count, need_backup);
    
    // 4. 执行备份
    for (int i = 0; i < new_count; i++) {
        if (!need_backup[i]) continue;
        
        char src_path[512], dst_path[512];
        snprintf(src_path, sizeof(src_path), "%s/%s", src_dir, new_list[i].filename);
        snprintf(dst_path, sizeof(dst_path), "%s/%s.zlib", backup_dir, new_list[i].filename);
        
        // 压缩
        compress_file(src_path, dst_path);
        
        // 校验
        new_list[i].checksum = calc_file_crc32(dst_path);
        new_list[i].is_compressed = 1;
    }
    
    // 5. 保存新的备份清单
    save_backup_list(backup_dir, new_list, new_count);
    
    // 清理
    free(old_list);
    free(new_list);
    
    return 0;
}

避坑指南——我踩过的那些坑

做这个工具时,我遇到了几个典型问题,分享给你:

  • 路径长度问题:Windows下路径超过260字符会出问题。我后来改用相对路径存储,运行时再拼接绝对路径。
  • 文件被占用:备份过程中,源文件可能正在被写入。我加了重试机制,最多重试3次,每次间隔100ms。
  • 磁盘空间不足:压缩前先检查剩余空间,不够就报错退出。别等到写一半才崩溃。
  • 增量备份的边界情况:文件被删除后又重新创建,时间戳可能比旧文件还早。我额外比较了inode号(Linux下)或文件索引(Windows下)。

总结一下:一个靠谱的备份工具,核心就三点——知道要备份什么(增量)、怎么存(压缩)、怎么保证没错(校验)。把这三点做好,你的数据就安全了。

好了,这就是《C语言文件操作专题》的最后一讲。三十讲下来,我们从最基础的fopen讲到了今天的综合实战。说实话,文件操作看起来简单,但真正用好、用对,需要很多实践。希望这些内容对你有帮助。

如果你在实际项目中遇到了文件操作的问题,欢迎交流。记住:写代码容易,写出可靠的代码不容易。多思考边界情况,多测试异常流程,你的代码会越来越稳。


公众号:蓝海资料掘金营,微信deep3321