/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.io.backup;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.FileFactory;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
/**
* Saves file in as crash-safe a manner as possible.
* <p>
* In order to prevent system or muCommander failures to corrupt configuration files,
* the BackupOutputStream implements the following algorithm:
* <ul>
* <li>Write its content to a backup file instead of the requested file</li>
* <li>When close is called, copy the content of the backup file over the original file</li>
* </ul>
* This way, if a crash was to happen while configuration files are being saved, either of the
* following will happen:
* <ul>
* <li>
* The backup file is not properly saved, but the original configuration is left untouched.
* We have lost <i>some</i> information (modifications since last save) but not <i>all</i>.
* </li>
* <li>
* The original file is not properly saved, but the backup file is correct. This is easy to check,
* as the backup and original file should always have the same size. If they don't, then the backup
* file should be used rather than the original one.
* </li>
* </ul>
* </p>
* <p>
* Files that have been saved by this class should be read with {@link BackupInputStream}
* in order to make sure that an uncorrupt version of them is loaded.
* </p>
* <p>
* The <code>BackupOutputStream</code> monitors all of its own I/O operations. If an error occurs, then the backup
* operation will not be performed when {@link #close()} is called. It's possible to force the backup operation by
* using the {@link #close(boolean)} method.
* </p>
* @see BackupInputStream
* @author Nicolas Rinaudo
*/
public class BackupOutputStream extends OutputStream implements BackupConstants {
// - Instance fields --------------------------------------------------------
// --------------------------------------------------------------------------
/** The underlying OutputStream */
private OutputStream out;
/** Path of the original file. */
private AbstractFile target;
/** Path to the backup file. */
private AbstractFile backup;
/** Whether or not an error occured while writing to the backup file. */
private boolean error;
// - Initialisation ---------------------------------------------------------
// --------------------------------------------------------------------------
/**
* Opens a backup output stream on the specified file.
* @param file file on which to open a backup output stream.
* @exception IOException thrown if any IO error occurs.
*/
public BackupOutputStream(File file) throws IOException {this(FileFactory.getFile(file.getAbsolutePath()));}
/**
* Opens a backup output stream on the specified file.
* @param file file on which to open a backup output stream.
* @exception IOException thrown if any IO error occurs.
*/
public BackupOutputStream(String file) throws IOException {this(FileFactory.getFile((new File(file)).getAbsolutePath()));}
/**
* Opens a backup output stream on the specified file.
* @param file file on which to open a backup output stream.
* @exception IOException thrown if any IO error occurs.
*/
public BackupOutputStream(AbstractFile file) throws IOException {this(file, FileFactory.getFile(file.getAbsolutePath() + BACKUP_SUFFIX));}
/**
* Opens an output stream on the specified file using the specified backup file.
* @param file file on which to open the backup output stream.
* @param save file that will be used for backup.
* @exception IOException thrown if any IO error occurs.
*/
private BackupOutputStream(AbstractFile file, AbstractFile save) throws IOException {
out = save.getOutputStream();
target = file;
backup = save;
}
// - Error catching ---------------------------------------------------------
// --------------------------------------------------------------------------
/**
* Flushes this output stream and forces any buffered output bytes to be written out to the stream.
* <p>
* This method calls the <code>flush()</code> method of its underlying output stream.
* </p>
* <p>
* If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be
* forced through the {@link #close(boolean)} method.
* </p>
* @throws IOException if an I/O error occurs.
*/
@Override
public void flush() throws IOException {
if(error)
out.flush();
else {
try {out.flush();}
catch(IOException e) {
error = true;
throw e;
}
}
}
/**
* Writes b.length bytes to this output stream.
* <p>
* This method calls the <code>write(byte[] b)</code> method of its underlying output stream.
* </p>
* <p>
* If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be
* forced through the {@link #close(boolean)} method.
* </p>
* @param b the data to be written.
* @throws IOException if an I/O error occurs.
*/
@Override
public void write(byte[] b) throws IOException {
if(error)
out.write(b);
else {
try {out.write(b);}
catch(IOException e) {
error = true;
throw e;
}
}
}
/**
* Writes len bytes from the specified byte array starting at offset off to this output stream.
* <p>
* This method calls the <code>write(byte[] b, int off, int len)</code> method of its underlying output stream.
* </p>
* <p>
* If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be
* forced through the {@link #close(boolean)} method.
* </p>
* @param b the data to be written.
* @param off the start offset in the data.
* @param len the number of bytes to write.
* @throws IOException if an I/O error occurs.
*/
@Override
public void write(byte[] b, int off, int len) throws IOException {
if(error)
out.write(b, off, len);
else {
try {out.write(b, off, len);}
catch(IOException e) {
error = true;
throw e;
}
}
}
/**
* Writes the specified byte to this output stream.
* <p>
* This method calls the <code>write(byte b)</code> method of its underlying output stream.
* </p>
* <p>
* If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be
* forced through the {@link #close(boolean)} method.
* </p>
* @param b the data to be written.
* @throws IOException if an I/O error occurs.
*/
@Override
public void write(int b) throws IOException {
if(error)
out.write(b);
else {
try {out.write(b);}
catch(IOException e) {
error = true;
throw e;
}
}
}
// - Backup -----------------------------------------------------------------
// --------------------------------------------------------------------------
/**
* Overwrites the target file with the backup one.
* @exception IOException thrown if any IO related error occurs.
*/
private void backup() throws IOException {
// Deletes the destination file (AbstractFile.copyTo now fails when the destination exists).
if(target.exists())
target.delete();
// We're not using backup.moveTo(target) because we want to make absolutely sure
// that if an error occurs in the middle of the operation, at least one of the two files
// is complete.
backup.copyTo(target);
backup.delete();
}
/**
* Finishes the backup operation.
* @exception IOException thrown if any IO related error occurs.
*/
@Override
public void close() throws IOException {close(!error);}
/**
* Closes the output stream.
* <p>
* The <code>backup</code> parameter is meant for those cases when an error happened
* while writing to the stream: if it did, we don't want to propagate to the target
* file, and thus should prevent the backup operation from being performed.
* </p>
* @param backup whether or not to overwrite the target file by the backup one.
* @exception IOException thrown if any IO related error occurs.
*/
public void close(boolean backup) throws IOException {
// Closes the underlying output stream.
out.flush();
out.close();
if(backup)
backup();
}
}