Componente zip


Introducción

La compresión y descompresión de archivos hoy en día es necesaria, en aras de improvisar varias tareas, como la transferencia de archivos entre servidores. Hay muchas APIs por ahí que nos permiten añadir funcionalidad de compresión y descompresión de archivos a nuestros programas. Recientemente, me ví en la necesidad de emplear algún componente que pudiese ser empleado tanto en C++ como en VBScript, por lo que comencé a buscar algunas APIs. Encontré una excelente en el artículo de Lucian Wischik. Sin embargo, esta API está escrita para C/C++, y no es utilizable en VBS. Entonces, decidí hacer un envoltorio alrededor de esta API, y crear un componente ATL/COM.

Antecedentes

El API que estoy empleando es limpia, simple y elegante, y no requiere dependencia externa. Encontré que ésta es bastante útil, y de hecho la emplée en uno de mis proyectos. Como antecedente, puedes consultar el artículo de Lucian Wischik, antes mencionado, para que tengas una idea más profunda de cómo trabaja. A final de cuentas, este componente es sólo un envoltorio de la susodicha API.

Decidí crear el componente empleando ATL por dos razones. Primera, me gusta ATL. Segunda, no me gusta COM. Emplear ATL me asegura que no tengo que lidiar directamente con COM. Más aún, emplée el AppWizard y el ATL Wizard para generar el código. Por ende, sólo expondré el código relacionado con la implementación de las propiedades y métodos. Usé y probé el componente empleando VIsual C++ 6.0, sin embargo no debería haber problema alguno al compilarlo con una nueva versión de Visual C++ (NOTA: hace algunas lunas me ví en la necesidad de emplear este código con Visual C++ 8.0, y no tuve problema alguno, aunque la versión de ATL 8 difiere significativamente de ATL 3; espero alguno de estos días poner a disposición de todos la versión actualizada).

Conceptos de diseño

Primero, me gustaría hacer algunos comentarios antes de mostrar el código. Hay dos formas de usar el componente: para crear un archivo zip, o para descomprimir los archivos existentes en algún archivo zip. Presento a continuación el diagrama de flujo para crear el archivo zip.


Unos comentarios. Después de instanciar el componente, debemos rellenar varias propiedades (como el directorio de entrada, el nombre de archivo, el directorio de salida, etc) en aras de poder crear el archivo zip. Luego, debemos indicarle al componente los archivos que debe agregar, todos los que queramos. Nota: si el archivo no se encuentra, éste no será agregado. Cuando agregamos el archivo, no va a crear el archivo zip, sino que mantendrá solamente una referencia a dicho archivo -junto con su directorio. Esto es así porque una vez que se cree el archivo zip, no se podrá añadir otro archivo (por el diseño del API original). Por ende, tendrás que agregar todos los archivos que requieras incluir en el zip antes de crearlo. Una vez hecho esto, será necesaria una llamada al metodo que creará el archivo zip con los archivos añadidos previamente. Nota: si entre el momento en que el archivo se añade (es decir, su referencia) y el momento en que se crea el zip, algún archivo es removido, éste simplemente no se agregará; empero, no se lanzará excepción alguna que indique este error.

El siguiente diagrama ilustra el flujo necesario para descomprimir archivos.


Como puedes observar, el flujo es muy directo. Una vez que las propiedades del archivo zip son establecidas, se debe abrir el archivo zip. Esto proveerá al componente con información sobre el archivo, como su nombre, el porcentaje de compresión, el número de archivos, etc. Una vez que esto sea hecho, se puede descomprimir uno o más archivos. Después de esto, sebe cerrarse el zip.

¿Listo? Está bien, veamos el código.

Implementación

Primero, veamos la declaración de la co-clase.

// ZipUtility.h : Declaration of the CZipUtility

#ifndef __ZIPUTILITY_H_
#define __ZIPUTILITY_H_

#include "resource.h" // main symbols
#include "ZipFileInfo.h"

class ATL_NO_VTABLE CZipUtility :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CZipUtility, &CLSID_ZipUtility>,
public ISupportErrorInfo,
public IDispatchImpl<IZipUtility,
&IID_IZipUtility, &LIBID_ZIPPERLib>
{
public:
CZipUtility()
{
}

DECLARE_REGISTRY_RESOURCEID(IDR_ZIPUTILITY)

DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CZipUtility)
COM_INTERFACE_ENTRY(IZipUtility)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
END_COM_MAP()

