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

Ignore the boring SSH error message - Host identification has changed!

The problem If you work with virtual machines in clouds, or you run an SSH server in Docker containers, then you've probably met the following error message during making ssh connection: (I'm connecting through SSH to a docker container) ~$ ssh -p 8822 root@localhost @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! Someone could be eavesdropping on you right now (man-in-the-middle attack)! It is also possible that a host key has just been changed. The fingerprint for the ECDSA key sent by the remote host is SHA256:smYv5yA0n9/YrBgJMUCk5dYPWGj7bTpU40M9aFBQ72Y. Please contact your system administrator. Add correct host key in /home/jcacek/.ssh/known_hosts to get rid of this message. Offending ECDSA key in /home/jcacek/.ssh/known_hosts:107 remove with: ssh-keygen -f "/home/jcacek/.ssh/know

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) # un