C++ MiniZip實現目錄壓縮與解壓

2023-11-22 18:00:38

Zlib是一個開源的資料壓縮庫,提供了一種通用的資料壓縮和解壓縮演演算法。它最初由Jean-Loup GaillyMark Adler開發,旨在成為一個高效、輕量級的壓縮庫,其被廣泛應用於許多領域,包括網路通訊、檔案壓縮、資料庫系統等。其壓縮演演算法是基於DEFLATE演演算法,這是一種無失真資料壓縮演演算法,通常能夠提供相當高的壓縮比。

在Zlib專案中的contrib目錄下有一個minizip子專案,minizip實際上不是zlib庫的一部分,而是一個獨立的開源庫,用於處理ZIP壓縮檔案格式。它提供了對ZIP檔案的建立和解壓的簡單介面。minizip在很多情況下與zlib一起使用,因為ZIP壓縮通常使用了DEFLATE壓縮演演算法。通過對minizip庫的二次封裝則可實現針對目錄的壓縮與解壓功能。

如果你想使用minizip通常你需要下載並編譯它,然後將其連結到你的專案中。

編譯Zlib庫很簡單,解壓檔案並進入到\zlib-1.3\contrib\vstudio目錄下,根據自己編譯器版本選擇不同的目錄,這裡我選擇vc12,進入後開啟zlibvc.sln等待生成即可。

成功後可獲得兩個檔案分別是zlibstat.libzlibwapi.lib如下圖;

接著設定參照目錄,這裡需要多設定一個minizip標頭檔案,該標頭檔案是zlib裡面的一個子專案。

lib庫則需要包含zlibstat.libzlibwapi.lib這兩個檔案,此處讀者可以自行放入到一個目錄下;

ZIP 遞迴壓縮目錄

如下所示程式碼是一個使用zlib庫實現的簡單資料夾壓縮工具的C++程式。該程式提供了壓縮資料夾到 ZIP 檔案的功能,支援遞迴地新增檔案和子資料夾,利用了 Windows API 和 zlib 庫的函數。

#define ZLIB_WINAPI
#include <string>
#include <iostream>
#include <vector>
#include <Shlwapi.h> 
#include <zip.h>
#include <unzip.h>
#include <zlib.h>

using namespace std;

#pragma comment(lib, "Shlwapi.lib")
#pragma comment(lib, "zlibstat.lib")

bool nyAddfiletoZip(zipFile zfile, const std::string& fileNameinZip, const std::string& srcfile)
{
  // 目錄如果為空則直接返回
  if (NULL == zfile || fileNameinZip.empty())
  {
    return 0;
  }

  int nErr = 0;
  zip_fileinfo zinfo = { 0 };
  tm_zip tmz = { 0 };
  zinfo.tmz_date = tmz;
  zinfo.dosDate = 0;
  zinfo.internal_fa = 0;
  zinfo.external_fa = 0;

  char sznewfileName[MAX_PATH] = { 0 };
  memset(sznewfileName, 0x00, sizeof(sznewfileName));
  strcat_s(sznewfileName, fileNameinZip.c_str());
  if (srcfile.empty())
  {
    strcat_s(sznewfileName, "\\");
  }

  nErr = zipOpenNewFileInZip(zfile, sznewfileName, &zinfo, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_DEFAULT_COMPRESSION);
  if (nErr != ZIP_OK)
  {
    return false;
  }
  if (!srcfile.empty())
  {
    // 開啟原始檔
    FILE* srcfp = _fsopen(srcfile.c_str(), "rb", _SH_DENYNO);
    if (NULL == srcfp)
    {
      std::cout << "開啟原始檔失敗" << std::endl;
      return false;
    }

    // 讀入原始檔寫入zip檔案
    int numBytes = 0;
    char* pBuf = new char[1024 * 100];
    if (NULL == pBuf)
    {
      std::cout << "新建緩衝區失敗" << std::endl;
      return 0;
    }
    while (!feof(srcfp))
    {
      memset(pBuf, 0x00, sizeof(pBuf));
      numBytes = fread(pBuf, 1, sizeof(pBuf), srcfp);
      nErr = zipWriteInFileInZip(zfile, pBuf, numBytes);
      if (ferror(srcfp))
      {
        break;
      }
    }
    delete[] pBuf;
    fclose(srcfp);
  }
  zipCloseFileInZip(zfile);

  return true;
}

