EternalWindows
Cabinet API / キャビネットファイルの作成

FCIコンテキストを作成したら、いよいよキャビネットにファイルを追加することになります。 ファイルを追加するには、FCIAddFileを呼び出します。

BOOL FCIAddFile(
    HFCI hfci,
    char* pszSourceFile,
    char* pszFileName,
    BOOL fExecute,
    PFNFCIGETNEXTCABINET GetNextCab,
    PFNFCISTATUS pfnProgress,
    PFNFCIGETOPENINFO pfnOpenInfo,
    TCOMP typeCompress
);

hfciは、FCIコンテキストを指定します。 pszSourceFileは、キャビネットに追加したいファイルのフルパスを指定します。 pszFileNameは、追加したファイルのキャビネット上での名前を指定します。 fExecuteは、追加したファイルをキャビネットの展開時に実行するかどうかを指定します。 pfnfcigncは、新しいキャビネットが作成されるときに呼ばれるコールバック関数の アドレスを指定します。 この引数はNULLを指定することができます。 pfnfcisは、ファイルの圧縮状況を知るために呼ばれるコールバック関数のアドレスを 指定します。 pfnfcigoiは、FCIAddFileが内部でpszSourceFileをオープンするときに呼ばれる コールバック関数のアドレスを指定します。 typeCompressは、ファイルの圧縮形式を示す定数を指定します。 pvは、コールバック関数にデータを渡すために用意した変数のアドレスを指定します。 関数が成功した場合はTRUE、失敗した場合はFALSEが返ります。

FCIAddFileを呼び出した際の動作は、主に2種類考えられます。 1つは、pszSourceFileから読み取ったデータを圧縮し、 その圧縮データをフォルダに書き込み、制御を返す場合です。 そして、もう1つは先の処理に加えてフォルダをキャビネットにコピーする場合です。 これらの場合におけるフォルダというのは、あくまで一時的なバッファに過ぎません。 結論から述べると、バッファに全ての圧縮データをまとめてからキャビネットにコピーするのか、 圧縮データをバッファに書き込み、直ぐにキャビネットにコピーするのかの違いです。 これらの動作内容について、FCIAddFileのコールバック関数がどのように 関わってくるかについては後の節で説明します。

FCIによって内部的に作成されたキャビネットは、 FCIFlushCabinetによってディスク上に作成することができます。 つまり、この関数によってキャビネットファイルが作成されます。 FCIAddFileによって書き込まれたフォルダ(バッファ)が残っている場合は、 この関数によってキャビネットにコピーされます。

BOOL FCIFlushCabinet(
    HFCI hfci,
    BOOL fGetNextCab,
    PFNFCIGETNEXTCABINET GetNextCab,
    PFNFCISTATUS pfnProgress
);

hfciは、FCIコンテキストを指定します。 fGetNextCabは、新しいキャビネットを作成するかどうかを指定します。 pfnfcigncは、新しいキャビネットが作成されるときに呼ばれるコールバック関数の アドレスを指定します。 fGetNextCabにTRUEをした場合は、この引数をNULLにすることはできません。

今回のプログラムは、FCIAddFileとFCIFlushCabinetを呼び出して、 実際にディスク上にキャビネットファイルを作成します。 出力先や追加するファイル名などはプログラムにハードコードされているため、 必要に応じて修正してください。

#include <windows.h>
#include <shlwapi.h>
#include <fci.h>

#pragma comment (lib, "shlwapi.lib")
#pragma comment (lib, "cabinet.lib")

BOOL StartFci(LPSTR lpszOutputDirectoryPath, LPSTR lpszCreateFileName, LPSTR *lpszAddFilePath, int nFileCount, TCOMP tcomp);
HFCI CreateFciContext(LPSTR lpszOutputDirectoryPath, LPSTR lpszCreateFileName, PERF perf);
BOOL AddFile(HFCI hfci, LPSTR *lpszFilePath, int nFileCount, TCOMP tcomp);

int DIAMONDAPI GetOpenInfoProc(char *pszName, USHORT *pdate, USHORT *ptime, USHORT *pattribs, int *err, void *pv);
long DIAMONDAPI StatusProc(UINT typeStatus, ULONG cb1, ULONG cb2, void *pv);
int DIAMONDAPI FilePlaceProc(PCCAB pccab, char *pszFile, long cbFile, BOOL fContinuation, void *pv);

void * DIAMONDAPI AllocProc(ULONG cb);
void DIAMONDAPI FreeProc(void *memory);
int DIAMONDAPI OpenProc(char *pszFile, int oflag, int pmode, int *err, void *pv);
UINT DIAMONDAPI ReadProc(int hf, void *memory, UINT cb, int *err, void *pv);
UINT DIAMONDAPI WriteProc(int hf, void *memory, UINT cb, int *err, void *pv);
int DIAMONDAPI CloseProc(int hf, int *err, void *pv);
long DIAMONDAPI SeekProc(int hf, long dist, int seektype, int *err, void *pv);
int DIAMONDAPI DeleteProc(char *pszFile, int *err, void *pv);
BOOL DIAMONDAPI TempFileProc(char *pszTempName, int cbTempName, void *pv);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	char  szOutputDirectoryPath[] = "c:\\";
	char  szCreateFileName[] = "sample.cab";
	int   nFileCount = 2;
	LPSTR lpszAddFilePath[] = {"c:\\sample.txt", "c:\\sample.bmp"};
	TCOMP tcomp = tcompTYPE_MSZIP;
	BOOL  bResult;

	bResult = StartFci(szOutputDirectoryPath, szCreateFileName, lpszAddFilePath, nFileCount, tcomp);
	if (bResult)
		MessageBox(NULL, TEXT("キャビネットファイルを作成しました。"), TEXT("OK"), MB_OK);
	
	return 0;
}

BOOL StartFci(LPSTR lpszOutputDirectoryPath, LPSTR lpszCreateFileName, LPSTR *lpszAddFilePath, int nFileCount, TCOMP tcomp)
{
	HFCI  hfci;
	ERF   erf;
	BOOL  bResult;
	TCHAR szBuf[256];
	
	hfci = CreateFciContext(lpszOutputDirectoryPath, lpszCreateFileName, &erf);
	if (hfci == NULL) {
		wsprintf(szBuf, TEXT("FCIコンテキストの作成に失敗しました。 ErrorCode %d"), erf.erfOper);
		MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
		return FALSE;
	}

	bResult = AddFile(hfci, lpszAddFilePath, nFileCount, tcomp);
	if (!bResult) {
		wsprintf(szBuf, TEXT("ファイルの追加に失敗しました。 ErrorCode %d"), erf.erfOper);
		MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
		FCIDestroy(hfci);
		return FALSE;
	}

	bResult = FCIFlushCabinet(hfci, FALSE, NULL, StatusProc);
	if (!bResult) {
		wsprintf(szBuf, TEXT("キャビネットファイルの作成に失敗しました。 ErrorCode %d"), erf.erfOper);
		MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
		FCIDestroy(hfci);
		return FALSE;
	}

	FCIDestroy(hfci);

	return TRUE;
}

HFCI CreateFciContext(LPSTR lpszOutputDirectoryPath, LPSTR lpszCreateFileName, PERF perf)
{
	HFCI hfci;
	CCAB cab;

	ZeroMemory(&cab, sizeof(CCAB));
	lstrcpyA(cab.szCab, lpszCreateFileName);
	lstrcpyA(cab.szCabPath, lpszOutputDirectoryPath);

	hfci = FCICreate(perf, FilePlaceProc, AllocProc, FreeProc, 
		OpenProc, ReadProc, WriteProc, CloseProc, SeekProc, DeleteProc,
		TempFileProc, &cab, NULL);
	
	return hfci;
}

BOOL AddFile(HFCI hfci, LPSTR *lpszFilePath, int nFileCount, TCOMP tcomp)
{
	int   i;
	BOOL  bResult = FALSE;
	LPSTR lpszFileName;

	for (i = 0; i < nFileCount; i++) {
		lpszFileName = PathFindFileNameA(lpszFilePath[i]);
		bResult = FCIAddFile(hfci, lpszFilePath[i], lpszFileName, FALSE, NULL, StatusProc, GetOpenInfoProc, tcomp);
		if (!bResult)
			break;
	}

	return bResult;
}

