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    }