// ISupportsErrorInfo
STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);

// IZipUtility
public:
STDMETHOD(ExistsFile)(BSTR strFileName, BOOL* pVal);
STDMETHOD(Open)();
STDMETHOD(get_Password)(/*[out, retval]*/ BSTR *pVal);
STDMETHOD(put_Password)(/*[in]*/ BSTR newVal);
STDMETHOD(get_FullFileName)(/*[out, retval]*/ BSTR *pVal);
STDMETHOD(Unzip)();
STDMETHOD(Zip)();
STDMETHOD(get_Count)(/*[out, retval]*/ long *pVal);
STDMETHOD(AddFile)(BSTR strFileName, BSTR strNewName);
STDMETHOD(get_FileName)(/*[out, retval]*/ BSTR *pVal);
STDMETHOD(put_FileName)(/*[in]*/ BSTR newVal);
STDMETHOD(get_OutputPath)(/*[out, retval]*/ BSTR *pVal);
STDMETHOD(put_OutputPath)(/*[in]*/ BSTR newVal);
STDMETHOD(get_InputPath)(/*[out, retval]*/ BSTR *pVal);
STDMETHOD(put_InputPath)(/*[in]*/ BSTR newVal);

private:
_bstr_t m_bstrFileName;
_bstr_t m_bstrOutputPath;
_bstr_t m_bstrInputPath;
_bstr_t m_bstrPassword;
ZipFileInfoVtr m_vtrFiles;

_bstr_t CalcFullFileName();
};

#endif //__ZIPUTILITY_H_

Como podemos ver, ahí están todas nuestras variables y los métodos de los que dispondrá nuestro componente. Ahora, analicemos las propiedades del mismo.

InputPath. Establece o devuelve el directorio desde donde se tomarán los archivos.

STDMETHODIMP CZipUtility::get_InputPath(BSTR *pVal)
{
*pVal = m_bstrInputPath.copy();

return S_OK;
}

STDMETHODIMP CZipUtility::put_InputPath(BSTR newVal)
{
m_bstrInputPath = newVal;

return S_OK;
}

OutputPath. Establece o devuelve el directorio a donde se extraerán los archivos.

STDMETHODIMP CZipUtility::get_OutputPath(BSTR *pVal)
{
*pVal = m_bstrOutputPath.copy();

return S_OK;
}

STDMETHODIMP CZipUtility::put_OutputPath(BSTR newVal)
{
m_bstrOutputPath = newVal;

return S_OK;
}

FileName. Establece o devuelve el nombre del archivo zip.

STDMETHODIMP CZipUtility::get_FileName(BSTR *pVal)
{
*pVal = m_bstrFileName.copy();

return S_OK;
}

STDMETHODIMP CZipUtility::put_FileName(BSTR newVal)
{
m_bstrFileName = newVal;

return S_OK;
}

Password. Establece el devuelve la contraseña aplicada al archivo zip.

STDMETHODIMP CZipUtility::get_Password(BSTR *pVal)
{
*pVal = m_bstrPassword.copy();

return S_OK;
}

STDMETHODIMP CZipUtility::put_Password(BSTR newVal)
{
m_bstrPassword = newVal;

return S_OK;
}

Hasta ahora, creo que todo es claro. Nota que los valores simplemente son tomados de / guardados en las variables miembros. Las siguientes propiedades son de solo lectura, nada especial.

Count. Regresa el número de archivos que contiene el zip.

STDMETHODIMP CZipUtility::get_Count(long *pVal)
{
*pVal = (long)m_vtrFiles.size();

return S_OK;
}

