001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hdfs.util;
019
020import java.io.File;
021import java.io.FileNotFoundException;
022import java.io.FileOutputStream;
023import java.io.FilterOutputStream;
024import java.io.IOException;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import org.apache.hadoop.io.IOUtils;
029
030/**
031 * A FileOutputStream that has the property that it will only show
032 * up at its destination once it has been entirely written and flushed
033 * to disk. While being written, it will use a .tmp suffix.
034 * 
035 * When the output stream is closed, it is flushed, fsynced, and
036 * will be moved into place, overwriting any file that already
037 * exists at that location.
038 * 
039 * <b>NOTE</b>: on Windows platforms, it will not atomically
040 * replace the target file - instead the target file is deleted
041 * before this one is moved into place.
042 */
043public class AtomicFileOutputStream extends FilterOutputStream {
044
045  private static final String TMP_EXTENSION = ".tmp";
046  
047  private final static Log LOG = LogFactory.getLog(
048      AtomicFileOutputStream.class);
049  
050  private final File origFile;
051  private final File tmpFile;
052  
053  public AtomicFileOutputStream(File f) throws FileNotFoundException {
054    // Code unfortunately must be duplicated below since we can't assign anything
055    // before calling super
056    super(new FileOutputStream(new File(f.getParentFile(), f.getName() + TMP_EXTENSION)));
057    origFile = f.getAbsoluteFile();
058    tmpFile = new File(f.getParentFile(), f.getName() + TMP_EXTENSION).getAbsoluteFile();
059  }
060
061  @Override
062  public void close() throws IOException {
063    boolean triedToClose = false, success = false;
064    try {
065      flush();
066      ((FileOutputStream)out).getChannel().force(true);
067
068      triedToClose = true;
069      super.close();
070      success = true;
071    } finally {
072      if (success) {
073        boolean renamed = tmpFile.renameTo(origFile);
074        if (!renamed) {
075          // On windows, renameTo does not replace.
076          if (!origFile.delete() || !tmpFile.renameTo(origFile)) {
077            throw new IOException("Could not rename temporary file " +
078                tmpFile + " to " + origFile);
079          }
080        }
081      } else {
082        if (!triedToClose) {
083          // If we failed when flushing, try to close it to not leak an FD
084          IOUtils.closeStream(out);
085        }
086        // close wasn't successful, try to delete the tmp file
087        if (!tmpFile.delete()) {
088          LOG.warn("Unable to delete tmp file " + tmpFile);
089        }
090      }
091    }
092  }
093
094  /**
095   * Close the atomic file, but do not "commit" the temporary file
096   * on top of the destination. This should be used if there is a failure
097   * in writing.
098   */
099  public void abort() {
100    try {
101      super.close();
102    } catch (IOException ioe) {
103      LOG.warn("Unable to abort file " + tmpFile, ioe);
104    }
105    if (!tmpFile.delete()) {
106      LOG.warn("Unable to delete tmp file during abort " + tmpFile);
107    }
108  }
109
110}