Scompattare uno zip multipart in Java su Windows con Process, exec() e waitFor()

Uno zip multipart è un tipo di archivio utilizzato per comprimere file di grandi dimensioni che vengono compressi suddividendoli in più file, in modo da rendere più semplice il loro trasferimento. I singoli file in cui lo zip viene “spezzato” devono poi essere riassemblati, in modo da ottenere l’archivio zip originale e poter quindi estrarre il suo contenuto. Nel post di oggi vediamo come gestire questi archivi creati come zip multipart da un’applicazione Java che gira in ambiente Windows, tramite l’invocazione di comandi di sistema operativo.
Per prima cosa vediamo come creare uno zip multipart. Utilizzando ad esempio 7zip per la creazione di un archivio zip, occorre selezionare l’opzione “Dividi in più file” specificando la dimensione massima di ciascuno dei file in cui lo zip sarà suddiviso. Nella figura seguente ad esempio, proviamo a zippare il file del JDK di Java7 in un archivio che verrà “spezzato” in file dalla dimensione massima di 40 MegaByte (indicando 40M come valore).

Creare uno Zip Multipart

Il risultato di questa operazione è la creazione di una serie di file con estensione .zip seguita da un progressivo che numera le varie parti in cui il file è stato splittato.
La figura seguente mostra questo risultato, in cui possiamo notare anche come la dimensione dei primi 3 archivi sia di 40MB, cioè il valore massimo che avevamo impostato per le singole parti.

Parti di uno zip multipart

Ora che abbiamo ottenuto uno zip multipart, possiamo tornare al nostro problema originale che è quello di gestire un file di questo tipo da un applicativo desktop java. Per prima cosa dobbiamo trovare il modo di ricostruire un file zip globale, assemblando le diverse parti in cui il file originale è stato suddiviso e ci è stato inviato.
Per assemblare le varie parti possiamo utilizzare il comando dos copy con l’opzione /B che indica che si tratta di file binari. In particolare il comando che dovremmo sottomettere è il seguente:

copy /B jdk-7u67-windows-x64.zip.* globalFile.zip 

Nell’immagine seguente testiamo l’esecuzione di questo comando dal prompt di dos.

Zip multipart assemblato con copy /b

Nella stessa cartella in cui si trovavano i diversi file .zip001, .zip002, ecc, dovrebbe ora essere presente un nuovo file zip chiamato “globalFile.zip” che rappresenta l’archivio ricostruito nella sua interezza. A questo punto dobbiamo verificare che il processo sia andato a buon fine e che il file .zip sia stato creato correttamente e non risulti magari corrotto. Proviamo quindi ad aprirlo con un tool grafico, come ad esempio 7zip.
L’immagine seguente mostra che il file “globalFile.zip” è stato correttamente creato, che si apre normalmente e che al suo interno è presente il file di partenza che avevamo compresso come file zip multipart.

zip multipart riassemblato

Ora che abbiamo un normale file .zip possiamo estrarre il suo contenuto invocando un comando di un tool per la gestione dei file compressi. Nell’immagine seguente, utilizziamo il comando winrar dal prompt di dos, dopo averlo ovviamente preventivamente installato, per estrarre il file originale dall’archivio (evidenziato in azzurro). Il comando viene invocato con l’opzione x che sta ad indicare che vogliamo effettuare un’estrazione.
Il comando esatto è il seguente (eventualmente preceduto dal path assoluto se il path di winrar non è stato aggiunto alla variabile d’ambiente):

winrar x globalFile.zip

file estratti da zip multipart

Ora che abbiamo visto i comandi da utilizzare per riassemblare e scompattare uno zip multipart, vediamo come invocarli dalla nostra applicazione java.
Scriviamo una classe “UnzipMultipart” che avrà un metodo statico “unzip”, che prende come parametri di input la directory in cui si trovano i vari “pezzi” del nostro multipart e la cartella di output in cui vogliamo estrarre i files contenuti, ed effettuerà tale estrazione invocando i comandi di sistema che abbiamo appena analizzato.

Il codice seguente mostra il prototipo del metodo unzip e la sua invocazione dal main di test, in cui gli si passa il path in cui si trovano i file .zip001, .zip002, ecc ed il path di output (che in questo caso coincidono)

public class UnzipMultipart {

    public static void main (String[] args) {
           unzip("C:\\Users\\MolinariD\\Desktop\\MULTIPART","C:\\Users\\MolinariD\\Desktop\\MULTIPART");
    }

    public static void unzip(String ZipSourceDir, String destDir)  {

		// IMPLEMENTAZIONE
    }

Per prima cosa il nostro metodo dovrà recuperare la lista di tutte le parti che compongono lo zip e stabilire il nome dello zip complessivo in cui assemblerà le parti:

File fi = new File(ZipSourceDir);
String[] multipartFiles = fi.list();
String outputFile = ZipSourceDir + "\\zip_globale.zip";
String fileName = ZipSourceDir + "\\" + multipartFiles[0];

Ora che abbiamo la lista dei vari files possiamo comporre il comando copy per assemblarli. Per farlo dobbiamo prima rimuovere da essi il suffisso .zip.01, .zip02, ecc e poi specificare la cartella di output.

String copyCMD = "copy /B \"" + fileName.substring(0, fileName.length() - 4) + ".*\" \"" + outputFile + "\"";
System.out.println(copyCMD);

Una volta costruito il comando occorrà poi invocarlo effettivamente. Per eseguire un comando dos da Java dobbiamo utilizzare un oggetto Process e mandarlo in esecuzione tramite la chiamata Runtime.getRuntime().exec(“command”), che può però scatenare una IOException. Inoltre, se vogliamo attendere la terminazione del processo che abbiamo lanciato prima di procedere, dobbiamo invocare sull’oggetto Process il metodo waitFor().

Process p;
try {
	p = Runtime.getRuntime().exec("cmd.exe /c " + copyCMD);
	
	try {
		p.waitFor();
	}
	catch (InterruptedException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
}
catch (IOException e1) {
	System.out.println("ERROR! COPY command fails!");
}

Questa implementazione però non è sufficiente a garantire la terminazione del processo che esegue il comando, in quanto esso potrebbe rimanere “appeso” in attesa di svuotare i buffer di scrittura sullo standard error in caso di errore o di lettura dalla standard input. In sostanza, occorre forzare il consumo dei buffer di questi stream in modo che il comando non resti in attesa ad oltranza senza terminare mai la sua esecuzione. La cosa migliore da fare è leggere continuamente da questi stream fino al raggiungimento della loro fine, cioè quanto il metodo read() restituisce -1, in un thread dedicato.
Modifichiamo quindi ora il codice che esegue il comando di copy tramite exec(), aggiungendo la creazione di due threads che in parallelo consumeranno i buffer dello standard input e dello standard error.

Process p;
try {
	p = Runtime.getRuntime().exec("cmd.exe /c " + copyCMD);

	// necessari per eseguire correttamente il processo da Java
	// ed evitare che resti "appeso"
	final InputStream in = p.getInputStream();
	new Thread(new Runnable() {

		@Override
		public void run() {
			try {
				while (in.read() != -1) {
					;
				}
			}
			catch (IOException e) {
				// TODO Auto-generated catch block
			}
		}
	}).start();

	final InputStream er = p.getErrorStream();
	new Thread(new Runnable() {

		@Override
		public void run() {
			try {
				while (er.read() != -1) {
					;
				}
			}
			catch (IOException e) {
				// TODO Auto-generated catch block
			}
		}
	}).start();

	try {
		p.waitFor();
	}
	catch (InterruptedException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}

	er.close();
	in.close();
}
catch (IOException e1) {
	System.out.println("ERROR! COPY command fails!");
}

System.out.println("Command executed: " + copyCMD);

In questo modo abbiamo la garanzia che l’esecuzione del comando termini.
Ora che abbiamo visto come eseguire correttamente dei comandi di sistema da Java tramite oggetti Process ed i metodi exec() e waiFor(), procediamo nell’implementazione del nostro metodo per la gestione di file zip multipart.
Dopo aver eseguito il comando copy per assemblare la varie parti in unico zip, utilizziamo il comando mkdir per creare una nuova directory, che chiameremo multipart, in cui estrarremo i files contenuti nello zip. Anche il comando mkdir può fornire un output sullo standard error, ad esempio nel caso in cui la directory che vogliamo creare esista già. Per essere sicuri di non incappare in situazioni di stallo con il comando appeso che non termina, aggiungiamo nuovamente i due threads che consumano gli streams.

try {
	p = Runtime.getRuntime().exec("cmd.exe /c mkdir \"" + destDir + "\\multipart\"");

	// necessari per eseguire correttamente il processo da Java
	// ed evitare che resti "appeso"
	final InputStream in = p.getInputStream();
	new Thread(new Runnable() {

		@Override
		public void run() {
			try {
				while (in.read() != -1) {
					;
				}
			}
			catch (IOException e) {
				// TODO Auto-generated catch block
			}
		}
	}).start();

	final InputStream er = p.getErrorStream();
	new Thread(new Runnable() {

		@Override
		public void run() {
			try {
				while (er.read() != -1) {
					;
				}
			}
			catch (IOException e) {
				// TODO Auto-generated catch block
			}
		}
	}).start();

	try {
		p.waitFor();
	}
	catch (InterruptedException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}

	er.close();
	in.close();
}
catch (IOException e1) {
	System.out.println("ERROR! MKDIR command fails!");
}

System.out.println("Command executed: mkdir \"" + destDir + "\\multipart\"");

Infine, l’ultimo comando di sistema che dobbiamo invocare è quello per scompattare il contenuto del file zip nella cartella multipart appena creata. In questo caso utilizziamo il comando winrar, di cui configuriamo il path in una variabile statica per semplicità:

private static String winrarPath = "C:\\Users\\MolinariD\\Documents\\winrar";

Il comando lo invochiamo nel solido modo:

try {
	Process p2 = Runtime.getRuntime().exec("cmd.exe /c " + winrarPath + " x \"" + outputFile
			+ "\" \"" + destDir + "\\multipart\"");

	// necessari per eseguire correttamente il processo da Java
    final InputStream in = p2.getInputStream();
	new Thread(new Runnable() {

		@Override
		public void run() {
			try {
				while (in.read() != -1) {
					;
				}
			}
			catch (IOException e) {
			 // TODO Auto-generated catch block
			}
		}
	}).start();

	final InputStream er = p2.getErrorStream();
	new Thread(new Runnable() {

		@Override
		public void run() {
			try {
				while (er.read() != -1) {
					;
				}
			}
			catch (IOException e) {
			 // TODO Auto-generated catch block
			}
		}
	}).start();

	try {
		p2.waitFor();
	}
	catch (InterruptedException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}

	er.close();
	in.close();

	// System.out.println("COMANDO UNZIP EXIT STATUS: " + p2.exitValue());
}
catch (IOException e) {
	System.out.println("ERROR! WINRAR command fails");
	e.printStackTrace();
}

System.out.println("Command executed: " + winrarPath + " x \"" + outputFile
			+ "\" \"" + destDir + "\\multipart\"");

Arrivati a questo punto, dovremmo avere nella cartella multipart i files estratti dall’archivio zip. Per verificarlo facciamo un list() della cartella e stampiamo il nome di tutti i files contenuti:

File f = new File(destDir + "\\multipart");
String[] files = f.list();
System.out.println("Files included in the multipart zip: ");
int i = 0;
for (String file : files) {
	System.out.println("\t" + (++i) + ") " + destDir + "\\multipart\\" + file);
}

Se tutto è andato a buon fine, nel nostro caso dovremmo ottenere il seguente risultato:

Command executed: copy /B "C:\Users\MolinariD\Desktop\MULTIPART\jdk-7u67-windows-x64.zip.*" "C:\Users\MolinariD\Desktop\MULTIPART\zip_globale.zip"
Command executed: mkdir "C:\Users\MolinariD\Desktop\MULTIPART\multipart"
Command executed: C:\Users\MolinariD\Documents\winrar x "C:\Users\MolinariD\Desktop\MULTIPART\zip_globale.zip" "C:\Users\MolinariD\Desktop\MULTIPART\multipart"
Files included in the multipart zip: 
	1) C:\Users\MolinariD\Desktop\MULTIPART\multipart\jdk-7u67-windows-x64.exe

Verifichiamo sul file system che effettivamente la cartella ed il file siano presenti e non corrotti:

creazione cartella per estrazione multipart

zip multipart estratto tramite java

Di seguito il codice completo della classe UnzipMultipart e del metodo unzip che è comunque possibile scaricare qui:

import java.io.File;
import java.io.IOException;
import java.io.InputStream;

public class UnzipMultipart {