bool nyCollectfileInDirtoZip(zipFile zfile, const std::string& filepath, const std::string& parentdirName)
{
  if (NULL == zfile || filepath.empty())
  {
    return false;
  }
  bool bFile = false;
  std::string relativepath = "";
  WIN32_FIND_DATAA findFileData;

  char szpath[MAX_PATH] = { 0 };
  if (::PathIsDirectoryA(filepath.c_str()))
  {
    strcpy_s(szpath, sizeof(szpath) / sizeof(szpath[0]), filepath.c_str());
    int len = strlen(szpath) + strlen("\\*.*") + 1;
    strcat_s(szpath, len, "\\*.*");
  }
  else
  {
    bFile = true;
    strcpy_s(szpath, sizeof(szpath) / sizeof(szpath[0]), filepath.c_str());
  }

  HANDLE hFile = ::FindFirstFileA(szpath, &findFileData);
  if (NULL == hFile)
  {
    return false;
  }
  do
  {
    if (parentdirName.empty())
      relativepath = findFileData.cFileName;
    else
      // 生成zip檔案中的相對路徑
      relativepath = parentdirName + "\\" + findFileData.cFileName;

    // 如果是目錄
    if (findFileData.dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY)
    {
      // 去掉目錄中的.當前目錄和..前一個目錄
      if (strcmp(findFileData.cFileName, ".") != 0 && strcmp(findFileData.cFileName, "..") != 0)
      {
        nyAddfiletoZip(zfile, relativepath, "");

        char szTemp[MAX_PATH] = { 0 };
        strcpy_s(szTemp, filepath.c_str());
        strcat_s(szTemp, "\\");
        strcat_s(szTemp, findFileData.cFileName);
        nyCollectfileInDirtoZip(zfile, szTemp, relativepath);
      }
      continue;
    }
    char szTemp[MAX_PATH] = { 0 };
    if (bFile)
    {
      //注意:處理單獨檔案的壓縮
      strcpy_s(szTemp, filepath.c_str());
    }
    else
    {
      //注意:處理目錄檔案的壓縮
      strcpy_s(szTemp, filepath.c_str());
      strcat_s(szTemp, "\\");
      strcat_s(szTemp, findFileData.cFileName);
    }

    nyAddfiletoZip(zfile, relativepath, szTemp);

  } while (::FindNextFileA(hFile, &findFileData));
  FindClose(hFile);

  return true;
}

/*
* 函數功能 : 壓縮資料夾到目錄
* 備    注 : dirpathName 原始檔/資料夾
*      zipFileName 目的壓縮包
*      parentdirName 壓縮包內名字(資料夾名)
*/
bool nyCreateZipfromDir(const std::string& dirpathName, const std::string& zipfileName, const std::string& parentdirName)
{
  bool bRet = false;

  /*
  APPEND_STATUS_CREATE    建立追加
  APPEND_STATUS_CREATEAFTER 建立後追加(覆蓋方式)
  APPEND_STATUS_ADDINZIP    直接追加
  */
  zipFile zFile = NULL;
  if (!::PathFileExistsA(zipfileName.c_str()))
  {
    zFile = zipOpen(zipfileName.c_str(), APPEND_STATUS_CREATE);
  }
  else
  {
    zFile = zipOpen(zipfileName.c_str(), APPEND_STATUS_ADDINZIP);
  }
  if (NULL == zFile)
  {
    std::cout << "建立ZIP檔案失敗" << std::endl;
    return bRet;
  }

  if (nyCollectfileInDirtoZip(zFile, dirpathName, parentdirName))
  {
    bRet = true;
  }

  zipClose(zFile, NULL);

  return bRet;
}

