使用 Java 将文件附加到 zip 文件

Posted

技术标签:

【中文标题】使用 Java 将文件附加到 zip 文件【英文标题】:Appending files to a zip file with Java 【发布时间】:2011-01-14 11:18:41 【问题描述】:

我目前正在提取一个war文件的内容,然后将一些新文件添加到目录结构中,然后创建一个新的war文件。

这一切都是从 Java 以编程方式完成的 - 但我想知道复制战争文件然后只是附加文件是否更有效 - 这样我就不必等待战争扩大和然后必须再次压缩。

我似乎无法在文档或任何在线示例中找到执行此操作的方法。

谁能给点提示或指点?

更新:

其中一个答案中提到的 TrueZip 似乎是一个非常好的 Java 库,可以附加到 zip 文件中(尽管其他答案说不可能这样做)。

任何人有关于 TrueZip 的经验或反馈,或者可以推荐其他类似的库吗?

【问题讨论】:

在 truezip 邮件列表中找到这篇文章:truezip.dev.java.net/servlets/… 结论:truezip 当前不支持快速附加操作 【参考方案1】:

在 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");
    

【讨论】:

我们如何通过 smb 使用这个?我想将文件从 osx/linux 机器添加到 Windows 机器中的 zip 文件中。 Zip 文件系统无法真正处理文件夹结构中的空格。如需解决方法,请使用“%2520”对所有空格进行编码(另请参阅***.com/questions/9873845/…) 请注意,ZipFileSystem 默认情况下是vulnerable to OutOfMemoryError 大量输入。 使用 Files.copy 代替:try (FileSystem jarFs = FileSystems.newFileSystem(uri, env, null)) for(final Path newFilePath : newFilePathList) final Path pathInZipFile = jarFs.getPath("/ " + newFilePath.getFileName()); Files.copy(newFilePath, pathInZipFile, StandardCopyOption.REPLACE_EXISTING); 这个答案展示了如何做到这一点,但它是如何在幕后工作的?以 zip 格式更新文件是有效的还是相当于解压缩并构建一个新的 zip?【参考方案2】:

正如其他人所提到的,不可能将内容附加到现有的 zip(或战争)。但是,可以即时创建新的 zip,而无需临时将提取的内容写入磁盘。很难猜测这会快多少,但它是使用标准 Java 可以获得的最快速度(至少据我所知)。正如 Carlos Tasada 所提到的,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();
    

【讨论】:

我的 war 文件是 30Mb 压缩的 - 不确定这种方法是否是最好的方法,因为它需要大量内存 - 我已经在内存中缓存了很多数据库查询,这可能会使内存占地面积太大。 @Grouchal 实际上你永远不需要比BUFFER 更多的内存(我选择了 4MB,但你可以根据自己的需要随意调整它——减少它应该不会有什么坏处仅几 KB)。该文件永远不会完全存储在内存中。 这个想法是将现有战争的内容解压缩到BUFFER,然后将其压缩到一个新的存档中——一个接一个地进入。之后,您最终会得到准备好接收更多条目的相同存档。我选择在 answer.txt 中写入“42”。那是您应该放置代码以附加更多条目的地方。 这种方法与 gnlogic 提到的使用 TrueZip 相比如何? TrueZip 似乎真的附加到文件中 如果您使用这种方法得到 ZipException - invalid entry compressed size,请参阅coderanch.com/t/275390/Streams/java/…【参考方案3】:

我曾经有过类似的要求 - 但它是用于读取和写入 zip 档案(.war 格式应该类似)。我尝试使用现有的 Java Zip 流来完成它,但发现编写部分很麻烦——尤其是在涉及目录时。

我建议您尝试TrueZIP(开源 - apache 风格许可)库,它将任何存档公开为虚拟文件系统,您可以像普通文件系统一样在其中读取和写入。它对我来说就像一种魅力,极大地简化了我的开发。

【讨论】:

这看起来很不错 - 想知道是否有任何性能问题需要了解? 到目前为止,我已经能够有效地将它用于中等大小的文件(3 MB 等)。没有遇到任何性能问题。 Java 7 中有一个新选项,ZipFileSystem 必须注意,TrueZIP 的继承者 TrueVFS 在适当的时候使用 Java 7 NIO 2 功能,但提供 much more features 类似线程安全的异步并行压缩。 在separate answer 中添加了 TrueVFS 代码示例。【参考方案4】:

你可以使用我写的这段代码

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();
    

【讨论】:

使用此代码,新文件优先于旧文件 您也可以根据需要更改缓冲区大小,现在代码中的缓冲区大小仅适用于小文件 真的很喜欢这段代码,但我需要一些其他的东西,我需要将文件添加到 zip 中的文件夹中,而不仅仅是 zip 的根目录我在这里发布了我编辑的方法***.com/questions/9300115/…希望它可以帮助其他人非常感谢 Liam 的伟大基础代码并没有真正改变太多,但我认为现在这是一个很好的方法 :)【参考方案5】:

我不知道有什么 Java 库可以满足您的描述。但是你描述的很实用。您可以在 .NET 中使用 DotNetZip 进行操作。

Michael Krauklis 是正确的,您不能简单地将数据“附加”到 war 文件或 zip 文件中,但这并不是因为严格来说,在 war 文件中存在“文件结束”指示。这是因为war (zip) 格式包含一个目录,该目录通常出现在文件末尾,其中包含war 文件中各种条目的元数据。天真地附加到一个war文件会导致目录没有更新,所以你只有一个附加了垃圾的war文件。

需要一个理解格式的智能类,并且可以读取+更新war文件或zip文件,包括适当的目录。 DotNetZip 执行此操作,无需解压缩/重新压缩未更改的条目,就像您描述或希望的那样。

【讨论】:

【参考方案6】:

正如 Cheeso 所说,没有办法做到这一点。 AFAIK zip 前端与您在内部做的完全一样。

无论如何,如果您担心提取/压缩所有内容的速度,您可能想尝试SevenZipJBindings 库。

几个月前,我在blog 中介绍了这个库(对于自动推广感到抱歉)。举个例子,使用 java.util.zip 提取一个 104MB 的 zip 文件需要 12 秒,而使用这个库需要 4 秒。

在这两个链接中,您都可以找到有关如何使用它的示例。

希望对你有帮助。

【讨论】:

@carlos 关于您的博文:您使用的是哪个 Java 版本?我刚刚在 amd64 Linux 系统(4 核)上测试了使用标准 API (new ZipFile(file).size()) 和最新的 7Zip 绑定与 Java 1.6.0_17 获取 148M ZIP 存档的大小。到目前为止,标准 API 的性能优于 7Zip(至少对于您在博客上展示的任务:获取条目数)。 Java 平均需要 1.5 毫秒,而 7Zip 需要 350 毫秒来运行 100 次(不包括预热)。所以在我看来,没有必要在这种问题上扔原生库。 没有意识到这将使用本机库,感谢指出 - 不会进一步调查。 @Carlos:如果你有空闲时间,你能把提取和Apache common compress (commons.apache.org/compress)比较一下吗? @dma_k:我可以进行测试,但文档说“gzip 支持由 Java 类库的 java.util.zip 包提供。”所以我不希望有任何区别 我确认(在检查commons-compress 来源后):它尽可能利用可用的算法。他们创建了自己的ZipFile 实现,但它基于java.util.zip.Inflater 等。我也不希望有任何巨大的速度提升,但是从 .zip 文件中提取的比较可能对您来说很有趣,只是为了完整性。【参考方案7】:

看到这个bug report。

在任何类型的 结构化数据,如 zip 文件或 tar 文件不是您真正可以做到的 期待工作。这些文件格式 有一个内在的“文件结尾” 数据格式中内置的指示。

如果你真的想跳过 un-waring/re-waring 的中间步骤,你可以读取 war 文件文件,获取所有 zip 条目,然后写入一个新的 war 文件“附加”你想要的新条目添加。并不完美,但至少是一个更加自动化的解决方案。

【讨论】:

我不确定您提出的解决方案与我已经在做的有什么不同 - 这如何更加自动化? 我仍然很想了解您的解决方案-您说相反或取消战争然后重新战争我应该读取文件然后写入新的战争-这不是一回事吗?请你解释一下【参考方案8】:

另一个解决方案:您可能会发现下面的代码在其他情况下也很有用。我用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);
    

【讨论】:

【参考方案9】:

这是一个使用 servlet 获取响应并发送响应的简单代码

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);
            

【讨论】:

【参考方案10】:

以下是使用TrueVFS 将文件附加到现有 zip 的示例:

// 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()));

TrueZIP 的继任者 TrueVFS 在适当的时候使用 Java 7 NIO 2 功能,但提供 much more features 类似线程安全的异步并行压缩。

还要注意,Java 7 ZipFileSystem 默认情况下是 vulnerable to OutOfMemoryError 大量输入。

【讨论】:

【参考方案11】:

这是 Liam 答案的 Java 1.7 版本,它使用资源和 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();
          
        
      

    

  

【讨论】:

【参考方案12】:

如果您不想使用额外的库,这 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());

【讨论】:

【参考方案13】:

根据上面@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);
    

【讨论】:

以上是关于使用 Java 将文件附加到 zip 文件的主要内容,如果未能解决你的问题,请参考以下文章

如何在批处理文件中附加日期

如何在不使用 Java 的情况下压缩 .csv 文件并在电子邮件中附加 Oracle plsql

Python 2.6 不喜欢附加到 zip 文件中的现有档案

将数据文件附加到 CSV 文件并输出到另一个 CSV 文件

如何将库源附加到 JetBrains Rider

如何将文本附加到 Java 中的现有文件?