    private static String winrarPath = "C:\\Users\\MolinariD\\Documents\\winrar";

    public static void main (String[] args) {

        unzip("C:\\Users\\MolinariD\\Desktop\\MULTIPART","C:\\Users\\MolinariD\\Desktop\\MULTIPART");
    }

    public static void unzip(String ZipSourceDir, String destDir)  {

        File fi = new File(ZipSourceDir);
        String[] multipartFiles = fi.list();
        String outputFile = ZipSourceDir + "\\zip_globale.zip";
        String fileName = ZipSourceDir + "\\" + multipartFiles[0];

        String copyCMD = "copy /B \"" + fileName.substring(0, fileName.length() - 4) + ".*\" \"" + outputFile + "\"";

        Process p;
        try {
            p = Runtime.getRuntime()
                               .exec("cmd.exe /c " + copyCMD);

            // necessari per eseguire correttamente il processo da Java
            // ed evitare che resti "appeso"
            final InputStream in = p.getInputStream();
            new Thread(new Runnable() {

                @Override
                public void run() {
                    try {
                        while (in.read() != -1) {
                            ;
                        }
                    }
                    catch (IOException e) {
                        // TODO Auto-generated catch block
                    }
                }
            }).start();

            final InputStream er = p.getErrorStream();
            new Thread(new Runnable() {

                @Override
                public void run() {
                    try {
                        while (er.read() != -1) {
                            ;
                        }
                    }
                    catch (IOException e) {
                        // TODO Auto-generated catch block
                    }
                }
            }).start();

            try {
                p.waitFor();
            }
            catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            er.close();
            in.close();
        }
        catch (IOException e1) {
            System.out.println("ERROR! COPY command fails!");
        }

        System.out.println("Command executed: " + copyCMD);

        try {
            p = Runtime.getRuntime().exec("cmd.exe /c mkdir \"" + destDir
                                          + "\\multipart\"");
            // necessari per eseguire correttamente il processo da Java
            // ed evitare che resti "appeso"
            final InputStream in = p.getInputStream();
            new Thread(new Runnable() {

                @Override
                public void run() {
                    try {
                        while (in.read() != -1) {
                            ;
                        }
                    }
                    catch (IOException e) {
                        // TODO Auto-generated catch block
                    }
                }
            }).start();

            final InputStream er = p.getErrorStream();
            new Thread(new Runnable() {

                @Override
                public void run() {
                    try {
                        while (er.read() != -1) {
                            ;
                        }
                    }
                    catch (IOException e) {
                        // TODO Auto-generated catch block
                    }
                }
            }).start();

            try {
                p.waitFor();
            }
            catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            er.close();
            in.close();
        }
        catch (IOException e1) {
            System.out.println("ERROR! MKDIR command fails!");
        }

        System.out.println("Command executed: mkdir \"" + destDir + "\\multipart\"");

        try {
            Process p2 = Runtime.getRuntime().exec("cmd.exe /c " + winrarPath + " x \"" + outputFile
                    + "\" \"" + destDir + "\\multipart\"");

            // necessari per eseguire correttamente il processo da Java
            final InputStream in = p2.getInputStream();
            new Thread(new Runnable() {

                @Override
                public void run() {
                    try {
                        while (in.read() != -1) {
                            ;
                        }
                    }
                    catch (IOException e) {
                     // TODO Auto-generated catch block
                    }
                }
            }).start();

            final InputStream er = p2.getErrorStream();
            new Thread(new Runnable() {

                @Override
                public void run() {
                    try {
                        while (er.read() != -1) {
                            ;
                        }
                    }
                    catch (IOException e) {
                     // TODO Auto-generated catch block
                    }
                }
            }).start();

            try {
                p2.waitFor();
            }
            catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            er.close();
            in.close();

            // System.out.println("COMANDO UNZIP EXIT STATUS: " + p2.exitValue());
        }
        catch (IOException e) {
            System.out.println("ERROR! WINRAR command fails");
            e.printStackTrace();
        }

        System.out.println("Command executed: " + winrarPath + " x \"" + outputFile
                    + "\" \"" + destDir + "\\multipart\"");

        File f = new File(destDir + "\\multipart");
        String[] files = f.list();
        System.out.println("Files included in the multipart zip: ");
        int i = 0;
        for (String file : files) {
            System.out.println("\t" + (++i) + ") " + destDir
                        + "\\multipart\\" + file);
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *