使用libzip壓縮檔案和資料夾

charlee44發表於2024-07-12

簡單說說自己遇到的坑:

  1. 分清楚三個元件:zlib、minizip和libzip。zlib是底層和最基礎的C庫,用於使用Deflate演算法壓縮和解壓縮檔案流或者單個檔案,但是如果要壓縮資料夾就很麻煩,主要是不知道如何歸檔,在zip內部形成對應的目錄。這時就需要用更高階別的庫,也就是minizip或libzip。

  2. minizip、libzip隨著版本迭代介面一直變化,我連續使用了通義千問、文心一言、gemini三個AI,基本上沒給出能使用的程式碼,主要是函式介面總是不對,或者引數多了或者少了。像這種情況就不要再參考AI給出的答案了,趕緊翻官方文件才是正經。

  3. minizip和libzip都是基於zlib實現的,都嘗試使用過,感覺還是libzip的介面設計更清晰一點,官方文件說明也還不錯。

  4. 壓縮資料夾的功能需要藉助於操作檔案系統的庫來組織zip內部的歸檔目錄,我這裡使用的是C++17的std::filesystem。

具體程式碼實現如下:

#include <zip.h>

#include <filesystem>
#include <fstream>
#include <iostream>

using namespace std;

void CompressFile2Zip(std::filesystem::path unZipFilePath,
                      const char* relativeName, zip_t* zipArchive) {
  std::ifstream file(unZipFilePath, std::ios::binary);
  file.seekg(0, std::ios::end);
  size_t bufferSize = file.tellg();
  char* bufferData = (char*)malloc(bufferSize);

  file.seekg(0, std::ios::beg);
  file.read(bufferData, bufferSize);

  //第四個引數如果非0,會自動託管申請的資源,直到zip_close之前自動銷燬。
  zip_source_t* source =
      zip_source_buffer(zipArchive, bufferData, bufferSize, 1);

  if (source) {
    if (zip_file_add(zipArchive, relativeName, source, ZIP_FL_OVERWRITE) < 0) {
      std::cerr << "Failed to add file " << unZipFilePath
                << " to zip: " << zip_strerror(zipArchive) << std::endl;
      zip_source_free(source);
    }
  } else {
    std::cerr << "Failed to create zip source for " << unZipFilePath << ": "
              << zip_strerror(zipArchive) << std::endl;
  }
}

void CompressFile(std::filesystem::path unZipFilePath,
                  std::filesystem::path zipFilePath) {
  int errorCode = 0;
  zip_t* zipArchive = zip_open(zipFilePath.generic_u8string().c_str(),
                               ZIP_CREATE | ZIP_TRUNCATE, &errorCode);
  if (zipArchive) {
    CompressFile2Zip(unZipFilePath, unZipFilePath.filename().string().c_str(),
                     zipArchive);

    errorCode = zip_close(zipArchive);
    if (errorCode != 0) {
      zip_error_t zipError;
      zip_error_init_with_code(&zipError, errorCode);
      std::cerr << zip_error_strerror(&zipError) << std::endl;
      zip_error_fini(&zipError);
    }
  } else {
    zip_error_t zipError;
    zip_error_init_with_code(&zipError, errorCode);
    std::cerr << "Failed to open output file " << zipFilePath << ": "
              << zip_error_strerror(&zipError) << std::endl;
    zip_error_fini(&zipError);
  }
}

void CompressDirectory2Zip(std::filesystem::path rootDirectoryPath,
                           std::filesystem::path directoryPath,
                           zip_t* zipArchive) {
  if (rootDirectoryPath != directoryPath) {
    if (zip_dir_add(zipArchive,
                    std::filesystem::relative(directoryPath, rootDirectoryPath)
                        .generic_u8string()
                        .c_str(),
                    ZIP_FL_ENC_UTF_8) < 0) {
      std::cerr << "Failed to add directory " << directoryPath
                << " to zip: " << zip_strerror(zipArchive) << std::endl;
    }
  }

  for (const auto& entry : std::filesystem::directory_iterator(directoryPath)) {
    if (entry.is_regular_file()) {
      CompressFile2Zip(
          entry.path().generic_u8string(),
          std::filesystem::relative(entry.path(), rootDirectoryPath)
              .generic_u8string()
              .c_str(),
          zipArchive);
    } else if (entry.is_directory()) {
      CompressDirectory2Zip(rootDirectoryPath, entry.path().generic_u8string(),
                            zipArchive);
    }
  }
}

void CompressDirectory(std::filesystem::path directoryPath,
                       std::filesystem::path zipFilePath) {
  int errorCode = 0;
  zip_t* zipArchive = zip_open(zipFilePath.generic_u8string().c_str(),
                               ZIP_CREATE | ZIP_TRUNCATE, &errorCode);
  if (zipArchive) {
    CompressDirectory2Zip(directoryPath, directoryPath, zipArchive);

    errorCode = zip_close(zipArchive);
    if (errorCode != 0) {
      zip_error_t zipError;
      zip_error_init_with_code(&zipError, errorCode);
      std::cerr << zip_error_strerror(&zipError) << std::endl;
      zip_error_fini(&zipError);
    }
  } else {
    zip_error_t zipError;
    zip_error_init_with_code(&zipError, errorCode);
    std::cerr << "Failed to open output file " << zipFilePath << ": "
              << zip_error_strerror(&zipError) << std::endl;
    zip_error_fini(&zipError);
  }
}

int main() {
  //壓縮檔案
  //CompressFile("C:/Data/Builder/Demo/view.tmp", "C:/Data/Builder/Demo/view.zip");

  //壓縮資料夾
  CompressDirectory("C:/Data/Builder/Demo", "C:/Data/Builder/Demo.zip");

  return 0;
}

關於使用的libzip,有以下幾點值得注意:

  1. libzip壓縮的zip內部的檔名預設採用UTF-8編碼。
  2. libzip要求使用正斜槓 ('/') 作為目錄分隔符。
  3. libzip操作不同的zip執行緒安全,操作同一個zip執行緒不安全。
  4. zip_source_buffer這個函式的介面的第四個引數如果非0,會自動託管申請的資源。官方文件提到需要保證傳入zip_source_buffer的資料資源需要保證跟zip_source_t一樣的宣告週期,但是筆者經過測試,正確的行為應該是傳入zip_source_buffer的資料資源需要保證呼叫zip_close之前都有效,否則就有問題。

相關文章