Добавление файлов в zip-файл с помощью Java
В настоящее время я извлекаю содержимое файла войны, а затем добавляю несколько новых файлов в структуру каталогов, а затем создаю новый файл войны.
Все это делается программно из Java - но мне интересно, не будет ли эффективнее скопировать файл войны, а затем просто добавить файлы - тогда мне не придется ждать, пока война расширяется, а затем приходится быть сжатым снова.
Я не могу найти способ сделать это в документации или каких-либо онлайн-примеров.
Кто-нибудь может дать несколько советов или указателей?
ОБНОВИТЬ:
TrueZip, как упомянуто в одном из ответов, кажется очень хорошей библиотекой Java для добавления в zip-файл (несмотря на другие ответы, в которых говорится, что это невозможно).
Кто-нибудь имеет опыт или отзывы о TrueZip или может порекомендовать другие подобные библиотеки?
14 ответов
В Java 7 мы получили Zip File System, которая позволяет добавлять и изменять файлы в zip (jar, war) без ручной перепаковки.
Мы можем напрямую записывать файлы внутри zip-файлов, как в следующем примере.
Map<String, String> env = new HashMap<>();
env.put("create", "true");
Path path = Paths.get("test.zip");
URI uri = URI.create("jar:" + path.toUri());
try (FileSystem fs = FileSystems.newFileSystem(uri, env))
{
Path nf = fs.getPath("new.txt");
try (Writer writer = Files.newBufferedWriter(nf, StandardCharsets.UTF_8, StandardOpenOption.CREATE)) {
writer.write("hello");
}
}
Как уже упоминалось, невозможно добавить контент в существующий zip (или войну). Тем не менее, можно создать новый zip-файл на лету без временной записи извлеченного содержимого на диск. Трудно догадаться, насколько быстрее это будет, но это самый быстрый способ получить (по крайней мере, насколько я знаю) стандартную Java. Как упомянул Карлос Тасада, SevenZipJBindings может выжать у вас несколько дополнительных секунд, но перенос этого подхода на SevenZipJBindings будет все же быстрее, чем использование временных файлов с той же библиотекой.
Вот некоторый код, который записывает содержимое существующего zip (war.zip) и добавляет дополнительный файл (answer.txt) к новому zip (append.zip). Все, что нужно, это Java 5 или более поздняя версия, дополнительные библиотеки не требуются.
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
public class Main {
// 4MB buffer
private static final byte[] BUFFER = new byte[4096 * 1024];
/**
* copy input to output stream - available in several StreamUtils or Streams classes
*/
public static void copy(InputStream input, OutputStream output) throws IOException {
int bytesRead;
while ((bytesRead = input.read(BUFFER))!= -1) {
output.write(BUFFER, 0, bytesRead);
}
}
public static void main(String[] args) throws Exception {
// read war.zip and write to append.zip
ZipFile war = new ZipFile("war.zip");
ZipOutputStream append = new ZipOutputStream(new FileOutputStream("append.zip"));
// first, copy contents from existing war
Enumeration<? extends ZipEntry> entries = war.entries();
while (entries.hasMoreElements()) {
ZipEntry e = entries.nextElement();
System.out.println("copy: " + e.getName());
append.putNextEntry(e);
if (!e.isDirectory()) {
copy(war.getInputStream(e), append);
}
append.closeEntry();
}
// now append some extra content
ZipEntry e = new ZipEntry("answer.txt");
System.out.println("append: " + e.getName());
append.putNextEntry(e);
append.write("42\n".getBytes());
append.closeEntry();
// close
war.close();
append.close();
}
}
Некоторое время назад у меня было похожее требование - но оно было для чтения и записи zip-архивов (формат.war должен быть похожим). Я попытался сделать это с существующими потоками Java Zip, но сочинял часть написания громоздкой - особенно когда каталоги там, где это было необходимо.
Я рекомендую вам опробовать библиотеку TrueZIP (с открытым исходным кодом - по лицензии apache), которая предоставляет любой архив в виде виртуальной файловой системы, в которую вы можете читать и писать, как обычная файловая система. Это сработало как шарм для меня и значительно упростило мое развитие.
Вы можете использовать этот фрагмент кода, который я написал
public static void addFilesToZip(File source, File[] files)
{
try
{
File tmpZip = File.createTempFile(source.getName(), null);
tmpZip.delete();
if(!source.renameTo(tmpZip))
{
throw new Exception("Could not make temp file (" + source.getName() + ")");
}
byte[] buffer = new byte[1024];
ZipInputStream zin = new ZipInputStream(new FileInputStream(tmpZip));
ZipOutputStream out = new ZipOutputStream(new FileOutputStream(source));
for(int i = 0; i < files.length; i++)
{
InputStream in = new FileInputStream(files[i]);
out.putNextEntry(new ZipEntry(files[i].getName()));
for(int read = in.read(buffer); read > -1; read = in.read(buffer))
{
out.write(buffer, 0, read);
}
out.closeEntry();
in.close();
}
for(ZipEntry ze = zin.getNextEntry(); ze != null; ze = zin.getNextEntry())
{
out.putNextEntry(ze);
for(int read = zin.read(buffer); read > -1; read = zin.read(buffer))
{
out.write(buffer, 0, read);
}
out.closeEntry();
}
out.close();
tmpZip.delete();
}
catch(Exception e)
{
e.printStackTrace();
}
}
Я не знаю библиотеки Java, которая делает то, что вы описываете. Но то, что вы описали, практично. Вы можете сделать это в.NET, используя DotNetZip.
Майкл Крауклис прав, что вы не можете просто "добавить" данные в файл войны или zip-файл, но это не потому, что в файле войны, строго говоря, есть указание "конец файла". Это происходит потому, что формат war (zip) включает каталог, который обычно присутствует в конце файла, который содержит метаданные для различных записей в файле war. Наивное добавление к файлу войны не приводит к обновлению каталога, поэтому у вас просто есть файл войны с добавленным мусором.
Необходим интеллектуальный класс, который понимает формат и может читать + обновлять файл военных действий или zip-файл, включая соответствующий каталог. DotNetZip делает это, не распаковывая / не сжимая неизмененные записи, как вы описали или пожелали.
Как говорит Cheeso, это невозможно сделать. AFAIK пользовательские интерфейсы на молнии делают то же самое, что и вы внутри.
В любом случае, если вас беспокоит скорость извлечения / сжатия всего, вы можете попробовать библиотеку SevenZipJBindings.
Я освещал эту библиотеку в своем блоге несколько месяцев назад (извините за авто-продвижение). В качестве примера, извлечение файла ZIP размером 104 МБ с использованием java.util.zip заняло у меня 12 секунд, в то время как использование этой библиотеки заняло 4 секунды.
В обеих ссылках вы можете найти примеры того, как его использовать.
Надеюсь, поможет.
Смотрите этот отчет об ошибке.
Использование режима добавления для любых структурированных данных, таких как zip-файлы или tar-файлы, - это не то, чего вы действительно можете ожидать. Эти форматы файлов имеют встроенную индикацию "конец файла", встроенную в формат данных.
Если вы действительно хотите пропустить промежуточный этап отмены / повторной обработки, вы можете прочитать файл war-файла, получить все записи zip, а затем записать в новый war-файл, "добавляя" новые записи, которые вы хотите добавить. Не идеально, но, по крайней мере, более автоматизированное решение.
Это простой код, чтобы получить ответ с помощью сервлета и отправить ответ
myZipPath = bla bla...
byte[] buf = new byte[8192];
String zipName = "myZip.zip";
String zipPath = myzippath+ File.separator+"pdf" + File.separator+ zipName;
File pdfFile = new File("myPdf.pdf");
ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipPath));
ZipEntry zipEntry = new ZipEntry(pdfFile.getName());
out.putNextEntry(zipEntry);
InputStream in = new FileInputStream(pdfFile);
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
out.closeEntry();
in.close();
out.close();
FileInputStream fis = new FileInputStream(zipPath);
response.setContentType("application/zip");
response.addHeader("content-disposition", "attachment;filename=" + zipName);
OutputStream os = response.getOutputStream();
int length = is.read(buffer);
while (length != -1)
{
os.write(buffer, 0, length);
length = is.read(buffer);
}
Еще одно решение: приведенный ниже код может оказаться полезным и в других ситуациях. Я использовал ant таким образом для компиляции каталогов Java, генерации jar-файлов, обновления zip-файлов,...
public static void antUpdateZip(String zipFilePath, String libsToAddDir) {
Project p = new Project();
p.init();
Target target = new Target();
target.setName("zip");
Zip task = new Zip();
task.init();
task.setDestFile(new File(zipFilePath));
ZipFileSet zipFileSet = new ZipFileSet();
zipFileSet.setPrefix("WEB-INF/lib");
zipFileSet.setDir(new File(libsToAddDir));
task.addFileset(zipFileSet);
task.setUpdate(true);
task.setProject(p);
task.init();
target.addTask(task);
target.setProject(p);
p.addTarget(target);
DefaultLogger consoleLogger = new DefaultLogger();
consoleLogger.setErrorPrintStream(System.err);
consoleLogger.setOutputPrintStream(System.out);
consoleLogger.setMessageOutputLevel(Project.MSG_DEBUG);
p.addBuildListener(consoleLogger);
try {
// p.fireBuildStarted();
// ProjectHelper helper = ProjectHelper.getProjectHelper();
// p.addReference("ant.projectHelper", helper);
// helper.parse(p, buildFile);
p.executeTarget(target.getName());
// p.fireBuildFinished(null);
} catch (BuildException e) {
p.fireBuildFinished(e);
throw new AssertionError(e);
}
}
Вот примеры того, как легко файлы могут быть добавлены к существующему zip с использованием TrueVFS:
// append a file to archive under different name
TFile.cp(new File("existingFile.txt"), new TFile("archive.zip", "entry.txt"));
// recusively append a dir to the root of archive
TFile src = new TFile("dirPath", "dirName");
src.cp_r(new TFile("archive.zip", src.getName()));
TrueVFS, преемник TrueZIP, при необходимости использует функции Java 7 NIO 2, но предлагает гораздо больше функций, таких как поточно-безопасное асинхронное параллельное сжатие.
Помните также, что Java 7 ZipFileSystem по умолчанию уязвима для OutOfMemoryError на огромных входах.
Это работает на 100%, если вы не хотите использовать дополнительные библиотеки.. 1) во-первых, класс, который добавляет файлы в zip-файл..
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class AddZip {
public void AddZip() {
}
public void addToZipFile(ZipOutputStream zos, String nombreFileAnadir, String nombreDentroZip) {
FileInputStream fis = null;
try {
if (!new File(nombreFileAnadir).exists()) {//NO EXISTE
System.out.println(" No existe el archivo : " + nombreFileAnadir);return;
}
File file = new File(nombreFileAnadir);
System.out.println(" Generando el archivo '" + nombreFileAnadir + "' al ZIP ");
fis = new FileInputStream(file);
ZipEntry zipEntry = new ZipEntry(nombreDentroZip);
zos.putNextEntry(zipEntry);
byte[] bytes = new byte[1024];
int length;
while ((length = fis.read(bytes)) >= 0) {zos.write(bytes, 0, length);}
zos.closeEntry();
fis.close();
} catch (FileNotFoundException ex ) {
Logger.getLogger(AddZip.class.getName()).log(Level.SEVERE, null, ex);
} catch (IOException ex) {
Logger.getLogger(AddZip.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
2) вы можете позвонить в свой контроллер..
//in the top
try {
fos = new FileOutputStream(rutaZip);
zos = new ZipOutputStream(fos);
} catch (FileNotFoundException ex) {
Logger.getLogger(UtilZip.class.getName()).log(Level.SEVERE, null, ex);
}
...
//inside your method
addZip.addToZipFile(zos, pathFolderFileSystemHD() + itemFoto.getNombre(), "foto/" + itemFoto.getNombre());
Основываясь на ответе @sfussenegger выше, следующий код используется для добавления к файлу jar и его загрузки:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Resource resourceFile = resourceLoader.getResource("WEB-INF/lib/custom.jar");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(baos, StandardCharsets.ISO_8859_1);) {
try (ZipFile zin = new ZipFile(resourceFile.getFile(), StandardCharsets.ISO_8859_1);) {
zin.stream().forEach((entry) -> {
try {
zos.putNextEntry(entry);
if (!entry.isDirectory()) {
zin.getInputStream(entry).transferTo(zos);
}
zos.closeEntry();
} catch (Exception ex) {
ex.printStackTrace();
}
});
}
/* build file records to be appended */
....
for (FileContents record : records) {
zos.putNextEntry(new ZipEntry(record.getFileName()));
zos.write(record.getBytes());
zos.closeEntry();
}
zos.flush();
}
response.setContentType("application/java-archive");
response.setContentLength(baos.size());
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"custom.jar\"");
try (BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream())) {
baos.writeTo(out);
}
}
zipFolder = report — это папка, в которой будут представлены все файлы, которые будут сжаты. Расположение zipFolder = "C:\JavaProgrammingPractice\MavenHelloWorld\bld_root_9\reports.zip"; readfilename: это файл, и его содержимое будет записано в zipFolder. Расположение файлов такое же, как и расположение zipFolder. Местоположение readfilename = "C:\JavaProgrammingPractice\MavenHelloWorld\bld_root_9\reports\readfilename"; fs.getPath(readfilename) = покажет абсолютный путь к файлу. nf.getFileName() = В качестве аргумента newBufferedWriter требуется только имя файла.
public static void addFileToZipFolder(String zipFolder, String readfilename) throws IOException {
Map<String, String> env = new HashMap<>();
env.put("create", "true");
Path path = Path.of(zipFolder);
URI uri = URI.create("jar:" + path.toUri());
try (FileSystem fs = FileSystems.newFileSystem(uri, env))
{
Path nf = fs.getPath(readfilename);
String text = Files.readString(Path.of(readfilename));
try (Writer writer = Files.newBufferedWriter(nf.getFileName(), StandardCharsets.UTF_8, StandardOpenOption.CREATE)) {
writer.write(text);
}
}
}
Вот версия ответа Liam для Java 1.7, которая использует try с ресурсами и Apache Commons IO.
Вывод записывается в новый zip-файл, но его можно легко изменить, чтобы записать в исходный файл.
/**
* Modifies, adds or deletes file(s) from a existing zip file.
*
* @param zipFile the original zip file
* @param newZipFile the destination zip file
* @param filesToAddOrOverwrite the names of the files to add or modify from the original file
* @param filesToAddOrOverwriteInputStreams the input streams containing the content of the files
* to add or modify from the original file
* @param filesToDelete the names of the files to delete from the original file
* @throws IOException if the new file could not be written
*/
public static void modifyZipFile(File zipFile,
File newZipFile,
String[] filesToAddOrOverwrite,
InputStream[] filesToAddOrOverwriteInputStreams,
String[] filesToDelete) throws IOException {
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(newZipFile))) {
// add existing ZIP entry to output stream
try (ZipInputStream zin = new ZipInputStream(new FileInputStream(zipFile))) {
ZipEntry entry = null;
while ((entry = zin.getNextEntry()) != null) {
String name = entry.getName();
// check if the file should be deleted
if (filesToDelete != null) {
boolean ignoreFile = false;
for (String fileToDelete : filesToDelete) {
if (name.equalsIgnoreCase(fileToDelete)) {
ignoreFile = true;
break;
}
}
if (ignoreFile) {
continue;
}
}
// check if the file should be kept as it is
boolean keepFileUnchanged = true;
if (filesToAddOrOverwrite != null) {
for (String fileToAddOrOverwrite : filesToAddOrOverwrite) {
if (name.equalsIgnoreCase(fileToAddOrOverwrite)) {
keepFileUnchanged = false;
}
}
}
if (keepFileUnchanged) {
// copy the file as it is
out.putNextEntry(new ZipEntry(name));
IOUtils.copy(zin, out);
}
}
}
// add the modified or added files to the zip file
if (filesToAddOrOverwrite != null) {
for (int i = 0; i < filesToAddOrOverwrite.length; i++) {
String fileToAddOrOverwrite = filesToAddOrOverwrite[i];
try (InputStream in = filesToAddOrOverwriteInputStreams[i]) {
out.putNextEntry(new ZipEntry(fileToAddOrOverwrite));
IOUtils.copy(in, out);
out.closeEntry();
}
}
}
}
}