Implement easylibstorage wrapper with tests, wire up console commands

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gmega 2026-01-30 11:13:08 -03:00
parent 6e6d049577
commit 691c2a8d8a
No known key found for this signature in database
GPG Key ID: 6290D34EAD824B18
7 changed files with 633 additions and 33 deletions

View File

@ -2,7 +2,9 @@ cmake_minimum_required(VERSION 3.14)
project(storageconsole C)
set(CMAKE_C_STANDARD 11)
add_executable(storageconsole main.c
add_executable(storageconsole
main.c
easylibstorage.c
easylibstorage.h
)
@ -28,3 +30,18 @@ else()
message(WARNING "libstorage not found. Build or provide it before linking.")
endif()
# --- Tests ---
enable_testing()
add_executable(test_runner
tests/test_runner.c
easylibstorage.c
tests/mock_libstorage.c
)
target_include_directories(test_runner PRIVATE
"${CMAKE_SOURCE_DIR}"
"${LOGOS_STORAGE_NIM_ROOT}/library"
)
add_test(NAME easylibstorage_tests COMMAND test_runner)

View File

@ -7,7 +7,7 @@ The headers for `libstorage` are located at `/home/giuliano/Work/Status/logos-st
The code for libstorage is located at `home/giuliano/Work/Status/logos-storage-nim/library`.
There are examples for how libstorage can be used in `/home/giuliano/Work/Status/logos-storage-nim/examples/c/examples.c`.
There are examples for how libstorage can be used in `/home/giuliano/Work/Status/logos-storage-nim/examples/c/storage.c`.
# Development Process (TDD)
You MUST follow a Test Driven Development approach. Since no test environment exists yet, your FIRST task is to:
@ -22,6 +22,8 @@ Then, for *each* function in `easylibstorage.h`:
**CRITICAL**: This refactoring step is VERY IMPORTANT. You MUST look for ways to simplify, deduplicate, and coalesce code here, WITHOUT OVERCOMPLICATING. **SIMPLICITY IS KEY AND YOUR GUIDING PRINCIPLE.**
# API Implementation Details
- **Configuration JSON.** The keys described in the node_config struct should be passed as kebab-case into the config JSON string. See an example
in the `storage.c` file. `bootstrap-node` must be passed as a string array.
- **Memory Management**: Clearly document who owns returned pointers (e.g., CIDs). Ensure no memory leaks.
- **Log Levels**: Map the `char *log_level` in the config to the internal `enum log_level`.
- **Download Return**: Note that `e_storage_download` currently returns `STORAGE_NODE` in the header. If this is a mistake, change it to return `int` (status code) and update the header.

240
easylibstorage.c Normal file
View File

@ -0,0 +1,240 @@
#include "easylibstorage.h"
#include "libstorage.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MAX_RETRIES 250
#define POLL_INTERVAL_US (100 * 1000)
#define DEFAULT_CHUNK_SIZE (64 * 1024)
typedef struct {
int ret;
char *msg;
size_t len;
progress_callback pcb;
int bytes_done;
} resp;
static resp *resp_alloc(void) {
resp *r = calloc(1, sizeof(resp));
r->ret = -1;
return r;
}
static void resp_free(resp *r) {
if (!r) return;
free(r->msg);
free(r);
}
static void resp_wait(resp *r) {
for (int i = 0; i < MAX_RETRIES && r->ret == -1; i++) {
usleep(POLL_INTERVAL_US);
}
}
// Callback for simple (non-progress) async operations.
static void on_complete(int ret, const char *msg, size_t len, void *userData) {
resp *r = userData;
if (!r) return;
free(r->msg);
r->msg = NULL;
r->len = 0;
if (msg && len > 0) {
r->msg = malloc(len + 1);
if (r->msg) {
memcpy(r->msg, msg, len);
r->msg[len] = '\0';
r->len = len;
}
}
r->ret = ret;
}
// Callback for operations that report progress before completing.
static void on_progress(int ret, const char *msg, size_t len, void *userData) {
resp *r = userData;
if (!r) return;
if (ret == RET_PROGRESS) {
r->bytes_done += (int) len;
if (r->pcb) {
r->pcb(0, r->bytes_done, ret);
}
return; // don't set r->ret yet — still in progress
}
// Final callback (RET_OK or RET_ERR)
free(r->msg);
r->msg = NULL;
r->len = 0;
if (msg && len > 0) {
r->msg = malloc(len + 1);
if (r->msg) {
memcpy(r->msg, msg, len);
r->msg[len] = '\0';
r->len = len;
}
}
r->ret = ret;
}
// Dispatches an async call, waits for completion, extracts the result.
// Returns RET_OK/RET_ERR. If dispatch_ret != RET_OK, returns RET_ERR immediately.
// If out is non-NULL and the response has a message, *out receives a strdup'd copy.
static int call_wait(int dispatch_ret, resp *r, char **out) {
if (dispatch_ret != RET_OK) {
resp_free(r);
return RET_ERR;
}
resp_wait(r);
int result = (r->ret == RET_OK) ? RET_OK : RET_ERR;
if (out) {
*out = r->msg ? strdup(r->msg) : NULL;
}
resp_free(r);
return result;
}
static int nim_initialized = 0;
STORAGE_NODE e_storage_new(node_config config) {
if (!nim_initialized) {
extern void libstorageNimMain(void);
libstorageNimMain();
nim_initialized = 1;
}
// Build JSON config string.
// Format: {"api-port":N,"disc-port":N,"data-dir":"...","log-level":"...","bootstrap-node":["..."]}
char json[2048];
int pos = 0;
pos += snprintf(json + pos, sizeof(json) - pos, "{\"api-port\":%d,\"disc-port\":%d", config.api_port,
config.disc_port);
if (config.data_dir) {
pos += snprintf(json + pos, sizeof(json) - pos, ",\"data-dir\":\"%s\"", config.data_dir);
}
if (config.log_level) {
pos += snprintf(json + pos, sizeof(json) - pos, ",\"log-level\":\"%s\"", config.log_level);
}
if (config.bootstrap_node) {
pos += snprintf(json + pos, sizeof(json) - pos, ",\"bootstrap-node\":[\"%s\"]", config.bootstrap_node);
}
snprintf(json + pos, sizeof(json) - pos, "}");
resp *r = resp_alloc();
void *ctx = storage_new(json, (StorageCallback) on_complete, r);
if (!ctx) {
resp_free(r);
return NULL;
}
resp_wait(r);
if (r->ret != RET_OK) {
resp_free(r);
return NULL;
}
resp_free(r);
return ctx;
}
int e_storage_start(STORAGE_NODE node) {
if (!node) return RET_ERR;
resp *r = resp_alloc();
return call_wait(storage_start(node, (StorageCallback) on_complete, r), r, NULL);
}
int e_storage_stop(STORAGE_NODE node) {
if (!node) return RET_ERR;
resp *r = resp_alloc();
return call_wait(storage_stop(node, (StorageCallback) on_complete, r), r, NULL);
}
int e_storage_destroy(STORAGE_NODE node) {
if (!node) return RET_ERR;
// Close first (tolerate failure)
resp *r = resp_alloc();
call_wait(storage_close(node, (StorageCallback) on_complete, r), r, NULL);
// Destroy
r = resp_alloc();
return call_wait(storage_destroy(node, (StorageCallback) on_complete, r), r, NULL);
}
char *e_storage_upload(STORAGE_NODE node, const char *filepath, progress_callback cb) {
if (!node || !filepath) return NULL;
// Init upload session
resp *r = resp_alloc();
char *session_id = NULL;
int ret = call_wait(storage_upload_init(node, filepath, DEFAULT_CHUNK_SIZE, (StorageCallback) on_complete, r), r,
&session_id);
if (ret != RET_OK || !session_id) {
free(session_id);
return NULL;
}
// Upload file with progress
r = resp_alloc();
r->pcb = cb;
char *cid = NULL;
ret = call_wait(storage_upload_file(node, session_id, (StorageCallback) on_progress, r), r, &cid);
free(session_id);
if (ret != RET_OK) {
free(cid);
return NULL;
}
if (cb) {
printf("\n"); // newline after progress output
}
return cid;
}
int e_storage_download(STORAGE_NODE node, const char *cid, const char *filepath, progress_callback cb) {
if (!node || !cid || !filepath) return RET_ERR;
// Init download
resp *r = resp_alloc();
int ret =
call_wait(storage_download_init(node, cid, DEFAULT_CHUNK_SIZE, false, (StorageCallback) on_complete, r), r,
NULL);
if (ret != RET_OK) return RET_ERR;
// Stream to file with progress
r = resp_alloc();
r->pcb = cb;
ret = call_wait(
storage_download_stream(node, cid, DEFAULT_CHUNK_SIZE, false, filepath, (StorageCallback) on_progress, r),
r, NULL);
if (cb) {
printf("\n");
}
return ret;
}

View File

@ -1,16 +1,7 @@
#ifndef STORAGECONSOLE_EASYLIBSTORAGE_H
#define STORAGECONSOLE_EASYLIBSTORAGE_H
#include <stdio.h>
#define STORAGE_NODE void *
#define CID char *
enum log_level {
WARN,
INFO,
DEBUG,
};
typedef struct {
int api_port;
@ -22,11 +13,17 @@ typedef struct {
typedef void (*progress_callback)(int total, int complete, int status);
// Creates a new storage node. Returns opaque pointer, or NULL on failure.
STORAGE_NODE e_storage_new(node_config config);
int e_storage_start(STORAGE_NODE node);
int e_storage_stop(STORAGE_NODE node);
int e_storage_destroy(STORAGE_NODE node);
CID e_storage_upload(STORAGE_NODE node, FILE *input, progress_callback cb);
STORAGE_NODE e_storage_download(STORAGE_NODE node, CID cid, FILE *output, progress_callback cb);
// Uploads a file. Returns CID string on success (caller must free), or NULL on failure.
char *e_storage_upload(STORAGE_NODE node, const char *filepath, progress_callback cb);
// Downloads content identified by cid to filepath. Returns 0 on success.
int e_storage_download(STORAGE_NODE node, const char *cid, const char *filepath, progress_callback cb);
#endif // STORAGECONSOLE_EASYLIBSTORAGE_H

139
main.c
View File

@ -1,5 +1,6 @@
#include "easylibstorage.h"
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -8,7 +9,7 @@ typedef struct {
void *ctx;
} console;
typedef void (*fn)(void *, console *);
typedef void (*fn)(char *, console *);
struct command {
const char *name;
@ -16,20 +17,16 @@ struct command {
fn command;
};
int n_commands();
int n_commands(void);
static const struct command commands[];
void cmd_help(void *_, console *c) {
void cmd_help(char *args, console *c) {
printf("Commands:\n");
for (int i = 0; i < n_commands(); i++) {
printf(" [%s]: %s\n", commands[i].name, commands[i].desc);
}
}
void cmd_quit(void *_, console *c) {
}
void progress_print(int total, int complete, int status) {
if (total > 0) {
printf("\r %d / %d bytes", complete, total);
@ -39,32 +36,127 @@ void progress_print(int total, int complete, int status) {
fflush(stdout);
}
void cmd_start(void *args, console *c) {
void cmd_start(char *args, console *c) {
if (c->ctx) {
printf("Node already running. Stop it first.\n");
return;
}
int api_port = 0, disc_port = 0;
char bootstrap[2048] = {0};
if (!args || sscanf(args, "%d %d %2047s", &api_port, &disc_port, bootstrap) < 2) {
printf("Usage: start [API_PORT] [DISC_PORT] [BOOTSTRAP_NODE]\n");
return;
}
node_config cfg = {0};
cfg.api_port = api_port;
cfg.disc_port = disc_port;
cfg.data_dir = "./data";
cfg.log_level = "INFO";
cfg.bootstrap_node = bootstrap[0] ? bootstrap : NULL;
printf("Creating node...\n");
STORAGE_NODE node = e_storage_new(cfg);
if (!node) {
printf("Failed to create node.\n");
return;
}
printf("Starting node...\n");
if (e_storage_start(node) != 0) {
printf("Failed to start node.\n");
e_storage_destroy(node);
return;
}
c->ctx = node;
printf("Node started on API port %d, discovery port %d.\n", api_port, disc_port);
}
void cmd_stop(void *args, console *c) {
void cmd_stop(char *args, console *c) {
if (!c->ctx) {
printf("No node running.\n");
return;
}
printf("Stopping node...\n");
e_storage_stop(c->ctx);
e_storage_destroy(c->ctx);
c->ctx = NULL;
printf("Node stopped.\n");
}
void cmd_upload(void *args, console *c) {
void cmd_upload(char *args, console *c) {
if (!c->ctx) {
printf("No node running. Start one first.\n");
return;
}
if (!args || args[0] == '\0') {
printf("Usage: upload [PATH]\n");
return;
}
char resolved[PATH_MAX];
if (!realpath(args, resolved)) {
printf("File not found: %s\n", args);
return;
}
printf("Uploading %s...\n", resolved);
char *cid = e_storage_upload(c->ctx, resolved, progress_print);
if (cid) {
printf("CID: %s\n", cid);
free(cid);
} else {
printf("Upload failed.\n");
}
}
void cmd_download(void *args, console *c) {
void cmd_download(char *args, console *c) {
if (!c->ctx) {
printf("No node running. Start one first.\n");
return;
}
char cid[256] = {0};
char path[2048] = {0};
if (!args || sscanf(args, "%255s %2047s", cid, path) < 2) {
printf("Usage: download [CID] [PATH]\n");
return;
}
printf("Downloading %s to %s...\n", cid, path);
if (e_storage_download(c->ctx, cid, path, progress_print) == 0) {
printf("Download complete.\n");
} else {
printf("Download failed.\n");
}
}
void cmd_quit(char *args, console *c) {
if (c->ctx) {
printf("Stopping node...\n");
e_storage_stop(c->ctx);
e_storage_destroy(c->ctx);
c->ctx = NULL;
}
exit(0);
}
static const struct command commands[] = {
{"help", "prints this help message", cmd_help},
{"quit", "quits this program", cmd_quit},
{"start", "[API PORT] [DISC PORT] [BOOTSTRAP NODE] creates and starts a node", cmd_start},
{"start", "[API_PORT] [DISC_PORT] [BOOTSTRAP_NODE] creates and starts a node", cmd_start},
{"stop", "stops and destroys the node", cmd_stop},
{"upload", "[PATH] uploads a file to the node", cmd_upload},
{"download", "[CID] [PATH] downloads content to a file", cmd_download},
};
int n_commands() { return sizeof(commands) / sizeof(commands[0]); }
int n_commands(void) { return sizeof(commands) / sizeof(commands[0]); }
int main(void) {
char buf[4096];
@ -82,22 +174,29 @@ int main(void) {
}
buf[strcspn(buf, "\n")] = 0;
char *cmd = strtok(buf, " ");
if (cmd == NULL) {
// user has input an empty string
if (buf[0] == '\0') {
continue;
}
// Split at first space: cmd points to command name, rest points to arguments
char *rest = strchr(buf, ' ');
if (rest) {
*rest = '\0';
rest++;
// Skip leading spaces in arguments
while (*rest == ' ') rest++;
if (*rest == '\0') rest = NULL;
}
for (i = 0; i < n_commands(); i++) {
if (strcmp(cmd, commands[i].name) == 0) {
char *arg = strtok(NULL, " ");
commands[i].command(arg, &c);
if (strcmp(buf, commands[i].name) == 0) {
commands[i].command(rest, &c);
break;
}
}
if (i == n_commands()) {
printf("Invalid command %s\n", buf);
printf("Invalid command: %s\n", buf);
}
}
}

90
tests/mock_libstorage.c Normal file
View File

@ -0,0 +1,90 @@
#include "libstorage.h"
#include <stdlib.h>
#include <string.h>
// A fake context to return from storage_new.
static int fake_ctx_data = 42;
void libstorageNimMain(void) {
// no-op
}
void *storage_new(const char *configJson, StorageCallback callback, void *userData) {
if (callback) {
callback(RET_OK, "ok", 2, userData);
}
return &fake_ctx_data;
}
int storage_start(void *ctx, StorageCallback callback, void *userData) {
if (!ctx) return RET_ERR;
if (callback) {
callback(RET_OK, "started", 7, userData);
}
return RET_OK;
}
int storage_stop(void *ctx, StorageCallback callback, void *userData) {
if (!ctx) return RET_ERR;
if (callback) {
callback(RET_OK, "stopped", 7, userData);
}
return RET_OK;
}
int storage_close(void *ctx, StorageCallback callback, void *userData) {
if (!ctx) return RET_ERR;
if (callback) {
callback(RET_OK, "closed", 6, userData);
}
return RET_OK;
}
int storage_destroy(void *ctx, StorageCallback callback, void *userData) {
if (!ctx) return RET_ERR;
if (callback) {
callback(RET_OK, "destroyed", 9, userData);
}
return RET_OK;
}
int storage_upload_init(void *ctx, const char *filepath, size_t chunkSize, StorageCallback callback, void *userData) {
if (!ctx) return RET_ERR;
// Return a fake session ID
const char *session_id = "mock-session-123";
if (callback) {
callback(RET_OK, session_id, strlen(session_id), userData);
}
return RET_OK;
}
int storage_upload_file(void *ctx, const char *sessionId, StorageCallback callback, void *userData) {
if (!ctx) return RET_ERR;
// Fire a progress callback first, then final OK with CID
if (callback) {
callback(RET_PROGRESS, "chunk", 5, userData);
const char *cid = "zDvZRwzmAbCdEfGhIjKlMnOpQrStUvWxYz0123456789ABCD";
callback(RET_OK, cid, strlen(cid), userData);
}
return RET_OK;
}
int storage_download_init(void *ctx, const char *cid, size_t chunkSize, bool local, StorageCallback callback,
void *userData) {
if (!ctx) return RET_ERR;
if (callback) {
callback(RET_OK, "init", 4, userData);
}
return RET_OK;
}
int storage_download_stream(void *ctx, const char *cid, size_t chunkSize, bool local, const char *filepath,
StorageCallback callback, void *userData) {
if (!ctx) return RET_ERR;
if (callback) {
callback(RET_PROGRESS, "data", 4, userData);
callback(RET_OK, "done", 4, userData);
}
return RET_OK;
}

155
tests/test_runner.c Normal file
View File

@ -0,0 +1,155 @@
#include "easylibstorage.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define RET_OK 0
#define RET_ERR 1
static int tests_run = 0;
static int tests_passed = 0;
#define RUN_TEST(fn) \
do { \
tests_run++; \
printf(" %-30s", #fn); \
fn(); \
tests_passed++; \
printf(" OK\n"); \
} while (0)
static node_config default_config(void) {
node_config cfg = {0};
cfg.api_port = 8080;
cfg.disc_port = 8090;
cfg.data_dir = "./test-data";
cfg.log_level = "WARN";
cfg.bootstrap_node = NULL;
return cfg;
}
// --- Tests ---
static void test_new(void) {
node_config cfg = default_config();
cfg.bootstrap_node = "spr:abc123";
STORAGE_NODE node = e_storage_new(cfg);
assert(node != NULL);
}
static void test_new_defaults(void) {
node_config cfg = {0};
cfg.api_port = 9000;
cfg.disc_port = 9010;
STORAGE_NODE node = e_storage_new(cfg);
assert(node != NULL);
}
static void test_start(void) {
node_config cfg = default_config();
STORAGE_NODE node = e_storage_new(cfg);
assert(node != NULL);
int ret = e_storage_start(node);
assert(ret == RET_OK);
}
static void test_start_null(void) {
int ret = e_storage_start(NULL);
assert(ret == RET_ERR);
}
static void test_stop(void) {
node_config cfg = default_config();
STORAGE_NODE node = e_storage_new(cfg);
assert(node != NULL);
e_storage_start(node);
int ret = e_storage_stop(node);
assert(ret == RET_OK);
}
static void test_destroy(void) {
node_config cfg = default_config();
STORAGE_NODE node = e_storage_new(cfg);
assert(node != NULL);
int ret = e_storage_destroy(node);
assert(ret == RET_OK);
}
static void test_destroy_null(void) {
int ret = e_storage_destroy(NULL);
assert(ret == RET_ERR);
}
static void test_upload(void) {
node_config cfg = default_config();
STORAGE_NODE node = e_storage_new(cfg);
assert(node != NULL);
e_storage_start(node);
char *cid = e_storage_upload(node, "/tmp/test.txt", NULL);
assert(cid != NULL);
assert(strlen(cid) > 0);
free(cid);
}
static void test_upload_null(void) {
char *cid = e_storage_upload(NULL, "/tmp/test.txt", NULL);
assert(cid == NULL);
}
static void test_download(void) {
node_config cfg = default_config();
STORAGE_NODE node = e_storage_new(cfg);
assert(node != NULL);
e_storage_start(node);
int ret = e_storage_download(node, "zDvZRwzmSomeCid", "/tmp/out.dat", NULL);
assert(ret == RET_OK);
}
static void test_download_null(void) {
int ret = e_storage_download(NULL, "zDvZRwzmSomeCid", "/tmp/out.dat", NULL);
assert(ret == RET_ERR);
}
static void test_full_lifecycle(void) {
node_config cfg = default_config();
cfg.bootstrap_node = "spr:node1";
STORAGE_NODE node = e_storage_new(cfg);
assert(node != NULL);
assert(e_storage_start(node) == RET_OK);
char *cid = e_storage_upload(node, "/tmp/lifecycle.txt", NULL);
assert(cid != NULL);
assert(strlen(cid) > 0);
assert(e_storage_download(node, cid, "/tmp/lifecycle_out.dat", NULL) == RET_OK);
free(cid);
assert(e_storage_stop(node) == RET_OK);
assert(e_storage_destroy(node) == RET_OK);
}
int main(void) {
printf("Running easylibstorage tests...\n");
RUN_TEST(test_new);
RUN_TEST(test_new_defaults);
RUN_TEST(test_start);
RUN_TEST(test_start_null);
RUN_TEST(test_stop);
RUN_TEST(test_destroy);
RUN_TEST(test_destroy_null);
RUN_TEST(test_upload);
RUN_TEST(test_upload_null);
RUN_TEST(test_download);
RUN_TEST(test_download_null);
RUN_TEST(test_full_lifecycle);
printf("\n%d/%d tests passed.\n", tests_passed, tests_run);
return (tests_passed == tests_run) ? 0 : 1;
}