diff --git a/ts3-client/core/src/main/java/com/ts3client/chatlog/ChatLogStore.java b/ts3-client/core/src/main/java/com/ts3client/chatlog/ChatLogStore.java index ec8ddb8..13e0f63 100644 --- a/ts3-client/core/src/main/java/com/ts3client/chatlog/ChatLogStore.java +++ b/ts3-client/core/src/main/java/com/ts3client/chatlog/ChatLogStore.java @@ -8,6 +8,8 @@ import com.ts3client.text.TsLink; import java.io.BufferedWriter; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -28,6 +30,11 @@ import java.util.regex.Pattern; */ public final class ChatLogStore { public static final int HISTORY_LIMIT = 1000; + /** + * How much of the end of a log history is read from: far more than {@link #HISTORY_LIMIT} + * entries take, without reading years of log on every connect. + */ + private static final int HISTORY_TAIL_BYTES = 4 << 20; private static final DateTimeFormatter BEGIN = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("HH:mm:ss"); @@ -57,15 +64,17 @@ public final class ChatLogStore { } public List serverHtmlHistory() throws IOException { - return last(readHtml(log(ChatLogScope.SERVER)), HISTORY_LIMIT); + return last(readHtml(log(ChatLogScope.SERVER), HISTORY_TAIL_BYTES), HISTORY_LIMIT); } public List channelHtmlHistory() throws IOException { - return last(readTextMessages(log(ChatLogScope.CHANNEL), ChatLogScope.CHANNEL), HISTORY_LIMIT); + return last(readTextMessages(log(ChatLogScope.CHANNEL), ChatLogScope.CHANNEL, HISTORY_TAIL_BYTES), + HISTORY_LIMIT); } public List privateHtmlHistory(String clientUniqueId) throws IOException { - return last(readTextMessages(privateLog(clientUniqueId), ChatLogScope.PRIVATE), HISTORY_LIMIT); + return last(readTextMessages(privateLog(clientUniqueId), ChatLogScope.PRIVATE, HISTORY_TAIL_BYTES), + HISTORY_LIMIT); } /** Entries of a whole log that match a query, in the order they were written. */ @@ -84,8 +93,9 @@ public final class ChatLogStore { public SearchResult search(ChatLogScope scope, String clientUniqueId, TextQuery query, int limit) throws IOException { List all = scope == ChatLogScope.SERVER - ? readHtml(log(scope)) - : readTextMessages(scope == ChatLogScope.PRIVATE ? privateLog(clientUniqueId) : log(scope), scope); + ? readHtml(log(scope), Long.MAX_VALUE) + : readTextMessages(scope == ChatLogScope.PRIVATE ? privateLog(clientUniqueId) : log(scope), scope, + Long.MAX_VALUE); List hits = new ArrayList<>(); for (String entry : all) { if (query.matches(HtmlText.plain(entry))) hits.add(entry); @@ -166,10 +176,11 @@ public final class ChatLogStore { if (!Files.exists(files.txt)) Files.createFile(files.txt); } - private static List readHtml(LogFiles files) throws IOException { + /** @param tailBytes how much of the end of the log to read */ + private static List readHtml(LogFiles files, long tailBytes) throws IOException { if (!Files.isRegularFile(files.html)) return Collections.emptyList(); List entries = new ArrayList<>(); - for (String line : Files.readAllLines(files.html, StandardCharsets.UTF_8)) { + for (String line : readLines(files.html, tailBytes)) { if (line.startsWith("

readTextMessages(LogFiles files, ChatLogScope scope) throws IOException { + private static List readTextMessages(LogFiles files, ChatLogScope scope, long tailBytes) + throws IOException { if (!Files.isRegularFile(files.txt)) return Collections.emptyList(); List entries = new ArrayList<>(); Matcher message = null; StringBuilder text = new StringBuilder(); - for (String line : Files.readAllLines(files.txt, StandardCharsets.UTF_8)) { + for (String line : readLines(files.txt, tailBytes)) { Matcher m = TEXT_MESSAGE.matcher(line); boolean begin = line.startsWith("*** "); if (!m.matches() && !begin) { @@ -205,6 +217,22 @@ public final class ChatLogStore { return entries; } + /** The lines in the last {@code tailBytes} of a file, less the one that was cut into. */ + private static List readLines(Path file, long tailBytes) throws IOException { + try (SeekableByteChannel channel = Files.newByteChannel(file)) { + long size = channel.size(); + if (size <= tailBytes) return Files.readAllLines(file, StandardCharsets.UTF_8); + ByteBuffer tail = ByteBuffer.allocate((int) tailBytes); + channel.position(size - tailBytes); + int read = 0; + while (tail.hasRemaining() && read >= 0) read = channel.read(tail); + String text = new String(tail.array(), 0, tail.position(), StandardCharsets.UTF_8); + List lines = new ArrayList<>(List.of(text.split("\r?\n", -1))); + lines.remove(0); + return lines; + } + } + private static List last(List entries, int limit) { if (entries.size() <= limit) return entries; return new ArrayList<>(entries.subList(entries.size() - limit, entries.size())); diff --git a/ts3-client/core/src/test/java/com/ts3client/chatlog/ChatLogStoreTest.java b/ts3-client/core/src/test/java/com/ts3client/chatlog/ChatLogStoreTest.java index 260b22b..2942b34 100644 --- a/ts3-client/core/src/test/java/com/ts3client/chatlog/ChatLogStoreTest.java +++ b/ts3-client/core/src/test/java/com/ts3client/chatlog/ChatLogStoreTest.java @@ -80,4 +80,20 @@ class ChatLogStoreTest { assertTrue(history.get(0).contains("first line

third line"), history.get(0)); assertTrue(history.get(1).contains("reply")); } + + @Test + void historyComesFromTheEndOfALongLog() throws Exception { + ChatLogStore logs = new ChatLogStore(temp, "+Tyg2JtxE8vRNZp+JiUBnmBh0MY="); + logs.appendMessage(ChatLogScope.CHANNEL, false, 5, "uid", "alice", "start"); + Path txt = logs.serverDir().resolve("channel.txt"); + StringBuilder filler = new StringBuilder(); + for (int i = 0; i < 200_000; i++) filler.append("<12:00:00> alice: žluťoučký kůň ").append(i).append('\n'); + Files.writeString(txt, filler, java.nio.file.StandardOpenOption.APPEND); + logs.appendMessage(ChatLogScope.CHANNEL, false, 5, "uid", "bob", "the latest"); + + List history = logs.channelHtmlHistory(); + assertEquals(ChatLogStore.HISTORY_LIMIT, history.size()); + assertTrue(history.get(history.size() - 1).contains("the latest")); + assertTrue(history.get(0).contains("žluťoučký kůň"), history.get(0)); + } }