Advertisement
Guest User

Updater.java

a guest
Apr 14th, 2014
44
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 22.76 KB | None | 0 0
  1. /*
  2. * Updater for Bukkit.
  3. *
  4. * This class provides the means to safely and easily update a plugin, or check to see if it is updated using dev.bukkit.org
  5. */
  6.  
  7. package com.github.Gamecube762.IsMinecraftDown;
  8.  
  9. import java.io.*;
  10. import java.net.MalformedURLException;
  11. import java.net.URL;
  12. import java.net.URLConnection;
  13. import java.util.Enumeration;
  14. import java.util.zip.ZipEntry;
  15. import java.util.zip.ZipFile;
  16.  
  17. import org.bukkit.configuration.file.YamlConfiguration;
  18. import org.bukkit.plugin.Plugin;
  19. import org.json.simple.JSONArray;
  20. import org.json.simple.JSONObject;
  21. import org.json.simple.JSONValue;
  22.  
  23. /**
  24. * Check dev.bukkit.org to find updates for a given plugin, and download the updates if needed.
  25. * <p/>
  26. * <b>VERY, VERY IMPORTANT</b>: Because there are no standards for adding auto-update toggles in your plugin's config, this system provides NO CHECK WITH YOUR CONFIG to make sure the user has allowed auto-updating.
  27. * <br>
  28. * It is a <b>BUKKIT POLICY</b> that you include a boolean value in your config that prevents the auto-com.github.Gamecube762.IsMinecraftDown.net.gravitydevelopment.updater from running <b>AT ALL</b>.
  29. * <br>
  30. * If you fail to include this option in your config, your plugin will be <b>REJECTED</b> when you attempt to submit it to dev.bukkit.org.
  31. * <p/>
  32. * An example of a good configuration option would be something similar to 'auto-update: true' - if this value is set to false you may NOT run the auto-com.github.Gamecube762.IsMinecraftDown.net.gravitydevelopment.updater.
  33. * <br>
  34. * If you are unsure about these rules, please read the plugin submission guidelines: http://goo.gl/8iU5l
  35. *
  36. * @author Gravity
  37. * @version 2.0
  38. */
  39.  
  40. public class Updater {
  41.  
  42. private Plugin plugin;
  43. private UpdateType type;
  44. private String versionName;
  45. private String versionLink;
  46. private String versionType;
  47. private String versionGameVersion;
  48.  
  49. private boolean announce; // Whether to announce file downloads
  50.  
  51. private URL url; // Connecting to RSS
  52. private File file; // The plugin's file
  53. private Thread thread; // Updater thread
  54.  
  55. private int id = -1; // Project's Curse ID
  56. private String apiKey = null; // BukkitDev ServerMods API key
  57. private static final String TITLE_VALUE = "name"; // Gets remote file's title
  58. private static final String LINK_VALUE = "downloadUrl"; // Gets remote file's download link
  59. private static final String TYPE_VALUE = "releaseType"; // Gets remote file's release type
  60. private static final String VERSION_VALUE = "gameVersion"; // Gets remote file's build version
  61. private static final String QUERY = "/servermods/files?projectIds="; // Path to GET
  62. private static final String HOST = "https://api.curseforge.com"; // Slugs will be appended to this to get to the project's RSS feed
  63.  
  64. private static final String[] NO_UPDATE_TAG = { "-DEV", "-PRE", "-SNAPSHOT" }; // If the version number contains one of these, don't update.
  65. private static final int BYTE_SIZE = 1024; // Used for downloading files
  66. private YamlConfiguration config; // Config file
  67. private String updateFolder;// The folder that downloads will be placed in
  68. private UpdateResult result = UpdateResult.SUCCESS; // Used for determining the outcome of the update process
  69.  
  70. /**
  71. * Gives the dev the result of the update process. Can be obtained by called getResult().
  72. */
  73. public enum UpdateResult {
  74. /**
  75. * The com.github.Gamecube762.IsMinecraftDown.net.gravitydevelopment.updater found an update, and has readied it to be loaded the next time the server restarts/reloads.
  76. */
  77. SUCCESS,
  78. /**
  79. * The com.github.Gamecube762.IsMinecraftDown.net.gravitydevelopment.updater did not find an update, and nothing was downloaded.
  80. */
  81. NO_UPDATE,
  82. /**
  83. * The server administrator has disabled the updating system
  84. */
  85. DISABLED,
  86. /**
  87. * The com.github.Gamecube762.IsMinecraftDown.net.gravitydevelopment.updater found an update, but was unable to download it.
  88. */
  89. FAIL_DOWNLOAD,
  90. /**
  91. * For some reason, the com.github.Gamecube762.IsMinecraftDown.net.gravitydevelopment.updater was unable to contact dev.bukkit.org to download the file.
  92. */
  93. FAIL_DBO,
  94. /**
  95. * When running the version check, the file on DBO did not contain the a version in the format 'vVersion' such as 'v1.0'.
  96. */
  97. FAIL_NOVERSION,
  98. /**
  99. * The id provided by the plugin running the com.github.Gamecube762.IsMinecraftDown.net.gravitydevelopment.updater was invalid and doesn't exist on DBO.
  100. */
  101. FAIL_BADID,
  102. /**
  103. * The server administrator has improperly configured their API key in the configuration
  104. */
  105. FAIL_APIKEY,
  106. /**
  107. * The com.github.Gamecube762.IsMinecraftDown.net.gravitydevelopment.updater found an update, but because of the UpdateType being set to NO_DOWNLOAD, it wasn't downloaded.
  108. */
  109. UPDATE_AVAILABLE
  110. }
  111.  
  112. /**
  113. * Allows the dev to specify the type of update that will be run.
  114. */
  115. public enum UpdateType {
  116. /**
  117. * Run a version check, and then if the file is out of date, download the newest version.
  118. */
  119. DEFAULT,
  120. /**
  121. * Don't run a version check, just find the latest update and download it.
  122. */
  123. NO_VERSION_CHECK,
  124. /**
  125. * Get information about the version and the download size, but don't actually download anything.
  126. */
  127. NO_DOWNLOAD
  128. }
  129.  
  130. /**
  131. * Initialize the com.github.Gamecube762.IsMinecraftDown.net.gravitydevelopment.updater
  132. *
  133. * @param plugin The plugin that is checking for an update.
  134. * @param id The dev.bukkit.org id of the project
  135. * @param file The file that the plugin is running from, get this by doing this.getFile() from within your main class.
  136. * @param type Specify the type of update this will be. See {@link Updater.UpdateType}
  137. * @param announce True if the program should announce the progress of new updates in console
  138. */
  139. public Updater(Plugin plugin, int id, File file, UpdateType type, boolean announce) {
  140. this.plugin = plugin;
  141. this.type = type;
  142. this.announce = announce;
  143. this.file = file;
  144. this.id = id;
  145. this.updateFolder = plugin.getServer().getUpdateFolder();
  146.  
  147. final File pluginFile = plugin.getDataFolder().getParentFile();
  148. final File updaterFile = new File(pluginFile, "Updater");
  149. final File updaterConfigFile = new File(updaterFile, "config.yml");
  150.  
  151. if (!updaterFile.exists()) {
  152. updaterFile.mkdir();
  153. }
  154. if (!updaterConfigFile.exists()) {
  155. try {
  156. updaterConfigFile.createNewFile();
  157. } catch (final IOException e) {
  158. plugin.getLogger().severe("The com.github.Gamecube762.IsMinecraftDown.net.gravitydevelopment.updater could not create a configuration in " + updaterFile.getAbsolutePath());
  159. e.printStackTrace();
  160. }
  161. }
  162. this.config = YamlConfiguration.loadConfiguration(updaterConfigFile);
  163.  
  164. this.config.options().header("This configuration file affects all plugins using the Updater system (version 2+ - http://forums.bukkit.org/threads/96681/ )" + '\n'
  165. + "If you wish to use your API key, read http://wiki.bukkit.org/ServerMods_API and place it below." + '\n'
  166. + "Some updating systems will not adhere to the disabled value, but these may be turned off in their plugin's configuration.");
  167. this.config.addDefault("api-key", "PUT_API_KEY_HERE");
  168. this.config.addDefault("disable", false);
  169.  
  170. if (this.config.get("api-key", null) == null) {
  171. this.config.options().copyDefaults(true);
  172. try {
  173. this.config.save(updaterConfigFile);
  174. } catch (final IOException e) {
  175. plugin.getLogger().severe("The com.github.Gamecube762.IsMinecraftDown.net.gravitydevelopment.updater could not save the configuration in " + updaterFile.getAbsolutePath());
  176. e.printStackTrace();
  177. }
  178. }
  179.  
  180. if (this.config.getBoolean("disable")) {
  181. this.result = UpdateResult.DISABLED;
  182. return;
  183. }
  184.  
  185. String key = this.config.getString("api-key");
  186. if (key.equalsIgnoreCase("PUT_API_KEY_HERE") || key.equals("")) {
  187. key = null;
  188. }
  189.  
  190. this.apiKey = key;
  191.  
  192. try {
  193. this.url = new URL(Updater.HOST + Updater.QUERY + id);
  194. } catch (final MalformedURLException e) {
  195. plugin.getLogger().severe("The project ID provided for updating, " + id + " is invalid.");
  196. this.result = UpdateResult.FAIL_BADID;
  197. e.printStackTrace();
  198. }
  199.  
  200. this.thread = new Thread(new UpdateRunnable());
  201. this.thread.start();
  202. }
  203.  
  204. /**
  205. * Get the result of the update process.
  206. */
  207. public UpdateResult getResult() {
  208. this.waitForThread();
  209. return this.result;
  210. }
  211.  
  212. /**
  213. * Get the latest version's release type (release, beta, or alpha).
  214. */
  215. public String getLatestType() {
  216. this.waitForThread();
  217. return this.versionType;
  218. }
  219.  
  220. /**
  221. * Get the latest version's game version.
  222. */
  223. public String getLatestGameVersion() {
  224. this.waitForThread();
  225. return this.versionGameVersion;
  226. }
  227.  
  228. /**
  229. * Get the latest version's name.
  230. */
  231. public String getLatestName() {
  232. this.waitForThread();
  233. return this.versionName;
  234. }
  235.  
  236. /**
  237. * Get the latest version's file link.
  238. */
  239. public String getLatestFileLink() {
  240. this.waitForThread();
  241. return this.versionLink;
  242. }
  243.  
  244. /**
  245. * As the result of Updater output depends on the thread's completion, it is necessary to wait for the thread to finish
  246. * before allowing anyone to check the result.
  247. */
  248. private void waitForThread() {
  249. if ((this.thread != null) && this.thread.isAlive()) {
  250. try {
  251. this.thread.join();
  252. } catch (final InterruptedException e) {
  253. e.printStackTrace();
  254. }
  255. }
  256. }
  257.  
  258. /**
  259. * Save an update from dev.bukkit.org into the server's update folder.
  260. */
  261. private void saveFile(File folder, String file, String u) {
  262. if (!folder.exists()) {
  263. folder.mkdir();
  264. }
  265. BufferedInputStream in = null;
  266. FileOutputStream fout = null;
  267. try {
  268. // Download the file
  269. final URL url = new URL(u);
  270. final int fileLength = url.openConnection().getContentLength();
  271. in = new BufferedInputStream(url.openStream());
  272. fout = new FileOutputStream(folder.getAbsolutePath() + "/" + file);
  273.  
  274. final byte[] data = new byte[Updater.BYTE_SIZE];
  275. int count;
  276. if (this.announce) {
  277. this.plugin.getLogger().info("About to download a new update: " + this.versionName);
  278. }
  279. long downloaded = 0;
  280. while ((count = in.read(data, 0, Updater.BYTE_SIZE)) != -1) {
  281. downloaded += count;
  282. fout.write(data, 0, count);
  283. final int percent = (int) ((downloaded * 100) / fileLength);
  284. if (this.announce && ((percent % 10) == 0)) {
  285. this.plugin.getLogger().info("Downloading update: " + percent + "% of " + fileLength + " bytes.");
  286. }
  287. }
  288. //Just a quick check to make sure we didn't leave any files from last time...
  289. for (final File xFile : new File(this.plugin.getDataFolder().getParent(), this.updateFolder).listFiles()) {
  290. if (xFile.getName().endsWith(".zip")) {
  291. xFile.delete();
  292. }
  293. }
  294. // Check to see if it's a zip file, if it is, unzip it.
  295. final File dFile = new File(folder.getAbsolutePath() + "/" + file);
  296. if (dFile.getName().endsWith(".zip")) {
  297. // Unzip
  298. this.unzip(dFile.getCanonicalPath());
  299. }
  300. if (this.announce) {
  301. this.plugin.getLogger().info("Finished updating.");
  302. }
  303. } catch (final Exception ex) {
  304. this.plugin.getLogger().warning("The auto-com.github.Gamecube762.IsMinecraftDown.net.gravitydevelopment.updater tried to download a new update, but was unsuccessful.");
  305. this.result = UpdateResult.FAIL_DOWNLOAD;
  306. } finally {
  307. try {
  308. if (in != null) {
  309. in.close();
  310. }
  311. if (fout != null) {
  312. fout.close();
  313. }
  314. } catch (final Exception ex) {
  315. }
  316. }
  317. }
  318.  
  319. /**
  320. * Part of Zip-File-Extractor, modified by Gravity for use with Bukkit
  321. */
  322. private void unzip(String file) {
  323. try {
  324. final File fSourceZip = new File(file);
  325. final String zipPath = file.substring(0, file.length() - 4);
  326. ZipFile zipFile = new ZipFile(fSourceZip);
  327. Enumeration<? extends ZipEntry> e = zipFile.entries();
  328. while (e.hasMoreElements()) {
  329. ZipEntry entry = e.nextElement();
  330. File destinationFilePath = new File(zipPath, entry.getName());
  331. destinationFilePath.getParentFile().mkdirs();
  332. if (entry.isDirectory()) {
  333. continue;
  334. } else {
  335. final BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry));
  336. int b;
  337. final byte buffer[] = new byte[Updater.BYTE_SIZE];
  338. final FileOutputStream fos = new FileOutputStream(destinationFilePath);
  339. final BufferedOutputStream bos = new BufferedOutputStream(fos, Updater.BYTE_SIZE);
  340. while ((b = bis.read(buffer, 0, Updater.BYTE_SIZE)) != -1) {
  341. bos.write(buffer, 0, b);
  342. }
  343. bos.flush();
  344. bos.close();
  345. bis.close();
  346. final String name = destinationFilePath.getName();
  347. if (name.endsWith(".jar") && this.pluginFile(name)) {
  348. destinationFilePath.renameTo(new File(this.plugin.getDataFolder().getParent(), this.updateFolder + "/" + name));
  349. }
  350. }
  351. entry = null;
  352. destinationFilePath = null;
  353. }
  354. e = null;
  355. zipFile.close();
  356. zipFile = null;
  357.  
  358. // Move any plugin data folders that were included to the right place, Bukkit won't do this for us.
  359. for (final File dFile : new File(zipPath).listFiles()) {
  360. if (dFile.isDirectory()) {
  361. if (this.pluginFile(dFile.getName())) {
  362. final File oFile = new File(this.plugin.getDataFolder().getParent(), dFile.getName()); // Get current dir
  363. final File[] contents = oFile.listFiles(); // List of existing files in the current dir
  364. for (final File cFile : dFile.listFiles()) // Loop through all the files in the new dir
  365. {
  366. boolean found = false;
  367. for (final File xFile : contents) // Loop through contents to see if it exists
  368. {
  369. if (xFile.getName().equals(cFile.getName())) {
  370. found = true;
  371. break;
  372. }
  373. }
  374. if (!found) {
  375. // Move the new file into the current dir
  376. cFile.renameTo(new File(oFile.getCanonicalFile() + "/" + cFile.getName()));
  377. } else {
  378. // This file already exists, so we don't need it anymore.
  379. cFile.delete();
  380. }
  381. }
  382. }
  383. }
  384. dFile.delete();
  385. }
  386. new File(zipPath).delete();
  387. fSourceZip.delete();
  388. } catch (final IOException ex) {
  389. this.plugin.getLogger().warning("The auto-com.github.Gamecube762.IsMinecraftDown.net.gravitydevelopment.updater tried to unzip a new update file, but was unsuccessful.");
  390. this.result = UpdateResult.FAIL_DOWNLOAD;
  391. ex.printStackTrace();
  392. }
  393. new File(file).delete();
  394. }
  395.  
  396. /**
  397. * Check if the name of a jar is one of the plugins currently installed, used for extracting the correct files out of a zip.
  398. */
  399. private boolean pluginFile(String name) {
  400. for (final File file : new File("plugins").listFiles()) {
  401. if (file.getName().equals(name)) {
  402. return true;
  403. }
  404. }
  405. return false;
  406. }
  407.  
  408. /**
  409. * Check to see if the program should continue by evaluation whether the plugin is already updated, or shouldn't be updated
  410. */
  411. private boolean versionCheck(String title) {
  412. if (this.type != UpdateType.NO_VERSION_CHECK) {
  413. final String version = this.plugin.getDescription().getVersion();
  414. if (title.split(" v").length == 2) {
  415. final String remoteVersion = title.split(" v")[1].split(" ")[0]; // Get the newest file's version number
  416.  
  417. if (this.hasTag(version) || version.equalsIgnoreCase(remoteVersion)) {
  418. // We already have the latest version, or this build is tagged for no-update
  419. this.result = UpdateResult.NO_UPDATE;
  420. return false;
  421. }
  422. } else {
  423. // The file's name did not contain the string 'vVersion'
  424. final String authorInfo = this.plugin.getDescription().getAuthors().size() == 0 ? "" : " (" + this.plugin.getDescription().getAuthors().get(0) + ")";
  425. this.plugin.getLogger().warning("The author of this plugin" + authorInfo + " has misconfigured their Auto Update system");
  426. this.plugin.getLogger().warning("File versions should follow the format 'PluginName vVERSION'");
  427. this.plugin.getLogger().warning("Please notify the author of this error.");
  428. this.result = UpdateResult.FAIL_NOVERSION;
  429. return false;
  430. }
  431. }
  432. return true;
  433. }
  434.  
  435. /**
  436. * Evaluate whether the version number is marked showing that it should not be updated by this program
  437. */
  438. private boolean hasTag(String version) {
  439. for (final String string : Updater.NO_UPDATE_TAG) {
  440. if (version.contains(string)) {
  441. return true;
  442. }
  443. }
  444. return false;
  445. }
  446.  
  447. private boolean read() {
  448. try {
  449. final URLConnection conn = this.url.openConnection();
  450. conn.setConnectTimeout(5000);
  451.  
  452. if (this.apiKey != null) {
  453. conn.addRequestProperty("X-API-Key", this.apiKey);
  454. }
  455. conn.addRequestProperty("User-Agent", "Updater (by Gravity)");
  456.  
  457. conn.setDoOutput(true);
  458.  
  459. final BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
  460. final String response = reader.readLine();
  461.  
  462. final JSONArray array = (JSONArray) JSONValue.parse(response);
  463.  
  464. if (array.size() == 0) {
  465. this.plugin.getLogger().warning("The com.github.Gamecube762.IsMinecraftDown.net.gravitydevelopment.updater could not find any files for the project id " + this.id);
  466. this.result = UpdateResult.FAIL_BADID;
  467. return false;
  468. }
  469.  
  470. this.versionName = (String) ((JSONObject) array.get(array.size() - 1)).get(Updater.TITLE_VALUE);
  471. this.versionLink = (String) ((JSONObject) array.get(array.size() - 1)).get(Updater.LINK_VALUE);
  472. this.versionType = (String) ((JSONObject) array.get(array.size() - 1)).get(Updater.TYPE_VALUE);
  473. this.versionGameVersion = (String) ((JSONObject) array.get(array.size() - 1)).get(Updater.VERSION_VALUE);
  474.  
  475. return true;
  476. } catch (final IOException e) {
  477. if (e.getMessage().contains("HTTP response code: 403")) {
  478. this.plugin.getLogger().warning("dev.bukkit.org rejected the API key provided in plugins/Updater/config.yml");
  479. this.plugin.getLogger().warning("Please double-check your configuration to ensure it is correct.");
  480. this.result = UpdateResult.FAIL_APIKEY;
  481. } else {
  482. this.plugin.getLogger().warning("The com.github.Gamecube762.IsMinecraftDown.net.gravitydevelopment.updater could not contact dev.bukkit.org for updating.");
  483. this.plugin.getLogger().warning("If you have not recently modified your configuration and this is the first time you are seeing this message, the site may be experiencing temporary downtime.");
  484. this.result = UpdateResult.FAIL_DBO;
  485. }
  486. e.printStackTrace();
  487. return false;
  488. }
  489. }
  490.  
  491. private class UpdateRunnable implements Runnable {
  492.  
  493. @Override
  494. public void run() {
  495. if (Updater.this.url != null) {
  496. // Obtain the results of the project's file feed
  497. if (Updater.this.read()) {
  498. if (Updater.this.versionCheck(Updater.this.versionName)) {
  499. if ((Updater.this.versionLink != null) && (Updater.this.type != UpdateType.NO_DOWNLOAD)) {
  500. String name = Updater.this.file.getName();
  501. // If it's a zip file, it shouldn't be downloaded as the plugin's name
  502. if (Updater.this.versionLink.endsWith(".zip")) {
  503. final String[] split = Updater.this.versionLink.split("/");
  504. name = split[split.length - 1];
  505. }
  506. Updater.this.saveFile(new File(Updater.this.plugin.getDataFolder().getParent(), Updater.this.updateFolder), name, Updater.this.versionLink);
  507. } else {
  508. Updater.this.result = UpdateResult.UPDATE_AVAILABLE;
  509. }
  510. }
  511. }
  512. }
  513. }
  514. }
  515. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement