-
Notifications
You must be signed in to change notification settings - Fork 316
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
#72 Add feature to rename files in zip
- Loading branch information
1 parent
c127a2e
commit 1cc0168
Showing
10 changed files
with
714 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
src/main/java/net/lingala/zip4j/tasks/AbstractModifyFileTask.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package net.lingala.zip4j.tasks; | ||
|
||
import net.lingala.zip4j.exception.ZipException; | ||
import net.lingala.zip4j.model.FileHeader; | ||
import net.lingala.zip4j.model.ZipModel; | ||
import net.lingala.zip4j.progress.ProgressMonitor; | ||
import net.lingala.zip4j.util.FileUtils; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.io.OutputStream; | ||
import java.io.RandomAccessFile; | ||
import java.util.Random; | ||
|
||
abstract class AbstractModifyFileTask<T> extends AsyncZipTask<T> { | ||
|
||
AbstractModifyFileTask(AsyncTaskParameters asyncTaskParameters) { | ||
super(asyncTaskParameters); | ||
} | ||
|
||
File getTemporaryFile(String zipPathWithName) { | ||
Random random = new Random(); | ||
File tmpFile = new File(zipPathWithName + random.nextInt(10000)); | ||
|
||
while (tmpFile.exists()) { | ||
tmpFile = new File(zipPathWithName + random.nextInt(10000)); | ||
} | ||
|
||
return tmpFile; | ||
} | ||
|
||
long getOffsetLocalFileHeader(FileHeader fileHeader) { | ||
long offsetLocalFileHeader = fileHeader.getOffsetLocalHeader(); | ||
|
||
if (fileHeader.getZip64ExtendedInfo() != null && fileHeader.getZip64ExtendedInfo().getOffsetLocalHeader() != -1) { | ||
offsetLocalFileHeader = fileHeader.getZip64ExtendedInfo().getOffsetLocalHeader(); | ||
} | ||
|
||
return offsetLocalFileHeader; | ||
} | ||
|
||
long getOffsetOfStartOfCentralDirectory(ZipModel zipModel) { | ||
long offsetStartCentralDir = zipModel.getEndOfCentralDirectoryRecord().getOffsetOfStartOfCentralDirectory(); | ||
|
||
if (zipModel.isZip64Format() && zipModel.getZip64EndOfCentralDirectoryRecord() != null) { | ||
offsetStartCentralDir = zipModel.getZip64EndOfCentralDirectoryRecord() | ||
.getOffsetStartCentralDirectoryWRTStartDiskNumber(); | ||
} | ||
|
||
return offsetStartCentralDir; | ||
} | ||
|
||
void cleanupFile(boolean successFlag, File zipFile, File temporaryZipFile) throws ZipException { | ||
if (successFlag) { | ||
restoreFileName(zipFile, temporaryZipFile); | ||
} else { | ||
temporaryZipFile.delete(); | ||
} | ||
} | ||
|
||
long copyFile(RandomAccessFile randomAccessFile, OutputStream outputStream, long start, long length, | ||
ProgressMonitor progressMonitor) throws IOException { | ||
FileUtils.copyFile(randomAccessFile, outputStream, start, start + length, progressMonitor); | ||
return length; | ||
} | ||
|
||
private void restoreFileName(File zipFile, File temporaryZipFile) throws ZipException { | ||
if (zipFile.delete()) { | ||
if (!temporaryZipFile.renameTo(zipFile)) { | ||
throw new ZipException("cannot rename modified zip file"); | ||
} | ||
} else { | ||
throw new ZipException("cannot delete old zip file"); | ||
} | ||
} | ||
} |
212 changes: 212 additions & 0 deletions
212
src/main/java/net/lingala/zip4j/tasks/RenameFileTask.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
package net.lingala.zip4j.tasks; | ||
|
||
import net.lingala.zip4j.exception.ZipException; | ||
import net.lingala.zip4j.headers.HeaderUtil; | ||
import net.lingala.zip4j.headers.HeaderWriter; | ||
import net.lingala.zip4j.model.FileHeader; | ||
import net.lingala.zip4j.model.ZipModel; | ||
import net.lingala.zip4j.model.enums.RandomAccessFileMode; | ||
import net.lingala.zip4j.progress.ProgressMonitor; | ||
import net.lingala.zip4j.util.InternalZipConstants; | ||
import net.lingala.zip4j.util.RawIO; | ||
import net.lingala.zip4j.util.Zip4jUtil; | ||
|
||
import java.io.File; | ||
import java.io.FileOutputStream; | ||
import java.io.IOException; | ||
import java.io.OutputStream; | ||
import java.io.RandomAccessFile; | ||
import java.nio.charset.Charset; | ||
import java.util.ArrayList; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
public class RenameFileTask extends AbstractModifyFileTask<RenameFileTask.RenameFileTaskParameters> { | ||
|
||
private ZipModel zipModel; | ||
private HeaderWriter headerWriter; | ||
private RawIO rawIO; | ||
private Charset charset; | ||
|
||
public RenameFileTask(ZipModel zipModel, HeaderWriter headerWriter, RawIO rawIO, Charset charset, AsyncTaskParameters asyncTaskParameters) { | ||
super(asyncTaskParameters); | ||
this.zipModel = zipModel; | ||
this.headerWriter = headerWriter; | ||
this.rawIO = rawIO; | ||
this.charset = charset; | ||
} | ||
|
||
@Override | ||
protected void executeTask(RenameFileTaskParameters taskParameters, ProgressMonitor progressMonitor) throws IOException { | ||
File temporaryFile = getTemporaryFile(zipModel.getZipFile().getPath()); | ||
Map<String, String> fileNamesMap = filterNonExistingEntriesAndAddSeparatorIfNeeded(taskParameters.fileNamesMap); | ||
|
||
try(RandomAccessFile inputStream = new RandomAccessFile(zipModel.getZipFile(), RandomAccessFileMode.WRITE.getValue()); | ||
OutputStream outputStream = new FileOutputStream(temporaryFile)) { | ||
|
||
long currentFileCopyPointer = 0; | ||
|
||
// Maintain a different list to iterate, so that when the file name is changed in the central directory | ||
// we still have access to the original file names. If iterating on the original list from central directory, | ||
// it might be that a file name has changed because of other file name, ex: if a directory name has to be changed | ||
// and the file is part of that directory, by the time the file has to be changed, its name might have changed | ||
// when changing the name of the directory. There is some overhead with this approach, but is safer. | ||
List<FileHeader> allUnchangedFileHeaders = new ArrayList<>(zipModel.getCentralDirectory().getFileHeaders()); | ||
|
||
for (FileHeader fileHeader : allUnchangedFileHeaders) { | ||
Map.Entry<String, String> fileNameMapForThisEntry = getCorrespondingEntryFromMap(fileHeader, fileNamesMap); | ||
progressMonitor.setFileName(fileHeader.getFileName()); | ||
|
||
if (fileNameMapForThisEntry == null) { | ||
// copy complete entry without any changes | ||
int headerSize = 30 + fileHeader.getFileNameLength() + fileHeader.getExtraFieldLength(); // 30 = all fixed lengths in local file header | ||
currentFileCopyPointer += copyFile(inputStream, outputStream, currentFileCopyPointer, headerSize + fileHeader.getCompressedSize(), progressMonitor); | ||
} else { | ||
String newFileName = getNewFileName(fileNameMapForThisEntry.getValue(), fileNameMapForThisEntry.getKey(), fileHeader.getFileName()); | ||
byte[] newFileNameBytes = newFileName.getBytes(charset); | ||
int headersOffset = newFileNameBytes.length - fileHeader.getFileNameLength(); | ||
|
||
currentFileCopyPointer = copyEntryAndChangeFileName(newFileNameBytes, fileHeader, currentFileCopyPointer, | ||
inputStream, outputStream, progressMonitor); | ||
|
||
updateHeadersInZipModel(fileHeader, newFileName, newFileNameBytes, headersOffset); | ||
} | ||
|
||
verifyIfTaskIsCancelled(); | ||
} | ||
|
||
headerWriter.finalizeZipFile(zipModel, outputStream, charset); | ||
|
||
cleanupFile(true, zipModel.getZipFile(), temporaryFile); | ||
} catch (Exception e) { | ||
cleanupFile(false, zipModel.getZipFile(), temporaryFile); | ||
throw e; | ||
} | ||
|
||
} | ||
|
||
@Override | ||
protected long calculateTotalWork(RenameFileTaskParameters taskParameters) { | ||
return zipModel.getZipFile().length(); | ||
} | ||
|
||
@Override | ||
protected ProgressMonitor.Task getTask() { | ||
return ProgressMonitor.Task.RENAME_FILE; | ||
} | ||
|
||
private long copyEntryAndChangeFileName(byte[] newFileNameBytes, FileHeader fileHeader, long start, | ||
RandomAccessFile inputStream, OutputStream outputStream, | ||
ProgressMonitor progressMonitor) throws IOException { | ||
long currentFileCopyPointer = start; | ||
|
||
currentFileCopyPointer += copyFile(inputStream, outputStream, currentFileCopyPointer, 26, progressMonitor); // 26 is offset until file name length | ||
|
||
rawIO.writeShortLittleEndian(outputStream, newFileNameBytes.length); | ||
|
||
currentFileCopyPointer += 2; // length of file name length | ||
currentFileCopyPointer += copyFile(inputStream, outputStream, currentFileCopyPointer, 2, progressMonitor); // 2 is for length of extra field length | ||
|
||
outputStream.write(newFileNameBytes); | ||
currentFileCopyPointer += fileHeader.getFileNameLength(); | ||
|
||
currentFileCopyPointer += copyFile(inputStream, outputStream, currentFileCopyPointer, | ||
fileHeader.getExtraFieldLength() + fileHeader.getCompressedSize(), progressMonitor); | ||
|
||
return currentFileCopyPointer; | ||
} | ||
|
||
private Map.Entry<String, String> getCorrespondingEntryFromMap(FileHeader fileHeaderToBeChecked, Map<String, | ||
String> fileNamesMap) { | ||
|
||
for (Map.Entry<String, String> fileHeaderToBeRenamed : fileNamesMap.entrySet()) { | ||
if (fileHeaderToBeChecked.getFileName().startsWith(fileHeaderToBeRenamed.getKey())) { | ||
return fileHeaderToBeRenamed; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
private void updateHeadersInZipModel(FileHeader fileHeader, String newFileName, byte[] newFileNameBytes, | ||
int headersOffset) throws ZipException { | ||
|
||
FileHeader fileHeaderToBeChanged = HeaderUtil.getFileHeader(zipModel, fileHeader.getFileName()); | ||
|
||
if (fileHeaderToBeChanged == null) { | ||
// If this is the case, then the file name in the header that was passed to this method was already changed. | ||
// In theory, should never be here. | ||
throw new ZipException("could not find any header with name: " + fileHeader.getFileName()); | ||
} | ||
|
||
fileHeaderToBeChanged.setFileName(newFileName); | ||
fileHeaderToBeChanged.setFileNameLength(newFileNameBytes.length); | ||
|
||
updateOffsetsForAllSubsequentFileHeaders(fileHeaderToBeChanged, headersOffset); | ||
|
||
zipModel.getEndOfCentralDirectoryRecord().setOffsetOfStartOfCentralDirectory( | ||
zipModel.getEndOfCentralDirectoryRecord().getOffsetOfStartOfCentralDirectory() + headersOffset); | ||
|
||
if (zipModel.isZip64Format()) { | ||
zipModel.getZip64EndOfCentralDirectoryRecord().setOffsetStartCentralDirectoryWRTStartDiskNumber( | ||
zipModel.getZip64EndOfCentralDirectoryRecord().getOffsetStartCentralDirectoryWRTStartDiskNumber() + headersOffset | ||
); | ||
|
||
zipModel.getZip64EndOfCentralDirectoryLocator().setOffsetZip64EndOfCentralDirectoryRecord( | ||
zipModel.getZip64EndOfCentralDirectoryLocator().getOffsetZip64EndOfCentralDirectoryRecord() + headersOffset | ||
); | ||
} | ||
} | ||
|
||
private Map<String, String> filterNonExistingEntriesAndAddSeparatorIfNeeded(Map<String, String> inputFileNamesMap) throws ZipException { | ||
Map<String, String> fileNamesMapToBeChanged = new HashMap<>(); | ||
for (Map.Entry<String, String> allNamesToBeChanged : inputFileNamesMap.entrySet()) { | ||
if (!Zip4jUtil.isStringNotNullAndNotEmpty(allNamesToBeChanged.getKey())) { | ||
continue; | ||
} | ||
|
||
FileHeader fileHeaderToBeChanged = HeaderUtil.getFileHeader(zipModel, allNamesToBeChanged.getKey()); | ||
if (fileHeaderToBeChanged != null) { | ||
if (fileHeaderToBeChanged.isDirectory() && !allNamesToBeChanged.getValue().endsWith(InternalZipConstants.ZIP_FILE_SEPARATOR)) { | ||
fileNamesMapToBeChanged.put(allNamesToBeChanged.getKey(), allNamesToBeChanged.getValue() + InternalZipConstants.ZIP_FILE_SEPARATOR); | ||
} else { | ||
fileNamesMapToBeChanged.put(allNamesToBeChanged.getKey(), allNamesToBeChanged.getValue()); | ||
} | ||
} | ||
} | ||
return fileNamesMapToBeChanged; | ||
} | ||
|
||
private void updateOffsetsForAllSubsequentFileHeaders(FileHeader fileHeaderModified, int offsetToAdd) throws ZipException { | ||
int indexOfFileHeader = HeaderUtil.getIndexOfFileHeader(zipModel, fileHeaderModified); | ||
List<FileHeader> allFileHeaders = zipModel.getCentralDirectory().getFileHeaders(); | ||
|
||
for (int i = indexOfFileHeader + 1; i < allFileHeaders.size(); i++) { | ||
FileHeader fileHeaderToUpdate = allFileHeaders.get(i); | ||
fileHeaderToUpdate.setOffsetLocalHeader(fileHeaderToUpdate.getOffsetLocalHeader() + offsetToAdd); | ||
} | ||
} | ||
|
||
private String getNewFileName(String newFileName, String oldFileName, String fileNameFromHeaderToBeChanged) throws ZipException { | ||
if (fileNameFromHeaderToBeChanged.equals(oldFileName)) { | ||
return newFileName; | ||
} else if (fileNameFromHeaderToBeChanged.startsWith(oldFileName)) { | ||
String fileNameWithoutOldName = fileNameFromHeaderToBeChanged.substring(oldFileName.length()); | ||
return newFileName + fileNameWithoutOldName; | ||
} | ||
|
||
// Should never be here. | ||
// If here by any chance, it means that the file header was marked as to-be-modified, even when the file names do not | ||
// match. Logic in the method getCorrespondingEntryFromMap() has to be checked | ||
throw new ZipException("old file name was neither an exact match nor a partial match"); | ||
} | ||
|
||
public static class RenameFileTaskParameters { | ||
private Map<String, String> fileNamesMap; | ||
|
||
public RenameFileTaskParameters(Map<String, String> fileNamesMap) { | ||
this.fileNamesMap = fileNamesMap; | ||
} | ||
} | ||
} |
Oops, something went wrong.