Skip to content
Snippets Groups Projects
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
No related branches found
No related tags found
No related merge requests found
...@@ -47,6 +47,8 @@ import uws.job.ErrorSummary; ...@@ -47,6 +47,8 @@ import uws.job.ErrorSummary;
import uws.job.Result; import uws.job.Result;
import uws.job.UWSJob; import uws.job.UWSJob;
import uws.job.user.JobOwner; 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.log.UWSLog.LogLevel;
import uws.service.request.UploadFile; import uws.service.request.UploadFile;
...@@ -70,7 +72,7 @@ import uws.service.request.UploadFile; ...@@ -70,7 +72,7 @@ import uws.service.request.UploadFile;
* </p> * </p>
* *
* @author Gr&eacute;gory Mantelet (CDS;ARI) * @author Gr&eacute;gory Mantelet (CDS;ARI)
* @version 4.2 (09/2017) * @version 4.4 (07/2018)
*/ */
public class LocalUWSFileManager implements UWSFileManager { public class LocalUWSFileManager implements UWSFileManager {
...@@ -510,18 +512,21 @@ public class LocalUWSFileManager implements UWSFileManager { ...@@ -510,18 +512,21 @@ public class LocalUWSFileManager implements UWSFileManager {
if (output != null){ if (output != null){
try{ try{
output.close(); output.close();
}catch(IOException ioe){} }catch(IOException ioe){
}
} }
if (input != null){ if (input != null){
try{ try{
input.close(); input.close();
}catch(IOException ioe){} }catch(IOException ioe){
}
} }
// In case of problem, the copy must be deleted: // In case of problem, the copy must be deleted:
if (!done && copy.exists()){ if (!done && copy.exists()){
try{ try{
copy.delete(); copy.delete();
}catch(SecurityException ioe){} }catch(SecurityException ioe){
}
} }
} }
} }
...@@ -724,8 +729,9 @@ public class LocalUWSFileManager implements UWSFileManager { ...@@ -724,8 +729,9 @@ public class LocalUWSFileManager implements UWSFileManager {
@Override @Override
public OutputStream getBackupOutput() throws IOException{ public OutputStream getBackupOutput() throws IOException{
File backupFile = new File(rootDirectory, getBackupFileName()); File backupFile = new File(rootDirectory, getBackupFileName());
File tempBackupFile = new File(rootDirectory, getBackupFileName() + ".temp-" + System.currentTimeMillis());
createParentDir(backupFile); createParentDir(backupFile);
return new FileOutputStream(backupFile); return new OutputStreamWithCloseAction(new FileOutputStream(tempBackupFile), new RotateFileAction(tempBackupFile, backupFile));
} }
/* ************** */ /* ************** */
......
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
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
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);
}
}
}
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){
}
}
}
}
}
package uws.service.file.io;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.File;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class TestRotateFileAction {
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_rotate_src_" + SUFFIX);
destFile = new File(TMP_DIR, "taptest_rotate_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 testConstructor(){
/* CASE: No source file => ERROR */
try{
new RotateFileAction(null, null);
fail("This construction should have failed because no source file has been provided!");
}catch(Exception ex){
assertEquals(NullPointerException.class, ex.getClass());
assertEquals("Missing source file!", ex.getMessage());
}
/* CASE: The source file does not exist => ERROR */
try{
assertFalse(srcFile.exists());
new RotateFileAction(srcFile, null);
fail("This construction should have failed because the source file does not exist.");
}catch(Exception ex){
assertEquals(IllegalArgumentException.class, ex.getClass());
assertEquals("The source file \"" + srcFile.getAbsolutePath() + "\" does not exist!", ex.getMessage());
}
/* CASE: The source file is a directory => ERROR */
try{
assertTrue(srcFile.mkdir());
new RotateFileAction(srcFile, null);
fail("This construction should have failed because the source file is a directory!");
}catch(Exception ex){
assertEquals(IllegalArgumentException.class, ex.getClass());
assertEquals("The source file \"" + srcFile.getAbsolutePath() + "\" is a directory instead of a regular file!", ex.getMessage());
}finally{
srcFile.delete();
}
/* CASE: No dest file => ERROR */
try{
assertTrue(srcFile.createNewFile());
new RotateFileAction(srcFile, null);
fail("This construction should have failed because the dest file is missing!");
}catch(Exception ex){
assertEquals(NullPointerException.class, ex.getClass());
assertEquals("Missing target file!", ex.getMessage());
}finally{
srcFile.delete();
}
/* CASE: the dest file exists and is a directory => ERROR */
try{
assertTrue(srcFile.createNewFile());
assertTrue(destFile.mkdir());
new RotateFileAction(srcFile, destFile);
fail("This construction should have failed because the dest file is a directory!");
}catch(Exception ex){
assertEquals(IllegalArgumentException.class, ex.getClass());
assertEquals("The target file \"" + destFile.getAbsolutePath() + "\" is a directory instead of a regular file!", ex.getMessage());
}finally{
srcFile.delete();
destFile.delete();
}
/* CASE: Everything is OK! */
try{
assertTrue(srcFile.createNewFile());
assertFalse(destFile.exists());
new RotateFileAction(srcFile, destFile);
}catch(Exception ex){
ex.printStackTrace(System.err);
fail("Unexpected error while construction! (see console for more details)");
}
}
@Test
public void testRun(){
/* CASE: Destination does not exist yet */
try{
// Create a source file:
assertTrue(srcFile.createNewFile());
// Ensure the dest file does exist yet:
assertFalse(destFile.exists());
// Run the action:
CloseAction action = new RotateFileAction(srcFile, destFile);
action.run();
/* Check the source file does not exist anymore
* BUT the dest one yes: */
assertFalse(srcFile.exists());
assertTrue(destFile.exists());
}catch(Exception ex){
ex.printStackTrace(System.err);
fail("Unexpected IO error! (see console for more details)");
}finally{
srcFile.delete();
destFile.delete();
}
/* CASE: Destination already exists */
try{
// Create a source file:
assertTrue(srcFile.createNewFile());
// Sleep for 1 second (so that the last modified date can be different):
Thread.sleep(1000);
// Already create the dest:
assertTrue(destFile.createNewFile());
long lastModified = destFile.lastModified();
// Run the action:
CloseAction action = new RotateFileAction(srcFile, destFile);
action.run();
/* Check the source file does not exist anymore
* AND the destination file does still exist
* BUT with a different lastModified date: */
assertFalse(srcFile.exists());
assertTrue(destFile.exists());
assertNotEquals(lastModified, destFile.lastModified());
}catch(Exception ex){
ex.printStackTrace(System.err);
fail("Unexpected IO error! (see console for more details)");
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment