| @@ -8,12 +8,53 @@ | |||||
| namespace rack { | namespace rack { | ||||
| struct RegisterButton : Button { | |||||
| void onAction(EventAction &e) override { | |||||
| std::thread t([&]() { | |||||
| systemOpenBrowser("https://vcvrack.com/"); | |||||
| }); | |||||
| t.detach(); | |||||
| } | |||||
| }; | |||||
| struct LogInButton : Button { | |||||
| TextField *emailField; | |||||
| TextField *passwordField; | |||||
| void onAction(EventAction &e) override { | |||||
| std::thread t(pluginLogIn, emailField->text, passwordField->text); | |||||
| t.detach(); | |||||
| passwordField->text = ""; | |||||
| } | |||||
| }; | |||||
| struct StatusLabel : Label { | |||||
| void step() override { | |||||
| text = pluginGetLoginStatus(); | |||||
| } | |||||
| }; | |||||
| struct ManageButton : Button { | |||||
| void onAction(EventAction &e) override { | |||||
| std::thread t([&]() { | |||||
| systemOpenBrowser("https://vcvrack.com/"); | |||||
| }); | |||||
| t.detach(); | |||||
| } | |||||
| }; | |||||
| struct SyncButton : Button { | struct SyncButton : Button { | ||||
| bool checked = false; | bool checked = false; | ||||
| /** Updates are available */ | |||||
| bool available = false; | bool available = false; | ||||
| /** Plugins have been updated */ | |||||
| bool completed = false; | bool completed = false; | ||||
| void step() override { | void step() override { | ||||
| // Check for plugin update on first step() | |||||
| if (!checked) { | if (!checked) { | ||||
| std::thread t([this]() { | std::thread t([this]() { | ||||
| if (pluginSync(true)) | if (pluginSync(true)) | ||||
| @@ -22,6 +63,7 @@ struct SyncButton : Button { | |||||
| t.detach(); | t.detach(); | ||||
| checked = true; | checked = true; | ||||
| } | } | ||||
| // Display message if we've completed updates | |||||
| if (completed) { | if (completed) { | ||||
| if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "All plugins have been updated. Close Rack and re-launch it to load new updates.")) { | if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "All plugins have been updated. Close Rack and re-launch it to load new updates.")) { | ||||
| windowClose(); | windowClose(); | ||||
| @@ -52,112 +94,83 @@ struct SyncButton : Button { | |||||
| }; | }; | ||||
| struct LogOutButton : Button { | |||||
| void onAction(EventAction &e) override { | |||||
| pluginLogOut(); | |||||
| } | |||||
| }; | |||||
| struct DownloadProgressBar : ProgressBar { | |||||
| void step() override { | |||||
| label = "Downloading"; | |||||
| std::string name = pluginGetDownloadName(); | |||||
| if (name != "") | |||||
| label += " " + name; | |||||
| setValue(100.0 * pluginGetDownloadProgress()); | |||||
| } | |||||
| }; | |||||
| struct CancelButton : Button { | |||||
| void onAction(EventAction &e) override { | |||||
| pluginCancelDownload(); | |||||
| } | |||||
| }; | |||||
| PluginManagerWidget::PluginManagerWidget() { | PluginManagerWidget::PluginManagerWidget() { | ||||
| box.size.y = BND_WIDGET_HEIGHT; | box.size.y = BND_WIDGET_HEIGHT; | ||||
| float margin = 5; | |||||
| { | { | ||||
| loginWidget = new Widget(); | |||||
| Vec pos = Vec(0, 0); | |||||
| struct RegisterButton : Button { | |||||
| void onAction(EventAction &e) override { | |||||
| std::thread t([&]() { | |||||
| systemOpenBrowser("https://vcvrack.com/"); | |||||
| }); | |||||
| t.detach(); | |||||
| } | |||||
| }; | |||||
| SequentialLayout *layout = Widget::create<SequentialLayout>(Vec(0, 0)); | |||||
| layout->spacing = 5; | |||||
| loginWidget = layout; | |||||
| Button *registerButton = new RegisterButton(); | Button *registerButton = new RegisterButton(); | ||||
| registerButton->box.pos = pos; | |||||
| registerButton->box.size.x = 75; | registerButton->box.size.x = 75; | ||||
| registerButton->text = "Register"; | registerButton->text = "Register"; | ||||
| loginWidget->addChild(registerButton); | loginWidget->addChild(registerButton); | ||||
| pos.x += registerButton->box.size.x; | |||||
| pos.x += margin; | |||||
| TextField *emailField = new TextField(); | TextField *emailField = new TextField(); | ||||
| emailField->box.pos = pos; | |||||
| emailField->box.size.x = 175; | emailField->box.size.x = 175; | ||||
| emailField->placeholder = "Email"; | emailField->placeholder = "Email"; | ||||
| loginWidget->addChild(emailField); | loginWidget->addChild(emailField); | ||||
| pos.x += emailField->box.size.x; | |||||
| pos.x += margin; | |||||
| PasswordField *passwordField = new PasswordField(); | PasswordField *passwordField = new PasswordField(); | ||||
| passwordField->box.pos = pos; | |||||
| passwordField->box.size.x = 175; | passwordField->box.size.x = 175; | ||||
| passwordField->placeholder = "Password"; | passwordField->placeholder = "Password"; | ||||
| loginWidget->addChild(passwordField); | loginWidget->addChild(passwordField); | ||||
| pos.x += passwordField->box.size.x; | |||||
| struct LogInButton : Button { | |||||
| TextField *emailField; | |||||
| TextField *passwordField; | |||||
| void onAction(EventAction &e) override { | |||||
| std::thread t(pluginLogIn, emailField->text, passwordField->text); | |||||
| t.detach(); | |||||
| passwordField->text = ""; | |||||
| } | |||||
| }; | |||||
| pos.x += margin; | |||||
| LogInButton *logInButton = new LogInButton(); | LogInButton *logInButton = new LogInButton(); | ||||
| logInButton->box.pos = pos; | |||||
| logInButton->box.size.x = 100; | logInButton->box.size.x = 100; | ||||
| logInButton->text = "Log in"; | logInButton->text = "Log in"; | ||||
| logInButton->emailField = emailField; | logInButton->emailField = emailField; | ||||
| logInButton->passwordField = passwordField; | logInButton->passwordField = passwordField; | ||||
| loginWidget->addChild(logInButton); | loginWidget->addChild(logInButton); | ||||
| pos.x += logInButton->box.size.x; | |||||
| struct StatusLabel : Label { | |||||
| void step() override { | |||||
| text = pluginGetLoginStatus(); | |||||
| } | |||||
| }; | |||||
| Label *label = new StatusLabel(); | Label *label = new StatusLabel(); | ||||
| label->box.pos = pos; | |||||
| loginWidget->addChild(label); | loginWidget->addChild(label); | ||||
| addChild(loginWidget); | addChild(loginWidget); | ||||
| } | } | ||||
| { | { | ||||
| manageWidget = new Widget(); | |||||
| Vec pos = Vec(0, 0); | |||||
| struct ManageButton : Button { | |||||
| void onAction(EventAction &e) override { | |||||
| std::thread t([&]() { | |||||
| systemOpenBrowser("https://vcvrack.com/"); | |||||
| }); | |||||
| t.detach(); | |||||
| } | |||||
| }; | |||||
| SequentialLayout *layout = Widget::create<SequentialLayout>(Vec(0, 0)); | |||||
| layout->spacing = 5; | |||||
| manageWidget = layout; | |||||
| Button *manageButton = new ManageButton(); | Button *manageButton = new ManageButton(); | ||||
| manageButton->box.pos = pos; | |||||
| manageButton->box.size.x = 125; | manageButton->box.size.x = 125; | ||||
| manageButton->text = "Manage plugins"; | manageButton->text = "Manage plugins"; | ||||
| manageWidget->addChild(manageButton); | manageWidget->addChild(manageButton); | ||||
| pos.x += manageButton->box.size.x; | |||||
| pos.x += margin; | |||||
| Button *syncButton = new SyncButton(); | Button *syncButton = new SyncButton(); | ||||
| syncButton->box.pos = pos; | |||||
| syncButton->box.size.x = 125; | syncButton->box.size.x = 125; | ||||
| syncButton->text = "Update plugins"; | syncButton->text = "Update plugins"; | ||||
| manageWidget->addChild(syncButton); | manageWidget->addChild(syncButton); | ||||
| pos.x += syncButton->box.size.x; | |||||
| struct LogOutButton : Button { | |||||
| void onAction(EventAction &e) override { | |||||
| pluginLogOut(); | |||||
| } | |||||
| }; | |||||
| pos.x += margin; | |||||
| Button *logOutButton = new LogOutButton(); | Button *logOutButton = new LogOutButton(); | ||||
| logOutButton->box.pos = pos; | |||||
| logOutButton->box.size.x = 100; | logOutButton->box.size.x = 100; | ||||
| logOutButton->text = "Log out"; | logOutButton->text = "Log out"; | ||||
| manageWidget->addChild(logOutButton); | manageWidget->addChild(logOutButton); | ||||
| @@ -166,37 +179,20 @@ PluginManagerWidget::PluginManagerWidget() { | |||||
| } | } | ||||
| { | { | ||||
| downloadWidget = new Widget(); | |||||
| Vec pos = Vec(0, 0); | |||||
| struct DownloadProgressBar : ProgressBar { | |||||
| void step() override { | |||||
| label = "Downloading"; | |||||
| std::string name = pluginGetDownloadName(); | |||||
| if (name != "") | |||||
| label += " " + name; | |||||
| setValue(100.0 * pluginGetDownloadProgress()); | |||||
| } | |||||
| }; | |||||
| SequentialLayout *layout = Widget::create<SequentialLayout>(Vec(0, 0)); | |||||
| layout->spacing = 5; | |||||
| downloadWidget = layout; | |||||
| ProgressBar *downloadProgress = new DownloadProgressBar(); | ProgressBar *downloadProgress = new DownloadProgressBar(); | ||||
| downloadProgress->box.pos = pos; | |||||
| downloadProgress->box.size.x = 300; | downloadProgress->box.size.x = 300; | ||||
| downloadProgress->setLimits(0, 100); | downloadProgress->setLimits(0, 100); | ||||
| downloadProgress->unit = "%"; | downloadProgress->unit = "%"; | ||||
| downloadWidget->addChild(downloadProgress); | downloadWidget->addChild(downloadProgress); | ||||
| pos.x += downloadProgress->box.size.x; | |||||
| // struct CancelButton : Button { | |||||
| // void onAction(EventAction &e) override { | |||||
| // pluginCancelDownload(); | |||||
| // } | |||||
| // }; | |||||
| // pos.x += margin; | |||||
| // Button *logOutButton = new CancelButton(); | |||||
| // logOutButton->box.pos = pos; | |||||
| // logOutButton->box.size.x = 100; | |||||
| // logOutButton->text = "Cancel"; | |||||
| // downloadWidget->addChild(logOutButton); | |||||
| // Button *cancelButton = new CancelButton(); | |||||
| // cancelButton->box.size.x = 100; | |||||
| // cancelButton->text = "Cancel"; | |||||
| // downloadWidget->addChild(cancelButton); | |||||
| addChild(downloadWidget); | addChild(downloadWidget); | ||||
| } | } | ||||
| @@ -5,8 +5,8 @@ namespace rack { | |||||
| std::string gApplicationName = "VCV Rack"; | std::string gApplicationName = "VCV Rack"; | ||||
| std::string gApplicationVersion = TOSTRING(VERSION); | std::string gApplicationVersion = TOSTRING(VERSION); | ||||
| std::string gApiHost = "https://api.vcvrack.com"; | |||||
| // std::string gApiHost = "http://localhost:8081"; | |||||
| // std::string gApiHost = "https://api.vcvrack.com"; | |||||
| std::string gApiHost = "http://localhost:8081"; | |||||
| RackWidget *gRackWidget = NULL; | RackWidget *gRackWidget = NULL; | ||||
| Toolbar *gToolbar = NULL; | Toolbar *gToolbar = NULL; | ||||
| @@ -126,98 +126,77 @@ static bool loadPlugin(std::string path) { | |||||
| return true; | return true; | ||||
| } | } | ||||
| static bool syncPlugin(json_t *pluginJ, bool dryRun) { | |||||
| json_t *slugJ = json_object_get(pluginJ, "slug"); | |||||
| if (!slugJ) | |||||
| static bool syncPlugin(std::string slug, json_t *manifestJ, bool dryRun) { | |||||
| // Get latest version | |||||
| json_t *latestVersionJ = json_object_get(manifestJ, "latestVersion"); | |||||
| if (!latestVersionJ) { | |||||
| warn("Could not get latest version of plugin %s", slug.c_str()); | |||||
| return false; | return false; | ||||
| std::string slug = json_string_value(slugJ); | |||||
| // Get community version | |||||
| std::string version; | |||||
| json_t *versionJ = json_object_get(pluginJ, "version"); | |||||
| if (versionJ) { | |||||
| version = json_string_value(versionJ); | |||||
| } | } | ||||
| std::string latestVersion = json_string_value(latestVersionJ); | |||||
| // Check whether we already have a plugin with the same slug and version | // Check whether we already have a plugin with the same slug and version | ||||
| for (Plugin *plugin : gPlugins) { | |||||
| if (plugin->slug == slug) { | |||||
| // plugin->version might be blank, so adding a version of the manifest will update the plugin | |||||
| if (plugin->version == version) | |||||
| return false; | |||||
| } | |||||
| } | |||||
| json_t *nameJ = json_object_get(pluginJ, "name"); | |||||
| if (!nameJ) | |||||
| Plugin *plugin = pluginGetPlugin(slug); | |||||
| if (plugin && plugin->version == latestVersion) { | |||||
| return false; | return false; | ||||
| std::string name = json_string_value(nameJ); | |||||
| } | |||||
| std::string download; | |||||
| std::string sha256; | |||||
| json_t *nameJ = json_object_get(manifestJ, "name"); | |||||
| std::string name; | |||||
| if (nameJ) { | |||||
| name = json_string_value(nameJ); | |||||
| } | |||||
| else { | |||||
| name = slug; | |||||
| } | |||||
| json_t *downloadsJ = json_object_get(pluginJ, "downloads"); | |||||
| if (downloadsJ) { | |||||
| #if ARCH_WIN | #if ARCH_WIN | ||||
| #define DOWNLOADS_ARCH "win" | |||||
| std::string arch = "win"; | |||||
| #elif ARCH_MAC | #elif ARCH_MAC | ||||
| #define DOWNLOADS_ARCH "mac" | |||||
| std::string arch = "mac"; | |||||
| #elif ARCH_LIN | #elif ARCH_LIN | ||||
| #define DOWNLOADS_ARCH "lin" | |||||
| std::string arch = "lin"; | |||||
| #endif | #endif | ||||
| json_t *archJ = json_object_get(downloadsJ, DOWNLOADS_ARCH); | |||||
| if (archJ) { | |||||
| // Get download URL | |||||
| json_t *downloadJ = json_object_get(archJ, "download"); | |||||
| if (downloadJ) | |||||
| download = json_string_value(downloadJ); | |||||
| // Get SHA256 hash | |||||
| json_t *sha256J = json_object_get(archJ, "sha256"); | |||||
| if (sha256J) | |||||
| sha256 = json_string_value(sha256J); | |||||
| } | |||||
| } | |||||
| json_t *productIdJ = json_object_get(pluginJ, "productId"); | |||||
| if (productIdJ) { | |||||
| download = gApiHost; | |||||
| download += "/download"; | |||||
| download += "?slug="; | |||||
| download += slug; | |||||
| download += "&token="; | |||||
| download += requestEscape(gToken); | |||||
| } | |||||
| if (download.empty()) { | |||||
| warn("Could not get download URL for plugin %s", slug.c_str()); | |||||
| return false; | |||||
| std::string downloadUrl; | |||||
| downloadUrl = gApiHost; | |||||
| downloadUrl += "/download"; | |||||
| if (dryRun) { | |||||
| downloadUrl += "/available"; | |||||
| } | } | ||||
| // If plugin is not loaded, download the zip file to /plugins | |||||
| downloadName = name; | |||||
| downloadProgress = 0.0; | |||||
| // Download zip | |||||
| std::string pluginsDir = assetLocal("plugins"); | |||||
| std::string pluginPath = pluginsDir + "/" + slug; | |||||
| std::string zipPath = pluginPath + ".zip"; | |||||
| bool success = requestDownload(download, zipPath, &downloadProgress); | |||||
| if (!success) { | |||||
| warn("Plugin %s download was unsuccessful", slug.c_str()); | |||||
| return false; | |||||
| downloadUrl += "?token=" + requestEscape(gToken); | |||||
| downloadUrl += "&slug=" + requestEscape(slug); | |||||
| downloadUrl += "&version=" + requestEscape(latestVersion); | |||||
| downloadUrl += "&arch=" + requestEscape(arch); | |||||
| if (dryRun) { | |||||
| // Check if available | |||||
| json_t *availableResJ = requestJson(METHOD_GET, downloadUrl, NULL); | |||||
| if (!availableResJ) { | |||||
| warn("Could not check whether download is available"); | |||||
| return false; | |||||
| } | |||||
| defer({ | |||||
| json_decref(availableResJ); | |||||
| }); | |||||
| json_t *successJ = json_object_get(availableResJ, "success"); | |||||
| return json_boolean_value(successJ); | |||||
| } | } | ||||
| else { | |||||
| downloadName = name; | |||||
| downloadProgress = 0.0; | |||||
| info("Downloading plugin %s %s %s", slug.c_str(), latestVersion.c_str(), arch.c_str()); | |||||
| if (!sha256.empty()) { | |||||
| // Check SHA256 hash | |||||
| std::string actualSha256 = requestSHA256File(zipPath); | |||||
| if (actualSha256 != sha256) { | |||||
| warn("Plugin %s does not match expected SHA256 checksum", slug.c_str()); | |||||
| // Download zip | |||||
| std::string pluginDest = assetLocal("plugins/" + slug + ".zip"); | |||||
| if (!requestDownload(downloadUrl, pluginDest, &downloadProgress)) { | |||||
| warn("Plugin %s download was unsuccessful", slug.c_str()); | |||||
| return false; | return false; | ||||
| } | } | ||||
| } | |||||
| downloadName = ""; | |||||
| return true; | |||||
| downloadName = ""; | |||||
| return true; | |||||
| } | |||||
| } | } | ||||
| static void loadPlugins(std::string path) { | static void loadPlugins(std::string path) { | ||||
| @@ -378,7 +357,6 @@ void pluginDestroy() { | |||||
| } | } | ||||
| bool pluginSync(bool dryRun) { | bool pluginSync(bool dryRun) { | ||||
| return false; | |||||
| if (gToken.empty()) | if (gToken.empty()) | ||||
| return false; | return false; | ||||
| @@ -389,74 +367,69 @@ bool pluginSync(bool dryRun) { | |||||
| downloadProgress = 0.0; | downloadProgress = 0.0; | ||||
| downloadName = "Updating plugins..."; | downloadName = "Updating plugins..."; | ||||
| } | } | ||||
| defer({ | |||||
| isDownloading = false; | |||||
| }); | |||||
| json_t *resJ = NULL; | |||||
| json_t *communityResJ = NULL; | |||||
| // Get user's plugins list | |||||
| json_t *pluginsReqJ = json_object(); | |||||
| json_object_set(pluginsReqJ, "token", json_string(gToken.c_str())); | |||||
| json_t *pluginsResJ = requestJson(METHOD_GET, gApiHost + "/plugins", pluginsReqJ); | |||||
| json_decref(pluginsReqJ); | |||||
| if (!pluginsResJ) { | |||||
| warn("Request for user's plugins failed"); | |||||
| return false; | |||||
| } | |||||
| defer({ | |||||
| json_decref(pluginsResJ); | |||||
| }); | |||||
| try { | |||||
| // Download plugin slugs | |||||
| json_t *reqJ = json_object(); | |||||
| json_object_set(reqJ, "version", json_string(gApplicationVersion.c_str())); | |||||
| json_object_set(reqJ, "token", json_string(gToken.c_str())); | |||||
| resJ = requestJson(METHOD_GET, gApiHost + "/plugins", reqJ); | |||||
| json_decref(reqJ); | |||||
| if (!resJ) | |||||
| throw std::runtime_error("No response from server"); | |||||
| json_t *errorJ = json_object_get(pluginsResJ, "error"); | |||||
| if (errorJ) { | |||||
| warn("Request for user's plugins returned an error: %s", json_string_value(errorJ)); | |||||
| return false; | |||||
| } | |||||
| json_t *errorJ = json_object_get(resJ, "error"); | |||||
| if (errorJ) | |||||
| throw std::runtime_error(json_string_value(errorJ)); | |||||
| // Download community plugins | |||||
| communityResJ = requestJson(METHOD_GET, gApiHost + "/community/plugins", NULL); | |||||
| if (!communityResJ) | |||||
| throw std::runtime_error("No response from server"); | |||||
| json_t *communityErrorJ = json_object_get(communityResJ, "error"); | |||||
| if (communityErrorJ) | |||||
| throw std::runtime_error(json_string_value(communityErrorJ)); | |||||
| // Check each plugin in list of plugin slugs | |||||
| json_t *pluginSlugsJ = json_object_get(resJ, "plugins"); | |||||
| json_t *communityPluginsJ = json_object_get(communityResJ, "plugins"); | |||||
| size_t index; | |||||
| json_t *pluginSlugJ; | |||||
| json_array_foreach(pluginSlugsJ, index, pluginSlugJ) { | |||||
| std::string slug = json_string_value(pluginSlugJ); | |||||
| // Search for plugin slug in community | |||||
| size_t communityIndex; | |||||
| json_t *communityPluginJ = NULL; | |||||
| json_array_foreach(communityPluginsJ, communityIndex, communityPluginJ) { | |||||
| json_t *communitySlugJ = json_object_get(communityPluginJ, "slug"); | |||||
| if (!communitySlugJ) | |||||
| continue; | |||||
| std::string communitySlug = json_string_value(communitySlugJ); | |||||
| if (slug == communitySlug) | |||||
| break; | |||||
| } | |||||
| // Get community manifests | |||||
| json_t *manifestsResJ = requestJson(METHOD_GET, gApiHost + "/community/manifests", NULL); | |||||
| if (!manifestsResJ) { | |||||
| warn("Request for community manifests failed"); | |||||
| return false; | |||||
| } | |||||
| defer({ | |||||
| json_decref(manifestsResJ); | |||||
| }); | |||||
| // Sync plugin | |||||
| if (syncPlugin(communityPluginJ, dryRun)) { | |||||
| available = true; | |||||
| } | |||||
| else { | |||||
| warn("Plugin %s not found in community", slug.c_str()); | |||||
| } | |||||
| } | |||||
| // Check each plugin in list of plugin slugs | |||||
| json_t *pluginsJ = json_object_get(pluginsResJ, "plugins"); | |||||
| if (!pluginsJ) { | |||||
| warn("No plugins array"); | |||||
| return false; | |||||
| } | } | ||||
| catch (std::runtime_error &e) { | |||||
| warn("Plugin sync error: %s", e.what()); | |||||
| json_t *manifestsJ = json_object_get(manifestsResJ, "manifests"); | |||||
| if (!manifestsJ) { | |||||
| warn("No manifests object"); | |||||
| return false; | |||||
| } | } | ||||
| if (communityResJ) | |||||
| json_decref(communityResJ); | |||||
| size_t slugIndex; | |||||
| json_t *slugJ; | |||||
| json_array_foreach(pluginsJ, slugIndex, slugJ) { | |||||
| std::string slug = json_string_value(slugJ); | |||||
| // Search for slug in manifests | |||||
| const char *manifestSlug; | |||||
| json_t *manifestJ = NULL; | |||||
| json_object_foreach(manifestsJ, manifestSlug, manifestJ) { | |||||
| if (slug == std::string(manifestSlug)) | |||||
| break; | |||||
| } | |||||
| if (resJ) | |||||
| json_decref(resJ); | |||||
| if (!manifestJ) | |||||
| continue; | |||||
| if (!dryRun) { | |||||
| isDownloading = false; | |||||
| if (syncPlugin(slug, manifestJ, dryRun)) { | |||||
| available = true; | |||||
| } | |||||
| } | } | ||||
| return available; | return available; | ||||
| @@ -131,8 +131,9 @@ bool requestDownload(std::string url, std::string filename, float *progress) { | |||||
| curl_easy_setopt(curl, CURLOPT_XFERINFODATA, progress); | curl_easy_setopt(curl, CURLOPT_XFERINFODATA, progress); | ||||
| curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); | ||||
| curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false); | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false); | ||||
| // Fail on 4xx and 5xx HTTP codes | |||||
| curl_easy_setopt(curl, CURLOPT_FAILONERROR, true); | |||||
| info("Downloading %s", url.c_str()); | |||||
| CURLcode res = curl_easy_perform(curl); | CURLcode res = curl_easy_perform(curl); | ||||
| curl_easy_cleanup(curl); | curl_easy_cleanup(curl); | ||||