FullFileName. Regresa el nombre completo del archivo zip (es lo mismo que OutputPath + "\" + FileName).

STDMETHODIMP CZipUtility::get_FullFileName(BSTR *pVal)
{
*pVal = CalcFullFileName().copy();

return S_OK;
}

Nuevamente, nada especial. Sin embargo, echa una mirada a la propiedad Count. Nota que obtiene el tamaño invocando a la función size() de nuestra variable std::vector<ZipInfoFile>. Quizás sea tiempo de introducir a la estructura ZipInfoFile. Nada elegante, es solo una estructura que contiene información relacionada al archivo zip.

struct ZipFileInfo
{
_bstr_t bstrFileName;
_bstr_t bstrNewName;
_bstr_t bstrPath;
long lCompSize;
};

typedef std::vector<ZipFileInfo> ZipFileInfoVtr;

Ahora, echémosle una mirada a nuestro primer método importante. AddFile agrega una referencia al archivo que será comprimido. Éste es el código.

STDMETHODIMP CZipUtility::AddFile(BSTR strFileName, BSTR strNewName)
{
ZipFileInfo zipInfo;

// can add ONLY if the zip file is being created
zipInfo.bstrFileName = strFileName;
zipInfo.bstrNewName = strNewName;
zipInfo.bstrPath = m_bstrInputPath;
m_vtrFiles.push_back(zipInfo);

return S_OK;
}

El parámetro strFileName es en realidad el nombre del archivo. Pero strNewName nos permite renombrar el archivo dentro del zip, si así se desea. También, nota que la información del archivo es almacenada dentro de nuestro vector.

Como mencioné antes, después de agregar tantos archivos como se desee, se requiere crear el zip. Esto se hace al llamar al método Zip(). Hasta este momento, finalmente empleamos el API de ZipUtils. He aquí el código.

STDMETHODIMP CZipUtility::Zip()
{
HZIP hZip;
_bstr_t bstrFullFileName;

hZip = CreateZip(CalcFullFileName(), m_bstrPassword);
if (!hZip) return S_FALSE;

for (ZipFileInfoVtr::iterator iterBegin =
m_vtrFiles.begin(); iterBegin !=
m_vtrFiles.end(); ++iterBegin)
{
ZipFileInfo zipInfo = *iterBegin;

bstrFullFileName = m_bstrInputPath +
_bstr_t(_T("\\")) + zipInfo.bstrFileName;
ZipAdd(hZip, zipInfo.bstrNewName, bstrFullFileName);
}
CloseZip(hZip);

return S_OK;
}

Simple, ¿no? Nota en primer lugar, que llamamos a la función CreateZip, que nos regresará un manejador -handle- a nuestro recién creado zip. Los parámetros son el nombre del zip (de hecho el nombre completo), y la contraseña, por si queremos bloquearlo. Si la función regresa NULL, lanzamos una función (recuerda que los componentes ATL/COM interpretarán S_FALSE como un error). Luego, iteramos sobre el vector, y recompilamos información sobre nuestro archivo previamente añadido. Luego llamamos a la función ZipAdd por cada elemento encontrado, de tal suerte que dicho archivo es añadido al zip. Nota: si ZipAdd falla, simplemente no se incluirá el archivo. Finalmente, cerramos el manejador.

Después de estos pasos, tenemos listo nuestro archivo zip. Pero ¿qué hay de la descompresión? Bien, después de establecer las propiedades correspondientes, tenemos que llamar primero al método Open. Éste leerá la estructura del zip, y cargará información relativa a éste.

STDMETHODIMP CZipUtility::Open()
{
HZIP hZip;
ZIPENTRY zipEntry;
ZIPENTRY zipItem;

m_vtrFiles.empty();

hZip = OpenZip(CalcFullFileName(), m_bstrPassword);
if (!hZip) return S_FALSE;

GetZipItem(hZip, -1, &zipEntry);
for (int i = 0; i < zipEntry.index; i++)
{
ZipFileInfo zipInfo;

GetZipItem(hZip, i, &zipItem);
zipInfo.bstrFileName = zipItem.name;
zipInfo.bstrNewName = zipItem.name;
zipInfo.lCompSize = zipItem.comp_size;
m_vtrFiles.push_back(zipInfo);
}
CloseZip(hZip);

return S_OK;
}

Como antes, abrimos el archivo zip. Luego, iteramos sobre cada elemento del zip para obtener su información. Luego, cerramos el manejador. Tras abrir el zip, llamamos al método Unzip para descomprimir los archivos al directorio determinado por OutputPath.

STDMETHODIMP CZipUtility::Unzip()
{
HZIP hZip;
ZIPENTRY zipEntry;
ZIPENTRY zipItem;
_bstr_t bstrOutputFile;

m_vtrFiles.empty();

hZip = OpenZip(CalcFullFileName(), m_bstrPassword);
if (!hZip) return S_FALSE;

GetZipItem(hZip, -1, &zipEntry);
for (int i = 0; i < zipEntry.index; i++)
{
ZipFileInfo zipInfo;

GetZipItem(hZip, i, &zipItem);
bstrOutputFile = m_bstrOutputPath +
_bstr_t(_T("\\")) + _bstr_t(zipItem.name);

UnzipItem(hZip, i, bstrOutputFile);
}
CloseZip(hZip);

return S_OK;
}

Finalmente, tenemos un método que nos permite determinar si un archivo está contenido en el zip o no. ExistsFile iterará sobre la estructura del zip, y si concuerda con el nombre de un archivo, regresará TRUE.

STDMETHODIMP CZipUtility::ExistsFile(BSTR strFileName, BOOL* pVal)
{
BOOL bExists = FALSE;
_bstr_t bstrFileName = strFileName;

for (ZipFileInfoVtr::iterator iterBegin =
m_vtrFiles.begin(); iterBegin !=
m_vtrFiles.end(); ++iterBegin)
{
ZipFileInfo zipInfo;

zipInfo = *iterBegin;
if (bstrFileName == zipInfo.bstrFileName) {
bExists = TRUE;
break;
}
}

*pVal = bExists;

return S_OK;
}
Eso es todo amigos. Ahora, ¿cómo empleamos el componente?

Usando el componente

Usar el componente es fácil -al menos tan fácil como emplear cualquier otro componente. En esta sección explicaré como emplearlo en un script de VB. De igual forma puedes emplearlo en C#, VB o C++.

El siguiente script es para crear un zip.


rem create a zip file
dim objZip

set objZip = CreateObject("ZIPPER.ZipUtility")
objZip.InputPath = "C:\Source"
objZip.OutputPath = "C:\Destiny"
objZip.FileName = "test.zip"
objZip.Password = "somePwd"
call objZip.AddFile("file_1.txt", "newfile_1.txt")
call objZip.AddFile("file_2.txt", "newfile_2.txt")
call objZip.AddFile("file_3.txt", "newfile_3.txt")
call objZip.Zip()
set objZip = nothing


Este script es para descomprimir archivos.

rem unzip files
dim objZip

set objZip = CreateObject("ZIPPER.ZipUtility")
objZip.InputPath = "C:\Source"
objZip.OutputPath = "C:\Destiny"
objZip.FileName = "test.zip"
objZip.Password = "somePwd"

on error goto ErrHandler
call objZip.Open()
MsgBox "There are " & objZip.Count & " files zipped"
if (not objZip.ExistsFile("file_1.txt")) then
MsgBox "file_1.txt does not exist"
end if
call objZip.Unzip()s

ErrHandler:
set objZip = nothing

Puntos de interés

Espero que con esto estés satisfecho. Hay, sin embargo, varias formas de improvisarlo. De hecho, uno de los problemas más molestos -en mi opinión- es que una vez que el zip se ha creado, no se pueden agregar más archivos. He estado pensando en lo siguiente: cuando se añada un archivo a un zip existente, eliminar el zip actual y recrearlo. Sin embargo, creo que esto disminuiría su desempeño, por lo que lo dejo a criterio del usuario.

Para finalizar, un par de notas. Primero, recuerda que antes de usar el componente, debes registrarlo. Usualmente, la mejor forma es llamando a revsvr32. Segundo, si planeas modificar este componente y distribuirlo en tus aplicaciones, recomiendo ampliamente cambiar tanto el IID como el CLSID de la interfase y la coclase, respectivamente, en aras de evitar problemas de compatibilidad.

Siempre ando buscando mejorar, por lo que cualqueir comentario, crítica o sugerencia es bienvenido.

Notas

1. Este artículo lo publiqué por primera vez en CodeProject. Puedes encontrar la versión original aquí.

2. El archivo adjunto ZipperComponent_component.zip contiene la DLL del componente.

3. El archivo adjunto ZipperComponent_src.zip contiene todo el código fuente relacionado.


ċ
ZipperComponent_component.zip
(94k)
Fernando Arturo Gómez Flores,
13 de dic. de 2008 1:26
ċ
ZipperComponent_src.zip
(98k)
Fernando Arturo Gómez Flores,
13 de dic. de 2008 1:27
Comments