#include "ssb.export.h" #include "log.h" #include "mem.h" #include "ssb.db.h" #include "ssb.h" #include "sqlite3.h" #include "uv.h" #include #include static void _write_file(const char* path, const void* blob, size_t size, bool force_add_trailing_newline) { FILE* file = fopen(path, "wb"); if (file) { fwrite(blob, 1, size, file); if (force_add_trailing_newline) { fputc('\n', file); } fclose(file); } else { tf_printf("Failed to open %s for write: %s.\n", path, strerror(errno)); } } static void _make_dir(const char* path) { #if defined(_WIN32) if (mkdir(path) && errno != EEXIST) #else if (mkdir(path, 0755) && errno != EEXIST) #endif { tf_printf("Failed to create directory %s: %s.\n", path, strerror(errno)); } } typedef struct _tf_export_t { tf_ssb_t* ssb; const char* parent; JSValue files; uv_fs_t req; bool done; } tf_export_t; static void _tf_ssb_export_scandir(uv_fs_t* req) { tf_export_t* export = req->data; JSContext* context = tf_ssb_get_context(export->ssb); uv_dirent_t ent; while (uv_fs_scandir_next(req, &ent) == 0) { if (ent.type == UV_DIRENT_FILE) { JSValue found = JS_GetPropertyStr(context, export->files, ent.name); if (JS_IsUndefined(found)) { size_t len = strlen(export->parent) + strlen(ent.name) + 2; char* path = tf_malloc(len); snprintf(path, len, "%s/%s", export->parent, ent.name); uv_fs_t req = { 0 }; int r = uv_fs_unlink(tf_ssb_get_loop(export->ssb), &req, path, NULL); if (r) { tf_printf("Failed to unlink %s: %s.\n", path, uv_strerror(r)); } uv_fs_req_cleanup(&req); tf_free(path); } JS_FreeValue(context, found); } } export->done = true; } void tf_ssb_export(tf_ssb_t* ssb, const char* key) { char user[256] = { 0 }; char path[256] = { 0 }; if (sscanf(key, "/~%255[^/]/%255s", user, path) != 2) { tf_printf("Unable to export %s.\n", key); return; } char app_blob_id[k_blob_id_len] = { 0 }; sqlite3* db = tf_ssb_acquire_db_reader(ssb); sqlite3_busy_timeout(db, 10000); sqlite3_stmt* statement; if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = ?1 AND key = 'path:' || ?2", -1, &statement, NULL) == SQLITE_OK) { if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, path, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW) { int len = sqlite3_column_bytes(statement, 0); if (len >= (int)sizeof(app_blob_id)) { len = sizeof(app_blob_id) - 1; } memcpy(app_blob_id, sqlite3_column_text(statement, 0), len); app_blob_id[len] = '\0'; } sqlite3_finalize(statement); } tf_ssb_release_db_reader(ssb, db); if (!*app_blob_id) { tf_printf("Did not find app blob ID for %s.\n", key); return; } uint8_t* blob = NULL; size_t size = 0; if (!tf_ssb_db_blob_get(ssb, app_blob_id, &blob, &size)) { tf_printf("Did not find blob for %s: %s.\n", key, app_blob_id); return; } char file_path[1024]; _make_dir("apps/"); snprintf(file_path, sizeof(file_path), "apps/%s", path); _make_dir(file_path); snprintf(file_path, sizeof(file_path), "apps/%s.json", path); JSContext* context = tf_ssb_get_context(ssb); JSValue app = JS_ParseJSON(context, (const char*)blob, size, NULL); tf_free(blob); JSValue files = JS_GetPropertyStr(context, app, "files"); JSPropertyEnum* ptab = NULL; uint32_t plen = 0; if (JS_GetOwnPropertyNames(context, &ptab, &plen, files, JS_GPN_STRING_MASK) == 0) { for (uint32_t i = 0; i < plen; ++i) { JSPropertyDescriptor desc; if (JS_GetOwnProperty(context, &desc, files, ptab[i].atom) == 1) { JSValue key = JS_AtomToString(context, ptab[i].atom); const char* file_name = JS_ToCString(context, key); const char* blob_id = JS_ToCString(context, desc.value); uint8_t* file_blob = NULL; size_t file_size = 0; if (tf_ssb_db_blob_get(ssb, blob_id, &file_blob, &file_size)) { snprintf(file_path, sizeof(file_path), "apps/%s/%s", path, file_name); _write_file(file_path, file_blob, file_size, false); tf_free(file_blob); } JS_FreeCString(context, file_name); JS_FreeValue(context, key); JS_FreeCString(context, blob_id); JS_FreeValue(context, desc.value); JS_FreeValue(context, desc.setter); JS_FreeValue(context, desc.getter); } } } for (uint32_t i = 0; i < plen; ++i) { JS_FreeAtom(context, ptab[i].atom); } js_free(context, ptab); JSAtom files_atom = JS_NewAtom(context, "files"); JS_DeleteProperty(context, app, files_atom, 0); JS_FreeAtom(context, files_atom); JSValue tab = JS_NewString(context, "\t"); JSValue json = JS_JSONStringify(context, app, JS_NULL, tab); JS_FreeValue(context, tab); size_t length = 0; const char* string = JS_ToCStringLen(context, &length, json); snprintf(file_path, sizeof(file_path), "apps/%s.json", path); _write_file(file_path, string, length, true); JS_FreeCString(context, string); JS_FreeValue(context, json); snprintf(file_path, sizeof(file_path), "apps/%s", path); tf_export_t export = { .parent = file_path, .ssb = ssb, .files = files, .req = { .data = &export, }, }; int r = uv_fs_scandir(tf_ssb_get_loop(ssb), &export.req, file_path, 0, _tf_ssb_export_scandir); if (r) { tf_printf("Failed to scan directory %s: %s.\n", file_path, uv_strerror(r)); } while (!export.done) { uv_run(tf_ssb_get_loop(ssb), UV_RUN_ONCE); } uv_fs_req_cleanup(&export.req); JS_FreeValue(context, files); JS_FreeValue(context, app); }