Skip to content

Commit

Permalink
#72 Add feature to rename files in zip
Browse files Browse the repository at this point in the history
  • Loading branch information
srikanth-lingala committed Mar 1, 2020
1 parent c127a2e commit 1cc0168
Show file tree
Hide file tree
Showing 10 changed files with 714 additions and 1 deletion.
50 changes: 50 additions & 0 deletions src/main/java/net/lingala/zip4j/ZipFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,12 @@
import net.lingala.zip4j.tasks.MergeSplitZipFileTask.MergeSplitZipFileTaskParameters;
import net.lingala.zip4j.tasks.RemoveEntryFromZipFileTask;
import net.lingala.zip4j.tasks.RemoveEntryFromZipFileTask.RemoveEntryFromZipFileTaskParameters;
import net.lingala.zip4j.tasks.RenameFileTask;
import net.lingala.zip4j.tasks.RenameFileTask.RenameFileTaskParameters;
import net.lingala.zip4j.tasks.SetCommentTask;
import net.lingala.zip4j.tasks.SetCommentTask.SetCommentTaskTaskParameters;
import net.lingala.zip4j.util.FileUtils;
import net.lingala.zip4j.util.RawIO;
import net.lingala.zip4j.util.Zip4jUtil;

import java.io.File;
Expand All @@ -54,6 +57,7 @@
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
Expand Down Expand Up @@ -688,6 +692,52 @@ public void removeFile(FileHeader fileHeader) throws ZipException {
new RemoveEntryFromZipFileTaskParameters(fileHeader, charset));
}

/**
* Renames the current entry
* @param fileHeader
* @param newFileName
* @throws ZipException
*/
public void renameFile(FileHeader fileHeader, String newFileName) throws ZipException {
if (fileHeader == null) {
throw new ZipException("File header is null");
}

renameFile(fileHeader.getFileName(), newFileName);
}

public void renameFile(String fileNameToRename, String newFileName) throws ZipException {
if (!Zip4jUtil.isStringNotNullAndNotEmpty(fileNameToRename)) {
throw new ZipException("file name to be changed is null or empty");
}

if (!Zip4jUtil.isStringNotNullAndNotEmpty(newFileName)) {
throw new ZipException("newFileName is null or empty");
}

renameFiles(Collections.singletonMap(fileNameToRename, newFileName));
}

public void renameFiles(Map<String, String> fileNamesMap) throws ZipException {
if (fileNamesMap == null) {
throw new ZipException("fileNamesMap is null");
}

if (fileNamesMap.size() == 0) {
return;
}

readZipInfo();

if (zipModel.isSplitArchive()) {
throw new ZipException("Zip file format does not allow updating split/spanned files");
}

AsyncZipTask.AsyncTaskParameters asyncTaskParameters = buildAsyncParameters();
new RenameFileTask(zipModel, headerWriter, new RawIO(), charset, asyncTaskParameters).execute(
new RenameFileTaskParameters(fileNamesMap));
}

/**
* Merges split zip files into a single zip file without the need to extract the
* files in the archive
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public FileHeader generateFileHeader(ZipParameters zipParameters, boolean isSpli
if (zipParameters.isEncryptFiles() && zipParameters.getEncryptionMethod() == EncryptionMethod.AES) {
fileHeader.setCompressionMethod(CompressionMethod.AES_INTERNAL_ONLY);
fileHeader.setAesExtraDataRecord(generateAESExtraDataRecord(zipParameters));
fileHeader.setExtraFieldLength(fileHeader.getExtraFieldLength() + InternalZipConstants.AES_EXTRA_DATA_RECORD_SIZE);
} else {
fileHeader.setCompressionMethod(zipParameters.getCompressionMethod());
}
Expand Down Expand Up @@ -91,6 +92,7 @@ public LocalFileHeader generateLocalFileHeader(FileHeader fileHeader) {
localFileHeader.setCompressedSize(fileHeader.getCompressedSize());
localFileHeader.setGeneralPurposeFlag(fileHeader.getGeneralPurposeFlag().clone());
localFileHeader.setDataDescriptorExists(fileHeader.isDataDescriptorExists());
localFileHeader.setExtraFieldLength(fileHeader.getExtraFieldLength());
return localFileHeader;
}

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/net/lingala/zip4j/model/FileHeader.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,9 @@ public String getFileComment() {
public void setFileComment(String fileComment) {
this.fileComment = fileComment;
}

@Override
public String toString() {
return getFileName();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class ProgressMonitor {

public enum State { READY, BUSY }
public enum Result { SUCCESS, WORK_IN_PROGRESS, ERROR, CANCELLED }
public enum Task { NONE, ADD_ENTRY, REMOVE_ENTRY, CALCULATE_CRC, EXTRACT_ENTRY, MERGE_ZIP_FILES, SET_COMMENT}
public enum Task { NONE, ADD_ENTRY, REMOVE_ENTRY, CALCULATE_CRC, EXTRACT_ENTRY, MERGE_ZIP_FILES, SET_COMMENT, RENAME_FILE}

private State state;
private long totalWork;
Expand Down
76 changes: 76 additions & 0 deletions src/main/java/net/lingala/zip4j/tasks/AbstractModifyFileTask.java
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 src/main/java/net/lingala/zip4j/tasks/RenameFileTask.java
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;
}
}
}
Loading

0 comments on commit 1cc0168

Please sign in to comment.