主要功能

nyCreateZipfromDir函數

bool nyCreateZipfromDir(const std::string& dirpathName, const std::string& zipfileName, const std::string& parentdirName);

功能:壓縮資料夾到指定的 ZIP 檔案。

引數:

  • dirpathName:原始檔夾路徑。
  • zipfileName:目標 ZIP 檔案路徑。
  • parentdirName:在 ZIP 檔案內的資料夾名(如果為空則不指定目錄)。

nyCollectfileInDirtoZip 函數

bool nyCollectfileInDirtoZip(zipFile zfile, const std::string& filepath, const std::string& parentdirName);

功能:遞迴地收集資料夾中的檔案,並將它們新增到已開啟的 ZIP 檔案中。

引數:

  • zfile:已開啟的 ZIP 檔案。
  • filepath:資料夾路徑。
  • parentdirName:在 ZIP 檔案內的相對資料夾名。

nyAddfiletoZip 函數

bool nyAddfiletoZip(zipFile zfile, const std::string& fileNameinZip, const std::string& srcfile);

功能:將指定檔案新增到已開啟的 ZIP 檔案中。

引數:

  • zfile:已開啟的 ZIP 檔案。
  • fileNameinZip:在 ZIP 檔案內的相對檔案路徑。
  • srcfile:原始檔路徑。

程式流程

  • 資料夾壓縮引數設定: 使用者提供原始檔夾路徑、目標 ZIP 檔案路徑,以及在 ZIP 檔案內的資料夾名。
  • ZIP 檔案開啟: 根據目標 ZIP 檔案是否存在,使用 zipOpen 函數開啟 ZIP 檔案。
  • 資料夾遞迴新增: 使用 nyCollectfileInDirtoZip 函數遞迴地收集資料夾中的檔案,並通過 nyAddfiletoZip 函數將它們新增到 ZIP 檔案中。
  • ZIP 檔案關閉: 使用 zipClose 函數關閉 ZIP 檔案。

範例用法

int main(int argc, char* argv[])
{
	std::string dirpath = "D:\\lyshark\\test";                   // 原始檔/資料夾
	std::string zipfileName = "D:\\lyshark\\test.zip";           // 目的壓縮包
	
	bool ref = nyCreateZipfromDir(dirpath, zipfileName, "lyshark");          // 包內檔名<如果為空則壓縮時不指定目錄>

	std::cout << "[LyShark] 壓縮狀態 " << ref << std::endl;
	system("pause");
	return 0;
}

上述呼叫程式碼,引數1指定為需要壓縮的檔案目錄,引數2指定為需要壓縮成目錄名,引數3為壓縮後該目錄的名字。

ZIP 遞迴解壓目錄

在這個C++程式中,實現了遞迴解壓縮ZIP檔案的功能。程式提供了以下主要功能:

  • replace_all 函數: 用於替換字串中的指定子串。
  • CreatedMultipleDirectory 函數: 用於建立多級目錄,確保解壓縮時的目錄結構存在。
  • UnzipFile 函數: 用於遞迴解壓縮 ZIP 檔案。該函數開啟 ZIP 檔案,獲取檔案資訊,然後逐個解析和處理 ZIP 檔案中的檔案或目錄。
#define ZLIB_WINAPI
#include <string>
#include <iostream>
#include <vector>
#include <Shlwapi.h> 
#include <zip.h>
#include <unzip.h>
#include <zlib.h>

using namespace std;

#pragma comment(lib, "Shlwapi.lib")
#pragma comment(lib, "zlibstat.lib")

