Dateien nach "src/main/java/zombie_striker/sr" hochladen

This commit is contained in:
2025-08-15 18:24:00 +00:00
parent 623e17ac58
commit 8f42a83f15
2 changed files with 1487 additions and 0 deletions

View File

@@ -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<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

@@ -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<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();
}
}
}