From 3e3ab69b14f4c51cdb9caa8b32d12d65cebbbd12 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 4 Dec 2019 17:04:46 +0100 Subject: [PATCH 1/8] Create function for copy on write --- .../java/sonia/scm/store/CopyOnWrite.java | 69 ++++++++++++ .../java/sonia/scm/store/CopyOnWriteTest.java | 101 ++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java create mode 100644 scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java b/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java new file mode 100644 index 0000000000..510ebb417a --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java @@ -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; + } +} diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java new file mode 100644 index 0000000000..338a491c0d --- /dev/null +++ b/scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java @@ -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); + } +} From c4ed6f917d16b1ce35f8e813f8d0a0bf3e731f3d Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 4 Dec 2019 17:16:17 +0100 Subject: [PATCH 2/8] Use copy on write in JAXB store implementations --- .../store/JAXBConfigurationEntryStore.java | 61 +++++++++---------- .../scm/store/JAXBConfigurationStore.java | 5 +- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java index 40cf03c8a8..deddaa9500 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java @@ -317,48 +317,47 @@ public class JAXBConfigurationEntryStore implements ConfigurationEntryStore { + try (IndentXMLStreamWriter writer = XmlStreams.createWriter(temp)) { + writer.writeStartDocument(); - // configuration start - writer.writeStartElement(TAG_CONFIGURATION); + // configuration start + writer.writeStartElement(TAG_CONFIGURATION); - Marshaller m = context.createMarshaller(); + Marshaller m = context.createMarshaller(); - m.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE); + m.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE); - for (Entry e : entries.entrySet()) - { + for (Entry e : entries.entrySet()) { - // entry start - writer.writeStartElement(TAG_ENTRY); + // entry start + writer.writeStartElement(TAG_ENTRY); - // key start - writer.writeStartElement(TAG_KEY); - writer.writeCharacters(e.getKey()); + // key start + writer.writeStartElement(TAG_KEY); + writer.writeCharacters(e.getKey()); - // key end - writer.writeEndElement(); + // key end + writer.writeEndElement(); - // value - JAXBElement je = new JAXBElement(QName.valueOf(TAG_VALUE), type, - e.getValue()); + // value + JAXBElement je = new JAXBElement<>(QName.valueOf(TAG_VALUE), type, + e.getValue()); - m.marshal(je, writer); + m.marshal(je, writer); - // entry end - writer.writeEndElement(); - } + // entry end + writer.writeEndElement(); + } - // configuration end - writer.writeEndElement(); - writer.writeEndDocument(); - } - catch (Exception ex) - { - throw new StoreException("could not store configuration", ex); - } + // configuration end + writer.writeEndElement(); + writer.writeEndDocument(); + } + }, + file.toPath() + ); } //~--- fields --------------------------------------------------------------- diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java index ac1477d7ea..92bd0fbcc4 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java @@ -113,7 +113,10 @@ public class JAXBConfigurationStore extends AbstractStore { Marshaller marshaller = context.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); - marshaller.marshal(object, configFile); + CopyOnWrite.withTemporaryFile( + temp -> marshaller.marshal(object, temp.toFile()), + configFile.toPath() + ); } catch (JAXBException ex) { throw new StoreException("failed to marshall object", ex); From 9ca2a576675d0b8212e7cb51fa4833340d36baf4 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 5 Dec 2019 07:43:26 +0100 Subject: [PATCH 3/8] Fix unit test --- .../src/test/java/sonia/scm/store/CopyOnWriteTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java index 338a491c0d..b0f9412359 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java @@ -63,7 +63,7 @@ class CopyOnWriteTest { new FileOutputStream(unchangedOriginalFile.toFile()).write("this should be kept".getBytes()); assertThrows( - IOException.class, + StoreException.class, () -> withTemporaryFile( file -> { throw new IOException("test"); @@ -79,7 +79,7 @@ class CopyOnWriteTest { new FileOutputStream(backedUpFile.toFile()).write("this should be kept".getBytes()); assertThrows( - IOException.class, + StoreException.class, () -> withTemporaryFile( Files::delete, backedUpFile)); From 791437a905bf5cb6ce61aecdc222dd4baaf78939 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 5 Dec 2019 07:57:47 +0100 Subject: [PATCH 4/8] Use copy on write in repository db --- .../scm/repository/xml/MetadataStore.java | 6 +++- .../scm/repository/xml/PathDatabase.java | 36 +++++++++++-------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java index f22180ff9a..3a81f54fb2 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java @@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory; import sonia.scm.ContextEntry; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; +import sonia.scm.store.CopyOnWrite; import sonia.scm.store.StoreConstants; import sonia.scm.update.UpdateStepRepositoryMetadataAccess; @@ -43,7 +44,10 @@ public class MetadataStore implements UpdateStepRepositoryMetadataAccess { try { Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); - marshaller.marshal(repository, resolveDataPath(path).toFile()); + CopyOnWrite.withTemporaryFile( + temp -> marshaller.marshal(repository, temp.toFile()), + resolveDataPath(path) + ); } catch (JAXBException ex) { throw new InternalRepositoryException(repository, "failed write repository metadata", ex); } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java index 70698aed59..c895b69840 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java @@ -4,6 +4,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.ContextEntry; import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.store.CopyOnWrite; import sonia.scm.xml.IndentXMLStreamWriter; import sonia.scm.xml.XmlStreams; @@ -40,23 +41,28 @@ class PathDatabase { ensureParentDirectoryExists(); LOG.trace("write repository path database to {}", storePath); - try (IndentXMLStreamWriter writer = XmlStreams.createWriter(storePath)) { - writer.writeStartDocument(ENCODING, VERSION); + CopyOnWrite.withTemporaryFile( + temp -> { + try (IndentXMLStreamWriter writer = XmlStreams.createWriter(temp)) { + writer.writeStartDocument(ENCODING, VERSION); - writeRepositoriesStart(writer, creationTime, lastModified); - for (Map.Entry e : pathDatabase.entrySet()) { - writeRepository(writer, e.getKey(), e.getValue()); - } - writer.writeEndElement(); + writeRepositoriesStart(writer, creationTime, lastModified); + for (Map.Entry e : pathDatabase.entrySet()) { + writeRepository(writer, e.getKey(), e.getValue()); + } + writer.writeEndElement(); - writer.writeEndDocument(); - } catch (XMLStreamException | IOException ex) { - throw new InternalRepositoryException( - ContextEntry.ContextBuilder.entity(Path.class, storePath.toString()).build(), - "failed to write repository path database", - ex - ); - } + writer.writeEndDocument(); + } catch (XMLStreamException | IOException ex) { + throw new InternalRepositoryException( + ContextEntry.ContextBuilder.entity(Path.class, storePath.toString()).build(), + "failed to write repository path database", + ex + ); + } + }, + storePath + ); } private void ensureParentDirectoryExists() { From b2bbd1d9b5e2b82c224032e654bb7a553210bd76 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 5 Dec 2019 10:06:17 +0100 Subject: [PATCH 5/8] Enhance error handling --- .../java/sonia/scm/store/CopyOnWrite.java | 68 ++++++++++++++----- .../java/sonia/scm/store/CopyOnWriteTest.java | 13 ++++ 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java b/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java index 510ebb417a..18df2b6271 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java @@ -1,5 +1,8 @@ package sonia.scm.store; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -7,20 +10,15 @@ import java.util.UUID; public final class CopyOnWrite { + private static final Logger LOG = LoggerFactory.getLogger(CopyOnWrite.class); + private CopyOnWrite() {} - public static void withTemporaryFile(FileWriter creator, Path targetFile) { + public static void withTemporaryFile(FileWriter writer, 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); - } + Path temporaryFile = createTemporaryFile(targetFile); + executeCallback(writer, targetFile, temporaryFile); + replaceOriginalFile(targetFile, temporaryFile); } private static void validateInput(Path targetFile) { @@ -32,38 +30,72 @@ public final class CopyOnWrite { } } - private static void replaceOriginalFile(Path targetFile, Path temporaryFile) throws IOException { + private static Path createTemporaryFile(Path targetFile) { + Path temporaryFile = targetFile.getParent().resolve(UUID.randomUUID().toString()); + try { + Files.createFile(temporaryFile); + } catch (IOException ex) { + LOG.error("Error creating temporary file {} to replace file {}", temporaryFile, targetFile); + throw new StoreException("could create temporary file", ex); + } + return temporaryFile; + } + + private static void executeCallback(FileWriter writer, Path targetFile, Path temporaryFile) { + try { + writer.write(temporaryFile); + } catch (RuntimeException e) { + throw e; + } catch (Exception ex) { + LOG.error("Error writing to temporary file {}. Target file {} has not been modified", temporaryFile, targetFile); + throw new StoreException("could not write temporary file", ex); + } + } + + private static void replaceOriginalFile(Path targetFile, Path temporaryFile) { Path backupFile = backupOriginalFile(targetFile); try { Files.move(temporaryFile, targetFile); if (backupFile != null) { Files.delete(backupFile); } - } catch (RuntimeException | IOException e) { + } catch (IOException e) { + LOG.error("Error renaming temporary file {} to target file {}", temporaryFile, targetFile); restoreBackup(targetFile, backupFile); - throw e; + throw new StoreException("could rename temporary file to target file", e); } } - private static Path backupOriginalFile(Path targetFile) throws IOException { + private static Path backupOriginalFile(Path targetFile) { Path directory = targetFile.getParent(); if (Files.exists(targetFile)) { Path backupFile = directory.resolve(UUID.randomUUID().toString()); - Files.move(targetFile, backupFile); + try { + Files.move(targetFile, backupFile); + } catch (IOException e) { + LOG.error("Could not backup original file {}. Aborting here so that original file will not be overwritten.", targetFile); + throw new StoreException("could not create backup of file", e); + } return backupFile; } else { return null; } } - private static void restoreBackup(Path targetFile, Path backupFile) throws IOException { + private static void restoreBackup(Path targetFile, Path backupFile) { if (backupFile != null) { - Files.move(backupFile, targetFile); + try { + Files.move(backupFile, targetFile); + LOG.info("Recovered original file {} from backup", targetFile); + } catch (IOException e) { + LOG.error("Could not replace original file {} with backup file {} after failure", targetFile, backupFile); + } } } @FunctionalInterface public interface FileWriter { + @SuppressWarnings("squid:S00112") // We do not want to limit exceptions here void write(Path t) throws Exception; } } diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java index b0f9412359..3767ac7c06 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java @@ -73,6 +73,19 @@ class CopyOnWriteTest { Assertions.assertThat(unchangedOriginalFile).hasContent("this should be kept"); } + @Test + void shouldNotWrapRuntimeExceptions(@TempDirectory.TempDir Path tempDir) throws IOException { + Path someFile = tempDir.resolve("something.txt"); + + assertThrows( + NullPointerException.class, + () -> withTemporaryFile( + file -> { + throw new NullPointerException("test"); + }, + someFile)); + } + @Test void shouldKeepBackupIfTemporaryFileIsMissing(@TempDirectory.TempDir Path tempDir) throws IOException { Path backedUpFile = tempDir.resolve("notToBeDeleted.txt"); From 503bd8fa16292a5dd413d1139cd21f6fbf4f6478 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 5 Dec 2019 10:10:26 +0100 Subject: [PATCH 6/8] Enhance error handling --- .../main/java/sonia/scm/store/CopyOnWrite.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java b/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java index 18df2b6271..034472a904 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java @@ -56,14 +56,12 @@ public final class CopyOnWrite { Path backupFile = backupOriginalFile(targetFile); try { Files.move(temporaryFile, targetFile); - if (backupFile != null) { - Files.delete(backupFile); - } } catch (IOException e) { LOG.error("Error renaming temporary file {} to target file {}", temporaryFile, targetFile); restoreBackup(targetFile, backupFile); throw new StoreException("could rename temporary file to target file", e); } + deleteBackupFile(backupFile); } private static Path backupOriginalFile(Path targetFile) { @@ -82,6 +80,17 @@ public final class CopyOnWrite { } } + private static void deleteBackupFile(Path backupFile) { + if (backupFile != null) { + try { + Files.delete(backupFile); + } catch (IOException e) { + LOG.warn("Could not delete backup file {}", backupFile); + throw new StoreException("could not delete backup file", e); + } + } + } + private static void restoreBackup(Path targetFile, Path backupFile) { if (backupFile != null) { try { From 9525cba06b137df07213d6dd5cf543f9be25ad8a Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 5 Dec 2019 15:20:47 +0100 Subject: [PATCH 7/8] suppress sonarqube nio performance warning --- scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java b/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java index 034472a904..135221ea82 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java @@ -21,6 +21,7 @@ public final class CopyOnWrite { replaceOriginalFile(targetFile, temporaryFile); } + @SuppressWarnings("squid:S3725") // performance of Files#isDirectory private static void validateInput(Path targetFile) { if (Files.isDirectory(targetFile)) { throw new IllegalArgumentException("target file has to be a regular file, not a directory"); @@ -36,7 +37,7 @@ public final class CopyOnWrite { Files.createFile(temporaryFile); } catch (IOException ex) { LOG.error("Error creating temporary file {} to replace file {}", temporaryFile, targetFile); - throw new StoreException("could create temporary file", ex); + throw new StoreException("could not create temporary file", ex); } return temporaryFile; } @@ -64,6 +65,7 @@ public final class CopyOnWrite { deleteBackupFile(backupFile); } + @SuppressWarnings("squid:S3725") // performance of Files#exists private static Path backupOriginalFile(Path targetFile) { Path directory = targetFile.getParent(); if (Files.exists(targetFile)) { From 2f7f160db3ca12fb6180319fb5e56794ef277598 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 5 Dec 2019 14:21:35 +0000 Subject: [PATCH 8/8] Close branch feature/copy_on_write