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.io;
020    
021    import com.mucommander.file.AbstractFile;
022    import com.mucommander.file.FileFactory;
023    
024    import java.io.File;
025    import java.io.IOException;
026    import java.io.OutputStream;
027    
028    /**
029     * Saves file in as crash-safe a manner as possible.
030     * <p>
031     * In order to prevent system or muCommander failures to corrupt configuration files,
032     * the BackupOutputStream implements the following algorithm:
033     * <ul>
034     *   <li>Write its content to a backup file instead of the requested file</li>
035     *   <li>When close is called, copy the content of the backup file over the original file</li>
036     * </ul>
037     * This way, if a crash was to happen while configuration files are being saved, either of the
038     * following will happen:
039     * <ul>
040     *   <li>
041     *     The backup file is not properly saved, but the original configuration is left untouched.
042     *     We have lost <i>some</i> information (modifications since last save) but not <i>all</i>.
043     *   </li>
044     *   <li>
045     *     The original file is not properly saved, but the backup file is correct. This is easy to check,
046     *     as the backup and original file should always have the same size. If they don't, then the backup
047     *     file should be used rather than the original one.
048     *   </li>
049     * </ul>
050     * </p>
051     * <p>
052     * Files that have been saved by this class should be read with {@link com.mucommander.io.BackupInputStream}
053     * in order to make sure that an uncorrupt version of them is loaded.
054     * </p>
055     * <p>
056     * The <code>BackupOutputStream</code> monitors all of its own I/O operations. If an error occurs, then the backup
057     * operation will not be performed when {@link #close()} is called. It's possible to force the backup operation by
058     * using the {@link #close(boolean)} method.
059     * </p>
060     * @see    com.mucommander.io.BackupInputStream
061     * @author Nicolas Rinaudo
062     */
063    public class BackupOutputStream extends OutputStream implements BackupConstants {
064        // - Instance fields --------------------------------------------------------
065        // --------------------------------------------------------------------------
066        /** The underlying OutputStream */
067        private OutputStream out;
068        /** Path of the original file. */
069        private AbstractFile     target;
070        /** Path to the backup file. */
071        private AbstractFile     backup;
072        /** Whether or not an error occured while writing to the backup file. */
073        private boolean          error;
074    
075    
076    
077        // - Initialisation ---------------------------------------------------------
078        // --------------------------------------------------------------------------
079        /**
080         * Opens a backup output stream on the specified file.
081         * @param     file        file on which to open a backup output stream.
082         * @exception IOException thrown if any IO error occurs.
083         */
084        public BackupOutputStream(File file) throws IOException {this(FileFactory.getFile(file.getAbsolutePath()));}
085    
086        /**
087         * Opens a backup output stream on the specified file.
088         * @param     file        file on which to open a backup output stream.
089         * @exception IOException thrown if any IO error occurs.
090         */
091        public BackupOutputStream(String file) throws IOException {this(FileFactory.getFile((new File(file)).getAbsolutePath()));}
092    
093        /**
094         * Opens a backup output stream on the specified file.
095         * @param     file        file on which to open a backup output stream.
096         * @exception IOException thrown if any IO error occurs.
097         */
098        public BackupOutputStream(AbstractFile file) throws IOException {this(file, FileFactory.getFile(file.getAbsolutePath() + BACKUP_SUFFIX));}
099    
100        /**
101         * Opens an output stream on the specified file using the specified backup file.
102         * @param     file        file on which to open the backup output stream.
103         * @param     save        file that will be used for backup.
104         * @exception IOException thrown if any IO error occurs.
105         */
106        private BackupOutputStream(AbstractFile file, AbstractFile save) throws IOException {
107            out = save.getOutputStream(false);
108            target = file;
109            backup = save;
110        }
111    
112    
113    
114        // - Error catching ---------------------------------------------------------
115        // --------------------------------------------------------------------------
116        /**
117         * Flushes this output stream and forces any buffered output bytes to be written out to the stream.
118         * <p>
119         * This method calls the <code>flush()</code> method of its underlying output stream.
120         * </p>
121         * <p>
122         * If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be
123         * forced through the {@link #close(boolean)} method.
124         * </p>
125         * @throws IOException if an I/O error occurs.
126         */
127        public void flush() throws IOException {
128            if(error)
129                out.flush();
130            else {
131                try {out.flush();}
132                catch(IOException e) {
133                    error = true;
134                    throw e;
135                }
136            }
137        }
138    
139        /**
140         * Writes b.length bytes to this output stream.
141         * <p>
142         * This method calls the <code>write(byte[] b)</code> method of its underlying output stream.
143         * </p>
144         * <p>
145         * If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be
146         * forced through the {@link #close(boolean)} method.
147         * </p>
148         * @param  b           the data to be written.
149         * @throws IOException if an I/O error occurs.
150         */
151        public void write(byte[] b) throws IOException {
152            if(error)
153                out.write(b);
154            else {
155                try {out.write(b);}
156                catch(IOException e) {
157                    error = true;
158                    throw e;
159                }
160            }
161        }
162    
163        /**
164         * Writes len bytes from the specified byte array starting at offset off to this output stream.
165         * <p>
166         * This method calls the <code>write(byte[] b, int off, int len)</code> method of its underlying output stream.
167         * </p>
168         * <p>
169         * If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be
170         * forced through the {@link #close(boolean)} method.
171         * </p>
172         * @param  b           the data to be written.
173         * @param  off         the start offset in the data.
174         * @param  len         the number of bytes to write.
175         * @throws IOException if an I/O error occurs.
176         */
177        public void write(byte[] b, int off, int len) throws IOException {
178            if(error)
179                out.write(b, off, len);
180            else {
181                try {out.write(b, off, len);}
182                catch(IOException e) {
183                    error = true;
184                    throw e;
185                }
186            }
187        }
188    
189        /**
190         * Writes the specified byte to this output stream.
191         * <p>
192         * This method calls the <code>write(byte b)</code> method of its underlying output stream.
193         * </p>
194         * <p>
195         * If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be
196         * forced through the {@link #close(boolean)} method.
197         * </p>
198         * @param  b           the data to be written.
199         * @throws IOException if an I/O error occurs.
200         */
201        public void write(int b) throws IOException {
202            if(error)
203                out.write(b);
204            else {
205                try {out.write(b);}
206                catch(IOException e) {
207                    error = true;
208                    throw e;
209                }
210            }
211        }
212    
213    
214    
215        // - Backup -----------------------------------------------------------------
216        // --------------------------------------------------------------------------
217        /**
218         * Overwrites the target file with the backup one.
219         * @exception IOException thrown if any IO related error occurs.
220         */
221        private void backup() throws IOException {
222            // Deletes the destination file (AbstractFile.copyTo now fails when the destination exists).
223            if(target.exists())
224                target.delete();
225    
226            // We're not using backup.moveTo(target) because we want to make absolutely sure
227            // that if an error occurs in the middle of the operation, at least one of the two files
228            // is complete.
229            backup.copyTo(target);
230            backup.delete();
231        }
232    
233        /**
234         * Finishes the backup operation.
235         * @exception IOException thrown if any IO related error occurs.
236         */
237        public void close() throws IOException {close(!error);}
238    
239        /**
240         * Closes the output stream.
241         * <p>
242         * The <code>backup</code> parameter is meant for those cases when an error happened
243         * while writing to the stream: if it did, we don't want to propagate to the target
244         * file, and thus should prevent the backup operation from being performed.
245         * </p>
246         * @param     backup      whether or not to overwrite the target file by the backup one.
247         * @exception IOException thrown if any IO related error occurs.
248         */
249        public void close(boolean backup) throws IOException {
250            // Closes the underlying output stream.
251            out.flush();
252            out.close();
253    
254            if(backup)
255                backup();
256        }
257    }