From 8f42a83f15a6c478c91d1cedf12399941b978133 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Fri, 15 Aug 2025 18:24:00 +0000 Subject: [PATCH] Dateien nach "src/main/java/zombie_striker/sr" hochladen --- src/main/java/zombie_striker/sr/Main.java | 731 ++++++++++++++++++ src/main/java/zombie_striker/sr/Updater.java | 756 +++++++++++++++++++ 2 files changed, 1487 insertions(+) create mode 100644 src/main/java/zombie_striker/sr/Main.java create mode 100644 src/main/java/zombie_striker/sr/Updater.java diff --git a/src/main/java/zombie_striker/sr/Main.java b/src/main/java/zombie_striker/sr/Main.java new file mode 100644 index 0000000..25bdd37 --- /dev/null +++ b/src/main/java/zombie_striker/sr/Main.java @@ -0,0 +1,731 @@ +package me.zombie_striker.sr; + +import com.jcraft.jsch.Channel; +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.Session; +import org.apache.commons.net.ftp.FTP; +import org.apache.commons.net.ftp.FTPClient; +import org.apache.commons.net.ftp.FTPSClient; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Chunk; +import org.bukkit.World; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; + +import java.io.*; +import java.net.SocketException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.zip.Deflater; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +public class Main extends JavaPlugin { + + private static List exceptions = new ArrayList(); + private static String prefix = "&6[&3ServerRestorer-Reborn&6]&8"; + private static String kickmessage = " Server wird auf den vorherigen Speicherstand zurückgesetzt. Bitte trete in wenigen Sekunden erneut bei."; + BukkitTask br = null; + private boolean saveTheConfig = false; + private long lastSave = 0; + private long timedist = 0; + private File master = null; + private File backups = null; + private boolean saveServerJar = false; + private boolean savePluiginJars = false; + private boolean currentlySaving = false; + private boolean automate = true; + private boolean useFTP = false; + private boolean useFTPS = false; + private boolean useSFTP = false; + private String serverFTP = "www.example.com"; + private String userFTP = "User"; + private String passwordFTP = "password"; + private int portFTP = 80; + private String naming_format = "Backup-%date%"; + private SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); + private String removeFilePath = ""; + private long maxSaveSize = -1; + private int maxSaveFiles = 1000; + private boolean deleteZipOnFail = false; + private boolean deleteZipOnFTP = false; + + private int hourToSaveAt = -1; + + private String separator = File.separator; + + + + private int compression = Deflater.BEST_COMPRESSION; + + public static File newFile(File destinationDir, ZipEntry zipEntry) throws IOException { + File destFile = new File(destinationDir, zipEntry.getName()); + + String destDirPath = destinationDir.getCanonicalPath(); + String destFilePath = destFile.getCanonicalPath(); + + if (!destFilePath.startsWith(destDirPath + File.separator)) { + throw new IOException("Entry is outside of the target dir: " + zipEntry.getName()); + } + + return destFile; + } + + private static boolean isExempt(String path) { + path = path.toLowerCase().trim(); + for (String s : exceptions) + if (path.endsWith(s.toLowerCase().trim())) + return true; + return false; + } + + public static String humanReadableByteCount(long bytes, boolean si) { + int unit = si ? 1000 : 1024; + if (bytes < unit) + return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(unit)); + String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); + return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); + } + + public static long folderSize(File directory) { + long length = 0; + if(directory==null)return -1; + + for (File file : directory.listFiles()) { + if (file.isFile()) + length += file.length(); + else + length += folderSize(file); + } + return length; + } + + public static File firstFileModified(File dir) { + File fl = dir; + File[] files = fl.listFiles(new FileFilter() { + public boolean accept(File file) { + return file.isFile(); + } + }); + long lastMod = Long.MAX_VALUE; + File choice = null; + for (File file : files) { + if (file.lastModified() < lastMod) { + choice = file; + lastMod = file.lastModified(); + } + } + return choice; + } + + public File getMasterFolder() { + return master; + } + + public File getBackupFolder() { + return backups; + } + + public long a(String path, long def) { + if (getConfig().contains(path)) + return getConfig().getLong(path); + saveTheConfig = true; + getConfig().set(path, def); + return def; + } + public Object a(String path, Object def) { + if (getConfig().contains(path)) + return getConfig().get(path); + saveTheConfig = true; + getConfig().set(path, def); + return def; + } + + @SuppressWarnings("unchecked") + @Override + public void onEnable() { + master = getDataFolder().getAbsoluteFile().getParentFile().getParentFile(); + String path = ((String) a("getBackupFileDirectory", "")); + backups = new File((path.isEmpty() ? master.getPath() : path) + File.separator+"backups"+ File.separator); + if (!backups.exists()) + backups.mkdirs(); + saveServerJar = (boolean) a("saveServerJar", false); + savePluiginJars = (boolean) a("savePluginJars", false); + + timedist = toTime((String) a("AutosaveDelay", "1D,0H")); + lastSave = a("LastAutosave", 0L); + + automate = (boolean) a("enableautoSaving", true); + + naming_format = (String) a("FileNameFormat", naming_format); + + String unPrefix = (String) a("prefix", "&6[&3ServerRestorer&6]&8"); + prefix = ChatColor.translateAlternateColorCodes('&', unPrefix); + String kicky = (String) a("kickMessage", unPrefix + " Restoring server to previous save. Please rejoin in a few seconds."); + kickmessage = ChatColor.translateAlternateColorCodes('&', kicky); + + useFTP = (boolean) a("EnableFTP", false); + useFTPS = (boolean) a("EnableFTPS", false); + useSFTP = (boolean) a("EnableSFTP", false); + serverFTP = (String) a("FTPAdress", serverFTP); + portFTP = (int) a("FTPPort", portFTP); + userFTP = (String) a("FTPUsername", userFTP); + passwordFTP = (String) a("FTPPassword", passwordFTP); + + + compression = (int) a("CompressionLevel_Max_9", compression); + + removeFilePath = (String) a("FTP_Directory", removeFilePath); + + hourToSaveAt = (int) a("AutoBackup-HourToBackup", hourToSaveAt); + + if (!getConfig().contains("exceptions")) { + exceptions.add("logs"); + exceptions.add("crash-reports"); + exceptions.add("backups"); + exceptions.add("dynmap"); + exceptions.add(".lock"); + exceptions.add("pixelprinter"); + } + exceptions = (List) a("exceptions", exceptions); + + maxSaveSize = toByteSize((String) a("MaxSaveSize", "10G")); + maxSaveFiles = (int) a("MaxFileSaved", 1000); + + deleteZipOnFTP = (boolean) a("DeleteZipOnFTPTransfer", false); + deleteZipOnFail = (boolean) a("DeleteZipIfFailed", false); + separator = (String) a("FolderSeparator", separator); + if (saveTheConfig) + saveConfig(); + if (automate) { + final JavaPlugin thi = this; + br = new BukkitRunnable() { + @Override + public void run() { + Calendar calendar = GregorianCalendar.getInstance(); // creates a new calendar instance + calendar.setTime(new Date()); // assigns calendar to given date + int hour = calendar.get(Calendar.HOUR_OF_DAY); + + if (System.currentTimeMillis() - lastSave >= timedist && (hourToSaveAt==-1 || hourToSaveAt == hour)) { + new BukkitRunnable() { + @Override + public void run() { + getConfig().set("LastAutosave", lastSave = (System.currentTimeMillis()-5000)); + save(Bukkit.getConsoleSender()); + saveConfig(); + } + }.runTaskLater(thi, 0); + return; + } + } + }.runTaskTimerAsynchronously(this, 20, 20*60); + } + + + + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + + if (args.length == 1) { + List list = new ArrayList<>(); + String[] commands = new String[]{"disableAutoSaver", "enableAutoSaver", "restore", "save","stop", "toggleOptions"}; + for (String f : commands) { + if (f.toLowerCase().startsWith(args[0].toLowerCase())) + list.add(f); + } + return list; + + } + + if (args.length > 1) { + if (args[0].equalsIgnoreCase("restore")) { + List list = new ArrayList<>(); + for (File f : getBackupFolder().listFiles()) { + if (f.getName().toLowerCase().startsWith(args[1].toLowerCase())) + list.add(f.getName()); + } + return list; + } + } + return super.onTabComplete(sender, command, alias, args); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("serverrestorer.command")) { + sender.sendMessage(prefix + ChatColor.RED + " You do not have permission to use this command."); + return true; + } + if (args.length == 0) { + sender.sendMessage(ChatColor.GOLD + "---===+Server Restorer+===---"); + sender.sendMessage("/sr save : Saves the server"); + sender.sendMessage("/sr stop : Stops creating a backup of the server"); + sender.sendMessage("/sr restore : Restores server to previous backup (automatically restarts)"); + sender.sendMessage("/sr enableAutoSaver [1H,6H,1D,7D] : Configure how long it takes to autosave"); + sender.sendMessage("/sr disableAutoSaver : Disables the autosaver"); + sender.sendMessage("/sr toggleOptions : TBD"); + return true; + } + if (args[0].equalsIgnoreCase("restore")) { + if(true) { + sender.sendMessage(prefix+ "Restore feature is temporarily disabled. Please load the files manually."); + return true; + } + if (!sender.hasPermission("serverrestorer.restore")) { + sender.sendMessage(prefix + ChatColor.RED + " You do not have permission to use this command."); + return true; + } + if (currentlySaving) { + sender.sendMessage(prefix + " The server is currently being saved. Please wait."); + return true; + } + if (args.length < 2) { + sender.sendMessage(prefix + " A valid backup file is required."); + return true; + } + File backup = new File(getBackupFolder(), args[1]); + if (!backup.exists()) { + sender.sendMessage(prefix + " The file \"" + args[1] + "\" does not exist."); + return true; + } + restore(backup); + sender.sendMessage(prefix + " Restoration complete."); + return true; + } + + if (args[0].equalsIgnoreCase("stop")) { + if (!sender.hasPermission("serverrestorer.save")) { + sender.sendMessage(prefix + ChatColor.RED + " You do not have permission to use this command."); + return true; + } + if (currentlySaving) { + currentlySaving=false; + return true; + } + sender.sendMessage(prefix + " The server is not currently being saved."); + return true; + } + if (args[0].equalsIgnoreCase("save")) { + if (!sender.hasPermission("serverrestorer.save")) { + sender.sendMessage(prefix + ChatColor.RED + " You do not have permission to use this command."); + return true; + } + if (currentlySaving) { + sender.sendMessage(prefix + " The server is currently being saved. Please wait."); + return true; + } + save(sender); + return true; + } + if (args[0].equalsIgnoreCase("disableAutoSaver")) { + if (!sender.hasPermission("serverrestorer.save")) { + sender.sendMessage(prefix + ChatColor.RED + " You do not have permission to use this command."); + return true; + } + if (br != null) + br.cancel(); + br = null; + getConfig().set("enableautoSaving", false); + saveConfig(); + sender.sendMessage(prefix + " Canceled delay."); + } + if (args[0].equalsIgnoreCase("enableAutoSaver")) { + if (!sender.hasPermission("serverrestorer.save")) { + sender.sendMessage(prefix + ChatColor.RED + " You do not have permission to use this command."); + return true; + } + if (args.length == 1) { + sender.sendMessage(prefix + " Please select a delay [E.G. 0.5H, 6H, 1D, 7D...]"); + return true; + } + String delay = args[1]; + getConfig().set("AutosaveDelay", delay); + getConfig().set("enableautoSaving", true); + saveConfig(); + if (br != null) + br.cancel(); + br = null; + br = new BukkitRunnable() { + @Override + public void run() { + if (System.currentTimeMillis() - lastSave > timedist) { + save(Bukkit.getConsoleSender()); + getConfig().set("LastAutosave", lastSave = System.currentTimeMillis()); + saveConfig(); + return; + } + } + }.runTaskTimerAsynchronously(this, 20, 20 * 60 * 30); + + sender.sendMessage(prefix + " Set the delay to \"" + delay + "\"."); + } + if (args[0].equalsIgnoreCase("toggleOptions")) { + if (!sender.hasPermission("serverrestorer.save")) { + sender.sendMessage(prefix + ChatColor.RED + " You do not have permission to use this command."); + return true; + } + sender.sendMessage(prefix + " Coming soon !"); + return true; + } + return true; + } + + public void save(CommandSender sender) { + currentlySaving = true; + sender.sendMessage(prefix + " Starting to save directory. Please wait."); + List autosave = new ArrayList<>(); + for (World loaded : Bukkit.getWorlds()) { + try { + loaded.save(); + if (loaded.isAutoSave()) { + autosave.add(loaded); + loaded.setAutoSave(false); + } + + } catch (Exception e) { + } + } + new BukkitRunnable() { + @Override + public void run() { + try { + try { + if(backups.listFiles().length > maxSaveFiles){ + for(int i = 0; i < backups.listFiles().length-maxSaveFiles; i++){ + File oldestBack = firstFileModified(backups); + sender.sendMessage(prefix + ChatColor.RED + oldestBack.getName() + + ": File goes over max amount of files that can be saved."); + oldestBack.delete(); + } + } + for (int j = 0; j < Math.min(maxSaveFiles, backups.listFiles().length - 1); j++) { + if (folderSize(backups) >= maxSaveSize) { + File oldestBack = firstFileModified(backups); + sender.sendMessage(prefix + ChatColor.RED + oldestBack.getName() + + ": The current save goes over the max savesize, and so the oldest file has been deleted. If you wish to save older backups, copy them to another location."); + oldestBack.delete(); + } else { + break; + } + } + } catch (Error | Exception e) { + } + final long time = lastSave = System.currentTimeMillis(); + Date d = new Date(lastSave); + File zipFile = new File(getBackupFolder(), + naming_format.replaceAll("%date%", dateformat.format(d)) + ".zip"); + if (!zipFile.exists()) { + zipFile.getParentFile().mkdirs(); + zipFile = new File(getBackupFolder(), + naming_format.replaceAll("%date%", dateformat.format(d)) + ".zip"); + zipFile.createNewFile(); + } + zipFolder(getMasterFolder().getPath(), zipFile.getPath()); + + long timeDif = (System.currentTimeMillis() - time) / 1000; + String timeDifS = (((int) (timeDif / 60)) + "M, " + (timeDif % 60) + "S"); + + if(!currentlySaving){ + for (World world : autosave) + world.setAutoSave(true); + sender.sendMessage(prefix + " Backup canceled."); + cancel(); + return; + } + + sender.sendMessage(prefix + " Done! Backup took:" + timeDifS); + File tempBackupCheck = new File(getMasterFolder(), "backups"); + sender.sendMessage(prefix + " Compressed server with size of " + + (humanReadableByteCount(folderSize(getMasterFolder()) + - (tempBackupCheck.exists() ? folderSize(tempBackupCheck) : 0), false)) + + " to " + humanReadableByteCount(zipFile.length(), false)); + currentlySaving = false; + for (World world : autosave) + world.setAutoSave(true); + if (useSFTP) { + try { + sender.sendMessage(prefix + " Starting SFTP Transfer"); + JSch jsch = new JSch(); + Session session = jsch.getSession(userFTP, serverFTP, portFTP); + session.setConfig("PreferredAuthentications", "password"); + session.setPassword(passwordFTP); + session.connect(1000 * 20); + Channel channel = session.openChannel("sftp"); + ChannelSftp sftp = (ChannelSftp) channel; + sftp.connect(1000 * 20); + } catch (Exception | Error e) { + sender.sendMessage( + prefix + " FAILED TO SFTP TRANSFER FILE: " + zipFile.getName() + ". ERROR IN CONSOLE."); + if (deleteZipOnFail) + zipFile.delete(); + e.printStackTrace(); + } + } else if (useFTPS) { + sender.sendMessage(prefix + " Starting FTPS Transfer"); + FileInputStream zipFileStream = new FileInputStream(zipFile); + FTPSClient ftpClient = new FTPSClient(); + try { + if (ftpClient.isConnected()) { + sender.sendMessage(prefix + "FTPSClient was already connected. Disconnecting"); + ftpClient.logout(); + ftpClient.disconnect(); + ftpClient = new FTPSClient(); + } + sendFTP(sender, zipFile, ftpClient, zipFileStream, removeFilePath); + if (deleteZipOnFTP) + zipFile.delete(); + } catch (Exception | Error e) { + sender.sendMessage( + prefix + " FAILED TO FTPS TRANSFER FILE: " + zipFile.getName() + ". ERROR IN CONSOLE."); + if (deleteZipOnFail) + zipFile.delete(); + e.printStackTrace(); + } finally { + try { + if (ftpClient.isConnected()) { + sender.sendMessage(prefix + "Disconnecting"); + ftpClient.logout(); + ftpClient.disconnect(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } else if (useFTP) { + sender.sendMessage(prefix + " Starting FTP Transfer"); + FileInputStream zipFileStream = new FileInputStream(zipFile); + FTPClient ftpClient = new FTPClient(); + try { + if (ftpClient.isConnected()) { + sender.sendMessage(prefix + "FTPClient was already connected. Disconnecting"); + ftpClient.logout(); + ftpClient.disconnect(); + ftpClient = new FTPClient(); + } + sendFTP(sender, zipFile, ftpClient, zipFileStream, removeFilePath); + if (deleteZipOnFTP) + zipFile.delete(); + } catch (Exception | Error e) { + sender.sendMessage( + prefix + " FAILED TO FTP TRANSFER FILE: " + zipFile.getName() + ". ERROR IN CONSOLE."); + if (deleteZipOnFail) + zipFile.delete(); + e.printStackTrace(); + } finally { + try { + if (ftpClient.isConnected()) { + sender.sendMessage(prefix + "Disconnecting"); + ftpClient.logout(); + ftpClient.disconnect(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }.runTaskAsynchronously(this); + } + + public void sendFTP(CommandSender sender, File zipFile, FTPClient ftpClient, FileInputStream zipFileStream, String path) + throws SocketException, IOException { + ftpClient.connect(serverFTP, portFTP); + ftpClient.login(userFTP, passwordFTP); + ftpClient.enterLocalPassiveMode(); + + ftpClient.setFileType(FTP.BINARY_FILE_TYPE); + + boolean done = ftpClient.storeFile(path + zipFile.getName(), zipFileStream); + zipFileStream.close(); + if (done) { + sender.sendMessage(prefix + " Transfered backup using FTP!"); + } else { + sender.sendMessage(prefix + " Something failed (maybe)! Status=" + ftpClient.getStatus()); + } + + } + + public long toTime(String time) { + long militime = 0; + for(String split : time.split(",")) { + split = split.trim(); + long k = 1; + if (split.toUpperCase().endsWith("H")) { + k *= 60 * 60; + } else if (split.toUpperCase().endsWith("D")) { + k *= 60 * 60 * 24; + } else { + k *= 60 * 60 * 24; + } + double j = Double.parseDouble(split.substring(0, split.length() - 1)); + militime += (j*k); + } + militime *= 1000; + return militime; + } + + public void restore(File backup) { + + //Kick all players + for (Player player : Bukkit.getOnlinePlayers()) + player.kickPlayer(kickmessage); + + //Disable all plugins safely. + for (Plugin p : Bukkit.getPluginManager().getPlugins()) { + if (p != this) { + try { + Bukkit.getPluginManager().disablePlugin(p); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + //Unload all worlds. + List names = new ArrayList<>(); + for (World w : Bukkit.getWorlds()) { + for (Chunk c : w.getLoadedChunks()) { + c.unload(false); + } + names.add(w.getName()); + Bukkit.unloadWorld(w, true); + } + for(String worldnames : names){ + File worldFile = new File(getMasterFolder(),worldnames); + if(worldFile.exists()) + worldFile.delete(); + } + + //Start overriding files. + File parentTo = getMasterFolder().getParentFile(); + try { + byte[] buffer = new byte[1024]; + ZipInputStream zis = new ZipInputStream(new FileInputStream(backup)); + ZipEntry zipEntry = zis.getNextEntry(); + while (zipEntry != null) { + try { + File newFile = newFile(parentTo, zipEntry); + FileOutputStream fos = new FileOutputStream(newFile); + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + fos.close(); + zipEntry = zis.getNextEntry(); + } catch (Exception e) { + e.printStackTrace(); + } + } + zis.closeEntry(); + zis.close(); + } catch (Exception e4) { + e4.printStackTrace(); + } + Bukkit.shutdown(); + } + + public void zipFolder(String srcFolder, String destZipFile) throws Exception { + ZipOutputStream zip = null; + FileOutputStream fileWriter = null; + + fileWriter = new FileOutputStream(destZipFile); + zip = new ZipOutputStream(fileWriter); + + + zip.setLevel(compression); + + addFolderToZip("", srcFolder, zip); + zip.flush(); + zip.close(); + } + + private void addFileToZip(String path, String srcFile, ZipOutputStream zip) { + try { + File folder = new File(srcFile); + if (!isExempt(srcFile)) { + + if(!currentlySaving) + return; + // this.savedBytes += folder.length(); + if (folder.isDirectory()) { + addFolderToZip(path, srcFile, zip); + } else { + if (folder.getName().endsWith("jar")) { + if (path.contains("plugins") && (!savePluiginJars) || (!path.contains("plugins") && (!saveServerJar))) { + return; + } + } + + byte[] buf = new byte['?']; + + FileInputStream in = new FileInputStream(srcFile); + zip.putNextEntry(new ZipEntry(path + separator + folder.getName())); + int len; + while ((len = in.read(buf)) > 0) { + zip.write(buf, 0, len); + } + in.close(); + } + } + }catch (FileNotFoundException e4){ + Bukkit.getConsoleSender().sendMessage(prefix + " FAILED TO ZIP FILE: " + srcFile+" Reason: "+e4.getClass().getName()); + e4.printStackTrace(); + }catch (IOException e5){ + if(!srcFile.endsWith(".db")) { + Bukkit.getConsoleSender().sendMessage(prefix + " FAILED TO ZIP FILE: " + srcFile + " Reason: " + e5.getClass().getName()); + e5.printStackTrace(); + }else{ + Bukkit.getConsoleSender().sendMessage(prefix + " Skipping file " + srcFile +" due to another process that has locked a portion of the file"); + } + + } + } + + private void addFolderToZip(String path, String srcFolder, ZipOutputStream zip) { + if ((!path.toLowerCase().contains("backups")) && (!isExempt(path))) { + try { + File folder = new File(srcFolder); + String[] arrayOfString; + int j = (arrayOfString = folder.list()).length; + for (int i = 0; i < j; i++) { + if(!currentlySaving) + break; + String fileName = arrayOfString[i]; + if (path.equals("")) { + addFileToZip(folder.getName(), srcFolder + separator + fileName, zip); + } else { + addFileToZip(path + separator + folder.getName(), srcFolder + separator + fileName, zip); + } + } + } catch (Exception e) { + } + } + } + + private long toByteSize(String s) { + long k = Long.parseLong(s.substring(0, s.length() - 1)); + if (s.toUpperCase().endsWith("G")) { + k *= 1000 * 1000 * 1000; + } else if (s.toUpperCase().endsWith("M")) { + k *= 1000 * 1000; + } else if (s.toUpperCase().endsWith("K")) { + k *= 1000; + } else { + k *= 10; + } + return k; + } +} diff --git a/src/main/java/zombie_striker/sr/Updater.java b/src/main/java/zombie_striker/sr/Updater.java new file mode 100644 index 0000000..fe11c6e --- /dev/null +++ b/src/main/java/zombie_striker/sr/Updater.java @@ -0,0 +1,756 @@ +package me.zombie_striker.sr; + +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.logging.Level; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; + +/** + * Checks and auto updates a plugin
+ *
+ * You must have a option in your config to disable the updater! + * + * @author Arsen + */ +public class Updater { + + private static final String HOST = "https://api.curseforge.com"; + private static final String QUERY = "/servermods/files?projectIds="; + private static final String AGENT = "Mozilla/5.0 Updater by ArsenArsen"; + private static final File WORKING_DIR = new File("plugins" + File.separator + "AUpdater" + File.separator); + private static final File BACKUP_DIR = new File(WORKING_DIR, "backups" + File.separator); + private static final File LOG_FILE = new File(WORKING_DIR, "updater.log"); + private static final File CONFIG_FILE = new File(WORKING_DIR, "global.yml"); + private static final char[] HEX_CHAR_ARRAY = "0123456789abcdef".toCharArray(); + private static final Pattern NAME_MATCH = Pattern.compile(".+\\sv?[0-9.]+"); + private static final String VERSION_SPLIT = "\\sv?"; + + + private int id = -1; + + private Plugin p; + private boolean debug = false; + private UpdateAvailability lastCheck = null; + private UpdateResult lastUpdate = UpdateResult.NOT_UPDATED; + private File pluginFile = null; + private String downloadURL = null; + private String futuremd5; + private String downloadName; + private List allowedChannels = Arrays.asList(Channel.ALPHA, Channel.BETA, Channel.RELEASE); + private List callbacks = new ArrayList<>(); + private SyncCallbackCaller caller = new SyncCallbackCaller(); + private List skipTags = new ArrayList<>(); + private String latest; + private FileConfiguration global; + + public boolean updaterActive = false; + + /** + * Makes the updater for a plugin + * + * @param p Plugin to update + */ + public Updater(Plugin p) { + this.p = p; + try { + pluginFile = new File(URLDecoder.decode(p.getClass().getProtectionDomain().getCodeSource().getLocation().getPath(), "UTF-8")); + } catch (UnsupportedEncodingException e) { + debug(e.toString()); + // Should not ever happen + } + latest = p.getDescription().getVersion(); + if (!CONFIG_FILE.exists()) { + try { + CONFIG_FILE.getParentFile().mkdirs(); + CONFIG_FILE.createNewFile(); + log("Created config file!"); + } catch (IOException e) { + p.getLogger().log(Level.SEVERE, "Could not create " + CONFIG_FILE.getName() + "!", e); + } + } + global = YamlConfiguration.loadConfiguration(CONFIG_FILE); + global.options().header("Updater by ArsenArsen\nGlobal config\nSets should updates be downloaded globaly"); + if (!global.isSet("update")) { + global.set("update", true); + try { + global.save(CONFIG_FILE); + } catch (IOException e) { + p.getLogger().log(Level.SEVERE, "Could not save default config file!", e); + } + } + if (!LOG_FILE.exists()) { + try { + LOG_FILE.getParentFile().mkdirs(); + LOG_FILE.createNewFile(); + log("Created log file!"); + } catch (IOException e) { + p.getLogger().log(Level.SEVERE, "Could not create " + LOG_FILE.getName() + "!", e); + } + } + updaterActive = global.getBoolean("update"); + } + + /** + * Makes the updater for a plugin with an ID + * + * @param p The plugin + * @param id Plugin ID + */ + public Updater(Plugin p, int id) { + this(p); + setID(id); + + } + + /** + * Makes the updater for a plugin with an ID + * + * @param p The plugin + * @param id Plugin ID + * @param download Set to true if your plugin needs to be immediately downloaded + * @param skipTags Tags, endings of a filename, that updater will ignore, must begin with a dash ('-') + */ + public Updater(Plugin p, int id, boolean download, String... skipTags) { + this(p); + setID(id); + for (String tag : skipTags) + if (tag.startsWith("-")) + this.skipTags.add(tag); + if (download && (checkForUpdates() == UpdateAvailability.UPDATE_AVAILABLE)) { + update(); + } + } + + /** + * Makes the updater for a plugin with an ID + * + * @param p The plugin + * @param id Plugin ID + * @param download Set to true if your plugin needs to be immediately downloaded + * @param skipTags Tags, endings of a filename, that updater will ignore, or null for none + * @param callbacks All update callbacks you need + */ + public Updater(Plugin p, int id, boolean download, String[] skipTags, UpdateCallback... callbacks) { + this(p); + setID(id); + this.callbacks.addAll(Arrays.asList(callbacks)); + if (skipTags != null) { + for (String tag : skipTags) + if (tag.startsWith("-")) + this.skipTags.add(tag); + } + if (global.getBoolean("update", true) && download && (checkForUpdates() == UpdateAvailability.UPDATE_AVAILABLE)) { + update(); + } + } + + /** + * Gets the plugin ID + * + * @return the plugin ID + */ + public int getID() { + return id; + } + + /** + * Sets the plugin ID + * + * @param id The plugin ID + */ + public void setID(int id) { + this.id = id; + } + + /** + * Adds a new callback + * + * @param callback Callback to register + */ + public void registerCallback(UpdateCallback callback) { + callbacks.add(callback); + } + + /** + * Attempts a update + * + * @throws IllegalStateException if the ID was not set + */ + public void update() { + debug(WORKING_DIR.getAbsolutePath()); + debug("Update!"); + if (id == -1) { + throw new IllegalStateException("Plugin ID is not set!"); + } + + if (lastCheck == null) { + checkForUpdates(); + } + + if (!BACKUP_DIR.exists() || !BACKUP_DIR.isDirectory()) { + BACKUP_DIR.mkdir(); + } + final Updater updater = this; + if (!global.getBoolean("update", true)) { + lastUpdate = UpdateResult.DISABLED; + debug("Disabled!"); + caller.call(callbacks, UpdateResult.DISABLED, updater); + return; + } + if (lastCheck == UpdateAvailability.UPDATE_AVAILABLE) { + new BukkitRunnable() { + + @Override + public void run() { + debug("Update STARTED!"); + p.getLogger().info("Starting update of " + p.getName()); + log("Updating " + p.getName() + "!"); + lastUpdate = download(true); + p.getLogger().log(Level.INFO, "Update done! Result: " + lastUpdate); + caller.call(callbacks, lastUpdate, updater); + } + }.runTaskAsynchronously(p); + } else if (lastCheck == UpdateAvailability.SM_UNREACHABLE) { + lastUpdate = UpdateResult.IOERROR; + debug("Fail!"); + caller.call(callbacks, UpdateResult.IOERROR, updater); + } else { + lastUpdate = UpdateResult.GENERAL_ERROR; + debug("Fail!"); + caller.call(callbacks, UpdateResult.IOERROR, updater); + } + } + + public UpdateResult download(boolean keepBackups) { + try { + if(keepBackups){ + Files.copy(pluginFile.toPath(), + new File(BACKUP_DIR, "backup-" + System.currentTimeMillis() + "-" + p.getName() + ".jar").toPath(), + StandardCopyOption.REPLACE_EXISTING); + //TODO: Considering the amount of times the plugin updates, I don't want there to be a huge file full of old jars. + } + File downloadTo = new File(pluginFile.getParentFile().getAbsolutePath() + + File.separator + "AUpdater" + File.separator, downloadName); + downloadTo.getParentFile().mkdirs(); + downloadTo.delete(); + if(keepBackups) + debug("Started download!"); + + downloadIsSeperateBecauseGotoGotRemoved(downloadTo); + + if(keepBackups){ + debug("Ended download!"); + if (!fileHash(downloadTo).equalsIgnoreCase(futuremd5)) + return UpdateResult.BAD_HASH; + if (downloadTo.getName().endsWith(".jar")) { + pluginFile.setWritable(true, false); + pluginFile.delete(); + if(keepBackups){ + debug("Started copy!"); + InputStream in = new FileInputStream(downloadTo); + File file = new File(pluginFile.getParentFile() + .getAbsoluteFile() + File.separator + "update" + File.separator, pluginFile.getName()); + file.getParentFile().mkdirs(); + file.createNewFile(); + OutputStream out = new FileOutputStream(file); + long bytes = copy(in, out); + p.getLogger().info("Update done! Downloaded " + bytes + " bytes!"); + log("Updated plugin " + p.getName() + " with " + bytes + "bytes!"); + } + return UpdateResult.UPDATE_SUCCEEDED; + } else + return unzip(downloadTo); + + + }else{ + return UpdateResult.UPDATE_SUCCEEDED; + } + } catch (IOException e) { + p.getLogger().log(Level.SEVERE, "Couldn't download update for " + p.getName(), e); + log("Failed to update " + p.getName() + "!", e); + return UpdateResult.IOERROR; + } + } + + /** + * God damn it Gosling, reference here. + */ + private void downloadIsSeperateBecauseGotoGotRemoved(File downloadTo) throws IOException { + URL url = new URL(downloadURL); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.addRequestProperty("User-Agent", AGENT); + connection.connect(); + if (connection.getResponseCode() >= 300 && connection.getResponseCode() < 400) { + downloadURL = connection.getHeaderField("Location"); + downloadIsSeperateBecauseGotoGotRemoved(downloadTo); + } else { + debug(connection.getResponseCode() + " " + connection.getResponseMessage() + " when requesting " + downloadURL); + copy(connection.getInputStream(), new FileOutputStream(downloadTo)); + } + } + + private long copy(InputStream in, OutputStream out) throws IOException { + long bytes = 0; + byte[] buf = new byte[0x1000]; + while (true) { + int r = in.read(buf); + if (r == -1) + break; + out.write(buf, 0, r); + bytes += r; + debug("Another 4K, current: " + r); + } + out.flush(); + out.close(); + in.close(); + return bytes; + } + + + private UpdateResult unzip(File download) { + ZipFile zipFile = null; + try { + zipFile = new ZipFile(download); + Enumeration entries = zipFile.entries(); + ZipEntry entry; + File updateFile = new File(pluginFile.getParentFile() + .getAbsoluteFile() + File.separator + "update" + File.separator, pluginFile.getName()); + while ((entry = entries.nextElement()) != null) { + File target = new File(updateFile, entry.getName()); + File inPlugins = new File(pluginFile.getParentFile(), entry.getName()); + if(!inPlugins.exists()){ + target = inPlugins; + } + if (!entry.isDirectory()) { + target.getParentFile().mkdirs(); + InputStream zipStream = zipFile.getInputStream(entry); + OutputStream fileStream = new FileOutputStream(target); + copy(zipStream, fileStream); + } + } + return UpdateResult.UPDATE_SUCCEEDED; + } catch (IOException e) { + if (e instanceof ZipException) { + p.getLogger().log(Level.SEVERE, "Could not unzip downloaded file!", e); + log("Update for " + p.getName() + "was an unknown filetype! ", e); + return UpdateResult.UNKNOWN_FILE_TYPE; + } else { + p.getLogger().log(Level.SEVERE, + "An IOException occured while trying to update %s!".replace("%s", p.getName()), e); + log("Update for " + p.getName() + "was an unknown filetype! ", e); + return UpdateResult.IOERROR; + } + } finally { + if (zipFile != null) { + try { + zipFile.close(); + } catch (IOException ignored) { + } + } + } + } + + /** + * Checks for new updates + * + * @param force Discards the cached state in order to get a new one, ignored if update check didn't run + * @return Is there any updates + * @throws IllegalStateException If the plugin ID is not set + */ + public UpdateAvailability checkForUpdates(boolean force) { + if (id == -1) { + throw new IllegalStateException("Plugin ID is not set!"); + } + + if (force || lastCheck == null) { + String target = HOST + QUERY + id; + debug(target); + try { + URL url = new URL(target); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.addRequestProperty("User-Agent", AGENT); + connection.connect(); + debug("Connecting!"); + BufferedReader responseReader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + StringBuilder responseBuffer = new StringBuilder(); + String line; + while ((line = responseReader.readLine()) != null) { + responseBuffer.append(line); + } + debug("All read!"); + responseReader.close(); + String response = responseBuffer.toString(); + int counter = 1; + if (connection.getResponseCode() == 200) { + + try { + debug("RESCODE 200"); + while (true) { + debug("Counter: " + counter); + JSONParser parser = new JSONParser(); + JSONArray json = (JSONArray) parser.parse(response); + if (json.size() - counter < 0) { + lastCheck = UpdateAvailability.NO_UPDATE; + debug("No update!"); + break; + } + JSONObject latest = (JSONObject) json.get(json.size() - counter); + futuremd5 = (String) latest.get("md5"); + String channel = (String) latest.get("releaseType"); + String name = (String) latest.get("name"); + if (allowedChannels.contains(Channel.matchChannel(channel.toUpperCase())) + && !hasTag(name)) { + String noTagName = name; + String oldVersion = p.getDescription().getVersion().replaceAll("-.*", ""); + for (String tag : skipTags) { + noTagName = noTagName.replace(tag, ""); + oldVersion = oldVersion.replace(tag, ""); + } + if (!NAME_MATCH.matcher(noTagName).matches()) { + lastCheck = UpdateAvailability.CANT_PARSE_NAME; + return lastCheck; + } + String[] splitName = noTagName.split(VERSION_SPLIT); + String version = splitName[splitName.length - 1]; + if (oldVersion.length() > version.length()) { + while (oldVersion.length() > version.length()) { + version += ".0"; + } + } else if (oldVersion.length() < version.length()) { + while (oldVersion.length() < version.length()) { + oldVersion += ".0"; + } + } + debug("Versions are same length"); + String[] splitOldVersion = oldVersion.split("\\."); + String[] splitVersion = version.split("\\."); + + Integer[] parsedOldVersion = new Integer[splitOldVersion.length]; + Integer[] parsedVersion = new Integer[splitVersion.length]; + + for (int i = 0; i < parsedOldVersion.length; i++) { + parsedOldVersion[i] = Integer.parseInt(splitOldVersion[i]); + } + for (int i = 0; i < parsedVersion.length; i++) { + parsedVersion[i] = Integer.parseInt(splitVersion[i]); + } + boolean update = false; + for (int i = 0; i < parsedOldVersion.length; i++) { + if (parsedOldVersion[i] < parsedVersion[i]) { + update = true; + break; + } + } + if (!update) { + lastCheck = UpdateAvailability.NO_UPDATE; + //Temp fix for downloads + downloadURL = ((String) latest.get("downloadUrl")).replace(" ", "%20"); + downloadName = (String) latest.get("fileName"); + } else { + lastCheck = UpdateAvailability.UPDATE_AVAILABLE; + downloadURL = ((String) latest.get("downloadUrl")).replace(" ", "%20"); + downloadName = (String) latest.get("fileName"); + } + break; + } else + counter++; + } + debug("While loop over!"); + } catch (ParseException e) { + p.getLogger().log(Level.SEVERE, "Could not parse API Response for " + target, e); + log("Could not parse API Response for " + target + " while updating " + p.getName(), e); + lastCheck = UpdateAvailability.CANT_UNDERSTAND; + } + } else { + log("Could not reach API for " + target + " while updating " + p.getName()); + lastCheck = UpdateAvailability.SM_UNREACHABLE; + } + } catch (IOException e) { + p.getLogger().log(Level.SEVERE, "Could not check for updates for plugin " + p.getName(), e); + log("Could not reach API for " + target + " while updating " + p.getName(), e); + lastCheck = UpdateAvailability.SM_UNREACHABLE; + } + } + log("Update check ran for " + p.getName() + "! Check resulted in " + lastCheck); + return lastCheck; + } + + private void debug(String message) { + if (debug) + p.getLogger().info(message + ' ' + new Throwable().getStackTrace()[1]); + } + + private void log(String message) { + try { + Files.write(LOG_FILE.toPath(), Collections.singletonList( + "[" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "] " + message), + StandardCharsets.UTF_8, StandardOpenOption.APPEND); + } catch (IOException e) { + p.getLogger().log(Level.SEVERE, "Could not log to " + LOG_FILE.getAbsolutePath() + "!", e); + } + } + + private void log(String message, Exception exception) { + StringWriter string = new StringWriter(); + PrintWriter print = new PrintWriter(string); + exception.printStackTrace(print); + log(message + " " + string.toString()); + try { + string.close(); + } catch (IOException ignored) { + } + print.close(); + } + + private boolean hasTag(String name) { + for (String tag : skipTags) { + if (name.toLowerCase().endsWith(tag.toLowerCase())) { + return true; + } + } + return false; + } + + /** + * Checks for new updates, non forcing cache override + * + * @return Is there any updates + * @throws IllegalStateException If the plugin ID is not set + */ + public UpdateAvailability checkForUpdates() { + return checkForUpdates(false); + } + + /** + * Checks did the update run successfully + * + * @return The update state + */ + public UpdateResult isUpdated() { + return lastUpdate; + } + + /** + * Sets allowed channels, AKA release types + * + * @param channels The allowed channels + */ + public void setChannels(Channel... channels) { + allowedChannels.clear(); + allowedChannels.addAll(Arrays.asList(channels)); + } + + /** + * Gets the latest version + * + * @return The latest version + */ + public String getLatest() { + return latest; + } + + /** + * Shows the outcome of an update + * + * @author Arsen + */ + public enum UpdateResult { + /** + * Update was successful + */ + UPDATE_SUCCEEDED, + + /** + * Update was not attempted yet + */ + NOT_UPDATED, + + /** + * Could not unpack the update + */ + UNKNOWN_FILE_TYPE, + + /** + * Miscellanies error occurred while update checking + */ + GENERAL_ERROR, + + /** + * Updater is globally disabled + */ + DISABLED, + /** + * The hashing algorithm and the remote hash had different results. + */ + BAD_HASH, + /** + * An unknown IO error occurred + */ + IOERROR + } + + /** + * Shows the outcome of an update check + * + * @author Arsen + */ + public enum UpdateAvailability { + /** + * There is an update + */ + UPDATE_AVAILABLE, + + /** + * You have the latest version + */ + NO_UPDATE, + + /** + * Could not reach server mods API + */ + SM_UNREACHABLE, + + /** + * Update name cannot be parsed, meaning the version cannot be compared + */ + CANT_PARSE_NAME, + + /** + * Could not parse response from server mods API + */ + CANT_UNDERSTAND + } + + public enum Channel { + /** + * Normal release + */ + RELEASE("release"), + + /** + * Beta release + */ + BETA("beta"), + + /** + * Alpha release + */ + ALPHA("alpha"); + + private String channel; + + Channel(String channel) { + this.channel = channel; + } + + /** + * Gets the channel value + * + * @return the channel value + */ + public String getChannel() { + return channel; + } + + /** + * Returns channel whose channel value matches the given string + * + * @param channel The channel value + * @return The Channel constant + */ + public static Channel matchChannel(String channel) { + for (Channel c : values()) { + if (c.channel.equalsIgnoreCase(channel)) { + return c; + } + } + return null; + } + } + + /** + * Calculates files MD5 hash + * + * @param file The file to digest + * @return The MD5 hex or null, if the operation failed + */ + public String fileHash(File file) { + FileInputStream is; + try { + is = new FileInputStream(file); + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] bytes = new byte[2048]; + int numBytes; + while ((numBytes = is.read(bytes)) != -1) { + md.update(bytes, 0, numBytes); + } + byte[] digest = md.digest(); + char[] hexChars = new char[digest.length * 2]; + for (int j = 0; j < digest.length; j++) { + int v = digest[j] & 0xFF; + hexChars[j * 2] = HEX_CHAR_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_CHAR_ARRAY[v & 0x0F]; + } + is.close(); + return new String(hexChars); + } catch (IOException | NoSuchAlgorithmException e) { + p.getLogger().log(Level.SEVERE, "Could not digest " + file.getPath(), e); + return null; + } + } + + /** + * Called right after update is done + * + * @author Arsen + */ + public interface UpdateCallback { + + void updated(UpdateResult updateResult, Updater updater); + } + + private class SyncCallbackCaller extends BukkitRunnable { + private List callbacks; + private UpdateResult updateResult; + private Updater updater; + + public void run() { + for (UpdateCallback callback : callbacks) { + callback.updated(updateResult, updater); + } + } + + void call(List callbacks, UpdateResult updateResult, Updater updater) { + this.callbacks = callbacks; + this.updateResult = updateResult; + this.updater = updater; + if (!Bukkit.getServer().isPrimaryThread()) + runTask(updater.p); + else run(); + } + + } +} \ No newline at end of file