diff --git a/tinylog-impl/pom.xml b/tinylog-impl/pom.xml index 8e6308f0b..066b44ac3 100644 --- a/tinylog-impl/pom.xml +++ b/tinylog-impl/pom.xml @@ -75,7 +75,12 @@ org.tinylog tinylog-api - + + org.codehaus.mojo + animal-sniffer-annotations + compile + + diff --git a/tinylog-impl/src/main/java/org/tinylog/writers/RollingFileWriter.java b/tinylog-impl/src/main/java/org/tinylog/writers/RollingFileWriter.java index 9a9713dfe..1d1b0ced4 100644 --- a/tinylog-impl/src/main/java/org/tinylog/writers/RollingFileWriter.java +++ b/tinylog-impl/src/main/java/org/tinylog/writers/RollingFileWriter.java @@ -17,10 +17,14 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import org.tinylog.Level; import org.tinylog.configuration.ServiceLoader; import org.tinylog.core.LogEntry; @@ -43,6 +47,7 @@ public final class RollingFileWriter extends AbstractFormatPatternWriter { private final int backups; private final boolean buffered; private final boolean writingThread; + private final DynamicPath linkToLatest; private final Charset charset; private ByteArrayWriter writer; @@ -72,8 +77,9 @@ public RollingFileWriter(final Map properties) throws FileNotFou path = new DynamicPath(getFileName(properties)); policies = createPolicies(properties.get("policies")); backups = properties.containsKey("backups") ? Integer.parseInt(properties.get("backups")) : -1; + linkToLatest = properties.containsKey("latest") ? new DynamicPath(properties.get("latest")) : null; - List files = path.getAllFiles(); + List files = filterOutSymlinks(path.getAllFiles()); String fileName; boolean append; @@ -96,7 +102,43 @@ public RollingFileWriter(final Map properties) throws FileNotFou charset = getCharset(properties); buffered = Boolean.parseBoolean(properties.get("buffered")); writingThread = Boolean.parseBoolean(properties.get("writingthread")); - writer = createByteArrayWriter(fileName, append, buffered, false, false); + writer = createByteArrayWriterAndLinkLatest(fileName, append, buffered, false, false); + } + + @IgnoreJRERequirement + private static List filterOutSymlinks(final List files) { + if (!RuntimeProvider.isAndroid()) { + List symlinks = new ArrayList(); + for (File file : files) { + if (Files.isSymbolicLink(file.toPath())) { + symlinks.add(file); + } + } + files.removeAll(symlinks); + } + return files; + } + + @IgnoreJRERequirement + private ByteArrayWriter createByteArrayWriterAndLinkLatest(final String fileName, final boolean append, final boolean buffered, + final boolean threadSafe, final boolean shared) throws FileNotFoundException { + ByteArrayWriter writer = createByteArrayWriter(fileName, append, buffered, threadSafe, shared); + if (linkToLatest != null) { + File logFile = new File(fileName); + File linkFile = new File(linkToLatest.resolve()); + if (!RuntimeProvider.isAndroid()) { + try { + Path linkPath = linkFile.toPath(); + Files.delete(linkPath); + Files.createSymbolicLink(linkPath, logFile.toPath()); + } catch (IOException exception) { + InternalLogger.log(Level.ERROR, exception, "Failed to create symlink '" + linkFile + "'"); + } + } else { + InternalLogger.log(Level.WARN, "Cannot create symlink to latest log segment on Android"); + } + } + return writer; } @Override @@ -145,9 +187,11 @@ private void internalWrite(final byte[] data) throws IOException { if (!canBeContinued(data, policies)) { writer.close(); + List existingFiles = filterOutSymlinks(path.getAllFiles()); + deleteBackups(existingFiles, backups); + String fileName = path.resolve(); - deleteBackups(path.getAllFiles(), backups); - writer = createByteArrayWriter(fileName, false, buffered, false, false); + writer = createByteArrayWriterAndLinkLatest(fileName, false, buffered, false, false); for (Policy policy : policies) { policy.reset(); diff --git a/tinylog-impl/src/test/java/org/tinylog/writers/RollingFileWriterTest.java b/tinylog-impl/src/test/java/org/tinylog/writers/RollingFileWriterTest.java index 1319a16f3..ebddf47bd 100644 --- a/tinylog-impl/src/test/java/org/tinylog/writers/RollingFileWriterTest.java +++ b/tinylog-impl/src/test/java/org/tinylog/writers/RollingFileWriterTest.java @@ -481,4 +481,53 @@ public void isRegistered() throws IOException { assertThat(writer).isInstanceOf(RollingFileWriter.class); } + /** + * Verifies that obsolete backup files will be deleted when rolling log file. + * + * @throws IOException + * Failed access to temporary folder or files + */ + @Test + public void updateSymlinkToLatestAtRollOver() throws IOException { + File file1 = folder.newFile("0"); + File file2 = folder.newFile("1"); + File file3 = folder.newFile("2"); + File file4 = folder.newFile("3"); + File file5 = folder.newFile("4"); + File linkToLatest = folder.newFile("latest"); + + file1.setLastModified(0); + file2.setLastModified(1000); + file3.setLastModified(2000); + file4.setLastModified(3000); + file5.delete(); + + Map properties = new HashMap<>(); + properties.put("file", new File(folder.getRoot(), "{count}").getAbsolutePath()); + properties.put("latest", linkToLatest.getAbsolutePath()); + properties.put("format", "{message}"); + properties.put("policies", "size: 10"); + properties.put("backups", "3"); + + RollingFileWriter writer = new RollingFileWriter(properties); + + assertThat(file1).exists(); + assertThat(file2).exists(); + assertThat(file3).exists(); + assertThat(file4).exists(); + assertThat(file5).doesNotExist(); + + writer.write(LogEntryBuilder.empty().message("First").create()); + writer.write(LogEntryBuilder.empty().message("Second").create()); + + assertThat(file1).doesNotExist(); + assertThat(file2).exists(); + assertThat(file3).exists(); + assertThat(file4).hasContent("First" + NEW_LINE); + assertThat(file5).hasContent("Second" + NEW_LINE); + + assertThat(linkToLatest.getCanonicalPath()).isEqualTo(file5.getCanonicalPath()); + + writer.close(); + } }