int DIAMONDAPI GetOpenInfoProc(char *pszName, USHORT *pdate, USHORT *ptime, USHORT *pattribs, int *err, void *pv)
{
	HANDLE   hFile;
	DWORD    dwDosFileAttributes;
	FILETIME fileTime;
	FILETIME localFileTime;

	hFile = CreateFileA(pszName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return -1;

	GetFileTime(hFile, NULL, NULL, &fileTime);
	FileTimeToLocalFileTime(&fileTime, &localFileTime);
	FileTimeToDosDateTime(&localFileTime, pdate, ptime);

	dwDosFileAttributes = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM | FILE_ATTRIBUTE_ARCHIVE;
	*pattribs = (USHORT)(GetFileAttributesA(pszName) & dwDosFileAttributes);

	return (int)hFile;
}

long DIAMONDAPI StatusProc(UINT typeStatus, ULONG cb1, ULONG cb2, void *pv)
{
	return 0;
}

int DIAMONDAPI FilePlaceProc(PCCAB pccab, char *pszFile, long cbFile, BOOL fContinuation, void *pv)
{
	return 0;
}

void * DIAMONDAPI AllocProc(ULONG cb)
{
	return HeapAlloc(GetProcessHeap(), 0, cb);
}

void DIAMONDAPI FreeProc(void *memory)
{
	HeapFree(GetProcessHeap(), 0, memory);
}

int DIAMONDAPI OpenProc(char *pszFile, int oflag, int pmode, int *err, void *pv)
{
	HANDLE hFile;

	hFile = CreateFileA(pszFile, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE && GetLastError() == ERROR_FILE_NOT_FOUND)
		hFile = CreateFileA(pszFile, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

	return (int)hFile;
}

UINT DIAMONDAPI ReadProc(int hf, void *memory, UINT cb, int *err, void *pv)
{
	DWORD dwReadByte;

	ReadFile((HANDLE)hf, memory, cb, &dwReadByte, NULL);
	
	return dwReadByte;
}

UINT DIAMONDAPI WriteProc(int hf, void *memory, UINT cb, int *err, void *pv)
{
	DWORD dwWriteByte;

	WriteFile((HANDLE)hf, memory, cb, &dwWriteByte, NULL);

	return dwWriteByte;
}

int DIAMONDAPI CloseProc(int hf, int *err, void *pv)
{
	CloseHandle((HANDLE)hf);

	return 0;
}

long DIAMONDAPI SeekProc(int hf, long dist, int seektype, int *err, void *pv)
{
	return SetFilePointer((HANDLE)hf, dist, 0, seektype);
}

int DIAMONDAPI DeleteProc(char *pszFile, int *err, void *pv)
{
	DeleteFileA(pszFile);

	return 0;
}

BOOL DIAMONDAPI TempFileProc(char *pszTempName, int cbTempName, void *pv)
{
	char szTempPath[256];

	GetTempPathA(cbTempName, szTempPath);
	GetTempFileNameA(szTempPath, "xx", 0, pszTempName);

	return TRUE;
}

WinMainで呼び出しているStartFciという自作関数は、 前節で紹介したCreateFciContextを始めとして、 ファイルの追加処理とキャビネットファイルを作成する処理を行っています。 lpszAddFilePathはキャビネットに追加したいファイルのフルパスを格納し、 ファイルの数はnFileCountで表されています。 よって、追加するファイルの数を変更する場合は、これらの変数を修正します。 TCOMPは圧縮形式を表す専用の型で、その定義はFCI.hより確認することができます。 ここでは、MSZIP形式の圧縮を施すものとします。 ファイル名を変更せずにこのプログラムを実行する場合は、 c:\にsample.txtとsample2.bmpを用意しておいてください。 ファイルサイズが高ければ、効果も確かめやすくなります。

StartFciでは、一連のFCI関数の呼び出しをこの関数で終結させています。 AddFileはFCIAddFileの呼び出しを内部で行い、 この関数が制御を返すとFCIFlushCabinetでキャビネットをディスクに書き込みます。 AddFile及びFCIFlushCabinetがERF構造体を引数として要求しないことから、 エラー時にERF構造体のerfOperメンバを参照するのは 不可解に思えるかもしれませんが、これは成立します。 おそらく、FCICreateにてERF構造体がFCIコンテキストに関連付けられているのでしょう。 FCIコンテキストには、この他にCCAB構造体のコピーも含まれています。

自作関数であるAddFileの内部を確認します。

BOOL AddFile(HFCI hfci, LPSTR *lpszFilePath, int nFileCount, TCOMP tcomp)
{
	int   i;
	BOOL  bResult = FALSE;
	LPSTR lpszFileName;

	for (i = 0; i < nFileCount; i++) {
		lpszFileName = PathFindFileNameA(lpszFilePath[i]);
		bResult = FCIAddFile(hfci, lpszFilePath[i], lpszFileName, FALSE, NULL, StatusProc, GetOpenInfoProc, tcomp);
		if (!bResult)
			break;
	}

	return bResult;
}

この関数は、ファイルの数だけFCIAddFileを繰り返して呼び出します。 第2引数は、追加したいファイルのフルパスであるため、そのまま指定できます。 第3引数は、キャビネット上におけるファイル名ですから、 ここに指定する文字列にはファイルのパスが含まれるべきはありません。 よって、PathFindFileNameでファイルパスから名前の部分のみを取り出すのです (shlwapi.hとshlwapi.libは、この関数の利用のために宣言されています)。 第4引数は追加したファイルをキャビネットの展開時に実行するかどうかで、 ここでは実行させないものとしてFALSEを指定します。 第5引数は、PFNFCIGETNEXTCABINET型のコールバック関数のアドレスですが、 ここではNULLを指定しています。 第6引数と第7引数のコールバック関数については、後の節で説明します。 第8引数は圧縮タイプであるため、tcompを指定します。

キャビネットの分割

ディスク上にキャビネットをファイルとして出力するとき、 そのファイルサイズを一定の量に留めることが可能となっています。 このようなとき、残りのまだ書き込まれていないデータは、 また別のキャビネットファイルとして出力することが可能で、 そのときのサイズも一定量にすることができます。 このようにキャビネットファイルを分割して作成したいような場合は、 CCAB構造体のcbメンバを適切に初期化します。

cab.cb = 500 * 1024;

このコードでは、1つのキャビネットファイルのサイズを500Kbに抑えようとしています。 他にすべき処理は、FCIAddFileに新しいキャビネットファイルが作成される可能性があることを伝えることです。

FCIAddFile(hfci, lpszFilePath[i], szFileName, FALSE, GetNextCabinet, StatusProc, GetOpenInfoProc, tcomp);

第5引数にPFNFCIGETNEXTCABINET型のコールバック関数を指定した場合、 新しいキャビネットファイルが作成されるときに、GetNextCabinet呼ばれることになります。 新しいキャビネットファイルが作成されるときというのは、 圧縮データを格納するバッファのサイズがキャビネットの理想とするサイズ、 つまりCCAB構造体のcbメンバを上回ってしまった場合です。 GetNextCabinetは、次のようなプロトタイプを持ちます。

BOOL GetNextCabinet(PCCAB pccab, ULONG cbPrevCab, void *pv)
{
	wsprintfA(pccab->szCab, "sample%d.cab", pccab->iCab);

	return TRUE;
}

新しいキャビネットが作成されようとしていることから、 この関数ではキャビネットの名前を必ず以前のものと変えなければなりません。 1つの特徴として、CCAB構造体のiCabメンバが、関数が呼ばれる度にインクリメントされることになっているため、 これを利用して新しいファイル名を作成するようなことができます。

キャビネットを分割することは、単純にファイルが増えてしまうことを意味し、 それら1つでも欠けてしまうと展開処理は行えないことになっています。 また、各キャビネットは次のキャビネットに関する情報を持っており、 最初に作成したキャビネットを展開するファイルとして指定しなければ成功しないという欠点もあります。 しかし、一連のキャビネットの中で最初に作成されたものだけをユーザーに見えるようにし、 残りのファイルは別フォルダに含めておくようなことをしている場合は、 ユーザーはサイズの低い単一のキャビネットファイルを扱えるということで、 ファイルを移動するときなどに処理の負荷を感じることはなくなるはずです。 もちろん、分割したからといってキャビネットがディスクに対して消費するサイズは減少しませんが、 以下のようなコードを記述した場合は、話は別です。

BOOL GetNextCabinet(PCCAB pccab, ULONG cbPrevCab, void *pv)
{
	lstrcpyA(pccab->szDisk, "D:\\");
	wsprintfA(pccab->szCab, "Install\\sample%d.cab", pccab->iCab);

	return TRUE;
}

このコードはアプリケーションをCD-ROM経由でインストールすることを念頭に置いており、 付属のセットアップアプリケーションは複数キャビネットのうち、 最初に作成されたsample.cabのみをユーザーのハードディスクにインストールします。 ユーザーがsample.cabを展開しようとしたとき、 sample.cabには次のキャビネットが"D:\\Install\\sample1.cab"であることを示す情報が含まれていますから、 CD-ROMをセットしていない場合は、少なくとも展開アプリケーションは、 ファイルが存在しないことをユーザーに通知してくるでしょう。 そして、CD-ROMをセットすれば展開処理は正常に終了するはずです。 この方法の最大の特徴は、分割されたファイルをCD-ROMに置いておくことで、 ユーザーのハードディスクを消費しないようにするという狙いですが、 必要に応じてCD-ROMをセットしなければならないという煩わしさもあります。 また、ドライブ名は無条件にD:\\と決め付けられるものでもありません。



戻る