From 611fe2bed70775020ea84cf7d2e1d65feb93c1ae Mon Sep 17 00:00:00 2001 From: TrickShotMLG02 Date: Tue, 2 Dec 2025 19:17:55 +0100 Subject: [PATCH 01/11] Added skeleton and config/message values for sftp support --- pom.xml | 5 ++ src/net/server_backup/Configuration.java | 12 +++- src/net/server_backup/Messages.java | 9 +++ .../server_backup/commands/CommandSftp.java | 11 ++++ src/net/server_backup/commands/Executor.java | 8 +++ .../server_backup/commands/TabCompleter.java | 25 +++++++++ src/net/server_backup/utils/SftpManager.java | 56 +++++++++++++++++++ 7 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 src/net/server_backup/commands/CommandSftp.java create mode 100644 src/net/server_backup/utils/SftpManager.java diff --git a/pom.xml b/pom.xml index 985fb9f..b0b6af6 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,11 @@ 3.0.2 compile + + com.hierynomus + sshj + 0.38.0 + diff --git a/src/net/server_backup/Configuration.java b/src/net/server_backup/Configuration.java index e91ed4a..95f9ee3 100644 --- a/src/net/server_backup/Configuration.java +++ b/src/net/server_backup/Configuration.java @@ -60,8 +60,8 @@ public static void loadConfig() { + "\nBackupLimiter - Type '0' to disable this feature. If you don't type '0' the feature 'DeleteOldBackups' will be disabled and this feature ('BackupLimiter') will be enabled." + "\nKeepUniqueBackups - Type 'true' to disable the deletion of unique backups. The plugin will keep the newest backup of all backed up worlds or folders, no matter how old it is." + "\nBlacklist - A list of files/directories that will not be backed up." - + "\nIMPORTANT FTP information: Set 'UploadBackup' to 'true' if you want to store your backups on a ftp server (sftp does not work at the moment - if you host your own server (e.g. vps/root server) you need to set up a ftp server on it)." - + "\nIf you use ftp backups, you can set 'DeleteLocalBackup' to 'true' if you want the plugin to remove the created backup from your server once it has been uploaded to your ftp server." + + "\nSet 'UploadBackup' to 'true' if you want to store your backups on a ftp/sftp server." + + "\nIf you use ftp/sftp backups, you can set 'DeleteLocalBackup' to 'true' if you want the plugin to remove the created backup from your server once it has been uploaded to your ftp/sftp server." + "\nCompressBeforeUpload compresses the backup to a zip file before uploading it. Set it to 'false' if you want the files to be uploaded directly to your ftp server." + "\nJoin the discord server if you need help or have a question: https://discord.gg/rNzngsCWFC"); ServerBackup.getInstance().getConfig().options().copyDefaults(true); @@ -119,6 +119,14 @@ public static void loadConfig() { ServerBackup.getInstance().getConfig().addDefault("Ftp.Server.Password", "password"); ServerBackup.getInstance().getConfig().addDefault("Ftp.Server.BackupDirectory", "Backups/"); + ServerBackup.getInstance().getConfig().addDefault("Sftp.UploadBackup", false); + ServerBackup.getInstance().getConfig().addDefault("Sftp.DeleteLocalBackup", false); + ServerBackup.getInstance().getConfig().addDefault("Sftp.Server.IP", "127.0.0.1"); + ServerBackup.getInstance().getConfig().addDefault("Sftp.Server.Port", 21); + ServerBackup.getInstance().getConfig().addDefault("Sftp.Server.User", "username"); + ServerBackup.getInstance().getConfig().addDefault("Sftp.Server.Password", "password"); + ServerBackup.getInstance().getConfig().addDefault("Sftp.Server.BackupDirectory", "Backups/"); + ServerBackup.getInstance().getConfig().addDefault("DynamicBackup", false); ServerBackup.getInstance().getConfig().addDefault("SendLogMessages", false); diff --git a/src/net/server_backup/Messages.java b/src/net/server_backup/Messages.java index d8d8aa4..e76c48f 100644 --- a/src/net/server_backup/Messages.java +++ b/src/net/server_backup/Messages.java @@ -40,6 +40,10 @@ public static void loadMessages() { messages.addDefault("Info.FtpUploadSuccess", "Ftp: Upload successfully. Backup stored on ftp server."); messages.addDefault("Info.FtpDownload", "Ftp: Downloading backup [%file%] ..."); messages.addDefault("Info.FtpDownloadSuccess", "Ftp: Download successful. Backup downloaded from ftp server."); + messages.addDefault("Info.SftpUpload", "Sftp: Uploading backup [%file%] ..."); + messages.addDefault("Info.SftpUploadSuccess", "Sftp: Upload successfully. Backup stored on sftp server."); + messages.addDefault("Info.SftpDownload", "Sftp: Downloading backup [%file%] ..."); + messages.addDefault("Info.SftpDownloadSuccess", "Sftp: Download successful. Backup downloaded from sftp server."); messages.addDefault("Error.NoPermission", "&cI'm sorry but you do not have permission to perform this command."); messages.addDefault("Error.NoBackups", "No backups found."); @@ -59,6 +63,11 @@ public static void loadMessages() { messages.addDefault("Error.FtpLocalDeletionFailed", "Ftp: Local backup deletion failed because the uploaded file was not found on the ftp server. Try again."); messages.addDefault("Error.FtpNotFound", "Ftp: ftp-backup %file% not found."); messages.addDefault("Error.FtpConnectionFailed", "Ftp: Error while connecting to FTP server."); + messages.addDefault("Error.SftpUploadFailed", "Sftp: Error while uploading backup to sftp server. Check server details in config.yml (ip, port, user, password)."); + messages.addDefault("Error.SftpDownloadFailed", "Sftp: Error while downloading backup to sftp server. Check server details in config.yml (ip, port, user, password)."); + messages.addDefault("Error.SftpLocalDeletionFailed", "Sftp: Local backup deletion failed because the uploaded file was not found on the sftp server. Try again."); + messages.addDefault("Error.SftpNotFound", "Sftp: sftp-backup %file% not found."); + messages.addDefault("Error.SftpConnectionFailed", "Sftp: Error while connecting to SFTP server."); Configuration.saveMessages(); diff --git a/src/net/server_backup/commands/CommandSftp.java b/src/net/server_backup/commands/CommandSftp.java new file mode 100644 index 0000000..0e65b30 --- /dev/null +++ b/src/net/server_backup/commands/CommandSftp.java @@ -0,0 +1,11 @@ +package net.server_backup.commands; + +import org.bukkit.command.CommandSender; + +public class CommandSftp { + + public static void execute(CommandSender sender, String[] args) { + // TODO: Implement + throw new UnsupportedOperationException("Not implemented yet"); + } +} diff --git a/src/net/server_backup/commands/Executor.java b/src/net/server_backup/commands/Executor.java index 4c873d4..2f2b6fd 100644 --- a/src/net/server_backup/commands/Executor.java +++ b/src/net/server_backup/commands/Executor.java @@ -40,6 +40,10 @@ public boolean onCommand(CommandSender sender, Command command, String label, St if (args[1].equalsIgnoreCase("list")) { Bukkit.dispatchCommand(sender, "backup ftp list 1"); } + } else if (args[0].equalsIgnoreCase("sftp")) { + if (args[1].equalsIgnoreCase("list")) { + Bukkit.dispatchCommand(sender, "backup sftp list 1"); + } } else if (args[0].equalsIgnoreCase("zip")) { CommandZip.execute(sender, args); } else if (args[0].equalsIgnoreCase("unzip")) { @@ -54,6 +58,8 @@ public boolean onCommand(CommandSender sender, Command command, String label, St CommandSearch.execute(sender, args); } else if (args[0].equalsIgnoreCase("ftp")) { CommandFtp.execute(sender, args); + } else if (args[0].equalsIgnoreCase("sftp")) { + CommandSftp.execute(sender, args); } else if (args[0].equalsIgnoreCase("dropbox")) { CommandDropbox.execute(sender, args); } @@ -85,6 +91,8 @@ private void sendHelp(CommandSender sender) { sender.sendMessage(""); sender.sendMessage("/backup ftp - download, upload or list ftp backup files"); sender.sendMessage(""); + sender.sendMessage("/backup sftp - download, upload or list sftp backup files"); + sender.sendMessage(""); sender.sendMessage("/backup dropbox upload - upload a backup to dropbox"); sender.sendMessage(""); sender.sendMessage("/backup shutdown - shut downs the server after backup tasks are finished"); diff --git a/src/net/server_backup/commands/TabCompleter.java b/src/net/server_backup/commands/TabCompleter.java index c6f884b..98386a5 100644 --- a/src/net/server_backup/commands/TabCompleter.java +++ b/src/net/server_backup/commands/TabCompleter.java @@ -2,6 +2,7 @@ import net.server_backup.Configuration; import net.server_backup.utils.FtpManager; +import net.server_backup.utils.SftpManager; import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.command.Command; @@ -30,6 +31,7 @@ public List onTabComplete(CommandSender sender, Command cmd, String labe commands.add("zip"); commands.add("unzip"); commands.add("ftp"); + commands.add("sftp"); commands.add("dropbox"); commands.add("tasks"); commands.add("shutdown"); @@ -82,6 +84,10 @@ public List onTabComplete(CommandSender sender, Command cmd, String labe commands.add("list"); commands.add("download"); commands.add("upload"); + } else if (args[0].equalsIgnoreCase("sftp")) { + commands.add("list"); + commands.add("download"); + commands.add("upload"); } else if (args[0].equalsIgnoreCase("dropbox")) { commands.add("upload"); } @@ -107,6 +113,25 @@ public List onTabComplete(CommandSender sender, Command cmd, String labe } } + } else if (args[0].equalsIgnoreCase("sftp")) { + if (args[1].equalsIgnoreCase("download")) { + SftpManager sftpm = new SftpManager(sender); + + List backups = sftpm.getSftpBackupList(false); + + for (String backup : backups) { + commands.add(backup.split(" ")[1]); + } + } else if (args[1].equalsIgnoreCase("upload")) { + File[] backups = new File(Configuration.backupDestination + "").listFiles(); + + for (File backup : backups) { + if (backup.getName().endsWith(".zip")) { + commands.add(backup.getName()); + } + } + } + } else if (args[0].equalsIgnoreCase("dropbox")) { File[] backups = new File(Configuration.backupDestination + "").listFiles(); diff --git a/src/net/server_backup/utils/SftpManager.java b/src/net/server_backup/utils/SftpManager.java new file mode 100644 index 0000000..295522d --- /dev/null +++ b/src/net/server_backup/utils/SftpManager.java @@ -0,0 +1,56 @@ +package net.server_backup.utils; + +import net.schmizz.sshj.SSHClient; +import net.server_backup.ServerBackup; +import org.bukkit.command.CommandSender; + +import java.io.IOException; +import java.util.List; + +public class SftpManager { + + private CommandSender sender; + + private static final String server = ServerBackup.getInstance().getConfig().getString("Sftp.Server.IP"); + private static final int port = ServerBackup.getInstance().getConfig().getInt("Sftp.Server.Port"); + private static final String user = ServerBackup.getInstance().getConfig().getString("Sftp.Server.User"); + private static final String pass = ServerBackup.getInstance().getConfig().getString("Sftp.Server.Password"); + + public SftpManager(CommandSender sender) { + this.sender = sender; + } + + boolean isSSL = true; + + ServerBackup serverBackup = ServerBackup.getInstance(); + + public void uploadFileToStfp(String filePath, boolean direct) { + // TODO: Implement + throw new UnsupportedOperationException("Not implemented yet"); + } + + public void downloadFileFromStfp(String filePath) { + // TODO: Implement + throw new UnsupportedOperationException("Not implemented yet"); + } + + public void deleteFile(String filePath) { + // TODO: Implement + throw new UnsupportedOperationException("Not implemented yet"); + } + + public List getSftpBackupList(boolean rawList) { + // TODO: Implement + throw new UnsupportedOperationException("Not implemented yet"); + } + + private void connect(SSHClient client) throws IOException { + // TODO: Implement + throw new UnsupportedOperationException("Not implemented yet"); + } + + private void disconnect(SSHClient client) throws IOException { + // TODO: Implement + throw new UnsupportedOperationException("Not implemented yet"); + } +} From efa45585ff3939d9915d931d8753c57e8d1dbace Mon Sep 17 00:00:00 2001 From: TrickShotMLG02 Date: Tue, 2 Dec 2025 19:42:43 +0100 Subject: [PATCH 02/11] fixed typo in methods --- src/net/server_backup/utils/SftpManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/net/server_backup/utils/SftpManager.java b/src/net/server_backup/utils/SftpManager.java index 295522d..1794f22 100644 --- a/src/net/server_backup/utils/SftpManager.java +++ b/src/net/server_backup/utils/SftpManager.java @@ -24,12 +24,12 @@ public SftpManager(CommandSender sender) { ServerBackup serverBackup = ServerBackup.getInstance(); - public void uploadFileToStfp(String filePath, boolean direct) { + public void uploadFileToSftp(String filePath, boolean direct) { // TODO: Implement throw new UnsupportedOperationException("Not implemented yet"); } - public void downloadFileFromStfp(String filePath) { + public void downloadFileFromSftp(String filePath) { // TODO: Implement throw new UnsupportedOperationException("Not implemented yet"); } From ddac5a58ae1a6bb17cbac2af400ee9714c2cfa1c Mon Sep 17 00:00:00 2001 From: TrickShotMLG02 Date: Tue, 2 Dec 2025 19:43:56 +0100 Subject: [PATCH 03/11] implemented CommandSftp --- .../server_backup/commands/CommandSftp.java | 84 ++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/src/net/server_backup/commands/CommandSftp.java b/src/net/server_backup/commands/CommandSftp.java index 0e65b30..8bc549f 100644 --- a/src/net/server_backup/commands/CommandSftp.java +++ b/src/net/server_backup/commands/CommandSftp.java @@ -1,11 +1,91 @@ package net.server_backup.commands; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.server_backup.ServerBackup; +import net.server_backup.core.OperationHandler; +import net.server_backup.utils.SftpManager; +import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.List; public class CommandSftp { public static void execute(CommandSender sender, String[] args) { - // TODO: Implement - throw new UnsupportedOperationException("Not implemented yet"); + if (args[1].equalsIgnoreCase("list")) { + Bukkit.getScheduler().runTaskAsynchronously(ServerBackup.getInstance(), () -> { + SftpManager sftpm = new SftpManager(sender); + + List backups = sftpm.getSftpBackupList(false); + + if (backups.size() == 0) { + sender.sendMessage(OperationHandler.processMessage("Error.NoSftpBackups")); + + return; + } + + try { + int page = Integer.valueOf(args[2]); + + if (backups.size() < page * 10 - 9) { + sender.sendMessage("Try a lower value."); + + return; + } + + if (backups.size() <= page * 10 && backups.size() >= page * 10 - 10) { + sender.sendMessage("----- Sftp-Backup " + Integer.valueOf(page * 10 - 9) + "-" + + backups.size() + "/" + backups.size() + " -----"); + } else { + sender.sendMessage("----- Sftp-Backup " + Integer.valueOf(page * 10 - 9) + "-" + + Integer.valueOf(page * 10) + "/" + backups.size() + " -----"); + } + sender.sendMessage(""); + + for (int i = page * 10 - 10; i < backups.size() && i < page * 10; i++) { + if (sender instanceof Player) { + Player p = (Player) sender; + + TextComponent msg = new TextComponent(backups.get(i)); + msg.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + new ComponentBuilder("Click to get Backup name").create())); + msg.setClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, + backups.get(i).split(" ")[1])); + + p.spigot().sendMessage(msg); + } else { + sender.sendMessage(backups.get(i)); + } + } + + int maxPages = backups.size() / 10; + + if (backups.size() % 10 != 0) { + maxPages++; + } + + sender.sendMessage(""); + sender.sendMessage("--------- Page " + page + "/" + maxPages + " ---------"); + } catch (Exception e) { + sender.sendMessage(OperationHandler.processMessage("Error.NotANumber").replaceAll("%input%", args[1])); + } + }); + } else if (args[1].equalsIgnoreCase("download")) { + Bukkit.getScheduler().runTaskAsynchronously(ServerBackup.getInstance(), () -> { + SftpManager sftpm = new SftpManager(sender); + + sftpm.downloadFileFromSftp(args[2]); + }); + } else if (args[1].equalsIgnoreCase("upload")) { + Bukkit.getScheduler().runTaskAsynchronously(ServerBackup.getInstance(), () -> { + SftpManager sftpm = new SftpManager(sender); + + sftpm.uploadFileToSftp(args[2], !ServerBackup.getInstance().getConfig().getBoolean("Sftp.CompressBeforeUpload")); + }); + } } } From 533caef605c94cbead6d025e455a8fd807944968 Mon Sep 17 00:00:00 2001 From: TrickShotMLG02 Date: Tue, 2 Dec 2025 19:49:02 +0100 Subject: [PATCH 04/11] changed function signature for connect and disconnect --- src/net/server_backup/utils/SftpManager.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/net/server_backup/utils/SftpManager.java b/src/net/server_backup/utils/SftpManager.java index 1794f22..d091b79 100644 --- a/src/net/server_backup/utils/SftpManager.java +++ b/src/net/server_backup/utils/SftpManager.java @@ -1,6 +1,7 @@ package net.server_backup.utils; import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.sftp.SFTPClient; import net.server_backup.ServerBackup; import org.bukkit.command.CommandSender; @@ -44,12 +45,12 @@ public List getSftpBackupList(boolean rawList) { throw new UnsupportedOperationException("Not implemented yet"); } - private void connect(SSHClient client) throws IOException { + private SFTPClient connect() throws IOException { // TODO: Implement throw new UnsupportedOperationException("Not implemented yet"); } - private void disconnect(SSHClient client) throws IOException { + private void disconnect(SFTPClient client) throws IOException { // TODO: Implement throw new UnsupportedOperationException("Not implemented yet"); } From 5f5366660cbd016ba68d080072f2771ac4f44270 Mon Sep 17 00:00:00 2001 From: TrickShotMLG02 Date: Tue, 2 Dec 2025 20:11:29 +0100 Subject: [PATCH 05/11] added sftp server fingerprint to config, implemented connect, disconnect for SftpManager --- src/net/server_backup/Configuration.java | 1 + src/net/server_backup/utils/SftpManager.java | 73 ++++++++++++++++++-- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/net/server_backup/Configuration.java b/src/net/server_backup/Configuration.java index 95f9ee3..f5da41e 100644 --- a/src/net/server_backup/Configuration.java +++ b/src/net/server_backup/Configuration.java @@ -125,6 +125,7 @@ public static void loadConfig() { ServerBackup.getInstance().getConfig().addDefault("Sftp.Server.Port", 21); ServerBackup.getInstance().getConfig().addDefault("Sftp.Server.User", "username"); ServerBackup.getInstance().getConfig().addDefault("Sftp.Server.Password", "password"); + ServerBackup.getInstance().getConfig().addDefault("Sftp.Server.Fingerprint", "SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); ServerBackup.getInstance().getConfig().addDefault("Sftp.Server.BackupDirectory", "Backups/"); ServerBackup.getInstance().getConfig().addDefault("DynamicBackup", false); diff --git a/src/net/server_backup/utils/SftpManager.java b/src/net/server_backup/utils/SftpManager.java index d091b79..8f34452 100644 --- a/src/net/server_backup/utils/SftpManager.java +++ b/src/net/server_backup/utils/SftpManager.java @@ -1,11 +1,14 @@ package net.server_backup.utils; import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.common.SecurityUtils; import net.schmizz.sshj.sftp.SFTPClient; +import net.schmizz.sshj.transport.verification.HostKeyVerifier; import net.server_backup.ServerBackup; import org.bukkit.command.CommandSender; import java.io.IOException; +import java.security.PublicKey; import java.util.List; public class SftpManager { @@ -16,6 +19,7 @@ public class SftpManager { private static final int port = ServerBackup.getInstance().getConfig().getInt("Sftp.Server.Port"); private static final String user = ServerBackup.getInstance().getConfig().getString("Sftp.Server.User"); private static final String pass = ServerBackup.getInstance().getConfig().getString("Sftp.Server.Password"); + private static final String fingerprint = ServerBackup.getInstance().getConfig().getString("Sftp.Server.Fingerprint"); public SftpManager(CommandSender sender) { this.sender = sender; @@ -45,13 +49,70 @@ public List getSftpBackupList(boolean rawList) { throw new UnsupportedOperationException("Not implemented yet"); } - private SFTPClient connect() throws IOException { - // TODO: Implement - throw new UnsupportedOperationException("Not implemented yet"); + /** + * Connects to the SFTP server using password authentication and returns an SFTPClient. + * + *

This method establishes an SSH connection to the configured server, authenticates + * with the provided username and password, and verifies the host key using a + * fingerprint from the configuration.

+ * + *

Note: The returned SFTPClient does not maintain a current working directory. + * All file operations, such as upload or download, must use the full remote path, + * including the target directory. For example: + *

+     * sftp.put(localFilePath, remoteDir + "/" + remoteFileName);
+     * 
+ *

+ * + * @param sshClient The SSHClient used to connect to SFTP server + * @return an active SFTPClient connected to the remote server + * @throws IOException if the connection, authentication, or host key verification fails + */ + private SFTPClient connect(SSHClient sshClient) throws IOException { + + // Create verifier for host key from config + HostKeyVerifier verifier = new HostKeyVerifier() { + @Override + public boolean verify(String hostname, int port, PublicKey key) { + String actualFingerprint = SecurityUtils.getFingerprint(key); + return actualFingerprint.equals(fingerprint); + } + + @Override + public List findExistingAlgorithms(String s, int i) { + return List.of(); + } + }; + + sshClient.addHostKeyVerifier(verifier); + + // establish connection + sshClient.connect(server, port); + sshClient.authPassword(user, pass); + + return sshClient.newSFTPClient(); } - private void disconnect(SFTPClient client) throws IOException { - // TODO: Implement - throw new UnsupportedOperationException("Not implemented yet"); + /** + * Closes the given SFTPClient and disconnects the associated SSHClient. + * + *

This method ensures that both the SFTP session and the underlying SSH + * connection are properly terminated to avoid resource leaks.

+ * + * @param sftpClient the active SFTPClient to close; may be null + * @param sshClient the SSHClient associated with the SFTPClient; may be null + * @throws IOException if an error occurs while closing the SFTPClient or disconnecting the SSHClient + */ + private void disconnect(SFTPClient sftpClient, SSHClient sshClient) throws IOException { + + // close sftp client + if (sftpClient != null) { + sftpClient.close(); + } + + // disconnect ssh client + if (sshClient != null && sshClient.isConnected()) { + sshClient.disconnect(); + } } } From c38d9751a78bc3d76cd31e47cfa9f1619ac67f76 Mon Sep 17 00:00:00 2001 From: TrickShotMLG02 Date: Tue, 2 Dec 2025 20:28:12 +0100 Subject: [PATCH 06/11] Implemented getSftpBackupList --- src/net/server_backup/utils/SftpManager.java | 44 +++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/net/server_backup/utils/SftpManager.java b/src/net/server_backup/utils/SftpManager.java index 8f34452..20c3959 100644 --- a/src/net/server_backup/utils/SftpManager.java +++ b/src/net/server_backup/utils/SftpManager.java @@ -2,6 +2,8 @@ import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.common.SecurityUtils; +import net.schmizz.sshj.sftp.RemoteResourceFilter; +import net.schmizz.sshj.sftp.RemoteResourceInfo; import net.schmizz.sshj.sftp.SFTPClient; import net.schmizz.sshj.transport.verification.HostKeyVerifier; import net.server_backup.ServerBackup; @@ -9,6 +11,7 @@ import java.io.IOException; import java.security.PublicKey; +import java.util.ArrayList; import java.util.List; public class SftpManager { @@ -20,6 +23,7 @@ public class SftpManager { private static final String user = ServerBackup.getInstance().getConfig().getString("Sftp.Server.User"); private static final String pass = ServerBackup.getInstance().getConfig().getString("Sftp.Server.Password"); private static final String fingerprint = ServerBackup.getInstance().getConfig().getString("Sftp.Server.Fingerprint"); + private static final String working_dir = ServerBackup.getInstance().getConfig().getString("Ftp.Server.BackupDirectory"); public SftpManager(CommandSender sender) { this.sender = sender; @@ -45,8 +49,44 @@ public void deleteFile(String filePath) { } public List getSftpBackupList(boolean rawList) { - // TODO: Implement - throw new UnsupportedOperationException("Not implemented yet"); + + List backups = new ArrayList<>(); + + SSHClient sshClient = new SSHClient(); + SFTPClient sftpClient = null; + + try { + sftpClient = connect(sshClient); + + List files = sftpClient.ls(working_dir, RemoteResourceInfo::isRegularFile); + + int c = 1; + + for (RemoteResourceInfo file : files) { + double fileSize = (double) file.getAttributes().getSize() / 1000 / 1000; + fileSize = Math.round(fileSize * 100.0) / 100.0; + + if (rawList) { + backups.add(file.getPath() + ":" + fileSize); + } else { + backups.add("§7[" + c + "]§f " + file.getName() + " §7[" + fileSize + "MB]"); + } + + c++; + } + } catch (IOException e) { + // TODO: Handle exception here + e.printStackTrace(); + } finally { + try { + disconnect(sftpClient, sshClient); + } catch (IOException e) { + // TODO: Handle exception here + e.printStackTrace(); + } + } + return backups; + } /** From f9c2a58b939bd2cf1a9a673596182b9c9e05670c Mon Sep 17 00:00:00 2001 From: TrickShotMLG02 Date: Tue, 2 Dec 2025 23:21:35 +0100 Subject: [PATCH 07/11] Implemented SftpManager --- src/net/server_backup/utils/SftpManager.java | 211 ++++++++++++++++++- 1 file changed, 201 insertions(+), 10 deletions(-) diff --git a/src/net/server_backup/utils/SftpManager.java b/src/net/server_backup/utils/SftpManager.java index 20c3959..e5cdaa8 100644 --- a/src/net/server_backup/utils/SftpManager.java +++ b/src/net/server_backup/utils/SftpManager.java @@ -2,14 +2,17 @@ import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.common.SecurityUtils; -import net.schmizz.sshj.sftp.RemoteResourceFilter; import net.schmizz.sshj.sftp.RemoteResourceInfo; import net.schmizz.sshj.sftp.SFTPClient; import net.schmizz.sshj.transport.verification.HostKeyVerifier; +import net.schmizz.sshj.xfer.FileSystemFile; +import net.server_backup.Configuration; import net.server_backup.ServerBackup; +import net.server_backup.core.OperationHandler; import org.bukkit.command.CommandSender; -import java.io.IOException; +import java.io.*; +import java.nio.file.Paths; import java.security.PublicKey; import java.util.ArrayList; import java.util.List; @@ -29,25 +32,213 @@ public SftpManager(CommandSender sender) { this.sender = sender; } - boolean isSSL = true; + ServerBackup backup = ServerBackup.getInstance(); - ServerBackup serverBackup = ServerBackup.getInstance(); + /*** + * Creates the Remote Path including Working Directory and a relative Path + * @param relativePath The relative path starting from the working directory + * @return The full path from the root of the sftp session + */ + private String getRemotePath(String relativePath) { + // convert \\ to / since sftp expects only / + return Paths.get(working_dir, relativePath).toString().replace('\\', '/'); + } + /** + * Uploads a local file to the configured SFTP server. + *

+ * Resolves the given {@code filePath}, connects to the SFTP server, + * uploads the file to the remote path, and optionally deletes the local file + * if configured. Sends messages to {@code sender} about progress and success/failure. + *

+ * + * @param filePath the path of the local file to upload + * @param direct currently unused flag indicating direct upload + */ public void uploadFileToSftp(String filePath, boolean direct) { - // TODO: Implement - throw new UnsupportedOperationException("Not implemented yet"); + File file = new File(filePath); + + if (!file.getPath().contains(Configuration.backupDestination.replaceAll("/", ""))) { + file = new File(Configuration.backupDestination + "//" + filePath); + filePath = file.getPath(); + } + + if (!file.exists()) { + sender.sendMessage(OperationHandler.processMessage("Error.NoBackupFound").replaceAll("%file%", file.getName())); + + return; + } + + SSHClient sshClient = new SSHClient(); + SFTPClient sftpClient = null; + + try { + sftpClient = connect(sshClient); + + sender.sendMessage(OperationHandler.processMessage("Info.SftpUpload").replaceAll("%file%", file.getName())); + OperationHandler.tasks.add("SFTP UPLOAD {" + filePath + "}"); + + + try { + FileSystemFile localFile = new FileSystemFile(file); + sftpClient.put(localFile, getRemotePath(filePath)); + sender.sendMessage(OperationHandler.processMessage("Info.SftpUploadSuccess")); + + if (ServerBackup.getInstance().getConfig().getBoolean("Ftp.DeleteLocalBackup")) { + boolean exists = false; + for (RemoteResourceInfo backup : sftpClient.ls(working_dir, RemoteResourceInfo::isRegularFile)) { + if (backup.getName().equalsIgnoreCase(file.getName())) { + exists = true; + } + } + + if (exists) { + file.delete(); + } else { + sender.sendMessage(OperationHandler.processMessage("Error.SftpLocalDeletionFailed")); + } + } + } catch (IOException e) { + sender.sendMessage(OperationHandler.processMessage("Error.SftpUploadFailed")); + e.printStackTrace(); + } + + } catch (IOException e) { + sender.sendMessage(OperationHandler.processMessage("Error.SftpUploadFailed")); + e.printStackTrace(); + } finally { + try { + disconnect(sftpClient, sshClient); + } catch (IOException e) { + // TODO: Handle exception here + e.printStackTrace(); + } + } } + /** + * Downloads a file from the configured SFTP server to the local backup destination. + *

+ * Checks if the file exists on the remote server, downloads it if found, + * and sends progress and status messages to {@code sender}. Handles + * connection and I/O exceptions and ensures the SFTP client is disconnected. + *

+ * + * @param filePath the path of the file to download from the remote server + */ public void downloadFileFromSftp(String filePath) { - // TODO: Implement - throw new UnsupportedOperationException("Not implemented yet"); + File file = new File(filePath); + + SSHClient sshClient = new SSHClient(); + SFTPClient sftpClient = null; + + try { + sftpClient = connect(sshClient); + + boolean exists = false; + + for (RemoteResourceInfo backup : sftpClient.ls(working_dir, RemoteResourceInfo::isRegularFile)) { + if (backup.getName().equalsIgnoreCase(file.getName())) { + exists = true; + } + } + + if (!exists) { + sender.sendMessage(OperationHandler.processMessage("Error.SftpNotFound").replaceAll("%file%", file.getName())); + + return; + } + + sender.sendMessage(OperationHandler.processMessage("Info.SftpDownload").replaceAll("%file%", file.getName())); + + File dFile = new File(Configuration.backupDestination + "//" + file.getPath()); + + try { + sftpClient.get(getRemotePath(filePath), dFile.getAbsolutePath()); + sender.sendMessage(OperationHandler.processMessage("Info.SftpDownloadSuccess")); + } catch (IOException e) { + sender.sendMessage(OperationHandler.processMessage("Error.SftpDownloadFailed")); + e.printStackTrace(); + } + + } catch (IOException e) { + sender.sendMessage(OperationHandler.processMessage("Error.SftpDownloadFailed")); + e.printStackTrace(); + } finally { + try { + disconnect(sftpClient, sshClient); + } catch (IOException e) { + // TODO: Handle exception here + e.printStackTrace(); + } + } } + /** + * Deletes a file from the configured SFTP server. + *

+ * Checks if the specified file exists on the remote server, deletes it if found, + * and sends progress and status messages to {@code sender}. Handles connection + * and I/O exceptions and ensures the SFTP client is disconnected. + *

+ * + * @param filePath the path of the file to delete on the remote server + */ public void deleteFile(String filePath) { - // TODO: Implement - throw new UnsupportedOperationException("Not implemented yet"); + File file = new File(filePath); + + SSHClient sshClient = new SSHClient(); + SFTPClient sftpClient = null; + + try { + sftpClient = connect(sshClient); + + boolean exists = false; + + for (RemoteResourceInfo backup : sftpClient.ls(working_dir, RemoteResourceInfo::isRegularFile)) { + if (backup.getName().equalsIgnoreCase(file.getName())) { + exists = true; + } + } + + if (!exists) { + sender.sendMessage(OperationHandler.processMessage("Error.SftpNotFound").replaceAll("%file%", file.getName())); + + return; + } + + sender.sendMessage(OperationHandler.processMessage("Info.FtpDeletion").replaceAll("%file%", file.getName())); + + try { + sftpClient.rm(getRemotePath(filePath)); + sender.sendMessage(OperationHandler.processMessage("Info.SftpDeletionSuccess")); + } catch (IOException e) { + sender.sendMessage(OperationHandler.processMessage("Error.SftpDeletionFailed")); + } + } catch (IOException e) { + sender.sendMessage(OperationHandler.processMessage("Error.SftpDeletionFailed")); + e.printStackTrace(); + } finally { + try { + disconnect(sftpClient, sshClient); + } catch (IOException e) { + // TODO: Handle exception here + e.printStackTrace(); + } + } } + /** + * Retrieves a list of backup files from the configured SFTP server. + *

+ * Lists all regular files in the remote working directory and returns + * them either in a raw format (path:size in MB) or a formatted display + * string with an index and file size. + *

+ * + * @param rawList if true, returns a raw "path:size" list; if false, returns a formatted list + * @return a list of backup file strings from the SFTP server + */ public List getSftpBackupList(boolean rawList) { List backups = new ArrayList<>(); From 0da6336dbef9aaf48aebaec25e3e7ec485cce094 Mon Sep 17 00:00:00 2001 From: TrickShotMLG <43967908+TrickShotMLG02@users.noreply.github.com> Date: Wed, 3 Dec 2025 02:33:14 +0100 Subject: [PATCH 08/11] Fixed sftp backup uploading --- src/net/server_backup/utils/SftpManager.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/net/server_backup/utils/SftpManager.java b/src/net/server_backup/utils/SftpManager.java index e5cdaa8..24b976f 100644 --- a/src/net/server_backup/utils/SftpManager.java +++ b/src/net/server_backup/utils/SftpManager.java @@ -1,7 +1,6 @@ package net.server_backup.utils; import net.schmizz.sshj.SSHClient; -import net.schmizz.sshj.common.SecurityUtils; import net.schmizz.sshj.sftp.RemoteResourceInfo; import net.schmizz.sshj.sftp.SFTPClient; import net.schmizz.sshj.transport.verification.HostKeyVerifier; @@ -26,7 +25,7 @@ public class SftpManager { private static final String user = ServerBackup.getInstance().getConfig().getString("Sftp.Server.User"); private static final String pass = ServerBackup.getInstance().getConfig().getString("Sftp.Server.Password"); private static final String fingerprint = ServerBackup.getInstance().getConfig().getString("Sftp.Server.Fingerprint"); - private static final String working_dir = ServerBackup.getInstance().getConfig().getString("Ftp.Server.BackupDirectory"); + private static final String working_dir = ServerBackup.getInstance().getConfig().getString("Sftp.Server.BackupDirectory"); public SftpManager(CommandSender sender) { this.sender = sender; @@ -81,7 +80,7 @@ public void uploadFileToSftp(String filePath, boolean direct) { try { FileSystemFile localFile = new FileSystemFile(file); - sftpClient.put(localFile, getRemotePath(filePath)); + sftpClient.put(localFile, getRemotePath(file.getName())); sender.sendMessage(OperationHandler.processMessage("Info.SftpUploadSuccess")); if (ServerBackup.getInstance().getConfig().getBoolean("Ftp.DeleteLocalBackup")) { @@ -305,8 +304,14 @@ private SFTPClient connect(SSHClient sshClient) throws IOException { HostKeyVerifier verifier = new HostKeyVerifier() { @Override public boolean verify(String hostname, int port, PublicKey key) { + return true; + + // TODO: FIND A SOLUTION + + /* String actualFingerprint = SecurityUtils.getFingerprint(key); return actualFingerprint.equals(fingerprint); + */ } @Override From 1747bbd7dd7024b55439beaf69087dbc15d3d019 Mon Sep 17 00:00:00 2001 From: TrickShotMLG <43967908+TrickShotMLG02@users.noreply.github.com> Date: Wed, 3 Dec 2025 03:05:07 +0100 Subject: [PATCH 09/11] Refine TODO comment for SftpManager --- src/net/server_backup/utils/SftpManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/net/server_backup/utils/SftpManager.java b/src/net/server_backup/utils/SftpManager.java index 24b976f..8211d80 100644 --- a/src/net/server_backup/utils/SftpManager.java +++ b/src/net/server_backup/utils/SftpManager.java @@ -306,7 +306,7 @@ private SFTPClient connect(SSHClient sshClient) throws IOException { public boolean verify(String hostname, int port, PublicKey key) { return true; - // TODO: FIND A SOLUTION + // TODO: FIND A SOLUTION TO CHECK FINGERPRINTS, OR A WAY TO LOAD "BouncyCastle" /* String actualFingerprint = SecurityUtils.getFingerprint(key); From 92abbfa844fa9cb7dbe8d576ede8dc18da8ea567 Mon Sep 17 00:00:00 2001 From: TrickShotMLG02 Date: Wed, 3 Dec 2025 18:52:19 +0100 Subject: [PATCH 10/11] added missing message and uploading backups to sftp if enabled in config --- src/net/server_backup/Messages.java | 1 + src/net/server_backup/core/ZipManager.java | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/net/server_backup/Messages.java b/src/net/server_backup/Messages.java index e76c48f..57c859a 100644 --- a/src/net/server_backup/Messages.java +++ b/src/net/server_backup/Messages.java @@ -53,6 +53,7 @@ public static void loadMessages() { messages.addDefault("Error.FolderExists", "There is already a folder named '%file%'."); messages.addDefault("Error.ZipExists", "There is already a ZIP file named '%file%.zip'."); messages.addDefault("Error.NoFtpBackups", "No ftp backups found."); + messages.addDefault("Error.NoSftpBackups", "No sftp backups found."); messages.addDefault("Error.NoTasks", "No backup tasks are running."); messages.addDefault("Error.AlreadyZip", "%file% is already a ZIP file."); messages.addDefault("Error.NotAZip", "%file% is not a ZIP file."); diff --git a/src/net/server_backup/core/ZipManager.java b/src/net/server_backup/core/ZipManager.java index f0f3005..b3f30fc 100644 --- a/src/net/server_backup/core/ZipManager.java +++ b/src/net/server_backup/core/ZipManager.java @@ -4,6 +4,7 @@ import net.server_backup.ServerBackup; import net.server_backup.utils.DropboxManager; import net.server_backup.utils.FtpManager; +import net.server_backup.utils.SftpManager; import org.apache.commons.io.FileUtils; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; @@ -179,6 +180,11 @@ public void zip() throws IOException { ftpm.uploadFileToFtp(targetFilePath, false); } + if (ServerBackup.getInstance().getConfig().getBoolean("Sftp.UploadBackup")) { + SftpManager sftpm = new SftpManager(sender); + sftpm.uploadFileToSftp(targetFilePath, false); + } + if (ServerBackup.getInstance().getConfig().getBoolean("CloudBackup.Dropbox")) { DropboxManager dm = new DropboxManager(sender); dm.uploadToDropbox(targetFilePath); From ed71600ba2b46b48dd3000de62e55e9b185c00ff Mon Sep 17 00:00:00 2001 From: TrickShotMLG02 Date: Wed, 3 Dec 2025 18:58:13 +0100 Subject: [PATCH 11/11] Fixed wrong config key for local backup deletion after sftp upload --- src/net/server_backup/utils/SftpManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/net/server_backup/utils/SftpManager.java b/src/net/server_backup/utils/SftpManager.java index 8211d80..82f8ed3 100644 --- a/src/net/server_backup/utils/SftpManager.java +++ b/src/net/server_backup/utils/SftpManager.java @@ -83,7 +83,7 @@ public void uploadFileToSftp(String filePath, boolean direct) { sftpClient.put(localFile, getRemotePath(file.getName())); sender.sendMessage(OperationHandler.processMessage("Info.SftpUploadSuccess")); - if (ServerBackup.getInstance().getConfig().getBoolean("Ftp.DeleteLocalBackup")) { + if (ServerBackup.getInstance().getConfig().getBoolean("Sftp.DeleteLocalBackup")) { boolean exists = false; for (RemoteResourceInfo backup : sftpClient.ls(working_dir, RemoteResourceInfo::isRegularFile)) { if (backup.getName().equalsIgnoreCase(file.getName())) {