Commit 474da7f4 authored by Grégory Mantelet's avatar Grégory Mantelet
Browse files

[UWS,TAP] Fix the backup file writing.

Instead of writing the new backup content in the final backup file directly,
write it first in a temporary file and then change the files name.

This fix prevents incomplete backup files (particularly in case of one backup
file per user) when stopping/restarting by force.
parent c38527a8
Loading
Loading
Loading
Loading
+66 −60
Original line number Diff line number Diff line
@@ -47,6 +47,8 @@ import uws.job.ErrorSummary;
import uws.job.Result;
import uws.job.UWSJob;
import uws.job.user.JobOwner;
import uws.service.file.io.OutputStreamWithCloseAction;
import uws.service.file.io.RotateFileAction;
import uws.service.log.UWSLog.LogLevel;
import uws.service.request.UploadFile;

@@ -70,7 +72,7 @@ import uws.service.request.UploadFile;
 * </p>
 *
 * @author Gr&eacute;gory Mantelet (CDS;ARI)
 * @version 4.2 (09/2017)
 * @version 4.4 (07/2018)
 */
public class LocalUWSFileManager implements UWSFileManager {

@@ -510,18 +512,21 @@ public class LocalUWSFileManager implements UWSFileManager {
			if (output != null){
				try{
					output.close();
				}catch(IOException ioe){}
				}catch(IOException ioe){
				}
			}
			if (input != null){
				try{
					input.close();
				}catch(IOException ioe){}
				}catch(IOException ioe){
				}
			}
			// In case of problem, the copy must be deleted:
			if (!done && copy.exists()){
				try{
					copy.delete();
				}catch(SecurityException ioe){}
				}catch(SecurityException ioe){
				}
			}
		}
	}
