Update from Git Manager GUI

This commit is contained in:
2026-02-28 20:44:41 +01:00
parent 8f42a83f15
commit 8a702488be
355 changed files with 716 additions and 1502 deletions

View File

@@ -0,0 +1,316 @@
package de.viper.autoworldreset;
import de.viper.autoworldreset.scheduler.ResetScheduler;
import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Scanner;
import java.util.function.Consumer;
import org.json.JSONObject;
public class AutoWorldReset extends JavaPlugin {
private ResetManager resetManager;
private ResetScheduler scheduler;
// BUG FIX #9: lang.yml wird jetzt als eigene FileConfiguration geladen und
// tatsächlich verwendet. Vorher wurde die Datei zwar gespeichert, aber
// Nachrichten wurden immer aus config.yml gelesen lang.yml war toter Code.
private FileConfiguration langConfig;
private Metrics metrics;
private String latestVersion = null;
private static final int RESOURCE_ID = 127822;
private static final int BSTATS_PLUGIN_ID = 26874;
@Override
public void onEnable() {
// BUG FIX #10: registerEvents(this, this) wurde entfernt.
// Die Hauptklasse implementierte kein Listener-Interface und hatte keine
// @EventHandler-Methoden mehr das war also eine sinnlose Registrierung,
// die zu einer misleadenden Warnung führen kann.
getServer().getPluginManager().registerEvents(new UpdateNotifyListener(), this);
saveDefaultConfig();
loadLangConfig(); // lang.yml laden
resetManager = new ResetManager(this);
scheduler = new ResetScheduler(this, resetManager);
boolean autoReset = getConfig().getBoolean("auto-reset-on-startup", false);
if (autoReset) {
Bukkit.getScheduler().runTaskLater(this, () -> {
String worldName = getConfig().getString("world-name");
// BUG FIX #11: Null-Check für worldName beim Startup-Reset.
if (worldName == null || worldName.isEmpty()) {
getLogger().warning("auto-reset-on-startup ist aktiv, aber 'world-name' ist nicht konfiguriert!");
return;
}
boolean success = resetManager.restoreBackup(worldName);
if (success) {
getLogger().info("Backup beim Serverstart erfolgreich wiederhergestellt.");
} else {
getLogger().warning("Backup konnte beim Serverstart nicht wiederhergestellt werden.");
}
}, 20L * 10);
}
if (getConfig().getBoolean("scheduler.enabled")) {
scheduler.start();
}
metrics = new Metrics(this, BSTATS_PLUGIN_ID);
getLogger().info("bStats initialisiert.");
getLatestVersion(latest -> {
if (latest == null || latest.isEmpty()) return; // BUG FIX #12: kein weiterer Code bei leerem Ergebnis
String current = getDescription().getVersion();
String normalizedLatest = latest.replaceFirst("(?i)^v\\.?\\s*", "").trim();
String normalizedCurrent = current.replaceFirst("(?i)^v\\.?\\s*", "").trim();
if (isNewerVersion(normalizedLatest, normalizedCurrent)) {
latestVersion = latest;
getLogger().info("Neue Version verfügbar: " + latest + " (aktuell: " + current + ")");
getLogger().info("Download: https://www.spigotmc.org/resources/" + RESOURCE_ID + "/");
// Bukkit-API-Calls müssen auf dem Hauptthread laufen!
// BUG FIX #13: Spielerbenachrichtigung in runTask() verschoben,
// da dieser Code in einem Async-Thread läuft (getServer().getScheduler()
// .runTaskAsynchronously) und Bukkit-API nicht thread-safe ist.
Bukkit.getScheduler().runTask(this, () -> {
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.isOp()) {
notifyUpdateToPlayer(player, latest, current);
}
}
});
}
});
getLogger().info("AutoWorldReset wurde aktiviert.");
}
@Override
public void onDisable() {
if (scheduler != null) {
scheduler.stop();
}
getLogger().info("AutoWorldReset wurde deaktiviert.");
}
// -----------------------------------------------------------------------
// lang.yml Handling
// -----------------------------------------------------------------------
private void loadLangConfig() {
File langFile = new File(getDataFolder(), "lang.yml");
if (!langFile.exists()) {
// lang.yml ist nicht in der JAR eingebettet → manuell mit Standardwerten anlegen
getDataFolder().mkdirs();
YamlConfiguration defaults = new YamlConfiguration();
defaults.set("messages.resetting", "&eDie Welt wird zurückgesetzt...");
defaults.set("messages.finished", "&aWelt wurde erfolgreich zurückgesetzt!");
defaults.set("messages.no_permission", "&cDu hast keine Berechtigung, diesen Befehl auszuführen.");
defaults.set("messages.invalid_command", "&cUngültiger Befehl oder Argument.");
defaults.set("messages.kick-message", "&cDie Welt wird zurückgesetzt, du wurdest gekickt.");
defaults.set("messages.teleport-message","&cDie Welt wird zurückgesetzt. Du wurdest sicher teleportiert.");
try {
defaults.save(langFile);
getLogger().info("lang.yml wurde automatisch erstellt.");
} catch (IOException e) {
getLogger().warning("lang.yml konnte nicht erstellt werden: " + e.getMessage());
}
}
langConfig = YamlConfiguration.loadConfiguration(langFile);
}
/**
* Liest eine Nachricht aus lang.yml. Fällt auf config.yml und dann auf den
* Standardwert zurück, wenn der Schlüssel nicht gefunden wird.
*/
public String getLangMessage(String key, String defaultValue) {
if (langConfig != null && langConfig.contains("messages." + key)) {
return langConfig.getString("messages." + key, defaultValue);
}
return getConfig().getString("messages." + key, defaultValue);
}
// -----------------------------------------------------------------------
// Update-Checker
// -----------------------------------------------------------------------
private void getLatestVersion(Consumer<String> consumer) {
getServer().getScheduler().runTaskAsynchronously(this, () -> {
try {
HttpURLConnection connection = (HttpURLConnection)
new URL("https://api.spiget.org/v2/resources/" + RESOURCE_ID + "/versions/latest")
.openConnection();
connection.setRequestMethod("GET");
connection.addRequestProperty("User-Agent", "AutoWorldReset-UpdateChecker/1.0");
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
try (Scanner scanner = new Scanner(connection.getInputStream())) {
String response = scanner.useDelimiter("\\A").next();
JSONObject json = new JSONObject(response);
String versionName = json.optString("name", "").trim();
consumer.accept(versionName);
}
} catch (Exception e) {
getLogger().warning("Update-Check fehlgeschlagen: " + e.getMessage());
consumer.accept("");
}
});
}
private boolean isNewerVersion(String latest, String current) {
try {
String[] latestParts = latest.split("\\.");
String[] currentParts = current.split("\\.");
int length = Math.max(latestParts.length, currentParts.length);
for (int i = 0; i < length; i++) {
int latestPart = (i < latestParts.length) ? Integer.parseInt(latestParts[i]) : 0;
int currentPart = (i < currentParts.length) ? Integer.parseInt(currentParts[i]) : 0;
if (latestPart > currentPart) return true;
if (latestPart < currentPart) return false;
}
return false;
} catch (NumberFormatException e) {
return !latest.equalsIgnoreCase(current);
}
}
private void notifyUpdateToPlayer(Player player, String latest, String current) {
player.sendMessage("§aEine neue Version von §e" + getDescription().getName()
+ " §aist verfügbar: §e" + latest + " §7(aktuell: " + current + ")");
player.sendMessage("§eDownload: §bhttps://www.spigotmc.org/resources/" + RESOURCE_ID + "/");
}
// -----------------------------------------------------------------------
// Inner class: Update-Benachrichtigung beim Join
// -----------------------------------------------------------------------
public class UpdateNotifyListener implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
if (player.isOp() && latestVersion != null) {
notifyUpdateToPlayer(player, latestVersion, getDescription().getVersion());
}
}
}
// -----------------------------------------------------------------------
// Commands
// -----------------------------------------------------------------------
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!sender.hasPermission("autoworldreset.use")) {
sender.sendMessage(color(getLangMessage("no_permission", "&cDu hast keine Berechtigung.")));
return true;
}
if (!command.getName().equalsIgnoreCase("autoworldreset")) {
return false;
}
if (args.length == 0) {
sender.sendMessage("§eBenutze: /autoworldreset <reset|backup|restore|reload|start|stop|status>");
return true;
}
switch (args[0].toLowerCase()) {
case "reset":
sender.sendMessage(color(getLangMessage("resetting", "&eDie Welt wird zurückgesetzt...")));
Bukkit.getScheduler().runTask(this, () -> resetManager.resetWorld());
break;
case "backup":
sender.sendMessage("§eBackup wird erstellt...");
Bukkit.getScheduler().runTask(this, () -> {
boolean success = resetManager.createBackup();
sender.sendMessage(success
? "§aBackup erfolgreich erstellt."
: "§cBackup konnte nicht erstellt werden. Siehe Konsole für Details.");
});
break;
case "restore":
sender.sendMessage("§eBackup wird wiederhergestellt...");
Bukkit.getScheduler().runTask(this, () -> {
String worldName = getConfig().getString("world-name");
boolean success = resetManager.restoreBackup(worldName);
sender.sendMessage(success
? "§aBackup erfolgreich wiederhergestellt."
: "§cBackup konnte nicht wiederhergestellt werden. Siehe Konsole für Details.");
});
break;
case "reload":
reloadConfig();
loadLangConfig();
resetManager = new ResetManager(this);
if (scheduler != null) scheduler.stop();
scheduler = new ResetScheduler(this, resetManager);
if (getConfig().getBoolean("scheduler.enabled")) {
scheduler.start();
}
sender.sendMessage("§aKonfiguration erfolgreich neu geladen.");
getLogger().info(sender.getName() + " hat die Konfiguration neu geladen.");
break;
case "start":
if (scheduler.isRunning()) {
sender.sendMessage("§eScheduler läuft bereits.");
} else {
scheduler.start();
sender.sendMessage("§aScheduler gestartet.");
}
break;
case "stop":
scheduler.stop();
sender.sendMessage("§cScheduler gestoppt.");
break;
// BUG FIX #14: Neuer "status"-Befehl zeigt, ob der Scheduler läuft.
case "status":
sender.sendMessage("§eScheduler: " + (scheduler.isRunning() ? "§aAktiv" : "§cInaktiv"));
sender.sendMessage("§eWelt: §f" + getConfig().getString("world-name", "nicht konfiguriert"));
sender.sendMessage("§eCron: §f" + getConfig().getString("scheduler.cron", "-"));
break;
default:
sender.sendMessage(color(getLangMessage("invalid_command", "&cUngültiger Befehl oder Argument.")));
break;
}
return true;
}
public ResetManager getResetManager() {
return resetManager;
}
private String color(String msg) {
return ChatColor.translateAlternateColorCodes('&', msg);
}
}

