mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-16 10:16:16 +01:00
Create function for copy on write
This commit is contained in:
69
scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java
Normal file
69
scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java
Normal file
@@ -0,0 +1,69 @@
|
||||
package sonia.scm.store;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class CopyOnWrite {
|
||||
|
||||
private CopyOnWrite() {}
|
||||
|
||||
public static void withTemporaryFile(FileWriter creator, Path targetFile) {
|
||||
validateInput(targetFile);
|
||||
|
||||
try {
|
||||
Path temporaryFile = Files.createFile(targetFile.getParent().resolve(UUID.randomUUID().toString()));
|
||||
|
||||
creator.write(temporaryFile);
|
||||
|
||||
replaceOriginalFile(targetFile, temporaryFile);
|
||||
} catch (Exception ex) {
|
||||
throw new StoreException("could not write file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateInput(Path targetFile) {
|
||||
if (Files.isDirectory(targetFile)) {
|
||||
throw new IllegalArgumentException("target file has to be a regular file, not a directory");
|
||||
}
|
||||
if (targetFile.getParent() == null) {
|
||||
throw new IllegalArgumentException("target file has to be specified with a parent directory");
|
||||
}
|
||||
}
|
||||
|
||||
private static void replaceOriginalFile(Path targetFile, Path temporaryFile) throws IOException {
|
||||
Path backupFile = backupOriginalFile(targetFile);
|
||||
try {
|
||||
Files.move(temporaryFile, targetFile);
|
||||
if (backupFile != null) {
|
||||
Files.delete(backupFile);
|
||||
}
|
||||
} catch (RuntimeException | IOException e) {
|
||||
restoreBackup(targetFile, backupFile);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private static Path backupOriginalFile(Path targetFile) throws IOException {
|
||||
Path directory = targetFile.getParent();
|
||||
if (Files.exists(targetFile)) {
|
||||
Path backupFile = directory.resolve(UUID.randomUUID().toString());
|
||||
Files.move(targetFile, backupFile);
|
||||
return backupFile;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void restoreBackup(Path targetFile, Path backupFile) throws IOException {
|
||||
if (backupFile != null) {
|
||||
Files.move(backupFile, targetFile);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface FileWriter {
|
||||
void write(Path t) throws Exception;
|
||||
}
|
||||
}
|
||||
101
scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java
Normal file
101
scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java
Normal file
@@ -0,0 +1,101 @@
|
||||
package sonia.scm.store;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static sonia.scm.store.CopyOnWrite.withTemporaryFile;
|
||||
|
||||
@ExtendWith(TempDirectory.class)
|
||||
class CopyOnWriteTest {
|
||||
|
||||
@Test
|
||||
void shouldCreateNewFile(@TempDirectory.TempDir Path tempDir) {
|
||||
Path expectedFile = tempDir.resolve("toBeCreated.txt");
|
||||
|
||||
withTemporaryFile(
|
||||
file -> new FileOutputStream(file.toFile()).write("great success".getBytes()),
|
||||
expectedFile);
|
||||
|
||||
Assertions.assertThat(expectedFile).hasContent("great success");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOverwriteExistingFile(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
Path expectedFile = tempDir.resolve("toBeOverwritten.txt");
|
||||
Files.createFile(expectedFile);
|
||||
|
||||
withTemporaryFile(
|
||||
file -> new FileOutputStream(file.toFile()).write("great success".getBytes()),
|
||||
expectedFile);
|
||||
|
||||
Assertions.assertThat(expectedFile).hasContent("great success");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailForDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> withTemporaryFile(
|
||||
file -> new FileOutputStream(file.toFile()).write("should not be written".getBytes()),
|
||||
tempDir));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailForMissingDirectory() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> withTemporaryFile(
|
||||
file -> new FileOutputStream(file.toFile()).write("should not be written".getBytes()),
|
||||
Paths.get("someFile")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepBackupIfTemporaryFileCouldNotBeWritten(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
Path unchangedOriginalFile = tempDir.resolve("notToBeDeleted.txt");
|
||||
new FileOutputStream(unchangedOriginalFile.toFile()).write("this should be kept".getBytes());
|
||||
|
||||
assertThrows(
|
||||
IOException.class,
|
||||
() -> withTemporaryFile(
|
||||
file -> {
|
||||
throw new IOException("test");
|
||||
},
|
||||
unchangedOriginalFile));
|
||||
|
||||
Assertions.assertThat(unchangedOriginalFile).hasContent("this should be kept");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepBackupIfTemporaryFileIsMissing(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
Path backedUpFile = tempDir.resolve("notToBeDeleted.txt");
|
||||
new FileOutputStream(backedUpFile.toFile()).write("this should be kept".getBytes());
|
||||
|
||||
assertThrows(
|
||||
IOException.class,
|
||||
() -> withTemporaryFile(
|
||||
Files::delete,
|
||||
backedUpFile));
|
||||
|
||||
Assertions.assertThat(backedUpFile).hasContent("this should be kept");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteExistingFile(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
Path expectedFile = tempDir.resolve("toBeReplaced.txt");
|
||||
new FileOutputStream(expectedFile.toFile()).write("this should be removed".getBytes());
|
||||
|
||||
withTemporaryFile(
|
||||
file -> new FileOutputStream(file.toFile()).write("overwritten".getBytes()),
|
||||
expectedFile);
|
||||
|
||||
Assertions.assertThat(Files.list(tempDir)).hasSize(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user