diff --git a/ts3-client/core/src/main/java/com/ts3client/net/ChannelAdmin.java b/ts3-client/core/src/main/java/com/ts3client/net/ChannelAdmin.java index 2419013..1a6bc69 100644 --- a/ts3-client/core/src/main/java/com/ts3client/net/ChannelAdmin.java +++ b/ts3-client/core/src/main/java/com/ts3client/net/ChannelAdmin.java @@ -32,6 +32,9 @@ import java.util.zip.CRC32; */ final class ChannelAdmin { + /** The directory a server's icons live in, where the server has one. */ + private static final String ICON_DIRECTORY = "icons"; + /** How long to wait for a whole icon to travel over the file connection. */ private static final long ICON_UPLOAD_TIMEOUT_MS = 30_000; @@ -178,14 +181,30 @@ final class ChannelAdmin { */ List listIcons() throws Exception { List ids = new ArrayList<>(); + boolean hasIconDirectory = false; for (RemoteFile file : fileTransfers().list(0, "", "/")) { - if (file.isDirectory() || !file.getName().startsWith("icon_")) continue; - long id = parseLong(file.getName().substring("icon_".length())); - if (id > 0) ids.add(id); + if (file.isDirectory()) { + hasIconDirectory |= file.getName().equals(ICON_DIRECTORY); + continue; + } + addIcon(ids, file); + } + // Newer servers keep the icons in their own directory rather than loose in the + // repository's root; both layouts exist, so whichever this server uses is read. + if (hasIconDirectory) { + for (RemoteFile file : fileTransfers().list(0, "", "/" + ICON_DIRECTORY)) { + if (!file.isDirectory()) addIcon(ids, file); + } } return ids; } + private static void addIcon(List ids, RemoteFile file) { + if (!file.getName().startsWith("icon_")) return; + long id = parseLong(file.getName().substring("icon_".length())); + if (id > 0 && !ids.contains(id)) ids.add(id); + } + /** * Uploads an image as a server icon. * diff --git a/ts3-client/core/src/main/java/com/ts3client/net/filetransfer/FileTransferManager.java b/ts3-client/core/src/main/java/com/ts3client/net/filetransfer/FileTransferManager.java index f2802e8..2f121cd 100644 --- a/ts3-client/core/src/main/java/com/ts3client/net/filetransfer/FileTransferManager.java +++ b/ts3-client/core/src/main/java/com/ts3client/net/filetransfer/FileTransferManager.java @@ -52,6 +52,8 @@ public final class FileTransferManager { /** TeamSpeak error id returned by {@code ftgetfilelist} for an empty directory. */ private static final int ERROR_DATABASE_EMPTY_RESULT = 0x0501; + /** How long to wait for the server to finish listing a directory. */ + private static final long LIST_TIMEOUT_MS = 15_000; private final LocalTeamspeakClientSocket socket; /** Host we are connected to, used when the server reports no dedicated file-transfer host. */ @@ -70,6 +72,14 @@ public final class FileTransferManager { * routes back to the waiting transfer thread. */ private final Map>> pendingInits = new ConcurrentHashMap<>(); + + /** + * The {@code ftgetfilelist} in progress. The server answers that command with a + * {@code notifyfilelist} event per entry and a closing {@code notifyfilelistfinished} + * rather than with rows of the command's own reply, so the entries are collected here + * until the listing ends. One listing may be in flight at a time. + */ + private volatile FileListRequest fileListRequest; private final TS3Listener ftEventListener = new TS3Listener() { @Override public void onUnknownEvent(UnknownTeamspeakEvent e) { @@ -88,34 +98,49 @@ public final class FileTransferManager { /** * Lists the files and subdirectories directly under {@code path} in the given * channel's repository. Returns an empty list for an empty directory. + * + *

Blocks until the server has sent the whole listing, so it must not be called from + * the event thread that delivers it. */ public List list(int channelId, String channelPassword, String path) throws Exception { + String directory = path == null || path.isEmpty() ? "/" : path; SingleCommand cmd = new SingleCommand("ftgetfilelist", ProtocolRole.CLIENT, new CommandSingleParameter("cid", Integer.toString(channelId)), new CommandSingleParameter("cpw", channelPassword == null ? "" : channelPassword), - new CommandSingleParameter("path", path == null || path.isEmpty() ? "/" : path)); + new CommandSingleParameter("path", directory)); - List files = new ArrayList<>(); - Iterable rows; + FileListRequest request = new FileListRequest(channelId, directory); + fileListRequest = request; try { - rows = socket.executeCommand(cmd).get(); + socket.executeCommand(cmd).complete(); + // The command's reply only acknowledges it; the entries are events, and the + // server marks their end with notifyfilelistfinished. + request.finished.get(LIST_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (com.github.manevolent.ts3j.command.CommandException e) { - if (e.getErrorId() == ERROR_DATABASE_EMPTY_RESULT) return files; - throw e; + if (e.getErrorId() != ERROR_DATABASE_EMPTY_RESULT) throw e; + } catch (TimeoutException e) { + throw new IOException("The server did not finish listing " + directory); + } finally { + fileListRequest = null; } - for (SingleCommand row : rows) { - Map m = row.toMap(); - String name = m.get("name"); - if (name == null || name.isEmpty()) continue; - files.add(new RemoteFile( - name, - m.getOrDefault("path", path), - parseLong(m.get("size")), - "0".equals(m.get("type")), - parseLong(m.get("datetime")))); - } - return files; + return new ArrayList<>(request.files); + } + + /** Collects one {@code notifyfilelist} entry for the listing in flight. */ + private void collectListEntry(Map row) { + FileListRequest request = fileListRequest; + if (request == null) return; + String cid = row.get("cid"); + if (cid != null && !cid.isEmpty() && parseLong(cid) != request.channelId) return; + String name = row.get("name"); + if (name == null || name.isEmpty()) return; + request.files.add(new RemoteFile( + name, + row.getOrDefault("path", request.path), + parseLong(row.get("size")), + "0".equals(row.get("type")), + parseLong(row.get("datetime")))); } /** Creates a new directory at {@code dirPath} (a full path such as {@code /new}). */ @@ -351,6 +376,15 @@ public final class FileTransferManager { String command = e.getCommand(); if (command == null) return; Map map = e.getMap(); + if (command.equals("notifyfilelist")) { + collectListEntry(map); + return; + } + if (command.equals("notifyfilelistfinished")) { + FileListRequest request = fileListRequest; + if (request != null) request.finished.complete(null); + return; + } Integer ftfid = tryParseInt(map.get("clientftfid")); if (ftfid == null) return; CompletableFuture> future = pendingInits.get(ftfid); @@ -454,4 +488,17 @@ public final class FileTransferManager { String m = r.getMessage(); return m != null ? m : r.getClass().getSimpleName(); } + + /** One in-flight {@code ftgetfilelist} and the entries received for it so far. */ + private static final class FileListRequest { + final int channelId; + final String path; + final List files = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CompletableFuture finished = new CompletableFuture<>(); + + FileListRequest(int channelId, String path) { + this.channelId = channelId; + this.path = path; + } + } }