Pharos 0.7.23
Modern Web Framework & Web App Hosting Service.
Loading...
Searching...
No Matches
native_backup_restore.c
Go to the documentation of this file.
1/**
2 * @file
3 * @brief Implements native backup and restore reporting for app state and sqlite snapshots.
4 */
5
7
9#include "native_fs.h"
10#include "native_json_config.h"
11#include "native_support.h"
12
13#include <stdio.h>
14#include <stdlib.h>
15#include <string.h>
16#include <time.h>
17
18#ifdef _WIN32
19#define native_backup_restore_strdup _strdup
20#else
21#define native_backup_restore_strdup strdup
22#endif
23
24static char *native_default_backend_local(const AppRuntime *app) {
25 char *backend = app != NULL ? conf_lookup_value(app->runtime_conf_path, "db_backend") : NULL;
26 if (backend == NULL || backend[0] == '\0') {
27 free(backend);
28 return native_backup_restore_strdup("sqlite");
29 }
30 return backend;
31}
32
34 char *db_path = app != NULL ? conf_lookup_value(app->runtime_conf_path, "db_path") : NULL;
35 char buffer[4096];
36 if (db_path != NULL && db_path[0] != '\0') {
37 return db_path;
38 }
39 free(db_path);
40 snprintf(buffer, sizeof(buffer), "/var/lib/pharos/%s.sqlite3", app != NULL && app->app_id != NULL ? app->app_id : "app");
41 return native_backup_restore_strdup(buffer);
42}
43
44static char *native_app_conf_path(const char *app_root) {
45 char *app_conf = NULL;
46 char *pharos_conf = NULL;
47 if (app_root == NULL) {
48 return NULL;
49 }
50 app_conf = path_join(app_root, "app.conf");
51 if (app_conf != NULL && path_exists(app_conf)) {
52 return app_conf;
53 }
54 free(app_conf);
55 pharos_conf = path_join(app_root, "pharos.conf");
56 if (pharos_conf != NULL && path_exists(pharos_conf)) {
57 return pharos_conf;
58 }
59 free(pharos_conf);
60 return NULL;
61}
62
63static char *native_app_conf_value_path(const char *app_root, const char *key) {
64 char *conf_path = native_app_conf_path(app_root);
65 char *raw_value = NULL;
66 char *resolved = NULL;
67 char *conf_dir = NULL;
68 if (conf_path == NULL || key == NULL) {
69 free(conf_path);
70 return NULL;
71 }
72 raw_value = conf_lookup_value(conf_path, key);
73 if (raw_value == NULL || raw_value[0] == '\0') {
74 free(conf_path);
75 free(raw_value);
76 return NULL;
77 }
78 conf_dir = path_dirname(conf_path);
79 if (conf_dir != NULL) {
80 resolved = resolve_relative_to(conf_dir, raw_value);
81 }
82 free(conf_dir);
83 free(conf_path);
84 free(raw_value);
85 return resolved;
86}
87
88static char *native_backup_base_dir(const AppRuntime *app) {
89 char *app_value = NULL;
90 char *runtime_value = NULL;
91 char *fallback = NULL;
92 if (app != NULL && app->app_root != NULL) {
93 app_value = native_app_conf_value_path(app->app_root, "backup_dir");
94 if (app_value != NULL && app_value[0] != '\0') {
95 return app_value;
96 }
97 free(app_value);
98 }
99 runtime_value = app != NULL ? conf_lookup_value(app->runtime_conf_path, "backup_dir") : NULL;
100 if (runtime_value != NULL && runtime_value[0] != '\0') {
101 return runtime_value;
102 }
103 free(runtime_value);
104 fallback = path_join(app != NULL ? app->app_root : NULL, "var/backups");
105 return fallback;
106}
107
108static char *native_backup_timestamp_utc(void) {
109 char buffer[32];
110 time_t now = time(NULL);
111 struct tm tm_now;
112#ifdef _WIN32
113 gmtime_s(&tm_now, &now);
114#else
115 gmtime_r(&now, &tm_now);
116#endif
117 if (strftime(buffer, sizeof(buffer), "%Y%m%dT%H%M%SZ", &tm_now) == 0) {
118 return NULL;
119 }
120 return native_backup_restore_strdup(buffer);
121}
122
123static long native_json_top_level_long(const char *text, const char *key, long fallback_value) {
124 char pattern[256];
125 const char *cursor;
126 const char *colon;
127 char *endptr = NULL;
128 long value;
129 if (text == NULL || key == NULL) {
130 return fallback_value;
131 }
132 snprintf(pattern, sizeof(pattern), "\"%s\"", key);
133 cursor = strstr(text, pattern);
134 if (cursor == NULL) {
135 return fallback_value;
136 }
137 colon = strchr(cursor + strlen(pattern), ':');
138 if (colon == NULL) {
139 return fallback_value;
140 }
141 colon++;
142 while (*colon == ' ' || *colon == '\t' || *colon == '\r' || *colon == '\n') {
143 colon++;
144 }
145 value = strtol(colon, &endptr, 10);
146 if (endptr == colon) {
147 return fallback_value;
148 }
149 return value;
150}
151
153 char *manifest_path = NULL;
154 char *manifest_text = NULL;
155 char *relative_path = NULL;
156 char *resolved = NULL;
157 if (app == NULL || app->app_root == NULL) {
158 return NULL;
159 }
160 manifest_path = path_join(app->app_root, "pharos.app.json");
161 if (manifest_path == NULL || !path_exists(manifest_path)) {
162 free(manifest_path);
163 return NULL;
164 }
165 manifest_text = read_file_text(manifest_path);
166 if (manifest_text == NULL) {
167 free(manifest_path);
168 return NULL;
169 }
170 relative_path = json_find_string_in_section(manifest_text, "fixtures", "migration_fixture");
171 if (relative_path != NULL && relative_path[0] != '\0') {
172 resolved = resolve_relative_to(app->app_root, relative_path);
173 }
174 free(manifest_path);
175 free(manifest_text);
176 free(relative_path);
177 return resolved;
178}
179
180static long native_migration_schema_version(const char *migration_fixture_path) {
181 char *fixture_text;
182 long schema_version = 0;
183 if (migration_fixture_path == NULL || !path_exists(migration_fixture_path)) {
184 return 0;
185 }
186 fixture_text = read_file_text(migration_fixture_path);
187 if (fixture_text == NULL) {
188 return 0;
189 }
190 schema_version = native_json_top_level_long(fixture_text, "schema_version", 0);
191 free(fixture_text);
192 return schema_version;
193}
194
195static int native_write_backup_state_snapshot(const AppRuntime *app, const char *output_path) {
196 if (app != NULL && app->state_path != NULL && path_exists(app->state_path)) {
197 return copy_file_binary(app->state_path, output_path);
198 }
199 return write_text_file(output_path, "{}\n");
200}
201
202static char *native_metadata_artifact_value(const char *metadata_path, const char *key) {
203 char *metadata_text = NULL;
204 char *value = NULL;
205 if (metadata_path == NULL || !path_exists(metadata_path)) {
206 return NULL;
207 }
208 metadata_text = read_file_text(metadata_path);
209 if (metadata_text == NULL) {
210 return NULL;
211 }
212 value = json_find_string_in_section(metadata_text, "artifacts", key);
213 free(metadata_text);
214 return value;
215}
216
217char *build_native_backup_report_local_json(const ServeConfig *config, const AppRuntime *app, const char *backend_override, const char *output_dir_override, char **error_out) {
218 char *backend = NULL;
219 char *base_dir = NULL;
220 char *timestamp = NULL;
221 char *target_dir = NULL;
222 char *backup_dir_path = NULL;
223 char *state_backup_path = NULL;
224 char *metadata_path = NULL;
225 char *sqlite_db_path = NULL;
226 char *sqlite_snapshot_path = NULL;
227 char *migration_fixture = NULL;
228 char *migration_fixture_json = NULL;
229 char *target_id_json = NULL;
230 char *backend_json = NULL;
231 char *backup_dir_json = NULL;
232 char *metadata_path_json = NULL;
233 char *state_path_json = NULL;
234 char *sqlite_snapshot_path_json = NULL;
235 char *schema_json = NULL;
236 char *metadata_json = NULL;
237 char *result = NULL;
238 long schema_version = 0;
239 size_t metadata_needed;
240 size_t needed;
241
242 if (error_out != NULL) {
243 *error_out = NULL;
244 }
245 if (config == NULL || app == NULL || config->target_id == NULL) {
246 if (error_out != NULL) {
247 *error_out = native_backup_restore_strdup("native backup requires a resolved app runtime");
248 }
249 return NULL;
250 }
251
252 backend = (backend_override != NULL && backend_override[0] != '\0') ? native_backup_restore_strdup(backend_override) : native_default_backend_local(app);
253 if (backend == NULL || !streq(backend, "sqlite")) {
254 free(backend);
255 if (error_out != NULL) {
256 *error_out = native_backup_restore_strdup("native backup currently supports sqlite-backed apps only");
257 }
258 return NULL;
259 }
260
261 base_dir = (output_dir_override != NULL && output_dir_override[0] != '\0') ? native_backup_restore_strdup(output_dir_override) : native_backup_base_dir(app);
262 timestamp = native_backup_timestamp_utc();
263 target_dir = path_join(base_dir, config->target_id);
264 backup_dir_path = path_join(target_dir, timestamp);
265 state_backup_path = path_join(backup_dir_path, "state.json");
266 metadata_path = path_join(backup_dir_path, "metadata.json");
267 sqlite_db_path = native_default_sqlite_path_local(app);
268 sqlite_snapshot_path = path_join(backup_dir_path, "database.sqlite3");
269 migration_fixture = native_migration_fixture_path(app);
270 schema_version = native_migration_schema_version(migration_fixture);
271
272 if (base_dir == NULL || timestamp == NULL || target_dir == NULL || backup_dir_path == NULL || state_backup_path == NULL || metadata_path == NULL || sqlite_db_path == NULL || sqlite_snapshot_path == NULL) {
273 if (error_out != NULL) {
274 *error_out = native_backup_restore_strdup("native backup could not resolve required paths");
275 }
276 goto cleanup;
277 }
278 if (!path_exists(sqlite_db_path)) {
279 if (error_out != NULL) {
280 *error_out = native_backup_restore_strdup("sqlite backup requires an existing database file");
281 }
282 goto cleanup;
283 }
284 if (ensure_directory(backup_dir_path) != 0) {
285 if (error_out != NULL) {
286 *error_out = native_backup_restore_strdup("native backup could not create the backup directory");
287 }
288 goto cleanup;
289 }
290 if (copy_file_binary(sqlite_db_path, sqlite_snapshot_path) != 0) {
291 if (error_out != NULL) {
292 *error_out = native_backup_restore_strdup("native backup could not copy the sqlite snapshot");
293 }
294 goto cleanup;
295 }
296 if (native_write_backup_state_snapshot(app, state_backup_path) != 0) {
297 if (error_out != NULL) {
298 *error_out = native_backup_restore_strdup("native backup could not write the state scratch snapshot");
299 }
300 goto cleanup;
301 }
302
303 migration_fixture_json = migration_fixture != NULL ? json_string_dup_native(migration_fixture) : native_backup_restore_strdup("null");
304 schema_json = native_backup_restore_strdup("pharos.backup.bundle.v1");
305 target_id_json = json_string_dup_native(config->target_id);
306 backend_json = json_string_dup_native(backend);
307 backup_dir_json = json_string_dup_native(backup_dir_path);
308 metadata_path_json = json_string_dup_native(metadata_path);
309 state_path_json = json_string_dup_native(state_backup_path);
310 sqlite_snapshot_path_json = json_string_dup_native(sqlite_snapshot_path);
311 if (migration_fixture_json == NULL || schema_json == NULL || target_id_json == NULL || backend_json == NULL || backup_dir_json == NULL || metadata_path_json == NULL || state_path_json == NULL || sqlite_snapshot_path_json == NULL) {
312 if (error_out != NULL) {
313 *error_out = native_backup_restore_strdup("native backup could not allocate report fields");
314 }
315 goto cleanup;
316 }
317
318 metadata_needed = strlen(schema_json) + strlen(target_id_json) + strlen(backend_json) + strlen(timestamp) + strlen(state_path_json) + strlen(sqlite_snapshot_path_json) + strlen(migration_fixture_json) + 256;
319 metadata_json = (char *)malloc(metadata_needed);
320 if (metadata_json == NULL) {
321 if (error_out != NULL) {
322 *error_out = native_backup_restore_strdup("native backup could not allocate metadata output");
323 }
324 goto cleanup;
325 }
326 snprintf(metadata_json, metadata_needed, "{\"schema\":\"pharos.backup.bundle.v1\",\"target_kind\":\"app\",\"target_id\":%s,\"backend\":%s,\"environment\":\"development\",\"created_at\":\"%s\",\"schema_version\":%ld,\"migration_fixture\":%s,\"artifacts\":{\"state_json\":%s,\"sqlite_snapshot\":%s}}", target_id_json, backend_json, timestamp, schema_version, migration_fixture_json, state_path_json, sqlite_snapshot_path_json);
327 if (write_text_file(metadata_path, metadata_json) != 0) {
328 if (error_out != NULL) {
329 *error_out = native_backup_restore_strdup("native backup could not write metadata.json");
330 }
331 goto cleanup;
332 }
333
334 needed = strlen(target_id_json) + strlen(backend_json) + strlen(backup_dir_json) + strlen(metadata_path_json) + strlen(state_path_json) + strlen(sqlite_snapshot_path_json) + 192;
335 result = (char *)malloc(needed);
336 if (result != NULL) {
337 snprintf(result, needed, "{\"report_id\":\"pharos-backup-v1\",\"target_kind\":\"app\",\"target_id\":%s,\"backend\":%s,\"status\":\"backed_up\",\"backup_dir\":%s,\"metadata_path\":%s,\"state_path\":%s,\"sqlite_snapshot\":%s}", target_id_json, backend_json, backup_dir_json, metadata_path_json, state_path_json, sqlite_snapshot_path_json);
338 }
339 if (result == NULL && error_out != NULL) {
340 *error_out = native_backup_restore_strdup("native backup could not allocate the backup report");
341 }
342
343cleanup:
344 free(backend);
345 free(base_dir);
346 free(timestamp);
347 free(target_dir);
348 free(backup_dir_path);
349 free(state_backup_path);
350 free(metadata_path);
351 free(sqlite_db_path);
352 free(sqlite_snapshot_path);
353 free(migration_fixture);
354 free(migration_fixture_json);
355 free(target_id_json);
356 free(backend_json);
357 free(backup_dir_json);
358 free(metadata_path_json);
359 free(state_path_json);
360 free(sqlite_snapshot_path_json);
361 free(schema_json);
362 free(metadata_json);
363 return result;
364}
365
366char *build_native_restore_report_local_json(const ServeConfig *config, const AppRuntime *app, const char *backend_override, const char *input_dir, char **error_out) {
367 char *backend = NULL;
368 char *metadata_path = NULL;
369 char *state_path = NULL;
370 char *sqlite_snapshot_path = NULL;
371 char *metadata_snapshot_value = NULL;
372 char *metadata_state_value = NULL;
373 char *sqlite_db_path = NULL;
374 char *db_parent = NULL;
375 char *tmp_db_path = NULL;
376 char *target_id_json = NULL;
377 char *backend_json = NULL;
378 char *input_dir_json = NULL;
379 char *result = NULL;
380 size_t needed;
381
382 if (error_out != NULL) {
383 *error_out = NULL;
384 }
385 if (config == NULL || app == NULL || config->target_id == NULL) {
386 if (error_out != NULL) {
387 *error_out = native_backup_restore_strdup("native restore requires a resolved app runtime");
388 }
389 return NULL;
390 }
391 if (input_dir == NULL || input_dir[0] == '\0') {
392 if (error_out != NULL) {
393 *error_out = native_backup_restore_strdup("restore requires --input-dir");
394 }
395 return NULL;
396 }
397
398 backend = (backend_override != NULL && backend_override[0] != '\0') ? native_backup_restore_strdup(backend_override) : native_default_backend_local(app);
399 if (backend == NULL || !streq(backend, "sqlite")) {
400 free(backend);
401 if (error_out != NULL) {
402 *error_out = native_backup_restore_strdup("native restore currently supports sqlite-backed apps only");
403 }
404 return NULL;
405 }
406
407 metadata_path = path_join(input_dir, "metadata.json");
408 state_path = path_join(input_dir, "state.json");
409 metadata_snapshot_value = native_metadata_artifact_value(metadata_path, "sqlite_snapshot");
410 metadata_state_value = native_metadata_artifact_value(metadata_path, "state_json");
411 sqlite_snapshot_path = metadata_snapshot_value != NULL && metadata_snapshot_value[0] != '\0' ? metadata_snapshot_value : path_join(input_dir, "database.sqlite3");
412 metadata_snapshot_value = NULL;
413 sqlite_db_path = native_default_sqlite_path_local(app);
414 db_parent = path_dirname(sqlite_db_path);
415 tmp_db_path = sqlite_db_path != NULL ? (char *)malloc(strlen(sqlite_db_path) + 16) : NULL;
416
417 if (metadata_path == NULL || sqlite_db_path == NULL || db_parent == NULL || tmp_db_path == NULL) {
418 if (error_out != NULL) {
419 *error_out = native_backup_restore_strdup("native restore could not resolve required paths");
420 }
421 goto cleanup;
422 }
423 snprintf(tmp_db_path, strlen(sqlite_db_path) + 16, "%s.restore.tmp", sqlite_db_path);
424 if (!path_exists(metadata_path)) {
425 if (error_out != NULL) {
426 *error_out = native_backup_restore_strdup("missing restore metadata");
427 }
428 goto cleanup;
429 }
430 if (sqlite_snapshot_path == NULL || !path_exists(sqlite_snapshot_path)) {
431 if (error_out != NULL) {
432 *error_out = native_backup_restore_strdup("sqlite restore requires a database snapshot and will not rebuild database truth from JSON");
433 }
434 goto cleanup;
435 }
436 if (ensure_directory(db_parent) != 0) {
437 if (error_out != NULL) {
438 *error_out = native_backup_restore_strdup("native restore could not create the sqlite parent directory");
439 }
440 goto cleanup;
441 }
442 if (copy_file_binary(sqlite_snapshot_path, tmp_db_path) != 0) {
443 if (error_out != NULL) {
444 *error_out = native_backup_restore_strdup("native restore could not copy the sqlite snapshot");
445 }
446 goto cleanup;
447 }
448 if (rename(tmp_db_path, sqlite_db_path) != 0) {
449 if (error_out != NULL) {
450 *error_out = native_backup_restore_strdup("native restore could not replace the sqlite database");
451 }
452 goto cleanup;
453 }
454 if (app->state_path != NULL) {
455 const char *state_source = metadata_state_value != NULL && metadata_state_value[0] != '\0' ? metadata_state_value : state_path;
456 if (state_source != NULL && path_exists(state_source)) {
457 copy_file_binary(state_source, app->state_path);
458 }
459 }
460
461 target_id_json = json_string_dup_native(config->target_id);
462 backend_json = json_string_dup_native(backend);
463 input_dir_json = json_string_dup_native(input_dir);
464 if (target_id_json == NULL || backend_json == NULL || input_dir_json == NULL) {
465 if (error_out != NULL) {
466 *error_out = native_backup_restore_strdup("native restore could not allocate report fields");
467 }
468 goto cleanup;
469 }
470 needed = strlen(target_id_json) + strlen(backend_json) + strlen(input_dir_json) + 128;
471 result = (char *)malloc(needed);
472 if (result != NULL) {
473 snprintf(result, needed, "{\"report_id\":\"pharos-restore-v1\",\"target_kind\":\"app\",\"target_id\":%s,\"backend\":%s,\"input_dir\":%s,\"status\":\"restored\"}", target_id_json, backend_json, input_dir_json);
474 }
475 if (result == NULL && error_out != NULL) {
476 *error_out = native_backup_restore_strdup("native restore could not allocate the restore report");
477 }
478
479cleanup:
480 free(backend);
481 free(metadata_path);
482 free(state_path);
483 free(sqlite_snapshot_path);
484 free(metadata_snapshot_value);
485 free(metadata_state_value);
486 free(sqlite_db_path);
487 free(db_parent);
488 free(tmp_db_path);
489 free(target_id_json);
490 free(backend_json);
491 free(input_dir_json);
492 return result;
493}
char * json_string_dup_native(const char *value)
Duplicates a string while escaping it for JSON string output.
Declares helpers that build native artifact and report payload strings.
static char * native_metadata_artifact_value(const char *metadata_path, const char *key)
#define native_backup_restore_strdup
char * build_native_restore_report_local_json(const ServeConfig *config, const AppRuntime *app, const char *backend_override, const char *input_dir, char **error_out)
Builds a JSON report describing a native app restore operation.
static char * native_backup_timestamp_utc(void)
char * build_native_backup_report_local_json(const ServeConfig *config, const AppRuntime *app, const char *backend_override, const char *output_dir_override, char **error_out)
Builds a JSON report describing a native app backup operation.
static char * native_default_backend_local(const AppRuntime *app)
static char * native_migration_fixture_path(const AppRuntime *app)
static long native_json_top_level_long(const char *text, const char *key, long fallback_value)
static char * native_app_conf_path(const char *app_root)
static char * native_default_sqlite_path_local(const AppRuntime *app)
static int native_write_backup_state_snapshot(const AppRuntime *app, const char *output_path)
static long native_migration_schema_version(const char *migration_fixture_path)
static char * native_backup_base_dir(const AppRuntime *app)
static char * native_app_conf_value_path(const char *app_root, const char *key)
Declares native backup and restore report builders.
int ensure_directory(const char *path)
Creates a directory path and any missing parent directories.
Definition native_fs.c:31
int write_text_file(const char *path, const char *content)
Writes a text buffer to a file path.
Definition native_fs.c:66
int copy_file_binary(const char *source_path, const char *target_path)
Copies one file as binary data.
Definition native_fs.c:93
Declares native filesystem helper routines.
char * conf_lookup_value(const char *path, const char *key)
Reads a Pharos conf file and looks up one key.
char * json_find_string_in_section(const char *text, const char *section, const char *key)
Finds a JSON string value for a key inside a named section object.
Declares lightweight JSON and conf value lookup helpers.
char * path_join(const char *left, const char *right)
Joins two path segments using the native path separator.
int streq(const char *a, const char *b)
Tests whether two strings are exactly equal.
char * path_dirname(const char *path)
Returns the parent directory component of a path.
char * resolve_relative_to(const char *base_dir, const char *value)
Resolves a path value relative to a base directory when needed.
char * read_file_text(const char *path)
Reads an entire file into a newly allocated text buffer.
int path_exists(const char *path)
Tests whether a filesystem path currently exists.
Declares shared low-level support helpers used across the Pharos native runtime.
Loaded runtime metadata for one Pharos app artifact.
Parsed native serve configuration used to bootstrap the runtime listener.