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 package com.mucommander.job;
020
021 import com.mucommander.file.AbstractFile;
022 import com.mucommander.file.util.FileSet;
023 import com.mucommander.text.Translator;
024 import com.mucommander.ui.dialog.file.FileCollisionDialog;
025 import com.mucommander.ui.dialog.file.ProgressDialog;
026 import com.mucommander.ui.icon.IconManager;
027 import com.mucommander.ui.main.MainFrame;
028 import com.mucommander.ui.viewer.ViewerRegistrar;
029
030 import java.io.IOException;
031 import java.io.InputStream;
032 import java.io.OutputStream;
033 import java.security.MessageDigest;
034
035 /**
036 * This job calculates a checksum for a list of files and stores the results in a checksum file.
037 *
038 * <p>The format of this file is a de facto standard ; a line is created for each file and goes like this:
039 * <pre>
040 * e7e9576b9e55940b4b8522a65902d4cd readme.txt
041 * 119abda7c941135d5bf382c386bca2ca i386/debian-40r1-i386-DVD-1.iso
042 * 3c0d332902b9b8dfec43ba02d1618c6e ppc/debian-40r1-ppc-DVD-1.iso
043 * ...
044 * </pre>
045 * The path of each file is relative to the checksum file's path. In the above example, <code>readme.txt</code> and
046 * the checksum file are located in the same folder. Note that 2 space characters (and not just one as anyone in his
047 * right mind would think) separate the hexadecimal checksum from the file path.
048 * </p>
049 *
050 * <p>The above file format is used for all checksum algorithms but one: CRC32, which uses the special SFV format where
051 * the checksum for each file is written as follow:
052 * <pre>
053 * wne-ebai.r00 697115b2
054 * wne-ebai.r01 f80a8443
055 * ...
056 * </pre>
057 * </p>
058 *
059 * @author Maxence Bernard
060 */
061 public class CalculateChecksumJob extends TransferFileJob {
062
063 /** The checksum file where the checksum of each file is written */
064 private AbstractFile checksumFile;
065 /** The OutputStream of the checksum file */
066 private OutputStream checksumFileOut;
067
068 /** The path to the base source folder, i.e. the folder which contains all the files this job operates on */
069 private String baseSourcePath;
070
071 /** True if the SFV format is used rather than the default 'SUMS' format */
072 private boolean useSfvFormat;
073
074 /** The MessageDigest that serves to calculate the checksum */
075 private MessageDigest digest;
076
077
078 public CalculateChecksumJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, AbstractFile checksumFile, MessageDigest digest) {
079 super(progressDialog, mainFrame, files);
080
081 this.checksumFile = checksumFile;
082 this.digest = digest;
083 this.useSfvFormat = digest.getAlgorithm().equalsIgnoreCase("CRC32");
084
085 this.baseSourcePath = baseSourceFolder.getAbsolutePath(true);
086 }
087
088
089 ////////////////////////////////////
090 // TransferFileJob implementation //
091 ////////////////////////////////////
092
093 protected boolean processFile(AbstractFile file, Object recurseParams) {
094 // Skip directories
095 if(file.isDirectory()) {
096 do { // Loop for retry
097 try {
098 // for each file in folder...
099 AbstractFile children[] = file.ls();
100 for(int i=0; i<children.length && getState()!=INTERRUPTED; i++) {
101 // Notify job that we're starting to process this file (needed for recursive calls to processFile)
102 nextFile(children[i]);
103 processFile(children[i], null);
104 }
105
106 return true;
107 }
108 catch(IOException e) {
109 // file.ls() failed
110 int ret = showErrorDialog(Translator.get("error"), Translator.get("cannot_read_folder", file.getName()));
111 // Retry loops
112 if(ret==RETRY_ACTION)
113 continue;
114 // Cancel, skip or close dialog returns false
115 return false;
116 }
117 } while(true);
118 }
119
120 // Calculate the file's checksum
121 do { // Loop for retry
122 InputStream in = null;
123 String line;
124 String checksum;
125 try {
126 // Resets the digest before use
127 digest.reset();
128
129 in = null;
130 in = setCurrentInputStream(file.getInputStream());
131
132 // Determine the path relative to the base source folder
133 String relativePath = file.getAbsolutePath();
134 relativePath = relativePath.substring(baseSourcePath.length(), relativePath.length());
135
136 // Write a new line in the checksum file, in the appropriate format
137 checksum = AbstractFile.calculateChecksum(in, digest);
138 if(useSfvFormat) {
139 // SFV format for CRC32 checksums
140 line = relativePath + " " + checksum; // 1 space character
141 }
142 else {
143 // 'SUMS' format for other checksum algorithms
144 line = checksum + " " + relativePath; // 2 space characters, that's how the format is
145 }
146
147 line += '\n';
148
149 // Close the InputStream, we're done with it
150 in.close();
151
152 checksumFileOut.write(line.getBytes("utf-8"));
153
154 return true;
155 }
156 catch(IOException e) {
157 // Close the InputStream, a new one will be created when retrying
158 if(in!=null) {
159 try { in.close(); }
160 catch(IOException e2){}
161 }
162
163 // If the job was interrupted by the user at the time the exception occurred, it most likely means that
164 // the IOException was caused by the stream being closed as a result of the user interruption.
165 // If that is the case, the exception should not be interpreted as an error.
166 // Same goes if the current file was skipped.
167 if(getState()==INTERRUPTED || wasCurrentFileSkipped())
168 return false;
169
170 if(com.mucommander.Debug.ON) com.mucommander.Debug.trace("Caught IOException: "+e);
171
172 int ret = showErrorDialog(Translator.get("error"), Translator.get("error_while_transferring", file.getAbsolutePath()));
173 // Retry loops
174 if(ret==RETRY_ACTION) {
175 // Reset processed bytes currentFileByteCounter
176 getCurrentFileByteCounter().reset();
177
178 continue;
179 }
180
181 // Cancel, skip or close dialog return false
182 return false;
183 }
184 } while(true);
185 }
186
187 protected boolean hasFolderChanged(AbstractFile folder) {
188 // This job modifies the folder where the checksum file is
189 return folder.equals(checksumFile.getParentSilently()); // Note: parent may be null
190 }
191
192
193 ////////////////////////
194 // Overridden methods //
195 ////////////////////////
196
197 protected void jobStarted() {
198 super.jobStarted();
199
200 // Check for file collisions, i.e. if the file already exists in the destination
201 int collision = FileCollisionChecker.checkForCollision(null, checksumFile);
202 if(collision!=FileCollisionChecker.NO_COLLOSION) {
203 // File already exists in destination, ask the user what to do (cancel, overwrite,...) but
204 // do not offer the multiple files mode options such as 'skip' and 'apply to all'.
205 int choice = waitForUserResponse(new FileCollisionDialog(progressDialog, mainFrame, collision, null, checksumFile, false, false));
206
207 // Overwrite file
208 if (choice== FileCollisionDialog.OVERWRITE_ACTION) {
209 // Do nothing, simply continue and file will be overwritten
210 }
211 // 'Cancel' or close dialog interrupts the job
212 else {
213 interrupt();
214 return;
215 }
216 }
217
218 // Loop for retry
219 do {
220 try {
221 // Tries to get an OutputStream on the destination file
222 this.checksumFileOut = checksumFile.getOutputStream(false);
223
224 break;
225
226 }
227 catch(Exception e) {
228 int choice = showErrorDialog(Translator.get("error"),
229 Translator.get("cannot_write_file", checksumFile.getName()),
230 new String[] {CANCEL_TEXT, RETRY_TEXT},
231 new int[] {CANCEL_ACTION, RETRY_ACTION}
232 );
233
234 // Retry loops
235 if(choice == RETRY_ACTION)
236 continue;
237
238 // 'Cancel' or close dialog interrupts the job
239 interrupt();
240 return;
241 }
242 } while(true);
243 }
244
245 protected void jobCompleted() {
246 super.jobCompleted();
247
248 // Open the checksum file in a viewer
249 ViewerRegistrar.createViewerFrame(mainFrame, checksumFile, IconManager.getImageIcon(checksumFile.getIcon()).getImage());
250 }
251
252 protected void jobStopped() {
253 super.jobStopped();
254
255 // Close the checksum file's OutputStream
256 if(checksumFileOut !=null) {
257 try { checksumFileOut.close(); }
258 catch(IOException e2){
259 // No need to inform the user
260 }
261 }
262 }
263 }