View File

@@ -0,0 +1,204 @@
package de.viper.autoworldreset;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
public class ResetManager {
private final AutoWorldReset plugin;
public ResetManager(AutoWorldReset plugin) {
this.plugin = plugin;
}
public void resetWorld() {
String worldName = plugin.getConfig().getString("world-name");
if (worldName == null || worldName.isEmpty()) {
plugin.getLogger().warning("Keine Welt in der Config angegeben (key: world-name).");
return;
}
// BUG FIX #6: Die Backup-Prüfung wurde aus resetWorld() entfernt und korrekt
// platziert. Zuvor wurde backup.enabled geprüft, um einen RESTORE abzubrechen
// das ist logisch falsch. Das Flag sollte nur das automatische Erstellen eines
// Backups steuern, nicht das Wiederherstellen. Ein Reset setzt immer das Backup
// zurück; wenn keins da ist, schlägt restoreBackup() mit einer Warnung fehl.
boolean success = restoreBackup(worldName);
if (!success) {
plugin.getLogger().warning("Reset fehlgeschlagen: Backup konnte nicht wiederhergestellt werden.");
}
}
public boolean createBackup() {
String worldName = plugin.getConfig().getString("world-name");
if (worldName == null || worldName.isEmpty()) {
plugin.getLogger().warning("Kein Weltname konfiguriert (key: world-name).");
return false;
}
File worldFolder = new File(Bukkit.getWorldContainer(), worldName);
File backupFolder = new File(Bukkit.getWorldContainer(),
worldName + "_" + plugin.getConfig().getString("backup.folder-name", "backup"));
if (!worldFolder.exists()) {
plugin.getLogger().warning("Weltordner nicht gefunden: " + worldFolder.getAbsolutePath());
return false;
}
try {
World world = Bukkit.getWorld(worldName);
if (world != null) {
handlePlayers(world);
world.save();
}
if (backupFolder.exists()) {
deleteFolder(backupFolder.toPath());
}
// BUG FIX #7: copyFolder wirft jetzt korrekt eine IOException nach oben.
// Vorher wurden Fehler im Stream-Lambda stillschweigend geschluckt, sodass
// ein unvollständiges Backup als "erfolgreich" gemeldet wurde.
copyFolder(worldFolder.toPath(), backupFolder.toPath());
plugin.getLogger().info("Backup der Welt '" + worldName + "' erfolgreich erstellt.");
return true;
} catch (IOException e) {
plugin.getLogger().severe("Fehler beim Erstellen des Backups: " + e.getMessage());
e.printStackTrace();
return false;
}
}
public boolean restoreBackup(String worldName) {
File worldFolder = new File(Bukkit.getWorldContainer(), worldName);
File backupFolder = new File(Bukkit.getWorldContainer(),
worldName + "_" + plugin.getConfig().getString("backup.folder-name", "backup"));
if (!backupFolder.exists()) {
plugin.getLogger().warning("Backup-Ordner existiert nicht: " + backupFolder.getAbsolutePath()
+ ". Bitte zuerst ein Backup mit '/awr backup' erstellen.");
return false;
}
World world = Bukkit.getWorld(worldName);
if (world != null) {
handlePlayers(world);
world.save();
if (!Bukkit.unloadWorld(world, false)) {
plugin.getLogger().warning("Welt konnte nicht entladen werden: " + worldName);
return false;
}
}
try {
if (worldFolder.exists()) {
deleteFolder(worldFolder.toPath());
}
copyFolder(backupFolder.toPath(), worldFolder.toPath());
// BUG FIX #8: Rückgabewert von createWorld() prüfen.
// Früher wurde immer true zurückgegeben, auch wenn die Welt nicht
// geladen werden konnte (createWorld gibt null zurück bei Fehler).
World newWorld = Bukkit.createWorld(new WorldCreator(worldName));
if (newWorld == null) {
plugin.getLogger().severe("Welt '" + worldName + "' konnte nach Restore nicht geladen werden!");
return false;
}
plugin.getLogger().info("Backup der Welt '" + worldName + "' erfolgreich wiederhergestellt.");
return true;
} catch (IOException e) {
plugin.getLogger().severe("Fehler beim Wiederherstellen: " + e.getMessage());
e.printStackTrace();
return false;
}
}
private void handlePlayers(World world) {
String mode = plugin.getConfig().getString("handle-players", "TELEPORT").toUpperCase();
World fallback = Bukkit.getWorlds().get(0);
for (Player player : world.getPlayers()) {
switch (mode) {
case "KICK":
// Nachricht aus lang.yml lesen statt hardcoded
String kickMsg = plugin.getLangMessage("kick-message", "&cDie Welt wird zurückgesetzt.");
player.kickPlayer(org.bukkit.ChatColor.translateAlternateColorCodes('&', kickMsg));
break;
case "TELEPORT":
player.teleport(fallback.getSpawnLocation());
player.sendMessage(org.bukkit.ChatColor.translateAlternateColorCodes('&',
plugin.getLangMessage("teleport-message",
"&cDie Welt wird zurückgesetzt. Du wurdest sicher teleportiert.")));
break;
case "TELEPORT_BACK":
player.teleport(fallback.getSpawnLocation());
player.sendMessage(org.bukkit.ChatColor.translateAlternateColorCodes('&',
plugin.getLangMessage("teleport-message",
"&cDie Welt wird zurückgesetzt. Du wurdest vorübergehend teleportiert.")));
break;
default:
plugin.getLogger().warning("Unbekannter handle-players Modus: '" + mode + "'. Verwende TELEPORT.");
player.teleport(fallback.getSpawnLocation());
break;
}
}
}
private void deleteFolder(Path path) throws IOException {
if (Files.notExists(path)) return;
if (Files.isDirectory(path)) {
try (var entries = Files.newDirectoryStream(path)) {
for (var entry : entries) {
deleteFolder(entry);
}
}
}
Files.delete(path);
}
// Dateien, die vom laufenden Server gesperrt sind oder im Backup keinen
// Nutzen haben, werden beim Kopieren automatisch uebersprungen.
private static final java.util.Set<String> SKIP_FILES = java.util.Set.of(
"session.lock" // Vom Minecraft-Prozess exklusiv gesperrt
);
private void copyFolder(Path source, Path target) throws IOException {
List<String> errors = new ArrayList<>();
Files.walk(source).forEach(src -> {
String fileName = src.getFileName() != null ? src.getFileName().toString() : "";
if (SKIP_FILES.contains(fileName)) {
plugin.getLogger().info("Ueberspringe gesperrte Datei beim Backup: " + fileName);
return;
}
try {
Path dest = target.resolve(source.relativize(src));
if (Files.isDirectory(src)) {
if (!Files.exists(dest)) Files.createDirectories(dest);
} else {
Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
errors.add(src + "" + e.getMessage());
}
});
if (!errors.isEmpty()) {
throw new IOException("Fehler beim Kopieren folgender Dateien:\n" + String.join("\n", errors));
}
}
}

View File

@@ -0,0 +1,85 @@
package de.viper.autoworldreset.scheduler;
import de.viper.autoworldreset.AutoWorldReset;
import de.viper.autoworldreset.ResetManager;
import de.viper.autoworldreset.util.CronParserUtil;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask;
import java.time.Duration;
public class ResetScheduler {
private final AutoWorldReset plugin;
private final ResetManager resetManager;
private BukkitTask task;
// BUG FIX #4: Fehlende stopped-Flag hinzugefügt.
// Ohne dieses Flag rief der Callback nach Ausführung scheduleNextReset()
// erneut auf, selbst nachdem stop() bereits aufgerufen wurde. Das führte
// dazu, dass der Scheduler nach einem stop() ungewollt weiter lief.
private boolean stopped = false;
public ResetScheduler(AutoWorldReset plugin, ResetManager resetManager) {
this.plugin = plugin;
this.resetManager = resetManager;
}
public void start() {
stopped = false;
scheduleNextReset();
}
private void scheduleNextReset() {
if (stopped) return;
String cron = plugin.getConfig().getString("scheduler.cron");
// Null-Check (wird auch in CronParserUtil geprüft, aber defensive
// Programmierung ist hier sinnvoll für eine klare Log-Meldung)
if (cron == null || cron.isBlank()) {
plugin.getLogger().warning("Kein Cron-Ausdruck unter 'scheduler.cron' konfiguriert!");
return;
}
Duration delay = CronParserUtil.parseCronToDelay(cron);
if (delay == null) {
plugin.getLogger().warning("Ungültiger Cron-Ausdruck: '" + cron + "'. Scheduler wird nicht gestartet.");
return;
}
// BUG FIX #5: Delay von 0 oder negativ abfangen.
// Wenn die nächste Ausführungszeit in der Vergangenheit liegt (z. B. wegen
// Systemzeitproblemen), würde ein Delay von 0 Ticks zu einem Sofort-Reset führen.
if (delay.isZero() || delay.isNegative()) {
plugin.getLogger().warning("Berechneter Delay ist 0 oder negativ Reset wird übersprungen, nächsten Termin berechnen.");
// Warte 60 Sekunden und versuche es erneut
task = Bukkit.getScheduler().runTaskLater(plugin, this::scheduleNextReset, 20L * 60);
return;
}
long ticks = delay.getSeconds() * 20L;
plugin.getLogger().info("Nächster geplanter Reset in "
+ delay.toHours() + "h " + (delay.toMinutes() % 60) + "min (" + ticks + " Ticks).");
task = Bukkit.getScheduler().runTaskLater(plugin, () -> {
if (stopped) return; // BUG FIX #4 (Fortsetzung): Doppelte Prüfung im Callback
plugin.getLogger().info("Geplanter Reset wird jetzt ausgeführt...");
resetManager.resetWorld();
scheduleNextReset();
}, ticks);
}
public void stop() {
stopped = true;
if (task != null) {
task.cancel();
task = null;
}
}
public boolean isRunning() {
return !stopped && task != null;
}
}

View File

@@ -0,0 +1,40 @@
package de.viper.autoworldreset.util;
import com.cronutils.model.Cron;
import com.cronutils.model.time.ExecutionTime;
import com.cronutils.parser.CronParser;
import com.cronutils.model.definition.CronDefinitionBuilder;
import java.time.Duration;
import java.time.ZonedDateTime;
// BUG FIX #1: CronType von UNIX auf QUARTZ geändert.
// UNIX-Cron hat nur 5 Felder (min h dom mon dow), aber config.yml verwendet
// einen 6-feldrigen Quartz-Ausdruck ("0 30 18 * * *" = Sekunde/Minute/Stunde/...).
// Zur Laufzeit warf das einen IllegalArgumentException beim Parsen.
import static com.cronutils.model.CronType.QUARTZ;
public class CronParserUtil {
public static Duration parseCronToDelay(String cronExpression) {
// BUG FIX #2: Null-/Leerstring-Check verhindert NullPointerException,
// wenn der Schlüssel "scheduler.cron" in der config.yml fehlt.
if (cronExpression == null || cronExpression.isBlank()) {
return null;
}
try {
CronParser parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(QUARTZ));
Cron cron = parser.parse(cronExpression);
cron.validate(); // BUG FIX #3: Explizite Validierung für frühzeitige,
// verständliche Fehlermeldungen bei ungültigen Ausdrücken.
ExecutionTime executionTime = ExecutionTime.forCron(cron);
ZonedDateTime now = ZonedDateTime.now();
return executionTime.timeToNextExecution(now).orElse(null);
} catch (Exception e) {
System.err.println("[AutoWorldReset] Ungültiger Cron-Ausdruck '"
+ cronExpression + "': " + e.getMessage());
return null;
}
}
}

