Simple Implementation Example of Auto-Notifying Users of App/Plugin Updates (and Maybe Downloading Them)
-
If you're clever, you can use nifty stuff with GIT to fully automate this process.
I am not. So I offer a less-ambitious implementation.
This code will (should) run as-is, and I've uploaded the data and payload file to my server, so you can simply cut and paste.
But it doesn't run correctly - there's something I couldn't quite work out. A fallback mechanism still delivers the download, but it's not optimal. If you can see where I've gone wrong, (or have improvements), please post, and I'll revise.
Further Reading:
- HISE's Server class documentation.
- HISE's Download class documentation.
- d.sealy's YouTube tutorial on file downloads.
- Immanuel Kant's Critique of Pure Reason.
////////////////////////////////////////////////////// // / // Version Check & Download / // / ////////////////////////////////////////////////////// // // This function checks to see if there's a newer // version of your app/plugin than the version the // functin is called from. // // There are two files. The first is an online data file // containing the information about the current // version of your app/plugin; the other is the // current version of your app/plugin. // // The data file is of the (non-nested) JSON variety. // It can be generated dynamically. But for ultimate // simplicity, it can simply be a static text file // that you manually edit (whenever you release a new // version of your exciting app/plugin). // // The data in this example has key/value pairs (both are // strings): the current version ID, and the filename of the // zipped app/plugin. You can create as many of these // pairs as you like in your data file, for whatever // nefarious purpose(s) you intend. // // We'll use GET instead of POST. With GET, you are // exposing your data, publicly. GET is simpler to // implement, though, and there shouldn't be anything in // your data file that you don't want people to see, // regardless. Do ensure you drop an index.html file // into any exposed directory. // // Much of this implementation example is architected // on error-handling, as it's non-Turing-decidable to // ensure that any network operation works as it // should (or at least as we cab reasonably expect). The // internet is a lawless place, and even when all the fates // align to grant us a functional download, you can reliably // count on the user's operating system to randomly throw up // roadblocks. All we can control is how we respond to these // problems, and aim to adequately inform users so they can // solve the problems themselves (instead of emailing us // about a problem we probably can't). // // It's helpful when tutorial code simply works, so // we're not making an actual version comparison, // and the referenced files are actually online (though not // the droids you're looking for). // // Speaking of which, I wasn't able to get the native download // code I wrote here to work on my machine/network/timeline. If // you spot the error (or know which Gods to appease), please // post a note here on the forum. // Here's a bunch of global variables. A better implementation // would employ more local scoping. But I'm a lazy, lazy man. const var baseUpdateURL = "https://bemhost.net"; const var dataFileName = "update_data.json"; const var downloadDirectory = "/hise_forum/"; const downloadsFolder = FileSystem.getFolder(FileSystem.Downloads); var downloadURL = null; var downloadObject = null; // Base-Level Function. inline function checkForAppUpdate() { // Let's first check if we can find an internet connection. Because if we can't... if (Server.isOnline()) { // Clear the Server Controller tile. Server.cleanFinishedDownloads(); // Specify the base URL where we're doing business. Server.setBaseURL(baseUpdateURL); // Clear the HTTP header, in case angry ferrets left data there. Server.setHttpHeader(""); local updateDataFile = downloadDirectory + dataFileName; // This function does two things: One is to access (and parse) your online // JSON data file; and second, to define the callback that's executed // when the server responds. The key phrase here is "when", because if // the server doesn't respond, the callback never rings. But if it does, // there's two variables returned that we'll use: "status" and "response". Server.callWithGET(updateDataFile, {}, function(status, response) { // Ask the server if it's ready to mingle. if (status == Server.StatusOK) { // Check if both our keys can be found in the online data file before proceeding. if (isDefined(response[0].version) && isDefined(response[0].filename)) { // Get the current version string. var versionString = response[0].version; // Compare the currently running app/plugin version with the one from the online data file. // The actual comparison here would be: (Engine.getVersion() == versionString), but in the interest // of making this example more useful, we'll just fire the "update available" condition. if (Engine.getVersion() == versionString) { // Get the file name to download from the data file. reg fileName = response[0].filename; // Here, we'll create the full download URL for our payload file. downloadURL = baseUpdateURL + downloadDirectory + fileName; // Ask the user about downloading the current version with a showYesNoWindow. I'm a // little disappointed that Christoph didn't also include the quantum-mechanics // version, showYesNoMaybeWindow. Anyway, a "yes" response executes the download callback. // We want to do something a little tricky here - pass a variable to a callback function, so // that's what the brackets are for around "filename". Engine.showYesNoWindow ( "Update Available", "An updated version of this tantalising product is available. Do you want to download it?", function[fileName] () { // Create a file stub. We'll use the handy FileSystem class here to provide a reference // to the user's download folder, and create the stub (as being inside out that folder). var fileStub = FileSystem.getFolder(FileSystem.Documents).getChildFile(fileName); // Next, we'll create a Download object called...something. The Download class // provides lots of fu house of exciting things to learn and do about the download, itself. var downloadObject = Server.downloadFile (downloadURL, {}, fileStub, DownloadCallback); } ); } else {} // The user is running the current version. Nothing to do. // Couldn't find the key/value pairs in the online data file. } else Engine.showMessageBox("Auto-update Error", "There was an error while checking for an updated version: Remote version data could not be accessed.", 0); // Server didn't respond with 200 code. } else Engine.showMessageBox("Auto-update Error", "There was an error while checking for an updated version: The remote server is offline.", 0); }); // Internet connection not found. } else Engine.showMessageBox( "Interwebs Not Found","As far as we can tell, you're not connected to the internet. So we can't automatically check if there's a new version of our award-awaiting product.", 0); } // One of the nice things about HISE's API for handling downloads is that there's many ways you // can architect your implementation. I chose to have a giant function to deal with many of the // possible problems, and a separate one to deal with the download, itself. Note that this is // not an inline function. And also, that there are still things that can go wrong here, even if // your code is perfect (unlike mine) - if that's the case, then the problem is likely the // OS or the user's doing. Probably. Maybe? function DownloadCallback () { if (this.data.finished) { // Our download was a success. if (this.data.success) Engine.showMessageBox("Download Complete", "The file has finished downloading.", 0); // This file WILL BE DOWNLOADED! else Engine.openWebsite(downloadURL); } }
-
@clevername27 I think
downloadURL = baseUpdateURL + downloadDirectory + fileName;
is causing your problem. Server.downloadFile() is looking for a subURL, so baseUpdateURL is redundant. Weirdly, using my own website worked regardless, but yours did kick off the fallback.
Either way, thanks for sharing this! Super helpful and posted right when I started trying to implement this exact feature :)
-
@ally Cheers, mate.