Přeskočit na hlavní obsah

Zipujeme efektivně

Jedna ze základních vlastností Javy je práce se ZIP archívy, ať už jsou to knihovny tříd a spustitelné JARy, webové aplikace (war), nebo třeba JEE bumbrlíčci (ear). Není tedy divu, že i přímo v základním API je implementována práce s těmito archívy. Slouží k tomu třídy v balíku java.util.zip a nejzajímavější z nich jsou ZipOutputStream a ZipInputStream.

Příkladem budiž vytvoření zipu:

//Vytvorime Zip
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("javlog.zip"));
//V Zipu chceme mit jeden textovy soubor
zos.putNextEntry(new ZipEntry("priznani.txt"));
//naplnime obsah textoveho souboru
zos.write("Máme rádi Javu!".getBytes());
//zavrem entry (priznani.txt)
zos.closeEntry();
//zavrem stream
zos.close();
a jeho rozbalení:
ZipInputStream zis = new ZipInputStream(new FileInputStream("javlog.zip"));
ZipEntry zipEntry;
//budem predpokladat, ze v ZIPu mame jen textove soubory a tak je vypisem do konzole
while ((zipEntry = zis.getNextEntry()) != null) {
    System.out.println("Soubor: " + zipEntry.getName());
    BufferedReader br = new BufferedReader(new InputStreamReader(zis));
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
    System.out.println();
    zis.closeEntry();
}
zis.close();

Jak je vidět, není tenhle přístup úplně přímočarý. Tím pádem je i větší prostor pro zavlečení nějaké chybky. Víte například, jak rozlišíte při vytváření ZIPu prázdný soubor od prázdného adresáře?

Tento základní přístup se často hodí v případě, kdy obsah, který chcete do zipu vložit, nemáte ve file systému, ale např. ho generujete přímo ve webové aplikaci. Pak daný ZipOutputStream vytvoříte nad OutputStreamem servletu a naládujete do něj data přímo z paměti nebo z databáze.

To co vývojář aplikace často potřebuje, je možnost jednoduše zazipovat část souborového systému (složku a všechno co je v ní), a nebo ze ZIPu zase všechno někam rozbalit. Samozřejmě, že se dají na internetu najít knihovny, které tuto funkcionalitu poskytují, ale částo jsou kanón na vrabce (opravdu potřebujete aby knihovna uměla i bzip2, 7z, arj a cab?), nebo naopak příliš jednoduché a nezvládnou operaci typu – sbal mi část filesystému a k tomu přidej tenhle vygenerovaný (v paměti) PDF report.

Takže by se v těchto případech hodilo malé rozšíření Zip*Stream tříd. Aby na jednu stranu bylo možné jednoduše pracovat přímo se souborovým systémem a na druhou, aby nebyl programátor ochuzen o to, co mu ZipStreamy umožňují. Hodily by se nám tedy metody:

void ZipOutputStream.put(File fileOrFolderToZip};
void ZipInputStream.unzip(File folderWhereToStoreUnzippedFiles);

A protože originální Zip*Stream třídy nejsou finální, nic nám nebrání v rozšíření. Má implementace přidává ještě dodatečné parametry, aby bylo použití univerzálnější.

cz.cacek.javlog.zip.ZipOutputStream extends java.util.zip.ZipOutputStream:

/**
 * Adds file or directory (with its content) to this {@link ZipOutputStream}.
 * If the given {@link File} doesn't exist, it does nothing and returns.
 * The path of the given {@link File} in the result zip is determined only
 * from the file name
 * and from the provided basePath. It doesn't take the file path into consideration 
 * (but the folder structure under the given folder is preserved).
 * 
 * @param file
 *            {@link File} (normal or directory) to add to the stream
 *            (must not be null)
 * @param basePath
 *            base path (path prefix) for added {@link File} (may be null)
 * @param contentOnly
 *            determines if the folder name (in case the file
 *            parameter is directory) should be added to the zip path. If
 *            true is provided only the folder content is added and the
 *            folder itself is not included.
 * @throws IOException
 */
public void put(final File file, final String basePath, boolean contentOnly) throws IOException;
cz.cacek.javlog.zip.ZipInputStream extends java.util.zip.ZipInputStream:
/**
 * Unzips the stream to the given base folder. If the baseFolder parameter
 * is null, the default (working) directory is used instead. If the
 * baseFolder doesn't exist it is created.
 * 
 * @param baseFolder
 *            base folder to unpack (may be null).
 * @param allowAbsolutePath
 *            flag which says if handling of absolute paths is allowed in
 *            the zip. If the flag is
 *            true and ZipEntry contains absolute path, the baseFolder
 *            parameter is not used and File is created from the absolute
 *            path in the ZipEntry.
 * @throws IllegalArgumentException
 *             base folder can't be created or file denoted by baseFolder
 *             parameter already exists,
 *             but it's not a directory.
 * @throws IOException
 */
public void unzip(final File baseFolder, final boolean allowAbsolutePath) throws IllegalArgumentException, IOException;

A použití? Chceme například rozbalit war soubor do nějaké složky, upravit obsah a zase ho zabalit:

final File baseFolder = new File("temp-webapp"); //folder to unpack the zip content (will be created)
final File warFile = new File("myapp.war"); //file to unpack