// 將字串內的old_value替換成new_value
std::string& replace_all(std::string& str, const std::string& old_value, const std::string& new_value)
{
	while (true)
	{
		std::string::size_type pos(0);
		if ((pos = str.find(old_value)) != std::string::npos)
			str.replace(pos, old_value.length(), new_value);
		else
			break;
	}
	return str;
}

// 建立多級目錄
BOOL CreatedMultipleDirectory(const std::string& direct)
{
	std::string Directoryname = direct;
	if (Directoryname[Directoryname.length() - 1] != '\\')
	{
		Directoryname.append(1, '\\');
	}
	std::vector< std::string> vpath;
	std::string strtemp;
	BOOL  bSuccess = FALSE;
	for (int i = 0; i < Directoryname.length(); i++)
	{
		if (Directoryname[i] != '\\')
		{
			strtemp.append(1, Directoryname[i]);
		}
		else
		{
			vpath.push_back(strtemp);
			strtemp.append(1, '\\');
		}
	}
	std::vector< std::string>::iterator vIter = vpath.begin();
	for (; vIter != vpath.end(); vIter++)
	{
		bSuccess = CreateDirectoryA(vIter->c_str(), NULL) ? TRUE : FALSE;
	}
	return bSuccess;
}

/*
* 函數功能 : 遞迴解壓檔案目錄
* 備    注 : strFilePath 壓縮包路徑
*      strTempPath 解壓到
*/
void UnzipFile(const std::string& strFilePath, const std::string& strTempPath)
{
	int nReturnValue;
	string tempFilePath;
	string srcFilePath(strFilePath);
	string destFilePath;

	// 開啟zip檔案
	unzFile unzfile = unzOpen(srcFilePath.c_str());
	if (unzfile == NULL)
	{
		return;
	}

	// 獲取zip檔案的資訊
	unz_global_info* pGlobalInfo = new unz_global_info;
	nReturnValue = unzGetGlobalInfo(unzfile, pGlobalInfo);
	if (nReturnValue != UNZ_OK)
	{
		std::cout << "封包: " << pGlobalInfo->number_entry << endl;
		return;
	}

	// 解析zip檔案
	unz_file_info* pFileInfo = new unz_file_info;
	char szZipFName[MAX_PATH] = { 0 };
	char szExtraName[MAX_PATH] = { 0 };
	char szCommName[MAX_PATH] = { 0 };

	// 存放從zip中解析出來的內部檔名
	for (int i = 0; i < pGlobalInfo->number_entry; i++)
	{
		// 解析得到zip中的檔案資訊
		nReturnValue = unzGetCurrentFileInfo(unzfile, pFileInfo, szZipFName, MAX_PATH, szExtraName, MAX_PATH, szCommName, MAX_PATH);
		if (nReturnValue != UNZ_OK)
			return;

		std::cout << "解壓檔名: " << szZipFName << endl;

		string strZipFName = szZipFName;

		// 如果是目錄則執行建立遞迴目錄名
		if (pFileInfo->external_fa == FILE_ATTRIBUTE_DIRECTORY || (strZipFName.rfind('/') == strZipFName.length() - 1))
		{
			destFilePath = strTempPath + "//" + szZipFName;
			CreateDirectoryA(destFilePath.c_str(), NULL);
		}

		// 如果是檔案則解壓縮並建立
		else
		{
			// 建立檔案 儲存完整路徑
			string strFullFilePath;
			tempFilePath = strTempPath + "/" + szZipFName;
			strFullFilePath = tempFilePath;

			int nPos = tempFilePath.rfind("/");
			int nPosRev = tempFilePath.rfind("\\");
			if (nPosRev == string::npos && nPos == string::npos)
				continue;

			size_t nSplitPos = nPos > nPosRev ? nPos : nPosRev;
			destFilePath = tempFilePath.substr(0, nSplitPos + 1);

			if (!PathIsDirectoryA(destFilePath.c_str()))
			{
				// 將路徑格式統一
				destFilePath = replace_all(destFilePath, "/", "\\");
				// 建立多級目錄
				int bRet = CreatedMultipleDirectory(destFilePath);
			}
			strFullFilePath = replace_all(strFullFilePath, "/", "\\");

			HANDLE hFile = CreateFileA(strFullFilePath.c_str(), GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_FLAG_WRITE_THROUGH, NULL);
			if (hFile == INVALID_HANDLE_VALUE)
			{
				return;
			}

			// 開啟檔案
			nReturnValue = unzOpenCurrentFile(unzfile);
			if (nReturnValue != UNZ_OK)
			{
				CloseHandle(hFile);
				return;
			}

			// 讀取檔案
			uLong BUFFER_SIZE = pFileInfo->uncompressed_size;;
			void* szReadBuffer = NULL;
			szReadBuffer = (char*)malloc(BUFFER_SIZE);
			if (NULL == szReadBuffer)
			{
				break;
			}

			while (TRUE)
			{
				memset(szReadBuffer, 0, BUFFER_SIZE);
				int nReadFileSize = 0;

				nReadFileSize = unzReadCurrentFile(unzfile, szReadBuffer, BUFFER_SIZE);

				// 讀取檔案失敗
				if (nReadFileSize < 0)
				{
					unzCloseCurrentFile(unzfile);
					CloseHandle(hFile);
					return;
				}
				// 讀取檔案完畢
				else if (nReadFileSize == 0)
				{
					unzCloseCurrentFile(unzfile);
					CloseHandle(hFile);
					break;
				}
				// 寫入讀取的內容
				else
				{
					DWORD dWrite = 0;
					BOOL bWriteSuccessed = WriteFile(hFile, szReadBuffer, BUFFER_SIZE, &dWrite, NULL);
					if (!bWriteSuccessed)
					{
						unzCloseCurrentFile(unzfile);
						CloseHandle(hFile);
						return;
					}
				}
			}
			free(szReadBuffer);
		}
		unzGoToNextFile(unzfile);
	}

	delete pFileInfo;
	delete pGlobalInfo;

	// 關閉
	if (unzfile)
	{
		unzClose(unzfile);
	}
}