@@ -724,8 +729,9 @@ public class LocalUWSFileManager implements UWSFileManager {
	@Override
	public OutputStream getBackupOutput() throws IOException{
		File backupFile = new File(rootDirectory, getBackupFileName());
		File tempBackupFile = new File(rootDirectory, getBackupFileName() + ".temp-" + System.currentTimeMillis());
		createParentDir(backupFile);
		return new FileOutputStream(backupFile);
		return new OutputStreamWithCloseAction(new FileOutputStream(tempBackupFile), new RotateFileAction(tempBackupFile, backupFile));
	}

	/* ************** */
+44 −0
Original line number Diff line number Diff line
package uws.service.file.io;

/*
 * This file is part of UWSLibrary.
 *
 * UWSLibrary is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * UWSLibrary 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with UWSLibrary.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright 2018 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
 */

import java.io.IOException;
import java.io.OutputStream;

/**
 * Action(s) executed when the function {@link OutputStreamWithCloseAction#close() close()}
 * is called on an {@link OutputStreamWithCloseAction} instance, after the
 * wrapped {@link OutputStream} has been successfully closed.
 *
 * @author Gr&eacute;gory Mantelet (CDS)
 * @version 4.4 (07/2018)
 * @since 4.4
 */
public interface CloseAction {

	/**
	 * This function is executed after the wrapped {@link OutputStream} of
	 * an {@link OutputStreamWithCloseAction} has been successfully closed.
	 *
	 * @throws IOException	If any error prevents this {@link CloseAction} to
	 *                    	run.
	 */
	public void run() throws IOException;
}
 No newline at end of file
+102 −0
Original line number Diff line number Diff line
package uws.service.file.io;

/*
 * This file is part of UWSLibrary.
 *
 * UWSLibrary is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * UWSLibrary 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with UWSLibrary.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright 2018 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
 */

import java.io.IOException;
import java.io.OutputStream;

/**
 * This {@link OutputStream} wraps another {@link OutputStream}. It forwards all
 * write requests to this inner {@link OutputStream}. The only difference lies
 * in its {@link #close()} function which runs a given {@link CloseAction} just
 * after having called {@link OutputStream#close() close()} on the inner
 * {@link OutputStream} successfully.
 *
 * @author Gr&eacute;gory Mantelet (CDS)
 * @version 4.4 (07/2018)
 * @since 4.4
 *
 * @see CloseAction
 */
public class OutputStreamWithCloseAction extends OutputStream {

	/** Wrapped {@link OutputStream}. */
	private final OutputStream output;

	/** Action(s) to run after the wrapped {@link OutputStream} has been
	 * successfully closed.
	 *
	 * <p><i>
	 * 	It can be <code>null</code>. In such case, the wrapped {@link OutputStream}
	 * 	will be closed and nothing else will be done.
	 * </i></p> **/
	private final CloseAction closeAction;

	/**
	 * Create an {@link OutputStreamWithCloseAction} instance.
	 *
	 * @param output	The {@link OutputStream} to wrap.
	 *              	<i>MANDATORY</i>
	 * @param action	The action(s) to run after the given {@link OutputStream}
	 *              	has been successfully closed.
	 *              	<i>OPTIONAL</i>
	 *
	 * @throws NullPointerException	If the given {@link OutputStream} is missing.
	 */
	public OutputStreamWithCloseAction(final OutputStream output, final CloseAction action) throws NullPointerException{
		if (output == null)
			throw new NullPointerException("Missing OutputStream to wrap!");
		else
			this.output = output;

		this.closeAction = action;
	}

	@Override
	public void write(final byte[] b) throws IOException{
		output.write(b);
	}

	@Override
	public void write(final byte[] b, final int off, final int len) throws IOException{
		output.write(b, off, len);
	}

	@Override
	public void write(final int b) throws IOException{
		output.write(b);
	}

	@Override
	public void flush() throws IOException{
		output.flush();
	}

	@Override
	public void close() throws IOException{
		// Close the output stream:
		output.close();

		// Once close, run the close action:
		if (closeAction != null)
			closeAction.run();
	}

}
 No newline at end of file
+90 −0
Original line number Diff line number Diff line
package uws.service.file.io;

/*
 * This file is part of UWSLibrary.
 *
 * UWSLibrary is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * UWSLibrary 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with UWSLibrary.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright 2018 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
 */

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;

/**
 * This close action rename a given file into another one. The target file is
 * deleted if it already exists.
 *
 * @author Gr&eacute;gory Mantelet (CDS)
 * @version 4.4 (07/2018)
 * @since 4.4
 */
public class RotateFileAction implements CloseAction {

	/** File to rotate (i.e. rename). */
	private final File sourceFile;

	/** File into which {@link #sourceFile} must be renamed. */
	private final File targetFile;

	/**
	 * Create the rotate action.
	 *
	 * @param sourceFile	File to rotate.
	 * @param targetFile	File into which sourceFile must be renamed.
	 *
	 * @throws NullPointerException		If any of the given file is missing.
	 * @throws IllegalArgumentException	If the source file does not exist,
	 *                                 	or if it is not a regular file,
	 *                                 	or if the target file exists but is not
	 *                                 	  a regular file.
	 */
	public RotateFileAction(final File sourceFile, final File targetFile) throws NullPointerException, IllegalArgumentException{
		// Ensure the source file exists and is a regular file:
		if (sourceFile == null)
			throw new NullPointerException("Missing source file!");
		else if (!sourceFile.exists())
			throw new IllegalArgumentException("The source file \"" + sourceFile.getAbsolutePath() + "\" does not exist!");
		else if (sourceFile.isDirectory())
			throw new IllegalArgumentException("The source file \"" + sourceFile.getAbsolutePath() + "\" is a directory instead of a regular file!");

		// Check that if the target file exists, it is a regular file:
		if (targetFile == null)
			throw new NullPointerException("Missing target file!");
		else if (targetFile.exists() && targetFile.isDirectory())
			throw new IllegalArgumentException("The target file \"" + targetFile.getAbsolutePath() + "\" is a directory instead of a regular file!");

		this.sourceFile = sourceFile;
		this.targetFile = targetFile;
	}

	@Override
	public void run() throws IOException{
		// Delete the target file if it already exists:
		try{
			Files.deleteIfExists(targetFile.toPath());
		}catch(IOException ioe){
			throw new IOException("Impossible to perform the file rotation! Cause: the former file can not be deleted.", ioe);
		}

		// Finally rename the source file into the given target file:
		try{
			Files.move(sourceFile.toPath(), targetFile.toPath());
		}catch(IOException ioe){
			throw new IOException("Impossible to perform the file rotation! Cause: [" + ioe.getClass() + "] " + ioe.getMessage(), ioe);
		}
	}

}
+103 −0
Original line number Diff line number Diff line
package uws.service.file.io;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class TestOutputStreamWithCloseAction {
	final String TMP_DIR = System.getProperty("java.io.tmpdir");

	File srcFile;
	File destFile;

	@Before
	public void setUp() throws Exception{
		final String SUFFIX = "" + System.currentTimeMillis();
		srcFile = new File(TMP_DIR, "taptest_stream_src_" + SUFFIX);
		destFile = new File(TMP_DIR, "taptest_stream_dest_" + SUFFIX);
	}

	@After
	public void tearDown() throws Exception{
		if (srcFile != null){
			srcFile.delete();
			srcFile = null;
		}
		if (destFile != null){
			destFile.delete();
			destFile = null;
		}
	}

	@Test
	public void testOutputStreamWithCloseAction(){
		/* CASE: Missing OutputStream to wrap => ERROR */
		OutputStreamWithCloseAction out = null;
		try{
			out = new OutputStreamWithCloseAction(null, null);
			fail("This construction should have failed because no OutputStream has been provided!");
		}catch(Exception ex){
			assertEquals(NullPointerException.class, ex.getClass());
			assertEquals("Missing OutputStream to wrap!", ex.getMessage());
		}finally{
			if (out != null){
				try{
					out.close();
				}catch(IOException ioe){
				}
			}
		}
	}

	@Test
	public void testClose(){
		OutputStreamWithCloseAction out = null;
		try{
			// Open the stream toward the temp file:
			out = new OutputStreamWithCloseAction(new FileOutputStream(srcFile), new RotateFileAction(srcFile, destFile));

			out.write("File\n".getBytes());
			out.flush();
			out.write("successfully written!".getBytes());

			// Close the stream, and so, rotate the files:
			out.close();

			// Check the files have been rotated:
			assertFalse(srcFile.exists());
			assertTrue(destFile.exists());

			// Check the content of the dest file:
			try(InputStream input = new FileInputStream(destFile)){
				byte[] buffer = new byte[256];
				int nbRead = 0;
				nbRead = input.read(buffer);
				assertEquals(26, nbRead);
				assertEquals("File\nsuccessfully written!", new String(buffer, 0, nbRead));
			}

		}catch(Exception ex){
			ex.printStackTrace(System.err);
			fail("Unexpected error! (see the console for more details)");
		}finally{
			if (out != null){
				try{
					out.close();
				}catch(IOException ioe){
				}
			}
		}
	}

}
Loading