//unpack ZIP
ZipInputStream zis = new ZipInputStream(new FileInputStream(warFile));
zis.unzip(baseFolder, false);
zis.close();

//TODO make some magic with files in the temp-webapp folder
new FileOutputStream(new File(baseFolder, "newEmptyFile.xxx")).close();

//repack the ZIP (WAR) file
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(warFile));
zos.put(baseFolder, null, true); //pack only the content of the folder - 3rd parameter is true
zos.close();

//TODO remove the baseFolder
//delete(baseFolder);

Toť vše, ani to nebolelo.

Upravené ZipStreamy připravené k použití najdete v tomto jaru (3 třídy + zdrojáky), nebo v zazipovaném Eclipse projektu i s JUnit testy.

A co používáte k práci s ZIP archívy vy? Podělte se v komentářích.

Komentáře

Sranda s timhle zacina, kdyz maji nazvy souboru nejakou interpunkci a nejlepe treba kus polske a kus ceske. To se pak deji kouzla po rozbaleni. Viz treba: http://www.etnetera.cz/cz/773-tech_life/tech_life_zip_file.html
Arnost píše…
K praci se ZIP jsem nedavno pouzil TrueZIP (http://truezip.java.net/).

Ma celkem pouzitelne API, ale vlastnost ktera se mi libila nejvic je schopnost pracovat se ZIPem jako s adresarem.

Funguje to i rekurzivne - ZIP zazipovany v ZIPu ktery je zazipovany v ZIPu. Idealni pro problem ktery jsem resil - otevrit EAR, najit WAR a precist a zapsat JAR v /WEB-INF/lib/ (vsechny 3 jsou vlastne ZIP)
Josef Cacek píše…
@NkD: různé charsety použité pro jména souborů v zipu jsou opravdu na odstřel - kdyby se alespoň někde v zipu zapisovala informace o použitém kódování, hned by se žilo radostněji;
Když se pracuje jen v Javě na straně vytváření archivu i rozbalování, tak to je v pohodě - prostě tam bude UTF-8. Ale když to pak chce člověk rozbalit na českých Windows ... tak se ucho utrhlo.

@Arnost: Možnosti Truezipu vypadadají dobře. Pro mě je to v současné době onen zmíněný kanón na vrabce, ale je dobré o tomhle projektu vědět.
Michal píše…
Někdy je výhodné i použití Commons VFS http://commons.apache.org/vfs/
Anonymní píše…
Cuzz chlapci, taky pridam svuj dil. Nedavno jsme si psali anti task do mavenu, ktery nahrazoval v EARu deployment descriptory, to je asi fuk, ale pekne jsme si nabehli s lomitkama v absolut path pri zpetnem zipovani. Vsechno se tvari ok, az do chvile, kdy se to zkompiluje na windows a deployuje na unixu.... bacha na to.

Populární příspěvky z tohoto blogu

Three ways to redirect HTTP requests to HTTPs in WildFly and JBoss EAP

WildFly application server (and JBoss EAP) supports several simple ways how to redirect the communication from plain HTTP to TLS protected HTTPs. This article presents 3 ways. Two are on the application level and the last one is on the server level valid for requests to all deployments. 1. Request confidentiality in the deployment descriptor The first way is based on the Servlet specification. You need to specify which URLs should be protected in the web.xml deployment descriptor. It's the same approach as the one used for specifying which URLs require authentication/authorization. Just instead of requesting an assigned role, you request a transport-guarantee . Sample content of the WEB-INF/web.xml <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1...

Enable Elytron in WildFly

Steps to enable Elytron in WildFly nightly builds. There is an ongoing effort to bring a new security subsystem Elytron to WildFly and JBoss EAP. For some time a custom server profile named standalone-elytron.xml  existed beside other profiles in standalone/configuration directory. It was possible to use it for playing with Elytron. The custom Elytron profile was removed now.  The Elytron subsystem is newly introduced to all standard server profiles. The thing is, the Elytron is not used by default and users have to enable it in the subsystems themselves. Let's look into how you can enable it. Get WildFly nightly build # Download WildFly nightly build wget --user=guest --password=guest https://ci.wildfly.org/httpAuth/repository/downloadAll/WF_Nightly/.lastSuccessful/artifacts.zip # unzip build artifacts zip. It contains WildFly distribution ZIP unzip artifacts.zip # get the WildFly distribution ZIP name as property WILDFLY_DIST_ZIP=$(ls wildfly-*-SNAPSHOT.zip)...

Simple TLS certificates in WildFly 18

It's just 2 weeks when WildFly 18 was released. It includes nice improvements in TLS certificates handling through ACME protocol (Automatic Certificate Management Environment), it greatly simplifies obtaining valid HTTPS certificates. There was already a support for the Let's Encrypt CA in WildFly 14 as Farah Juma described in her blog post last year. New WildFly version allows using other CA-s with ACME protocol support. It also adds new switch --lets-encrypt to interactive mode of security enable-ssl-http-server JBoss CLI commands. Let's try it. Before we jump on WildFly configuration, let's just mention the HTTPs can be used even in the default configuration and a self-signed certificate is generated on the fly. Nevertheless, it's not secure and you should not use it for any other purpose than testing. Use Let's Encrypt signed certificate for HTTPs application interface Start WildFly on a machine with the public IP address. Run it on the defaul...