Help with AutoUpdater

Discussion in 'Plugin Development' started by BlueMustache, Dec 8, 2013.

Thread Status:
Not open for further replies.
  1. Offline

    BlueMustache

    I recently added an auto updater to my plugin.
    My jar was rejected, because the folder was called Updater.
    I do not know how to change the Updater folder to PPMOTD.
    This updater was made by Gravity, so I don't know how to change it.
    Anyone have an idea?
    -Blue

    Code:java
    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 core;
    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-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-updater.
    33. * <br>
    34. * If you are unsure about these rules, please read the plugin submission guidelines: [url]http://goo.gl/8iU5l[/url]
    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 = "c1ace984a3cb15f46d20b600a6a6a1f485683f44"; // 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 = "[url]https://api.curseforge.com[/url]"; // 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 Updater.UpdateResult result = Updater.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 updater found an update, and has readied it to be loaded the next time the server restarts/reloads.
    76.   */
    77. SUCCESS,
    78. /**
    79.   * The 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 updater found an update, but was unable to download it.
    88.   */
    89. FAIL_DOWNLOAD,
    90. /**
    91.   * For some reason, the 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 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 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 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 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 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+ - [url]http://forums.bukkit.org/threads/96681/[/url] )" + '\n'
    165. + "If you wish to use your API key, read [url]http://wiki.bukkit.org/ServerMods_API[/url] 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 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 Updater.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. FileOutputStream fout = null;
    266. try {
    267. // Download the file
    268. final URL url = new URL(u);
    269. final int fileLength = url.openConnection().getContentLength();
    270. in = new BufferedInputStream(url.openStream());
    271. fout = new FileOutputStream(folder.getAbsolutePath() + "/" + file);
    272.  
    273. final byte[] data = new byte[Updater.BYTE_SIZE];
    274. int count;
    275. if (this.announce) {
    276. this.plugin.getLogger().info("About to download a new update: " + this.versionName);
    277. }
    278. long downloaded = 0;
    279. while ((count = in.read(data, 0, Updater.BYTE_SIZE)) != -1) {
    280. downloaded += count;
    281. fout.write(data, 0, count);
    282. final int percent = (int) ((downloaded * 100) / fileLength);
    283. if (this.announce && ((percent % 10) == 0)) {
    284. this.plugin.getLogger().info("Downloading update: " + percent + "% of " + fileLength + " bytes.");
    285. }
    286. }
    287. //Just a quick check to make sure we didn't leave any files from last time...
    288. for (final File xFile : new File(this.plugin.getDataFolder().getParent(), this.updateFolder).listFiles()) {
    289. if (xFile.getName().endsWith(".zip")) {
    290. xFile.delete();
    291. }
    292. }
    293. // Check to see if it's a zip file, if it is, unzip it.
    294. final File dFile = new File(folder.getAbsolutePath() + "/" + file);
    295. if (dFile.getName().endsWith(".zip")) {
    296. // Unzip
    297. this.unzip(dFile.getCanonicalPath());
    298. }
    299. if (this.announce) {
    300. this.plugin.getLogger().info("Finished updating.");
    301. }
    302. } catch (final Exception ex) {
    303. this.plugin.getLogger().warning("The auto-updater tried to download a new update, but was unsuccessful.");
    304. this.result = Updater.UpdateResult.FAIL_DOWNLOAD;
    305. } finally {
    306. try {
    307. if (in != null) {
    308. in.close();
    309. }
    310. if (fout != null) {
    311. fout.close();
    312. }
    313. } catch (final Exception ex) {
    314. }
    315. }
    316. }
    317.  
    318. /**
    319.   * Part of Zip-File-Extractor, modified by Gravity for use with Bukkit
    320.   */
    321. private void unzip(String file) {
    322. try {
    323. final File fSourceZip = new File(file);
    324. final String zipPath = file.substring(0, file.length() - 4);
    325. ZipFile zipFile = new ZipFile(fSourceZip);
    326. Enumeration<? extends ZipEntry> e = zipFile.entries();
    327. while (e.hasMoreElements()) {
    328. ZipEntry entry = e.nextElement();
    329. File destinationFilePath = new File(zipPath, entry.getName());
    330. destinationFilePath.getParentFile().mkdirs();
    331. if (entry.isDirectory()) {
    332. continue;
    333. } else {
    334. final BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry));
    335. int b;
    336. final byte buffer[] = new byte[Updater.BYTE_SIZE];
    337. final FileOutputStream fos = new FileOutputStream(destinationFilePath);
    338. final BufferedOutputStream bos = new BufferedOutputStream(fos, Updater.BYTE_SIZE);
    339. while ((b = bis.read(buffer, 0, Updater.BYTE_SIZE)) != -1) {
    340. bos.write(buffer, 0, b);
    341. }
    342. bos.flush();
    343. bos.close();
    344. bis.close();
    345. final String name = destinationFilePath.getName();
    346. if (name.endsWith(".jar") && this.pluginFile(name)) {
    347. destinationFilePath.renameTo(new File(this.plugin.getDataFolder().getParent(), this.updateFolder + "/" + name));
    348. }
    349. }
    350. entry = null;
    351. destinationFilePath = null;
    352. }
    353. e = null;
    354. zipFile.close();
    355. zipFile = null;
    356.  
    357. // Move any plugin data folders that were included to the right place, Bukkit won't do this for us.
    358. for (final File dFile : new File(zipPath).listFiles()) {
    359. if (dFile.isDirectory()) {
    360. if (this.pluginFile(dFile.getName())) {
    361. final File oFile = new File(this.plugin.getDataFolder().getParent(), dFile.getName()); // Get current dir
    362. final File[] contents = oFile.listFiles(); // List of existing files in the current dir
    363. for (final File cFile : dFile.listFiles()) // Loop through all the files in the new dir
    364. {
    365. boolean found = false;
    366. for (final File xFile : contents) // Loop through contents to see if it exists
    367. {
    368. if (xFile.getName().equals(cFile.getName())) {
    369. found = true;
    370. break;
    371. }
    372. }
    373. if (!found) {
    374. // Move the new file into the current dir
    375. cFile.renameTo(new File(oFile.getCanonicalFile() + "/" + cFile.getName()));
    376. } else {
    377. // This file already exists, so we don't need it anymore.
    378. cFile.delete();
    379. }
    380. }
    381. }
    382. }
    383. dFile.delete();
    384. }
    385. new File(zipPath).delete();
    386. fSourceZip.delete();
    387. } catch (final IOException ex) {
    388. this.plugin.getLogger().warning("The auto-updater tried to unzip a new update file, but was unsuccessful.");
    389. this.result = Updater.UpdateResult.FAIL_DOWNLOAD;
    390. ex.printStackTrace();
    391. }
    392. new File(file).delete();
    393. }
    394.  
    395. /**
    396.   * Check if the name of a jar is one of the plugins currently installed, used for extracting the correct files out of a zip.
    397.   */
    398. private boolean pluginFile(String name) {
    399. for (final File file : new File("plugins").listFiles()) {
    400. if (file.getName().equals(name)) {
    401. return true;
    402. }
    403. }
    404. return false;
    405. }
    406.  
    407. /**
    408.   * Check to see if the program should continue by evaluation whether the plugin is already updated, or shouldn't be updated
    409.   */
    410. private boolean versionCheck(String title) {
    411. if (this.type != UpdateType.NO_VERSION_CHECK) {
    412. final String version = this.plugin.getDescription().getVersion();
    413. if (title.split(" v").length == 2) {
    414. final String remoteVersion = title.split(" v")[1].split(" ")[0]; // Get the newest file's version number
    415.  
    416. if (this.hasTag(version) || version.equalsIgnoreCase(remoteVersion)) {
    417. // We already have the latest version, or this build is tagged for no-update
    418. this.result = Updater.UpdateResult.NO_UPDATE;
    419. return false;
    420. }
    421. } else {
    422. // The file's name did not contain the string 'vVersion'
    423. final String authorInfo = this.plugin.getDescription().getAuthors().size() == 0 ? "" : " (" + this.plugin.getDescription().getAuthors().get(0) + ")";
    424. this.plugin.getLogger().warning("The author of this plugin" + authorInfo + " has misconfigured their Auto Update system");
    425. this.plugin.getLogger().warning("File versions should follow the format 'PluginName vVERSION'");
    426. this.plugin.getLogger().warning("Please notify the author of this error.");
    427. this.result = Updater.UpdateResult.FAIL_NOVERSION;
    428. return false;
    429. }
    430. }
    431. return true;
    432. }
    433.  
    434. /**
    435.   * Evaluate whether the version number is marked showing that it should not be updated by this program
    436.   */
    437. private boolean hasTag(String version) {
    438. for (final String string : Updater.NO_UPDATE_TAG) {
    439. if (version.contains(string)) {
    440. return true;
    441. }
    442. }
    443. return false;
    444. }
    445.  
    446. private boolean read() {
    447. try {
    448. final URLConnection conn = this.url.openConnection();
    449. conn.setConnectTimeout(5000);
    450.  
    451. if (this.apiKey != null) {
    452. conn.addRequestProperty("X-API-Key", this.apiKey);
    453. }
    454. conn.addRequestProperty("User-Agent", "Updater (by Gravity)");
    455.  
    456. conn.setDoOutput(true);
    457.  
    458. final BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
    459. final String response = reader.readLine();
    460.  
    461. final JSONArray array = (JSONArray) JSONValue.parse(response);
    462.  
    463. if (array.size() == 0) {
    464. this.plugin.getLogger().warning("The updater could not find any files for the project id " + this.id);
    465. this.result = UpdateResult.FAIL_BADID;
    466. return false;
    467. }
    468.  
    469. this.versionName = (String) ((JSONObject) array.get(array.size() - 1)).get(Updater.TITLE_VALUE);
    470. this.versionLink = (String) ((JSONObject) array.get(array.size() - 1)).get(Updater.LINK_VALUE);
    471. this.versionType = (String) ((JSONObject) array.get(array.size() - 1)).get(Updater.TYPE_VALUE);
    472. this.versionGameVersion = (String) ((JSONObject) array.get(array.size() - 1)).get(Updater.VERSION_VALUE);
    473.  
    474. return true;
    475. } catch (final IOException e) {
    476. if (e.getMessage().contains("HTTP response code: 403")) {
    477. this.plugin.getLogger().warning("dev.bukkit.org rejected the API key provided in plugins/Updater/config.yml");
    478. this.plugin.getLogger().warning("Please double-check your configuration to ensure it is correct.");
    479. this.result = UpdateResult.FAIL_APIKEY;
    480. } else {
    481. this.plugin.getLogger().warning("The updater could not contact dev.bukkit.org for updating.");
    482. 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.");
    483. this.result = UpdateResult.FAIL_DBO;
    484. }
    485. e.printStackTrace();
    486. return false;
    487. }
    488. }
    489.  
    490. private class UpdateRunnable implements Runnable {
    491.  
    492. @Override
    493. public void run() {
    494. if (Updater.this.url != null) {
    495. // Obtain the results of the project's file feed
    496. if (Updater.this.read()) {
    497. if (Updater.this.versionCheck(Updater.this.versionName)) {
    498. if ((Updater.this.versionLink != null) && (Updater.this.type != UpdateType.NO_DOWNLOAD)) {
    499. String name = Updater.this.file.getName();
    500. // If it's a zip file, it shouldn't be downloaded as the plugin's name
    501. if (Updater.this.versionLink.endsWith(".zip")) {
    502. final String[] split = Updater.this.versionLink.split("/");
    503. name = split[split.length - 1];
    504. }
    505. Updater.this.saveFile(new File(Updater.this.plugin.getDataFolder().getParent(), Updater.this.updateFolder), name, Updater.this.versionLink);
    506. } else {
    507. Updater.this.result = UpdateResult.UPDATE_AVAILABLE;
    508. }
    509. }
    510. }
    511. }
    512. }
    513. }
    514. }
    515.  
     
  2. Offline

    GusGold

    BlueMustache
    If the code you are using is straight from the Updater 2.0 thread, then i don't think that is the issue as I use it with no problems.
     
  3. Offline

    BlueMustache

    It has problems though.
    It created an updater folder, instead off PPMOTD
     
  4. Offline

    zeeveener

    The Updater class makes the folder automatically. You don't need to change it.

    Make sure you have a config option to shut off the Updater.
    So long as the configurations from your plugin go into the getDataFolder(), you should be fine.
     
Thread Status:
Not open for further replies.

Share This Page