/******************************************************************************* * Copyright (c) 2015 IBH SYSTEMS GmbH. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBH SYSTEMS GmbH - initial API and implementation *******************************************************************************/ package org.eclipse.packagedrone.storage.apm.util; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.function.Function; /** * An OutputStream which replaces the specified file when closing the stream * <p> * The basic idea of this OutputStream is that when the stream is being fully * written and closed, the backing file will simply be swapped out. This should * ensure that only a completely written file will be able to replace the * original file. * </p> * <p> * The {@link #commit()} method needs to be called before calling close, * otherwise the file will not be overwritten with the new content, but the new * content will be discarded. * </p> * <p> * This implementation redirects all output to a file beside the original file * and atomically replaces the target file in the {@link #close()} method if the * {@link #commit()} method was called at least once before. * </p> */ public class ReplaceOnCloseOutputStream extends OutputStream { private final OutputStream out; private final Path targetName; private final Path tmp; private boolean commited; private boolean closed; public ReplaceOnCloseOutputStream ( final Path path ) throws IOException { this ( path, null ); } public ReplaceOnCloseOutputStream ( final Path path, final Function<OutputStream, OutputStream> streamCustomizer ) throws IOException { this.targetName = path; // select target file name "original.dat.swp" this.tmp = path.resolveSibling ( path.getName ( path.getNameCount () - 1 ).toString () + ".swp" ); // delete temp file ... ensure we can start fresh Files.deleteIfExists ( this.tmp ); if ( streamCustomizer != null ) { this.out = streamCustomizer.apply ( Files.newOutputStream ( this.tmp ) ); } else { this.out = Files.newOutputStream ( this.tmp ); } } @Override public void write ( final int b ) throws IOException { this.out.write ( b ); } @Override public void write ( final byte[] b ) throws IOException { this.out.write ( b ); } @Override public void write ( final byte[] b, final int off, final int len ) throws IOException { this.out.write ( b, off, len ); } @Override public void flush () throws IOException { this.out.flush (); } public void commit () { this.commited = true; } @Override public void close () throws IOException { if ( this.closed ) { return; } this.closed = true; try { this.out.close (); } finally { try { if ( this.commited ) { Files.move ( this.tmp, this.targetName, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING ); } } finally { Files.deleteIfExists ( this.tmp ); } } } }