Skip to content

Commit

Permalink
fetch: Asset download utility (#1256)
Browse files Browse the repository at this point in the history
* fetch: Initial skeleton

* fetch: Define config scheme

* fetch: Load config file

* fetch: Build target-path

* fetch: Gather asset count

* fetch: Create rest session

* cmake: Add run.fetch target

* net: Fix bug in net_rest_destroy

* fetch: Initial json config

* fetch: Add origin license

* fetch: Initial fetch logic

* fetch: Refactor fetch logic

* fetch: Log when asset-save fails

* fetch: Log duration

* fetch: Fix target path create logic

* assets: Configure fetch

* cmake: Remove asset downloading

* fetch: Fix failing to save asset

* fetch: Add verbose log option

* fetch: Fix fetch ret-code

* fetch: Support origin auth

* fetch: Reduce sleep time

* fetch: Minor cleanup
  • Loading branch information
BastianBlokland authored Feb 1, 2025
1 parent 6463cd9 commit 9f9a63b
Show file tree
Hide file tree
Showing 9 changed files with 559 additions and 301 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
}
env:
CC: ${{matrix.config.cc}}
CMAKE_OPTS: "-DEXTERNAL_ASSETS=Off -DSANITIZE=On -DFAST=${{matrix.config.fast}} -DLTO=${{matrix.config.lto}}"
CMAKE_OPTS: "-DSANITIZE=On -DFAST=${{matrix.config.fast}} -DLTO=${{matrix.config.lto}}"
steps:
- name: checkout
uses: actions/checkout@v4
Expand Down
14 changes: 4 additions & 10 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ endif()
project(Volo VERSION 0.1.0 LANGUAGES C)

# Custom options.
set(FAST "Off" CACHE BOOL "Fast mode, disables various runtime validations")
set(TRACE "On" CACHE BOOL "Trace mode, enables runtime performance tracing")
set(LTO "Off" CACHE BOOL "Link time optimization")
set(SANITIZE "Off" CACHE BOOL "Should santiser instrumentation be enabled")
set(EXTERNAL_ASSETS "On" CACHE BOOL "Should external assets be downloaded")
set(FAST "Off" CACHE BOOL "Fast mode, disables various runtime validations")
set(TRACE "On" CACHE BOOL "Trace mode, enables runtime performance tracing")
set(LTO "Off" CACHE BOOL "Link time optimization")
set(SANITIZE "Off" CACHE BOOL "Should santiser instrumentation be enabled")

# Diagnostic information.
message(STATUS "Configuring Volo")
Expand All @@ -30,12 +29,10 @@ message(STATUS "* Fast: ${FAST}")
message(STATUS "* Trace: ${TRACE}")
message(STATUS "* Lto: ${LTO}")
message(STATUS "* Sanitize: ${SANITIZE}")
message(STATUS "* External assets: ${EXTERNAL_ASSETS}")

set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/modules")
include(cmake/compiler.cmake)
include(cmake/debug.cmake)
include(cmake/download.cmake)
include(cmake/findpkg.cmake)
include(cmake/platform.cmake)
include(cmake/test.cmake)
Expand All @@ -58,9 +55,6 @@ add_subdirectory(libs)
message(STATUS "Configuring applications")
add_subdirectory(apps)

message(STATUS "Configuring assets")
add_subdirectory(assets)

message(STATUS "Configuring utility targets")
configure_test_target()
configure_dbgsetup_target()
3 changes: 1 addition & 2 deletions apps/game/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ target_link_libraries(app_game PRIVATE
lib_ui
lib_vfx
)
add_dependencies(app_game volo_assets)
configure_debuggable(app_game)

add_custom_target(run.game COMMAND app_game
"-w" "--assets" "$<TARGET_PROPERTY:volo_assets,path>" VERBATIM USES_TERMINAL)
"-w" "--assets" "${CMAKE_SOURCE_DIR}/assets" VERBATIM USES_TERMINAL)
45 changes: 27 additions & 18 deletions apps/utilities/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,35 @@ target_link_libraries(app_schemasetup PRIVATE lib_app_cli lib_asset lib_log)
configure_debuggable(app_schemasetup)

add_custom_target(run.schemasetup COMMAND app_schemasetup "--out"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/arraytex.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/atlas.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/decal.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/fonttex.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/graphic.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/icon.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/inputs.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/level.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/prefabs.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/procmesh.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/proctex.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/products.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/script_import_mesh_binder.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/script_import_texture_binder.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/script_scene_binder.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/terrain.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/vfx.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/weapons.schema.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/arraytex.schema.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/atlas.schema.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/decal.schema.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/fonttex.schema.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/graphic.schema.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/icon.schema.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/inputs.schema.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/level.schema.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/prefabs.schema.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/procmesh.schema.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/proctex.schema.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/products.schema.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/script_import_mesh_binder.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/script_import_texture_binder.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/script_scene_binder.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/terrain.schema.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/vfx.schema.json"
"${CMAKE_SOURCE_DIR}/assets/schemas/weapons.schema.json"
VERBATIM USES_TERMINAL)

message(STATUS "> application: fetch")

add_executable(app_fetch fetch.c)
target_link_libraries(app_fetch PRIVATE lib_app_cli lib_net lib_log lib_data)
configure_debuggable(app_fetch)

add_custom_target(run.fetch COMMAND app_fetch
"${CMAKE_SOURCE_DIR}/assets/fetch.json" VERBATIM USES_TERMINAL)

message(STATUS "> application: dbgsetup")

add_executable(app_dbgsetup dbgsetup.c)
Expand Down
279 changes: 279 additions & 0 deletions apps/utilities/fetch.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
#include "app_cli.h"
#include "cli_app.h"
#include "cli_help.h"
#include "cli_parse.h"
#include "cli_read.h"
#include "cli_validate.h"
#include "core_alloc.h"
#include "core_array.h"
#include "core_dynstring.h"
#include "core_file.h"
#include "core_math.h"
#include "core_path.h"
#include "core_thread.h"
#include "core_time.h"
#include "data_read.h"
#include "data_utils.h"
#include "log_logger.h"
#include "log_sink_json.h"
#include "log_sink_pretty.h"
#include "net_http.h"
#include "net_init.h"
#include "net_rest.h"
#include "net_result.h"

/**
* Fetch - Utility to download external assets.
*/

#define fetch_worker_count 2

typedef struct {
String host;
String license;
String rootUri;
String authUser, authPass;
HeapArray_t(String) assets;
} FetchOrigin;

typedef struct {
String targetPath;
HeapArray_t(FetchOrigin) origins;
} FetchConfig;

static DataMeta g_fetchConfigMeta;

static void fetch_data_init(void) {
// clang-format off
data_reg_struct_t(g_dataReg, FetchOrigin);
data_reg_field_t(g_dataReg, FetchOrigin, host, data_prim_t(String), .flags = DataFlags_NotEmpty);
data_reg_field_t(g_dataReg, FetchOrigin, license, data_prim_t(String), .flags = DataFlags_Opt);
data_reg_field_t(g_dataReg, FetchOrigin, rootUri, data_prim_t(String));
data_reg_field_t(g_dataReg, FetchOrigin, authUser, data_prim_t(String), .flags = DataFlags_Opt | DataFlags_NotEmpty);
data_reg_field_t(g_dataReg, FetchOrigin, authPass, data_prim_t(String), .flags = DataFlags_Opt | DataFlags_NotEmpty);
data_reg_field_t(g_dataReg, FetchOrigin, assets, data_prim_t(String), .container = DataContainer_HeapArray, .flags = DataFlags_NotEmpty);

data_reg_struct_t(g_dataReg, FetchConfig);
data_reg_field_t(g_dataReg, FetchConfig, targetPath, data_prim_t(String));
data_reg_field_t(g_dataReg, FetchConfig, origins, t_FetchOrigin, .container = DataContainer_HeapArray);
// clang-format on

g_fetchConfigMeta = data_meta_t(t_FetchConfig);
}

static bool fetch_config_load(const String path, FetchConfig* out) {
// Open the file handle.
bool success = false;
File* file = null;
FileResult fileRes;
if ((fileRes = file_create(g_allocScratch, path, FileMode_Open, FileAccess_Read, &file))) {
log_e("Failed to open config file", log_param("err", fmt_text(file_result_str(fileRes))));
goto Ret;
}

// Map the file data.
String fileData;
if (UNLIKELY(fileRes = file_map(file, &fileData, FileHints_Prefetch))) {
log_e("Failed to map config file", log_param("err", fmt_text(file_result_str(fileRes))));
goto Ret;
}

// Parse the json.
DataReadResult result;
const Mem outMem = mem_create(out, sizeof(FetchConfig));
data_read_json(g_dataReg, fileData, g_allocHeap, g_fetchConfigMeta, outMem, &result);
if (UNLIKELY(result.error)) {
log_e("Failed to parse config file", log_param("err", fmt_text(result.errorMsg)));
goto Ret;
}
success = true;

Ret:
if (file) {
file_destroy(file);
}
return success;
}

static void fetch_config_destroy(FetchConfig* cfg) {
data_destroy(g_dataReg, g_allocHeap, g_fetchConfigMeta, mem_create(cfg, sizeof(FetchConfig)));
}

static u32 fetch_config_max_origin_assets(FetchConfig* cfg) {
u32 res = 0;
heap_array_for_t(cfg->origins, FetchOrigin, origin) {
res = math_max(res, (u32)origin->assets.count);
}
return res;
}

static String fetch_config_uri_scratch(const FetchOrigin* origin, const String asset) {
DynString result = dynstring_create(g_allocScratch, 256);
if (!string_starts_with(origin->rootUri, string_lit("/"))) {
dynstring_append_char(&result, '/');
}
dynstring_append(&result, origin->rootUri);
if (!string_ends_with(dynstring_view(&result), string_lit("/"))) {
dynstring_append_char(&result, '/');
}
if (string_starts_with(asset, string_lit("/"))) {
dynstring_append(&result, string_consume(asset, 1));
} else {
dynstring_append(&result, asset);
}
return dynstring_view(&result);
}

static NetHttpFlags fetch_http_flags(void) {
/**
* Enable Tls transport but do not enable certificate validation.
* This means traffic is encrypted and people cannot eavesdrop, however its trivial for someone
* to man-in-the-middle as we do not verify the server's authenticity.
* Please do not use this for security sensitive applications!
*/
return NetHttpFlags_TlsNoVerify;
}

static i32 fetch_run_origin(NetRest* rest, const String targetPath, const FetchOrigin* origin) {
i32 retCode = 0;

NetHttpAuth auth = {0};
if (!string_is_empty(origin->authUser)) {
auth = (NetHttpAuth){
.type = NetHttpAuthType_Basic,
.user = origin->authUser,
.pw = origin->authPass,
};
}

NetRestId* requests = alloc_array_t(g_allocHeap, NetRestId, origin->assets.count);

// Start a GET request for all assets.
for (u32 i = 0; i != origin->assets.count; ++i) {
const String uri = fetch_config_uri_scratch(origin, origin->assets.values[i]);
requests[i] = net_rest_get(rest, origin->host, uri, &auth, null);
}

// Save the results.
for (u32 i = 0; i != origin->assets.count; ++i) {
const NetRestId request = requests[i];
const String asset = origin->assets.values[i];

// Wait for the request to be done.
while (!net_rest_done(rest, request)) {
thread_sleep(time_milliseconds(100));
}

// Save the asset to disk.
const NetResult result = net_rest_result(rest, request);
if (result == NetResult_Success) {
const String path = path_build_scratch(targetPath, asset);
const String data = net_rest_data(rest, request);

FileResult saveRes = file_create_dir_sync(path_parent(path));
if (saveRes == FileResult_Success) {
saveRes = file_write_to_path_atomic(path, data);
}
if (saveRes != FileResult_Success) {
log_e(
"Asset save failed: '{}'",
log_param("asset", fmt_text(asset)),
log_param("path", fmt_path(path)),
log_param("error", fmt_text(file_result_str(saveRes))));
retCode = 2;
} else {
log_i(
"Asset fetched: '{}'",
log_param("asset", fmt_text(asset)),
log_param("size", fmt_size(data.size)));
}
} else {
log_e(
"Asset fetch failed: '{}'",
log_param("asset", fmt_text(asset)),
log_param("error", fmt_text(net_result_str(result))));
retCode = 1;
}
net_rest_release(rest, request);
}

alloc_free_array_t(g_allocHeap, requests, origin->assets.count);
return retCode;
}

static i32 fetch_run(const String configPath) {
const TimeSteady timeStart = time_steady_clock();

i32 retCode = 0;
NetRest* rest = null;
FetchConfig cfg;
if (!fetch_config_load(configPath, &cfg)) {
return 1;
}

DynString targetPath = dynstring_create(g_allocHeap, 128);
path_build(&targetPath, path_parent(configPath), cfg.targetPath);

const u32 maxOriginAssetCount = fetch_config_max_origin_assets(&cfg);
if (!maxOriginAssetCount) {
goto Done;
}
rest = net_rest_create(g_allocHeap, fetch_worker_count, maxOriginAssetCount, fetch_http_flags());

heap_array_for_t(cfg.origins, FetchOrigin, origin) {
const i32 originRet = fetch_run_origin(rest, dynstring_view(&targetPath), origin);
retCode = math_max(retCode, originRet);
}

Done:;
const TimeDuration duration = time_steady_duration(timeStart, time_steady_clock());
if (!retCode) {
log_i("Fetch finished", log_param("duration", fmt_duration(duration)));
} else {
log_e("Fetch failed", log_param("duration", fmt_duration(duration)));
}
if (rest) {
net_rest_destroy(rest);
}
fetch_config_destroy(&cfg);
dynstring_destroy(&targetPath);
return retCode;
}

static CliId g_optConfigPath, g_optVerbose, g_optHelp;

void app_cli_configure(CliApp* app) {
cli_app_register_desc(app, string_lit("Fetch utility."));

g_optConfigPath = cli_register_arg(app, string_lit("config"), CliOptionFlags_Required);
cli_register_desc(app, g_optConfigPath, string_lit("Path to a fetch config file."));
cli_register_validator(app, g_optConfigPath, cli_validate_file_regular);

g_optVerbose = cli_register_flag(app, 'v', string_lit("verbose"), CliOptionFlags_None);

g_optHelp = cli_register_flag(app, 'h', string_lit("help"), CliOptionFlags_None);
cli_register_desc(app, g_optHelp, string_lit("Display this help page."));
cli_register_exclusions(app, g_optHelp, g_optConfigPath);
cli_register_exclusions(app, g_optHelp, g_optVerbose);
}

i32 app_cli_run(const CliApp* app, const CliInvocation* invoc) {
i32 retCode = 0;
if (cli_parse_provided(invoc, g_optHelp)) {
cli_help_write_file(app, g_fileStdOut);
return retCode;
}

const LogMask logMask = cli_parse_provided(invoc, g_optVerbose) ? LogMask_All : ~LogMask_Debug;
log_add_sink(g_logger, log_sink_pretty_default(g_allocHeap, logMask));
log_add_sink(g_logger, log_sink_json_default(g_allocHeap, LogMask_All));

fetch_data_init();
net_init();

const String configPath = cli_read_string(invoc, g_optConfigPath, string_empty);
retCode = fetch_run(configPath);

net_teardown();
return retCode;
}
Loading

0 comments on commit 9f9a63b

Please sign in to comment.