|
|
@@ -0,0 +1,423 @@
|
|
|
+#include <util/threading.h>
|
|
|
+#include <util/platform.h>
|
|
|
+#include <util/darray.h>
|
|
|
+#include <util/dstr.h>
|
|
|
+#include <obs-data.h>
|
|
|
+#include <curl/curl.h>
|
|
|
+#include "file-updater.h"
|
|
|
+
|
|
|
+#define warn(msg, ...) \
|
|
|
+ blog(LOG_WARNING, "%s"msg, info->log_prefix, ##__VA_ARGS__)
|
|
|
+#define info(msg, ...) \
|
|
|
+ blog(LOG_WARNING, "%s"msg, info->log_prefix, ##__VA_ARGS__)
|
|
|
+
|
|
|
+struct update_info {
|
|
|
+ char error[CURL_ERROR_SIZE];
|
|
|
+ struct curl_slist *header;
|
|
|
+ DARRAY(uint8_t) file_data;
|
|
|
+ char *user_agent;
|
|
|
+ CURL *curl;
|
|
|
+ char *url;
|
|
|
+
|
|
|
+ /* directories */
|
|
|
+ char *local;
|
|
|
+ char *cache;
|
|
|
+ char *temp;
|
|
|
+
|
|
|
+ const char *remote_url;
|
|
|
+ obs_data_t *local_package;
|
|
|
+ obs_data_t *cache_package;
|
|
|
+ obs_data_t *remote_package;
|
|
|
+
|
|
|
+ confirm_file_callback_t callback;
|
|
|
+ void *param;
|
|
|
+
|
|
|
+ pthread_t thread;
|
|
|
+ bool thread_created;
|
|
|
+ char *log_prefix;
|
|
|
+};
|
|
|
+
|
|
|
+void update_info_destroy(struct update_info *info)
|
|
|
+{
|
|
|
+ if (!info)
|
|
|
+ return;
|
|
|
+
|
|
|
+ if (info->thread_created)
|
|
|
+ pthread_join(info->thread, NULL);
|
|
|
+
|
|
|
+ da_free(info->file_data);
|
|
|
+ bfree(info->log_prefix);
|
|
|
+ bfree(info->user_agent);
|
|
|
+ bfree(info->temp);
|
|
|
+ bfree(info->cache);
|
|
|
+ bfree(info->local);
|
|
|
+ bfree(info->url);
|
|
|
+
|
|
|
+ if (info->header)
|
|
|
+ curl_slist_free_all(info->header);
|
|
|
+ if (info->curl)
|
|
|
+ curl_easy_cleanup(info->curl);
|
|
|
+ if (info->local_package)
|
|
|
+ obs_data_release(info->local_package);
|
|
|
+ if (info->cache_package)
|
|
|
+ obs_data_release(info->cache_package);
|
|
|
+ if (info->remote_package)
|
|
|
+ obs_data_release(info->remote_package);
|
|
|
+ bfree(info);
|
|
|
+}
|
|
|
+
|
|
|
+static size_t http_write(uint8_t *ptr, size_t size, size_t nmemb,
|
|
|
+ struct update_info *info)
|
|
|
+{
|
|
|
+ size_t total = size * nmemb;
|
|
|
+ if (total)
|
|
|
+ da_push_back_array(info->file_data, ptr, total);
|
|
|
+
|
|
|
+ return total;
|
|
|
+}
|
|
|
+
|
|
|
+static bool do_http_request(struct update_info *info, const char *url)
|
|
|
+{
|
|
|
+ CURLcode code;
|
|
|
+ uint8_t null_terminator = 0;
|
|
|
+
|
|
|
+ da_resize(info->file_data, 0);
|
|
|
+ curl_easy_setopt(info->curl, CURLOPT_URL, url);
|
|
|
+ curl_easy_setopt(info->curl, CURLOPT_HTTPHEADER, info->header);
|
|
|
+ curl_easy_setopt(info->curl, CURLOPT_ERRORBUFFER, info->error);
|
|
|
+ curl_easy_setopt(info->curl, CURLOPT_WRITEFUNCTION, http_write);
|
|
|
+ curl_easy_setopt(info->curl, CURLOPT_WRITEDATA, info);
|
|
|
+ curl_easy_setopt(info->curl, CURLOPT_FAILONERROR, true);
|
|
|
+
|
|
|
+ code = curl_easy_perform(info->curl);
|
|
|
+ if (code != CURLE_OK) {
|
|
|
+ warn("Remote update of URL \"%s\" failed: %s", url,
|
|
|
+ info->error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ da_push_back(info->file_data, &null_terminator);
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+static char *get_path(const char *dir, const char *file)
|
|
|
+{
|
|
|
+ struct dstr str = {0};
|
|
|
+
|
|
|
+ dstr_copy(&str, dir);
|
|
|
+
|
|
|
+ if (str.array && dstr_end(&str) != '/' && dstr_end(&str) != '\\')
|
|
|
+ dstr_cat_ch(&str, '/');
|
|
|
+
|
|
|
+ dstr_cat(&str, file);
|
|
|
+ return str.array;
|
|
|
+}
|
|
|
+
|
|
|
+static inline obs_data_t *get_package(const char *base_path, const char *file)
|
|
|
+{
|
|
|
+ char *full_path = get_path(base_path, file);
|
|
|
+ obs_data_t *package = obs_data_create_from_json_file(full_path);
|
|
|
+ bfree(full_path);
|
|
|
+ return package;
|
|
|
+}
|
|
|
+
|
|
|
+static bool init_update(struct update_info *info)
|
|
|
+{
|
|
|
+ struct dstr user_agent = {0};
|
|
|
+
|
|
|
+ info->curl = curl_easy_init();
|
|
|
+ if (!info->curl) {
|
|
|
+ warn("Could not initialize Curl");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ info->local_package = get_package(info->local, "package.json");
|
|
|
+ info->cache_package = get_package(info->cache, "package.json");
|
|
|
+
|
|
|
+ dstr_copy(&user_agent, "User-Agent: ");
|
|
|
+ dstr_cat(&user_agent, info->user_agent);
|
|
|
+
|
|
|
+ info->header = curl_slist_append(info->header, user_agent.array);
|
|
|
+
|
|
|
+ dstr_free(&user_agent);
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+static void copy_local_to_cache(struct update_info *info, const char *file)
|
|
|
+{
|
|
|
+ char *local_file_path = get_path(info->local, file);
|
|
|
+ char *cache_file_path = get_path(info->cache, file);
|
|
|
+ char *temp_file_path = get_path(info->temp, file);
|
|
|
+
|
|
|
+ os_copyfile(local_file_path, temp_file_path);
|
|
|
+ os_unlink(cache_file_path);
|
|
|
+ os_rename(temp_file_path, cache_file_path);
|
|
|
+
|
|
|
+ bfree(local_file_path);
|
|
|
+ bfree(cache_file_path);
|
|
|
+ bfree(temp_file_path);
|
|
|
+}
|
|
|
+
|
|
|
+static void enum_files(obs_data_t *package,
|
|
|
+ bool (*enum_func)(void *param, obs_data_t *file),
|
|
|
+ void *param)
|
|
|
+{
|
|
|
+ obs_data_array_t *array = obs_data_get_array(package, "files");
|
|
|
+ size_t num;
|
|
|
+
|
|
|
+ if (!array)
|
|
|
+ return;
|
|
|
+
|
|
|
+ num = obs_data_array_count(array);
|
|
|
+
|
|
|
+ for (size_t i = 0; i < num; i++) {
|
|
|
+ obs_data_t *file = obs_data_array_item(array, i);
|
|
|
+ bool continue_enum = enum_func(param, file);
|
|
|
+ obs_data_release(file);
|
|
|
+
|
|
|
+ if (!continue_enum)
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ obs_data_array_release(array);
|
|
|
+}
|
|
|
+
|
|
|
+struct file_update_data {
|
|
|
+ const char *name;
|
|
|
+ int version;
|
|
|
+ bool newer;
|
|
|
+ bool found;
|
|
|
+};
|
|
|
+
|
|
|
+static bool newer_than_cache(void *param, obs_data_t *cache_file)
|
|
|
+{
|
|
|
+ struct file_update_data *input = param;
|
|
|
+ const char *name = obs_data_get_string(cache_file, "name");
|
|
|
+ int version = (int)obs_data_get_int(cache_file, "version");
|
|
|
+
|
|
|
+ if (strcmp(input->name, name) == 0) {
|
|
|
+ input->found = true;
|
|
|
+ input->newer = input->version > version;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+static bool update_files_to_local(void *param, obs_data_t *local_file)
|
|
|
+{
|
|
|
+ struct update_info *info = param;
|
|
|
+ struct file_update_data data = {
|
|
|
+ .name = obs_data_get_string(local_file, "name"),
|
|
|
+ .version = (int)obs_data_get_int(local_file, "version")
|
|
|
+ };
|
|
|
+
|
|
|
+ enum_files(info->cache_package, newer_than_cache, &data);
|
|
|
+ if (data.newer || !data.found)
|
|
|
+ copy_local_to_cache(info, data.name);
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+static int update_local_version(struct update_info *info)
|
|
|
+{
|
|
|
+ int local_version;
|
|
|
+ int cache_version = 0;
|
|
|
+
|
|
|
+ local_version = (int)obs_data_get_int(info->local_package, "version");
|
|
|
+ cache_version = (int)obs_data_get_int(info->cache_package, "version");
|
|
|
+
|
|
|
+ /* if local cached version is out of date, copy new version */
|
|
|
+ if (cache_version < local_version) {
|
|
|
+ enum_files(info->local_package, update_files_to_local, info);
|
|
|
+ copy_local_to_cache(info, "package.json");
|
|
|
+
|
|
|
+ obs_data_release(info->cache_package);
|
|
|
+ obs_data_addref(info->local_package);
|
|
|
+ info->cache_package = info->local_package;
|
|
|
+
|
|
|
+ return local_version;
|
|
|
+ }
|
|
|
+
|
|
|
+ return cache_version;
|
|
|
+}
|
|
|
+
|
|
|
+static inline bool do_relative_http_request(struct update_info *info,
|
|
|
+ const char *url, const char *file)
|
|
|
+{
|
|
|
+ char *full_url = get_path(url, file);
|
|
|
+ bool success = do_http_request(info, full_url);
|
|
|
+ bfree(full_url);
|
|
|
+ return success;
|
|
|
+}
|
|
|
+
|
|
|
+static inline void write_file_data(struct update_info *info,
|
|
|
+ const char *base_path, const char *file)
|
|
|
+{
|
|
|
+ char *full_path = get_path(base_path, file);
|
|
|
+ os_quick_write_utf8_file(full_path,
|
|
|
+ (char*)info->file_data.array,
|
|
|
+ info->file_data.num - 1, false);
|
|
|
+ bfree(full_path);
|
|
|
+}
|
|
|
+
|
|
|
+static inline void replace_file(const char *src_base_path,
|
|
|
+ const char *dst_base_path, const char *file)
|
|
|
+{
|
|
|
+ char *src_path = get_path(src_base_path, file);
|
|
|
+ char *dst_path = get_path(dst_base_path, file);
|
|
|
+
|
|
|
+ if (src_path && dst_path) {
|
|
|
+ os_unlink(dst_path);
|
|
|
+ os_rename(src_path, dst_path);
|
|
|
+ }
|
|
|
+
|
|
|
+ bfree(dst_path);
|
|
|
+ bfree(src_path);
|
|
|
+}
|
|
|
+
|
|
|
+static bool update_remote_files(void *param, obs_data_t *remote_file)
|
|
|
+{
|
|
|
+ struct update_info *info = param;
|
|
|
+
|
|
|
+ struct file_update_data data = {
|
|
|
+ .name = obs_data_get_string(remote_file, "name"),
|
|
|
+ .version = (int)obs_data_get_int(remote_file, "version")
|
|
|
+ };
|
|
|
+
|
|
|
+ enum_files(info->cache_package, newer_than_cache, &data);
|
|
|
+ if (!data.newer && data.found)
|
|
|
+ return true;
|
|
|
+
|
|
|
+ if (!do_relative_http_request(info, info->remote_url, data.name))
|
|
|
+ return true;
|
|
|
+
|
|
|
+ if (info->callback) {
|
|
|
+ struct file_download_data download_data;
|
|
|
+ bool confirm;
|
|
|
+
|
|
|
+ download_data.name = data.name;
|
|
|
+ download_data.version = data.version;
|
|
|
+ download_data.buffer.da = info->file_data.da;
|
|
|
+
|
|
|
+ confirm = info->callback(info->param, &download_data);
|
|
|
+
|
|
|
+ info->file_data.da = download_data.buffer.da;
|
|
|
+
|
|
|
+ if (!confirm) {
|
|
|
+ info("Update file '%s' (version %d) rejected",
|
|
|
+ data.name, data.version);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ write_file_data(info, info->temp, data.name);
|
|
|
+ replace_file(info->temp, info->cache, data.name);
|
|
|
+
|
|
|
+ info("Successfully updated file '%s' (version %d)",
|
|
|
+ data.name, data.version);
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+static void update_remote_version(struct update_info *info, int cur_version)
|
|
|
+{
|
|
|
+ int remote_version;
|
|
|
+
|
|
|
+ if (!do_http_request(info, info->url))
|
|
|
+ return;
|
|
|
+
|
|
|
+ if (!info->file_data.array || info->file_data.array[0] != '{') {
|
|
|
+ warn("Remote package does not exist or is not valid json");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ info->remote_package = obs_data_create_from_json(info->file_data.array);
|
|
|
+ if (!info->remote_package) {
|
|
|
+ warn("Failed to initialize remote package json");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ remote_version = (int)obs_data_get_int(info->remote_package, "version");
|
|
|
+ if (remote_version <= cur_version)
|
|
|
+ return;
|
|
|
+
|
|
|
+ write_file_data(info, info->temp, "package.json");
|
|
|
+
|
|
|
+ info->remote_url = obs_data_get_string(info->remote_package, "url");
|
|
|
+ if (!info->remote_url) {
|
|
|
+ warn("No remote url in package file");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* download new files */
|
|
|
+ enum_files(info->remote_package, update_remote_files, info);
|
|
|
+
|
|
|
+ replace_file(info->temp, info->cache, "package.json");
|
|
|
+
|
|
|
+ info("Successfully updated package (version %d)", remote_version);
|
|
|
+ return;
|
|
|
+}
|
|
|
+
|
|
|
+static void *update_thread(void *data)
|
|
|
+{
|
|
|
+ struct update_info *info = data;
|
|
|
+ int cur_version;
|
|
|
+
|
|
|
+ if (!init_update(info))
|
|
|
+ return NULL;
|
|
|
+
|
|
|
+ cur_version = update_local_version(info);
|
|
|
+ update_remote_version(info, cur_version);
|
|
|
+ os_rmdir(info->temp);
|
|
|
+ return NULL;
|
|
|
+}
|
|
|
+
|
|
|
+update_info_t *update_info_create(
|
|
|
+ const char *log_prefix,
|
|
|
+ const char *user_agent,
|
|
|
+ const char *update_url,
|
|
|
+ const char *local_dir,
|
|
|
+ const char *cache_dir,
|
|
|
+ confirm_file_callback_t confirm_callback,
|
|
|
+ void *param)
|
|
|
+{
|
|
|
+ struct update_info *info;
|
|
|
+ struct dstr dir = {0};
|
|
|
+
|
|
|
+ if (!log_prefix)
|
|
|
+ log_prefix = "";
|
|
|
+
|
|
|
+ if (os_mkdir(cache_dir) < 0) {
|
|
|
+ blog(LOG_WARNING, "%sCould not create cache directory %s",
|
|
|
+ log_prefix, cache_dir);
|
|
|
+ return NULL;
|
|
|
+ }
|
|
|
+
|
|
|
+ dstr_copy(&dir, cache_dir);
|
|
|
+ if (dstr_end(&dir) != '/' && dstr_end(&dir) != '\\')
|
|
|
+ dstr_cat_ch(&dir, '/');
|
|
|
+ dstr_cat(&dir, ".temp");
|
|
|
+
|
|
|
+ if (os_mkdir(dir.array) < 0) {
|
|
|
+ blog(LOG_WARNING, "%sCould not create temp directory %s",
|
|
|
+ log_prefix, cache_dir);
|
|
|
+ dstr_free(&dir);
|
|
|
+ return NULL;
|
|
|
+ }
|
|
|
+
|
|
|
+ info = bzalloc(sizeof(*info));
|
|
|
+ info->log_prefix = bstrdup(log_prefix);
|
|
|
+ info->user_agent = bstrdup(user_agent);
|
|
|
+ info->temp = dir.array;
|
|
|
+ info->local = bstrdup(local_dir);
|
|
|
+ info->cache = bstrdup(cache_dir);
|
|
|
+ info->url = get_path(update_url, "package.json");
|
|
|
+ info->callback = confirm_callback;
|
|
|
+ info->param = param;
|
|
|
+
|
|
|
+ if (pthread_create(&info->thread, NULL, update_thread, info) == 0)
|
|
|
+ info->thread_created = true;
|
|
|
+
|
|
|
+ return info;
|
|
|
+}
|