DeepSeek用C编写的支持Zstandard 压缩的 ZIP 工具

本意是想让它写一个能把一个目录下所有文件及子目录都压缩的工具就够了,又想,既然提要求了,干脆就多提几点,于是有了下面这个。

c 复制代码
/**
 * zipzstd.c - 支持 Zstandard 压缩的 ZIP 工具
 * 支持参数: 通配符, 递归, 压缩级别, 线程数, 输出文件, 解压缩, 排除顶级目录
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <zip.h>
#include <dirent.h>
#include <sys/stat.h>
#include <fnmatch.h>
#include <pthread.h>
#include <errno.h>
#include <getopt.h>
#include <libgen.h>

#ifdef _WIN32
#include <direct.h>
#define mkdir(path, mode) _mkdir(path)
#define PATH_SEPARATOR '\\'
#else
#include <unistd.h>
#include <libgen.h>
#define PATH_SEPARATOR '/'
#endif

// 配置结构体
typedef struct {
    int compress;           // 1=压缩, 0=解压
    int recursive;          // 递归子目录
    int compression_level;  // 压缩级别
    int thread_count;       // 线程数
    char *output_file;      // 输出文件名
    int no_top_dir;         // 不包括顶级目录
    char **input_patterns;  // 输入文件模式
    int pattern_count;      // 模式数量
} Config;

// 文件信息结构体
typedef struct {
    char *path;            // 文件完整路径
    char *archive_path;    // 在压缩包中的路径
    struct stat file_stat; // 文件状态信息
} FileInfo;

// 全局变量
FileInfo *file_list = NULL;
size_t file_count = 0;
size_t file_capacity = 0;
pthread_mutex_t file_mutex = PTHREAD_MUTEX_INITIALIZER;

// 初始化配置
void init_config(Config *config) {
    config->compress = 1;
    config->recursive = 0;
    config->compression_level = 6;
    config->thread_count = 1;
    config->output_file = NULL;
    config->no_top_dir = 0;
    config->input_patterns = NULL;
    config->pattern_count = 0;
}

// 释放配置资源
void free_config(Config *config) {
    if (config->output_file) free(config->output_file);
    for (int i = 0; i < config->pattern_count; i++) {
        free(config->input_patterns[i]);
    }
    free(config->input_patterns);
}

// 打印帮助信息
void print_help(const char *program_name) {
    printf("用法: %s [选项] <文件/模式...>\n", program_name);
    printf("选项:\n");
    printf("  -d          解压缩模式\n");
    printf("  -r          递归处理子目录\n");
    printf("  -数字       压缩级别 (1-22, 默认:6)\n");
    printf("  -T数字      线程数 (默认:1)\n");
    printf("  -o文件      输出文件名\n");
    printf("  -notop      不包括顶级目录\n");
    printf("  -h          显示帮助信息\n");
    printf("\n示例:\n");
    printf("  %s *.txt *.c -r -9 -T4 -o archive.zip\n", program_name);
    printf("  %s -d archive.zip -o ./output\n", program_name);
    printf("  %s docs/* -r -notop -o docs.zip\n", program_name);
}

// 解析命令行参数
int parse_arguments(int argc, char *argv[], Config *config) {
    static struct option long_options[] = {
        {"help", no_argument, 0, 'h'},
        {0, 0, 0, 0}
    };

    int opt;
    while ((opt = getopt_long(argc, argv, "drT:o:h", long_options, NULL)) != -1) {
        switch (opt) {
            case 'd':
                config->compress = 0;
                break;
            case 'r':
                config->recursive = 1;
                break;
            case 'T':
                config->thread_count = atoi(optarg);
                if (config->thread_count < 1) config->thread_count = 1;
                break;
            case 'o':
                config->output_file = strdup(optarg);
                break;
            case 'h':
                print_help(argv[0]);
                exit(0);
            default:
                // 检查数字参数 (压缩级别)
                if (optopt == 0 && optarg && optarg[0] == '-') {
                    char *p = optarg + 1;
                    if (*p >= '0' && *p <= '9') {
                        config->compression_level = atoi(p);
                        if (config->compression_level < 1) config->compression_level = 1;
                        if (config->compression_level > 22) config->compression_level = 22;
                        break;
                    }
                }
                fprintf(stderr, "未知选项: -%c\n", opt);
                return -1;
        }
    }

    // 检查 -notop 参数
    for (int i = optind; i < argc; i++) {
        if (strcmp(argv[i], "-notop") == 0) {
            config->no_top_dir = 1;
            // 移除这个参数
            for (int j = i; j < argc - 1; j++) {
                argv[j] = argv[j + 1];
            }
            argc--;
            break;
        }
    }

    // 获取输入模式
    if (optind < argc) {
        config->pattern_count = argc - optind;
        config->input_patterns = malloc(config->pattern_count * sizeof(char *));
        for (int i = 0; i < config->pattern_count; i++) {
            config->input_patterns[i] = strdup(argv[optind + i]);
        }
    } else {
        fprintf(stderr, "错误: 需要指定输入文件或模式\n");
        return -1;
    }

    return 0;
}

// 检查文件是否匹配模式
int matches_pattern(const char *filename, Config *config) {
    for (int i = 0; i < config->pattern_count; i++) {
        if (fnmatch(config->input_patterns[i], filename, 0) == 0) {
            return 1;
        }
    }
    return 0;
}

// 递归收集文件
void collect_files(const char *base_path, const char *current_path, Config *config) {
    char full_path[1024];
    if (strlen(current_path) > 0) {
        snprintf(full_path, sizeof(full_path), "%s%c%s", base_path, PATH_SEPARATOR, current_path);
    } else {
        snprintf(full_path, sizeof(full_path), "%s", base_path);
    }

    DIR *dir = opendir(full_path);
    if (!dir) return;

    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            continue;
        }

        char new_path[1024];
        if (strlen(current_path) > 0) {
            snprintf(new_path, sizeof(new_path), "%s%c%s", current_path, PATH_SEPARATOR, entry->d_name);
        } else {
            snprintf(new_path, sizeof(new_path), "%s", entry->d_name);
        }

        char absolute_path[1024];
        snprintf(absolute_path, sizeof(absolute_path), "%s%c%s", full_path, PATH_SEPARATOR, entry->d_name);

        struct stat st;
        if (stat(absolute_path, &st) == 0) {
            if (S_ISDIR(st.st_mode)) {
                if (config->recursive) {
                    collect_files(base_path, new_path, config);
                }
            } else {
                // 检查文件是否匹配模式
                if (matches_pattern(entry->d_name, config) || 
                    matches_pattern(new_path, config) ||
                    config->pattern_count == 0) {
                    
                    pthread_mutex_lock(&file_mutex);
                    
                    if (file_count >= file_capacity) {
                        file_capacity = file_capacity ? file_capacity * 2 : 64;
                        file_list = realloc(file_list, file_capacity * sizeof(FileInfo));
                    }
                    
                    file_list[file_count].path = strdup(absolute_path);
                    
                    // 处理压缩包中的路径
                    if (config->no_top_dir) {
                        // 移除顶级目录部分
                        char *first_sep = strchr(new_path, PATH_SEPARATOR);
                        if (first_sep) {
                            file_list[file_count].archive_path = strdup(first_sep + 1);
                        } else {
                            file_list[file_count].archive_path = strdup(new_path);
                        }
                    } else {
                        file_list[file_count].archive_path = strdup(new_path);
                    }
                    
                    file_list[file_count].file_stat = st;
                    file_count++;
                    
                    pthread_mutex_unlock(&file_mutex);
                }
            }
        }
    }
    closedir(dir);
}

// 压缩文件线程函数
void *compress_thread(void *arg) {
    struct zip *archive = (struct zip *)arg;
    
    while (1) {
        pthread_mutex_lock(&file_mutex);
        static size_t current_index = 0;
        if (current_index >= file_count) {
            pthread_mutex_unlock(&file_mutex);
            break;
        }
        FileInfo file = file_list[current_index++];
        pthread_mutex_unlock(&file_mutex);

        // 读取文件内容
        FILE *fp = fopen(file.path, "rb");
        if (!fp) {
            fprintf(stderr, "无法打开文件: %s\n", file.path);
            continue;
        }

        fseek(fp, 0, SEEK_END);
        long size = ftell(fp);
        fseek(fp, 0, SEEK_SET);

        char *buffer = malloc(size);
        fread(buffer, 1, size, fp);
        fclose(fp);

        // 添加到压缩包
        struct zip_source *source = zip_source_buffer(archive, buffer, size, 1);
        if (source) {
            zip_int64_t index = zip_file_add(archive, file.archive_path, source, ZIP_FL_ENC_UTF_8);
            if (index >= 0) {
                zip_set_file_compression(archive, index, ZIP_CM_ZSTD, 6);
                printf("已添加: %s -> %s\n", file.path, file.archive_path);
            } else {
                zip_source_free(source);
                fprintf(stderr, "添加文件失败: %s\n", file.archive_path);
            }
        } else {
            free(buffer);
            fprintf(stderr, "创建源失败: %s\n", file.archive_path);
        }

        free(file.path);
        free(file.archive_path);
    }

    return NULL;
}

// 压缩文件
int compress_files(Config *config) {
    if (!config->output_file) {
        config->output_file = strdup("output.zip");
    }

    // 收集文件
    for (int i = 0; i < config->pattern_count; i++) {
        struct stat st;
        if (stat(config->input_patterns[i], &st) == 0) {
            if (S_ISDIR(st.st_mode)) {
                collect_files(config->input_patterns[i], "", config);
            } else {
                // 单个文件
                pthread_mutex_lock(&file_mutex);
                if (file_count >= file_capacity) {
                    file_capacity = file_capacity ? file_capacity * 2 : 64;
                    file_list = realloc(file_list, file_capacity * sizeof(FileInfo));
                }
                file_list[file_count].path = strdup(config->input_patterns[i]);
                
                if (config->no_top_dir) {
                    char *base = basename(config->input_patterns[i]);
                    file_list[file_count].archive_path = strdup(base);
                } else {
                    file_list[file_count].archive_path = strdup(config->input_patterns[i]);
                }
                
                file_list[file_count].file_stat = st;
                file_count++;
                pthread_mutex_unlock(&file_mutex);
            }
        }
    }

    if (file_count == 0) {
        fprintf(stderr, "错误: 没有找到匹配的文件\n");
        return -1;
    }

    printf("找到 %zu 个文件进行压缩\n", file_count);

    // 创建压缩包
    int err;
    struct zip *archive = zip_open(config->output_file, ZIP_CREATE | ZIP_TRUNCATE, &err);
    if (!archive) {
        zip_error_t error;
        zip_error_init_with_code(&error, err);
        fprintf(stderr, "无法创建压缩包: %s\n", zip_error_strerror(&error));
        zip_error_fini(&error);
        return -1;
    }

    // 创建压缩线程
    pthread_t *threads = malloc(config->thread_count * sizeof(pthread_t));
    for (int i = 0; i < config->thread_count; i++) {
        pthread_create(&threads[i], NULL, compress_thread, archive);
    }

    // 等待所有线程完成
    for (int i = 0; i < config->thread_count; i++) {
        pthread_join(threads[i], NULL);
    }

    free(threads);

    // 关闭压缩包
    if (zip_close(archive) != 0) {
        fprintf(stderr, "关闭压缩包失败: %s\n", zip_strerror(archive));
        return -1;
    }

    printf("压缩完成: %s (%zu 个文件)\n", config->output_file, file_count);
    return 0;
}

// 解压文件
int decompress_files(Config *config) {
    if (!config->input_patterns || config->pattern_count == 0) {
        fprintf(stderr, "错误: 需要指定要解压的ZIP文件\n");
        return -1;
    }

    const char *zip_file = config->input_patterns[0];
    const char *output_dir = config->output_file ? config->output_file : ".";

    // 创建输出目录
    struct stat st;
    if (stat(output_dir, &st) != 0) {
        if (mkdir(output_dir, 0755) != 0) {
            fprintf(stderr, "无法创建输出目录: %s\n", output_dir);
            return -1;
        }
    }

    // 打开ZIP文件
    int err;
    struct zip *archive = zip_open(zip_file, 0, &err);
    if (!archive) {
        zip_error_t error;
        zip_error_init_with_code(&error, err);
        fprintf(stderr, "无法打开ZIP文件: %s\n", zip_error_strerror(&error));
        zip_error_fini(&error);
        return -1;
    }

    zip_int64_t num_entries = zip_get_num_entries(archive, 0);
    printf("开始解压 %s (%ld 个文件) 到 %s\n", zip_file, (long)num_entries, output_dir);

    // 解压所有文件
    for (zip_int64_t i = 0; i < num_entries; i++) {
        const char *name = zip_get_name(archive, i, 0);
        if (!name) continue;

        // 构建输出路径
        char output_path[1024];
        snprintf(output_path, sizeof(output_path), "%s%c%s", output_dir, PATH_SEPARATOR, name);

        // 创建目录
        char *dir_path = strdup(output_path);
        char *last_sep = strrchr(dir_path, PATH_SEPARATOR);
        if (last_sep) {
            *last_sep = '\0';
            struct stat dir_st;
            if (stat(dir_path, &dir_st) != 0) {
                mkdir(dir_path, 0755);
            }
        }
        free(dir_path);

        // 解压文件
        struct zip_file *zf = zip_fopen_index(archive, i, 0);
        if (zf) {
            FILE *outfile = fopen(output_path, "wb");
            if (outfile) {
                char buffer[8192];
                zip_int64_t nbytes;
                while ((nbytes = zip_fread(zf, buffer, sizeof(buffer))) > 0) {
                    fwrite(buffer, 1, nbytes, outfile);
                }
                fclose(outfile);
                printf("解压: %s\n", output_path);
            }
            zip_fclose(zf);
        }
    }

    zip_close(archive);
    printf("解压完成: %s\n", output_dir);
    return 0;
}

// 主函数
int main(int argc, char *argv[]) {
    Config config;
    init_config(&config);

    if (parse_arguments(argc, argv, &config) != 0) {
        free_config(&config);
        return 1;
    }

    int result;
    if (config.compress) {
        result = compress_files(&config);
    } else {
        result = decompress_files(&config);
    }

    // 清理资源
    free_config(&config);
    if (file_list) {
        for (size_t i = 0; i < file_count; i++) {
            free(file_list[i].path);
            free(file_list[i].archive_path);
        }
        free(file_list);
    }

    return result;
}

编译

bash 复制代码
export LIBRARY_PATH=$LIBRARY_PATH:/par/libzip-1.11.4/build/lib
export C_INCLUDE_PATH=$C_INCLUDE_PATH:/par/libzip-1.11.4/lib
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/par/libzip-1.11.4/build/lib
gcc zipzstd2.c -o zipzstd -lzip

执行

bash 复制代码
./zipzstd
错误: 需要指定输入文件或模式
./zipzstd -h
用法: ./zipzstd [选项] <文件/模式...>
选项:
  -d          解压缩模式
  -r          递归处理子目录
  -数字       压缩级别 (1-22, 默认:6)
  -T数字      线程数 (默认:1)
  -o文件      输出文件名
  -notop      不包括顶级目录
  -h          显示帮助信息

示例:
  ./zipzstd *.txt *.c -r -9 -T4 -o archive.zip
  ./zipzstd -d archive.zip -o ./output
  ./zipzstd docs/* -r -notop -o docs.zip

假定有如下目录

bash 复制代码
cd test
ls
ParserInterfaceTest  test_fdw_create.sql                  test_fdw_numericquery_solution.txt
datebool_test.xlsx   test_fdw_create_solution.txt         test_fdw_runall.sh
numeric_test.xlsx    test_fdw_dateboolquery.sql           test_fdw_stringquery.sql
sf0001               test_fdw_dateboolquery_solution.txt  test_fdw_stringquery_solution.txt
string_test.xlsx     test_fdw_numericquery.sql

把*.txt *.sql文件压到archive.zip

bash 复制代码
../zipzstd *.txt *.sql -o archive.zip
找到 8 个文件进行压缩
已添加: test_fdw_create_solution.txt -> test_fdw_create_solution.txt
已添加: test_fdw_dateboolquery_solution.txt -> test_fdw_dateboolquery_solution.txt
已添加: test_fdw_numericquery_solution.txt -> test_fdw_numericquery_solution.txt
已添加: test_fdw_stringquery_solution.txt -> test_fdw_stringquery_solution.txt
已添加: test_fdw_create.sql -> test_fdw_create.sql
已添加: test_fdw_dateboolquery.sql -> test_fdw_dateboolquery.sql
已添加: test_fdw_numericquery.sql -> test_fdw_numericquery.sql
已添加: test_fdw_stringquery.sql -> test_fdw_stringquery.sql
压缩完成: archive.zip (8 个文件)

再建立两个子目录,往两者各复制一个文件。想实验一下递归,结果没有把两个子目录压缩进去,还是刚才那些文件。

bash 复制代码
mkdir s
cp test_fdw_create.sql s
mkdir t
cp  test_fdw_create_solution.txt t
../zipzstd *.txt *.sql -r -o archiver.zip
找到 8 个文件进行压缩

压缩完成: archiver.zip (8 个文件)

试验解压缩,成功。

bash 复制代码
../zipzstd -d archiver.zip -o 3
开始解压 archiver.zip (8 个文件) 到 3
解压: 3/test_fdw_create_solution.txt
解压: 3/test_fdw_dateboolquery_solution.txt
解压: 3/test_fdw_numericquery_solution.txt
解压: 3/test_fdw_stringquery_solution.txt
解压: 3/test_fdw_create.sql
解压: 3/test_fdw_dateboolquery.sql
解压: 3/test_fdw_numericquery.sql
解压: 3/test_fdw_stringquery.sql
解压完成: 3

针对刚才未达预期的命令询问DeepSeek,他给出了如下正确的写法:

bash 复制代码
../zipzstd ./*.txt ./*.sql ./**/*.txt ./**/*.sql -r -o archiver3.zip
找到 18 个文件进行压缩
已添加: ./test_fdw_create_solution.txt -> ./test_fdw_create_solution.txt
已添加: ./test_fdw_dateboolquery_solution.txt -> ./test_fdw_dateboolquery_solution.txt
已添加: ./test_fdw_numericquery_solution.txt -> ./test_fdw_numericquery_solution.txt
已添加: ./test_fdw_stringquery_solution.txt -> ./test_fdw_stringquery_solution.txt
已添加: ./test_fdw_create.sql -> ./test_fdw_create.sql
已添加: ./test_fdw_dateboolquery.sql -> ./test_fdw_dateboolquery.sql
已添加: ./test_fdw_numericquery.sql -> ./test_fdw_numericquery.sql
已添加: ./test_fdw_stringquery.sql -> ./test_fdw_stringquery.sql
已添加: ./3/test_fdw_create_solution.txt -> ./3/test_fdw_create_solution.txt
已添加: ./3/test_fdw_dateboolquery_solution.txt -> ./3/test_fdw_dateboolquery_solution.txt
已添加: ./3/test_fdw_numericquery_solution.txt -> ./3/test_fdw_numericquery_solution.txt
已添加: ./3/test_fdw_stringquery_solution.txt -> ./3/test_fdw_stringquery_solution.txt
已添加: ./t/test_fdw_create_solution.txt -> ./t/test_fdw_create_solution.txt  <------------
已添加: ./3/test_fdw_create.sql -> ./3/test_fdw_create.sql
已添加: ./3/test_fdw_dateboolquery.sql -> ./3/test_fdw_dateboolquery.sql
已添加: ./3/test_fdw_numericquery.sql -> ./3/test_fdw_numericquery.sql
已添加: ./3/test_fdw_stringquery.sql -> ./3/test_fdw_stringquery.sql
已添加: ./s/test_fdw_create.sql -> ./s/test_fdw_create.sql   <------------
压缩完成: archiver3.zip (18 个文件)

可见它递归压缩了t、s,以及刚才解压缩生成的3目录。

我估计他做过类似的项目,刚才的代码除了一处宏定义缺一个下划线,一字未改,就能实现基本的功能,当然还有问题,但已经很不错了。