4

I have a zip file containing a folder structure like

  • main-folder/
    • subFolder1/
    • subFolder2/
    • subFolder3/
      • file3.1
      • file3.2

I would like to rename folder main-folder to let's say versionXY inside that very zip file using Java.

Is there a simpler way than extracting the whole zip file and recreating a new one using the new folder names?

Simon07
  • 304
  • 1
  • 3
  • 9

4 Answers4

4

Zip is an archive format, so mutating generally involves rewriting the file.

Some particular features of zip also get in the way (zip is full of "features"). As well as the central directory at the end of the archive, each component file is preceded by its file name. Zip doesn't have a concept of directories - file names are just strings that happen to include "/" characters (and substrings such as "../".

So, you really need to copy the file using ZipInputStream and ZipOutputStream, renaming as you go. If you really wanted to you could rewrite the file in place doing your own buffering. The process does cause the contents to be recompressed as the standard API has no means of obtaining the data in compressed form.

Edit: @Doval points out that @megasega's answer uses Zip File System Provider in NIO, new (relative to this answer) in Java SE 7. It's performance will likely be not great, as were the archive file systems in RISC OS' GUI of thirty years ago.

Tom Hawtin - tackline
  • 145,806
  • 30
  • 211
  • 305
  • 1
    The central directory supersedes the local directory headers so that zip files can be read and updated efficiently. The [Zip File System Provider](https://docs.oracle.com/javase/7/docs/technotes/guides/io/fsp/zipfilesystemprovider.html) used in [megasega's answer](https://stackoverflow.com/a/39038523/1272233) wasn't available at the time this answer was written but it updates the central directory without unzipping and rezipping files. – Doval Aug 07 '20 at 13:56
3

This is doing the trick. Blazing fast since it works only on the central directory and not the files.

//  rezip( zipfile, "/main-folder", "/versionXY" );

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;


protected void rezip( String zipfile, String olddir, String newdir ) {

    Path zipFilePath = Paths.get( zipfile );
    try (FileSystem fs = FileSystems.newFileSystem( zipFilePath, null )) {
        Path oldpathInsideZipPath = fs.getPath( olddir );
        if( ! Files.exists( Paths.get( newdir ) ) )
            Files.createDirectory( Paths.get( newdir ) );

        if ( Files.exists( oldpathInsideZipPath, LinkOption.NOFOLLOW_LINKS ) ) {
            Files.walkFileTree(oldpathInsideZipPath, new SimpleFileVisitor<Path>() {
                 @Override
                 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                     throws IOException
                 {
                     if( file.toString().indexOf( olddir ) > -1 ){
                         String a = file.toString().replaceAll( olddir, newdir );
                         Path b = fs.getPath( a );
                         if( ! Files.exists( b.getParent() ) ){
                             Files.createDirectories( b.getParent() );
                         }
                         Files.move( file, b, LinkOption.NOFOLLOW_LINKS );
                     }
                     return FileVisitResult.CONTINUE;
                 }
                 @Override
                 public FileVisitResult postVisitDirectory(Path dir, IOException e)
                     throws IOException
                 {
                     if (e == null) {
                         Files.delete(dir);
                         return FileVisitResult.CONTINUE;
                     } else {
                         // directory iteration failed
                         throw e;
                     }
                 }
             });
        }
        fs.close();
    } catch ( Exception e ) {
        e.printStackTrace();
    }
}
megasega
  • 31
  • 2
  • 1
    I didn't need a recursive solution but `FileSystems.newFileSystem` plus `Files.move` did the trick. The only caveat is that this will make an updated copy of the zip file and delete the old version, so for very large archives this can still be a bit slow and you need enough disk space for the copy. It's still an order of magnitude faster though. This allowed me to rename a file inside a 10+ GB archive that took several minutes to create in just a few seconds. – Doval Aug 07 '20 at 14:02
3

I think you'll be able to find help for this task using the Commons Compress, especially ZipArchiveEntry

Valentin Rocher
  • 11,667
  • 45
  • 59
3

I know you asked about Java but just for archival purposes I thought I would contribute a note about .NET.

DotNetZip is a .NET library for zip files that allows renaming of entries. As Tom Hawtin's reply states, directories are not first-class entities in the zip file metadata, and as a result, no zip libraries that I know of expose a "rename directory" verb. But some libraries allow you to rename all the entries that have names that indicate a particular directory, which gives you the result you want.

In DotNetZip, it would look like this:

 var regex = new Regex("/OldDirName/.*$");
 int renameCount= 0;
 using (ZipFile zip = ZipFile.Read(ExistingZipFile))
 {
    foreach (ZipEntry e in zip)
    {
        if (regex.IsMatch(e.FileName))
        {
            // rename here
            e.FileName = e.FileName.Replace("/OldDirName/", "/NewDirName/");
            renameCount++;
        }
    }
    if (renameCount > 0)
    {
        zip.Comment = String.Format("This archive has been modified. {0} entries have been renamed.", renameCount);
        // any changes to the entries are made permanent by Save()
        zip.Save();  // could also save to a new zip file here
    }
 }

You can also add or remove entries, inside the using clause.

If you save to the same file, then DotNetZip rewrites only the changed metadata - the entry headers and the central directory records for renamed entries, which saves time with large archives. If you save to a new file or stream, then all of the zip data gets written.

Cheeso
  • 189,189
  • 101
  • 473
  • 713
  • @Cheeso: The question was about Java library. We can't use your snippet. – dma_k Mar 16 '10 at 19:22
  • 4
    I know. I started the answer by saying that. I only wanted to put it there, in case someone else did a search for "rename" and "zip" and wasn't limited by Java. – Cheeso Mar 16 '10 at 19:23