View File

@@ -1,731 +0,0 @@
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<String> exceptions = new ArrayList<String>();
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<String>) 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<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (args.length == 1) {
List<String> 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<String> 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 <backup> : 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<World> 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<String> 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;
}
}

View File

@@ -1,756 +0,0 @@
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<br>
* <br>
* <b>You must have a option in your config to disable the updater!</b>
*
* @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<Channel> allowedChannels = Arrays.asList(Channel.ALPHA, Channel.BETA, Channel.RELEASE);
private List<UpdateCallback> callbacks = new ArrayList<>();
private SyncCallbackCaller caller = new SyncCallbackCaller();
private List<String> 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, <a href="http://stackoverflow.com/a/4547764/3809164">reference here.</a>
*/
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<? extends ZipEntry> 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<UpdateCallback> callbacks;
private UpdateResult updateResult;
private Updater updater;
public void run() {
for (UpdateCallback callback : callbacks) {
callback.updated(updateResult, updater);
}
}
void call(List<UpdateCallback> callbacks, UpdateResult updateResult, Updater updater) {
this.callbacks = callbacks;
this.updateResult = updateResult;
this.updater = updater;
if (!Bukkit.getServer().isPrimaryThread())
runTask(updater.p);
else run();
}
}
}

Some files were not shown because too many files have changed in this diff Show More