001 /*
002 * This file is part of muCommander, http://www.mucommander.com
003 * Copyright (C) 2002-2008 Maxence Bernard
004 *
005 * muCommander is free software; you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation; either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * muCommander is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program. If not, see <http://www.gnu.org/licenses/>.
017 */
018
019
020 package com.mucommander.job;
021
022 import com.mucommander.file.AbstractFile;
023 import com.mucommander.file.archiver.Archiver;
024 import com.mucommander.file.util.FileSet;
025 import com.mucommander.io.StreamUtils;
026 import com.mucommander.text.Translator;
027 import com.mucommander.ui.dialog.file.FileCollisionDialog;
028 import com.mucommander.ui.dialog.file.ProgressDialog;
029 import com.mucommander.ui.main.MainFrame;
030
031 import java.io.IOException;
032 import java.io.InputStream;
033 import java.io.OutputStream;
034
035
036 /**
037 * This FileJob is responsible for compressing a set of files into an archive file.
038 *
039 * @author Maxence Bernard
040 */
041 public class ArchiveJob extends TransferFileJob {
042
043 /** Destination archive file */
044 private AbstractFile destFile;
045
046 /** Base destination folder's path */
047 private String baseFolderPath;
048
049 /** Archiver instance that does the actual archiving */
050 private Archiver archiver;
051
052 /** Archive format */
053 private int archiveFormat;
054
055 /** Optional archive comment */
056 private String archiveComment;
057
058 /** Lock to avoid Archiver.close() to be called while data is being written */
059 private final Object ioLock = new Object();
060
061 /** OutputStream of the current file */
062 private OutputStream out;
063
064
065 public ArchiveJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, AbstractFile destFile, int archiveFormat, String archiveComment) {
066 super(progressDialog, mainFrame, files);
067
068 this.destFile = destFile;
069 this.archiveFormat = archiveFormat;
070 this.archiveComment = archiveComment;
071
072 this.baseFolderPath = baseSourceFolder.getAbsolutePath(false);
073 }
074
075
076 ////////////////////////////////////
077 // TransferFileJob implementation //
078 ////////////////////////////////////
079
080 protected boolean processFile(AbstractFile file, Object recurseParams) {
081 if(getState()==INTERRUPTED)
082 return false;
083
084 String filePath = file.getAbsolutePath(false);
085 String entryRelativePath = filePath.substring(baseFolderPath.length()+1, filePath.length());
086
087 // Process current file
088 do { // Loop for retry
089 try {
090 if (file.isDirectory() && !file.isSymlink()) {
091 // Create new directory entry in archive file
092 archiver.createEntry(entryRelativePath, file);
093
094 // Recurse on files
095 AbstractFile subFiles[] = file.ls();
096 boolean folderComplete = true;
097 for(int i=0; i<subFiles.length && getState()!=INTERRUPTED; i++) {
098 // Notify job that we're starting to process this file (needed for recursive calls to processFile)
099 nextFile(subFiles[i]);
100 if(!processFile(subFiles[i], null))
101 folderComplete = false;
102 }
103
104 return folderComplete;
105 }
106 else {
107 InputStream in = setCurrentInputStream(file.getInputStream());
108 // Synchronize this block to ensure that Archiver.close() is not closed while data is still being
109 // written to the archive OutputStream, this would cause ZipOutputStream to deadlock.
110 synchronized(ioLock) {
111 // Create a new file entry in archive and copy the current file
112 StreamUtils.copyStream(in, archiver.createEntry(entryRelativePath, file));
113 in.close();
114 }
115 return true;
116 }
117 }
118 // Catch Exception rather than IOException as ZipOutputStream has been seen throwing NullPointerException
119 catch(Exception e) {
120 // If job was interrupted by the user at the time when the exception occurred,
121 // it most likely means that the exception was caused by user cancellation.
122 // In this case, the exception should not be interpreted as an error.
123 if(getState()==INTERRUPTED)
124 return false;
125
126 if(com.mucommander.Debug.ON) com.mucommander.Debug.trace("Caught IOException: "+e);
127
128 int ret = showErrorDialog(Translator.get("pack_dialog.error_title"), Translator.get("error_while_transferring", file.getAbsolutePath()));
129 // Retry loops
130 if(ret==RETRY_ACTION) {
131 // Reset processed bytes currentFileByteCounter
132 getCurrentFileByteCounter().reset();
133
134 continue;
135 }
136 // Cancel, skip or close dialog return false
137 return false;
138 }
139 } while(true);
140 }
141
142 protected boolean hasFolderChanged(AbstractFile folder) {
143 // This job modifies the folder where the archive is
144 return folder.equals(destFile.getParentSilently()); // Note: parent may be null
145 }
146
147
148 ////////////////////////
149 // Overridden methods //
150 ////////////////////////
151
152 /**
153 * Overriden method to initialize the archiver and handle the case where the destination file already exists.
154 */
155 protected void jobStarted() {
156 super.jobStarted();
157
158 // Check for file collisions, i.e. if the file already exists in the destination
159 int collision = FileCollisionChecker.checkForCollision(null, destFile);
160 if(collision!=FileCollisionChecker.NO_COLLOSION) {
161 // File already exists in destination, ask the user what to do (cancel, overwrite,...) but
162 // do not offer the multiple files mode options such as 'skip' and 'apply to all'.
163 int choice = waitForUserResponse(new FileCollisionDialog(progressDialog, mainFrame, collision, null, destFile, false, false));
164
165 // Overwrite file
166 if (choice== FileCollisionDialog.OVERWRITE_ACTION) {
167 // Do nothing, simply continue and file will be overwritten
168 }
169 // 'Cancel' or close dialog interrupts the job
170 else {
171 interrupt();
172 return;
173 }
174 }
175
176 // Loop for retry
177 do {
178 try {
179 // Tries to get an Archiver instance.
180 this.archiver = Archiver.getArchiver(destFile, archiveFormat);
181 this.archiver.setComment(archiveComment);
182 this.out = archiver.getOutputStream();
183
184 break;
185
186 }
187 catch(Exception e) {
188 int choice = showErrorDialog(Translator.get("pack_dialog.error_title"),
189 Translator.get("cannot_write_file", destFile.getName()),
190 new String[] {CANCEL_TEXT, RETRY_TEXT},
191 new int[] {CANCEL_ACTION, RETRY_ACTION}
192 );
193
194 // Retry loops
195 if(choice == RETRY_ACTION)
196 continue;
197
198 // 'Cancel' or close dialog interrupts the job
199 interrupt();
200 return;
201 }
202 } while(true);
203 }
204
205 /**
206 * Overriden method to close the archiver.
207 */
208 public void jobStopped() {
209
210 // TransferFileJob.jobStopped() closes the current InputStream, this will cause copyStream() to return
211 super.jobStopped();
212
213 // Synchronize this block to ensure that Archiver.close() is not closed while data is still being
214 // written to the archive OutputStream, this would cause ZipOutputStream to deadlock.
215 synchronized(ioLock) {
216 // Try to close the archiver which in turns closes the archive OutputStream and underlying file OutputStream
217 if(archiver!=null) {
218 try { archiver.close(); }
219 catch(IOException e) {}
220 }
221
222 // Todo: the check below was made when using java.util.zip. Since java.util.zip is not used anymore,
223 // check if this is still necessary
224
225 // Makes sure the file OutputStream has been properly closed. Archive.close() normally closes the archive
226 // OutputStream which in turn should close the underlying file OutputStream, but for some strange reason,
227 // if no entry has been added to a Zip archive and the job is interrupted (e.g. the first file could not be read),
228 // ZipOutputStream.close() does not close the underlying OutputStream.
229 if(out!=null) {
230 try { out.close(); }
231 catch(IOException e) {}
232 }
233 }
234 }
235
236 public String getStatusString() {
237 return Translator.get("pack_dialog.packing_file", getCurrentFilename());
238 }
239 }