主要功能

replace_all 函數

std::string& replace_all(std::string& str, const std::string& old_value, const std::string& new_value)

功能:在字串 str 中替換所有的 old_value 為 new_value。

引數:

  • str:待處理的字串。
  • old_value:要被替換的子串。
  • new_value:替換後的新子串。

返回值:替換後的字串。

CreatedMultipleDirectory 函數

BOOL CreatedMultipleDirectory(const std::string& direct)

功能:建立多級目錄,確保路徑存在。

引數:

  • direct:目錄路徑。
  • 返回值:如果成功建立目錄返回 TRUE,否則返回 FALSE。

UnzipFile 函數

void UnzipFile(const std::string& strFilePath, const std::string& strTempPath)

功能:遞迴解壓縮 ZIP 檔案。

引數:

  • strFilePath:ZIP 檔案路徑。
  • strTempPath:解壓到的目標路徑。

該函數開啟 ZIP 檔案,獲取檔案資訊,然後逐個解析和處理 ZIP 檔案中的檔案或目錄。在解析過程中,根據檔案或目錄的屬性,建立相應的目錄結構,然後將檔案寫入目標路徑。

範例用法

int main(int argc, char* argv[])
{
	std::string srcFilePath = "D:\\lyshark\\test.zip";
	std::string tempdir = "D:\\lyshark\\test";

	// 如果傳入目錄不存在則建立
	if (!::PathFileExistsA(tempdir.c_str()))
	{
		CreatedMultipleDirectory(tempdir);
	}

	// 呼叫解壓函數
	UnzipFile(srcFilePath, tempdir);

	system("pause");
	return 0;
}

案例中,首先在解壓縮之前判斷傳入目錄是否存在,如果不存在則需要呼叫API建立目錄,如果存在則直接呼叫UnzipFIle解壓縮函數,實現解包,輸出效果圖如下;