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    }