From f62f3545b44b951518efdc354109cf71946cc2a7 Mon Sep 17 00:00:00 2001 From: cpq Date: Thu, 24 Sep 2026 15:42:50 +0100 Subject: [PATCH] Adjust dash to use core file manager --- mongoose.c | 250 +++--------------- mongoose.h | 44 +-- src/dash.c | 250 +++--------------- src/dash.h | 44 +-- test/unit_test.c | 69 ++--- tutorials/device-dashboard/array/dashboard.c | 2 +- tutorials/device-dashboard/full/dashboard.c | 46 +--- .../device-dashboard/full/dashboard.html | 112 +++++--- .../device-dashboard/minimal/dashboard.c | 2 +- 9 files changed, 173 insertions(+), 646 deletions(-) diff --git a/mongoose.c b/mongoose.c index 8b00f474..9c086aa1 100644 --- a/mongoose.c +++ b/mongoose.c @@ -1053,12 +1053,7 @@ static int mg_dash_array_size(struct mg_field_set *set, struct mg_dash_user *u) { int saved = *set->index, sz = -1; *set->index = -1; - if (set->fn) { - if (set->fn(MG_DASH_READ, u)) sz = *set->index; - } else if (set->get_dir) { - mg_dash_dir_read(set, u); - sz = *set->index; - } + if (set->fn && set->fn(MG_DASH_READ, u)) sz = *set->index; *set->index = saved; return sz; } @@ -1076,10 +1071,7 @@ static size_t mg_dash_print_array(mg_pfn_t fn, void *arg, va_list *ap) { for (;;) { bool done = to >= 0 && *set->index > to; if (!done) { - if (set->fn) - set->fn(MG_DASH_READ, u); - else if (set->get_dir) - mg_dash_dir_read(set, u); + if (set->fn) set->fn(MG_DASH_READ, u); done = *set->index < 0; } if (done) break; @@ -1161,8 +1153,7 @@ void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *set) { MG_ESC("change"), MG_ESC("params"), MG_ESC(set->name), sz); } else { int saved_idx = set->index != NULL ? *set->index : 0; - bool ok = set->fn ? set->fn(MG_DASH_READ, u) - : (set->get_dir ? mg_dash_dir_read(set, u) : true); + bool ok = set->fn ? set->fn(MG_DASH_READ, u) : true; if (!ok) { if (set->index != NULL) *set->index = saved_idx; continue; @@ -1209,8 +1200,7 @@ static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) { mg_json_get_num(json, json_path, (double *) f->value); } else if (f->type == MG_VAL_STR && f->value_size > 0) { struct mg_str tok = mg_json_get_tok(json, json_path); - ok = tok.len >= 2 && tok.buf[0] == '"' && - tok.buf[tok.len - 1] == '"'; + ok = tok.len >= 2 && tok.buf[0] == '"' && tok.buf[tok.len - 1] == '"'; if (ok) mg_json_unescape(json, json_path, (char *) f->value, f->value_size); } else if (f->type == MG_VAL_RAW && f->value_size > 0) { ok = mg_snprintf((char *) f->value, f->value_size, "%.*s", json.len, @@ -1247,68 +1237,6 @@ static int mg_dash_apply(struct mg_connection *c, struct mg_dash *dash, return total_count; } -bool mg_dash_dir_read(struct mg_field_set *set, struct mg_dash_user *u) { - char dir[256], fname[128] = ""; - struct mg_fs *fs = u->dash->upload_fs ? u->dash->upload_fs : &mg_fs_posix; - struct mg_field *name_field = NULL, *size_field = NULL; - size_t i; - - if (!set->get_dir(u, dir, sizeof(dir))) return false; - - for (i = 0; set->fields[i].name != NULL; i++) { - if (name_field == NULL && set->fields[i].type == MG_VAL_STR && - strcmp(set->fields[i].name, "name") == 0) - name_field = &set->fields[i]; - if (size_field == NULL && strcmp(set->fields[i].name, "size") == 0) - size_field = &set->fields[i]; - } - if (name_field == NULL) return false; - - if (*set->index == -1) { // Size query: count all files - int count = 0; - while (mg_fs_ls(fs, dir, fname, sizeof(fname))) count++; - *set->index = count; - return true; - } - - { // Regular read: scan to *set->index - int target = *set->index, cur = 0; - while (mg_fs_ls(fs, dir, fname, sizeof(fname))) { - if (cur++ == target) { - mg_snprintf((char *) name_field->value, name_field->value_size, "%s", - fname); - if (size_field != NULL) { - char path[512]; - size_t sz = 0; - mg_snprintf(path, sizeof(path), "%s/%s", dir, fname); - fs->st(path, &sz, NULL); - if (size_field->type == MG_VAL_UINT64) - *(uint64_t *) size_field->value = (uint64_t) sz; - else if (size_field->type == MG_VAL_INT) - *(int *) size_field->value = (int) sz; - } - return true; - } - } - *set->index = -1; // No more entries - return true; - } -} - -static bool mg_dash_set_file_name(struct mg_field_set *set, - struct mg_str name) { - size_t i; - for (i = 0; set->fields[i].name != NULL; i++) { - struct mg_field *f = &set->fields[i]; - if (f->type == MG_VAL_STR && strcmp(f->name, "name") == 0) { - mg_snprintf((char *) f->value, f->value_size, "%.*s", (int) name.len, - name.buf); - return true; - } - } - return false; -} - static inline void mg_log_http_req(struct mg_connection *c, struct mg_http_message *hm) { int len = 0; @@ -1341,77 +1269,6 @@ static inline void mg_log_http_req(struct mg_connection *c, c->send.buf + n)); } -// State of one file upload to a dashboard file array. Owned by the upload -// callback, which frees it in the end call -struct mg_dash_upload { - struct mg_mgr *mgr; // Manager, to notify WebSocket clients - struct mg_dash *dash; // Dashboard whose file arrays get notified - struct mg_fd *fd; // Destination file, opened by the request handler - size_t expected; // Number of body bytes to receive - size_t received; // Number of body bytes written -}; - -// Upload callback for mg_http_stream_body(). Notifies clients only if the whole -// body was written. A dropped or failed upload sends no notification -static bool mg_dash_upload_cb(struct mg_http_message *hm, struct mg_str *data, - void **p) { - struct mg_dash_upload *up = (struct mg_dash_upload *) *p; - bool ok; - if (hm != NULL) return true; // Start, the file is already open - if (data != NULL) { // Next chunk - ok = up->fd->fs->wr(up->fd->fd, data->buf, data->len) == data->len; - if (ok) up->received += data->len; - return ok; - } - ok = up->received == up->expected; // End - mg_fs_close(up->fd); - if (ok) { - // Notify every file-backed array: the upload could belong to any of them, - // and re-querying get_dir() per recipient is what mg_dash_send_change() - // does anyway (directories can be user-specific) - struct mg_field_set *set; - for (set = up->dash->sets; set != NULL; set = set->next) { - if (set->get_dir == NULL) continue; - *set->index = -1; // Signal mg_dash_send_change() to broadcast new size - mg_dash_send_change(up->mgr, set); - } - } - mg_free(up); - *p = NULL; - return ok; -} - -// Opens dir/name and starts streaming the request body into it. Returns true -// if streaming started, otherwise the error reply has been sent already -static bool mg_dash_start_upload(struct mg_connection *c, - struct mg_http_message *hm, - struct mg_dash *dash, struct mg_fs *fs, - const char *dir, struct mg_str name) { - char path[MG_PATH_MAX]; - struct mg_dash_upload *up = NULL; - struct mg_fd *fd = NULL; - size_t n = mg_snprintf(path, sizeof(path), "%s%c%.*s", dir, MG_DIRSEP, - (int) name.len, name.buf); - if (n < sizeof(path)) fd = mg_fs_open(fs, path, MG_FS_WRITE); - if (fd != NULL) up = (struct mg_dash_upload *) mg_calloc(1, sizeof(*up)); - if (up == NULL) { - if (fd != NULL) mg_fs_close(fd); - mg_http_reply(c, 500, MG_JSON_HEADERS, "Open failed\n"); - return false; - } - up->mgr = c->mgr; - up->dash = dash; - up->fd = fd; - up->expected = hm->body.len; - if (mg_http_stream_body(c, hm, mg_dash_upload_cb, up)) { - return true; // The callback owns up and fd now - } - mg_fs_close(fd); // Chunked body or unexpected method: not streamed - mg_free(up); - mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad request\n"); - return false; -} - static uint64_t mg_dash_make_expiration_time(struct mg_dash *dash) { unsigned t = (unsigned) dash->session_auto_expiration_seconds; if (t == 0) t = 3600; // Default expiration time in seconds @@ -1666,6 +1523,25 @@ static bool otacb(struct mg_http_message *hm, struct mg_str *data, void **p) { return mg_ota_end(); } +static void mg_serve_fs(struct mg_connection *c, struct mg_http_message *hm, + struct mg_dash_user *u, bool stream) { + struct mg_dash *dash = (struct mg_dash *) c->fn_data; + char root[128] = "/fs/="; + if (u == NULL) { + mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); + } else if (dash->files_dir(u, root + 5, sizeof(root) - 5) == false) { + mg_http_reply(c, 500, MG_JSON_HEADERS, "Upload dir error\n"); + } else { + struct mg_http_serve_opts o; + memset(&o, 0, sizeof(o)); + o.root_dir = root; + o.fs = dash->upload_fs ? dash->upload_fs : &mg_fs_posix; + o.allow_delete = o.allow_upload = true; + if (stream) mg_http_serve_upload(c, hm, &o); + else mg_http_serve_dir(c, hm, &o); + } +} + void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { struct mg_dash *dash = (struct mg_dash *) c->fn_data; struct mg_dash_cdata *d = (struct mg_dash_cdata *) c->data; @@ -1679,8 +1555,6 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { // Received headers - check authentication and possibly start uploads/ota struct mg_http_message *hm = (struct mg_http_message *) ev_data; struct mg_dash_user *u = mg_dash_authenticate(c, hm, dash); - struct mg_str parts[3]; - memset(parts, 0, sizeof(parts)); if (mg_match(hm->uri, mg_str("/api/hi"), NULL) || mg_match(hm->uri, mg_str("/api/logout"), NULL)) { @@ -1691,45 +1565,14 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { } else if (mg_match(hm->uri, mg_str("/api/login"), NULL) && u != NULL) { mg_handle_login(c, u); d->marker = CONN_HANDLED; + } else if (u != NULL && dash->files_dir != NULL && + mg_match(hm->uri, mg_str("/fs/#"), NULL)) { + mg_serve_fs(c, hm, u, true); + return; // Important - the serve_fs() call can invalidate hm } else if (mg_match(hm->uri, mg_str("/api/ota"), NULL)) { // Authenticated above. If streaming started, the stream handler owns // the connection and c->data, so skip the marker check below if (mg_http_stream_body(c, hm, otacb, NULL)) return; - } else if (mg_match(hm->uri, mg_str("/fs/*/*"), parts) && - (mg_strcasecmp(hm->method, mg_str("POST")) == 0 || - mg_strcasecmp(hm->method, mg_str("PUT")) == 0)) { - struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]); - struct mg_str name = parts[1]; - int len = - mg_url_decode(name.buf, name.len, (char *) name.buf, name.len + 1, 0); - if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len; - if (set == NULL || set->get_dir == NULL) { - mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found\n"); - d->marker = CONN_HANDLED; - } else if (u == NULL) { - mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); - d->marker = CONN_HANDLED; - } else if (!mg_path_is_sane(name)) { - mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad file name\n"); - d->marker = CONN_HANDLED; - } else { - mg_dash_set_file_name(set, name); - if (set->fn != NULL && !set->fn(MG_DASH_WRITE, u)) { - mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); - d->marker = CONN_HANDLED; - } else { - char dir[256]; - struct mg_fs *fs = dash->upload_fs ? dash->upload_fs : &mg_fs_posix; - if (!set->get_dir(u, dir, sizeof(dir))) { - mg_http_reply(c, 500, MG_JSON_HEADERS, "Upload dir error\n"); - d->marker = CONN_HANDLED; - } else if (mg_dash_start_upload(c, hm, dash, fs, dir, name)) { - return; // The stream handler owns the connection and c->data - } else { - d->marker = CONN_HANDLED; // Error reply has been sent - } - } - } } if (d->marker != '\0') mg_log_http_req(c, hm); } else if (ev == MG_EV_HTTP_MSG && d->marker != '\0') { @@ -1752,42 +1595,9 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { } else if (mg_match(hm->uri, mg_str("/api/websocket"), NULL)) { d->u = u; mg_ws_upgrade(c, hm, NULL); - } else if (mg_match(hm->uri, mg_str("/fs/*/*"), parts)) { - struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]); - if (set == NULL || set->get_dir == NULL) { - mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found"); - } else if (u == NULL) { - mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); - } else { - char dir[256], path[512]; - struct mg_fs *fs = dash->upload_fs ? dash->upload_fs : &mg_fs_posix; - struct mg_str name = parts[1]; - int len = mg_url_decode(name.buf, name.len, (char *) name.buf, - name.len + 1, 0); - if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len; - if (!mg_path_is_sane(name)) { - mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad file name\n"); - return; - } - if (!set->get_dir(u, dir, sizeof(dir))) { - mg_http_reply(c, 500, MG_JSON_HEADERS, "Upload dir error\n"); - return; - } - mg_snprintf(path, sizeof(path), "%s/%.*s", dir, name.len, name.buf); - if (mg_strcasecmp(hm->method, mg_str("DELETE")) == 0) { - mg_dash_set_file_name(set, name); - if (set->fn != NULL && !set->fn(MG_DASH_DELETE, u)) { - mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); - } else { - fs->rm(path); - *set->index = -1; // Signal mg_dash_send_change() to send new size - mg_dash_send_change(c->mgr, set); - mg_http_reply(c, 200, NULL, "true"); - } - } else { - mg_http_serve_file(c, hm, path, NULL); - } - } + } else if (dash->files_dir != NULL && + mg_match(hm->uri, mg_str("/fs/#"), NULL)) { + mg_serve_fs(c, hm, u, false); } else if (mg_match(hm->uri, mg_str("/api/del/*/*/*"), parts) || mg_match(hm->uri, mg_str("/api/del/*/*"), parts)) { mg_dash_handle_del(c, dash, u, parts); diff --git a/mongoose.h b/mongoose.h index 8500c569..99e4e992 100644 --- a/mongoose.h +++ b/mongoose.h @@ -4647,9 +4647,7 @@ struct mg_field_set { // fn sets *index = -1 to signal end of iteration. // Size query: framework sets *index = -1 before fn(READ); fn // sets *index = total size - bool (*get_dir)(const struct mg_dash_user *u, char *buf, - size_t len); // Non-NULL: file array - struct mg_field_set *next; // Next set in the list + struct mg_field_set *next; // Next set in the list }; // Custom URI handler, registered with MG_DASH_REGISTER_CUSTOM_HANDLER() @@ -4670,6 +4668,9 @@ struct mg_dash { int session_auto_expiration_seconds; // Session lifetime, 0: 1 hour default struct mg_dash_user *guest; // Used when authenticate == NULL struct mg_fs *upload_fs; // FS for uploads/listing, default &mg_fs_posix + // Resolves the built-in file manager's root dir for user u, e.g. per-user + // upload directories. NULL: file manager disabled + bool (*files_dir)(const struct mg_dash_user *u, char *buf, size_t len); }; #define MG_DASH_ADD_FIELD_SET(dash_, set_) \ @@ -4695,43 +4696,6 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data); // array size if *set->index < 0, or a single element update otherwise void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *); -// Default reader for file-backed array sets (set->get_dir != NULL) -// Usage example - this is a complete implemenation of the file manager: -// ```c -// static struct file { -// int index; -// char name[64]; -// size_t size; -// uint64_t checksum; -// } s_file; -// static struct mg_field_set set_files; -// -// static bool get_dir(const struct mg_dash_user *u, char *buf, size_t len) { -// (void) u; -// mkdir("/tmp/dashboard", 0755); -// mg_snprintf(buf, len, "%s", "/tmp/dashboard"); -// return true; -// } -// -// static struct mg_field fields_files[] = { -// {"name", MG_VAL_STR, s_file.name, sizeof(s_file.name)}, -// {"size", MG_VAL_UINT64, &s_file.size, 0}, -// {"checksum", MG_VAL_UINT64, &s_file.checksum, sizeof(s_file.checksum)}, -// {NULL, MG_VAL_INT, NULL, 0}, -// }; -// -// static bool files_fn(enum mg_dash_op op, struct mg_dash_user *u) { -// if (op == MG_DASH_WRITE) return s_uploads_enabled && u->level >= 7; -// if (op != MG_DASH_READ) return false; -// if (!mg_dash_dir_read(&set_files, u)) return false; -// s_file.checksum = 0; // Mock to show how to set custom fields -// return true; -// } -// -// static struct mg_field_set set_files = {"files", fields_files, files_fn, &s_file.index, get_dir, NULL}; -// ``` -bool mg_dash_dir_read(struct mg_field_set *set, struct mg_dash_user *u); - // Helper forward declarations for Mongoose CMSIS pack modules extern struct mg_mgr g_mgr; extern void mg_dash_init(struct mg_mgr *); diff --git a/src/dash.c b/src/dash.c index 00bdcab3..cb7cc83c 100644 --- a/src/dash.c +++ b/src/dash.c @@ -78,12 +78,7 @@ static int mg_dash_array_size(struct mg_field_set *set, struct mg_dash_user *u) { int saved = *set->index, sz = -1; *set->index = -1; - if (set->fn) { - if (set->fn(MG_DASH_READ, u)) sz = *set->index; - } else if (set->get_dir) { - mg_dash_dir_read(set, u); - sz = *set->index; - } + if (set->fn && set->fn(MG_DASH_READ, u)) sz = *set->index; *set->index = saved; return sz; } @@ -101,10 +96,7 @@ static size_t mg_dash_print_array(mg_pfn_t fn, void *arg, va_list *ap) { for (;;) { bool done = to >= 0 && *set->index > to; if (!done) { - if (set->fn) - set->fn(MG_DASH_READ, u); - else if (set->get_dir) - mg_dash_dir_read(set, u); + if (set->fn) set->fn(MG_DASH_READ, u); done = *set->index < 0; } if (done) break; @@ -186,8 +178,7 @@ void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *set) { MG_ESC("change"), MG_ESC("params"), MG_ESC(set->name), sz); } else { int saved_idx = set->index != NULL ? *set->index : 0; - bool ok = set->fn ? set->fn(MG_DASH_READ, u) - : (set->get_dir ? mg_dash_dir_read(set, u) : true); + bool ok = set->fn ? set->fn(MG_DASH_READ, u) : true; if (!ok) { if (set->index != NULL) *set->index = saved_idx; continue; @@ -234,8 +225,7 @@ static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) { mg_json_get_num(json, json_path, (double *) f->value); } else if (f->type == MG_VAL_STR && f->value_size > 0) { struct mg_str tok = mg_json_get_tok(json, json_path); - ok = tok.len >= 2 && tok.buf[0] == '"' && - tok.buf[tok.len - 1] == '"'; + ok = tok.len >= 2 && tok.buf[0] == '"' && tok.buf[tok.len - 1] == '"'; if (ok) mg_json_unescape(json, json_path, (char *) f->value, f->value_size); } else if (f->type == MG_VAL_RAW && f->value_size > 0) { ok = mg_snprintf((char *) f->value, f->value_size, "%.*s", json.len, @@ -272,68 +262,6 @@ static int mg_dash_apply(struct mg_connection *c, struct mg_dash *dash, return total_count; } -bool mg_dash_dir_read(struct mg_field_set *set, struct mg_dash_user *u) { - char dir[256], fname[128] = ""; - struct mg_fs *fs = u->dash->upload_fs ? u->dash->upload_fs : &mg_fs_posix; - struct mg_field *name_field = NULL, *size_field = NULL; - size_t i; - - if (!set->get_dir(u, dir, sizeof(dir))) return false; - - for (i = 0; set->fields[i].name != NULL; i++) { - if (name_field == NULL && set->fields[i].type == MG_VAL_STR && - strcmp(set->fields[i].name, "name") == 0) - name_field = &set->fields[i]; - if (size_field == NULL && strcmp(set->fields[i].name, "size") == 0) - size_field = &set->fields[i]; - } - if (name_field == NULL) return false; - - if (*set->index == -1) { // Size query: count all files - int count = 0; - while (mg_fs_ls(fs, dir, fname, sizeof(fname))) count++; - *set->index = count; - return true; - } - - { // Regular read: scan to *set->index - int target = *set->index, cur = 0; - while (mg_fs_ls(fs, dir, fname, sizeof(fname))) { - if (cur++ == target) { - mg_snprintf((char *) name_field->value, name_field->value_size, "%s", - fname); - if (size_field != NULL) { - char path[512]; - size_t sz = 0; - mg_snprintf(path, sizeof(path), "%s/%s", dir, fname); - fs->st(path, &sz, NULL); - if (size_field->type == MG_VAL_UINT64) - *(uint64_t *) size_field->value = (uint64_t) sz; - else if (size_field->type == MG_VAL_INT) - *(int *) size_field->value = (int) sz; - } - return true; - } - } - *set->index = -1; // No more entries - return true; - } -} - -static bool mg_dash_set_file_name(struct mg_field_set *set, - struct mg_str name) { - size_t i; - for (i = 0; set->fields[i].name != NULL; i++) { - struct mg_field *f = &set->fields[i]; - if (f->type == MG_VAL_STR && strcmp(f->name, "name") == 0) { - mg_snprintf((char *) f->value, f->value_size, "%.*s", (int) name.len, - name.buf); - return true; - } - } - return false; -} - static inline void mg_log_http_req(struct mg_connection *c, struct mg_http_message *hm) { int len = 0; @@ -366,77 +294,6 @@ static inline void mg_log_http_req(struct mg_connection *c, c->send.buf + n)); } -// State of one file upload to a dashboard file array. Owned by the upload -// callback, which frees it in the end call -struct mg_dash_upload { - struct mg_mgr *mgr; // Manager, to notify WebSocket clients - struct mg_dash *dash; // Dashboard whose file arrays get notified - struct mg_fd *fd; // Destination file, opened by the request handler - size_t expected; // Number of body bytes to receive - size_t received; // Number of body bytes written -}; - -// Upload callback for mg_http_stream_body(). Notifies clients only if the whole -// body was written. A dropped or failed upload sends no notification -static bool mg_dash_upload_cb(struct mg_http_message *hm, struct mg_str *data, - void **p) { - struct mg_dash_upload *up = (struct mg_dash_upload *) *p; - bool ok; - if (hm != NULL) return true; // Start, the file is already open - if (data != NULL) { // Next chunk - ok = up->fd->fs->wr(up->fd->fd, data->buf, data->len) == data->len; - if (ok) up->received += data->len; - return ok; - } - ok = up->received == up->expected; // End - mg_fs_close(up->fd); - if (ok) { - // Notify every file-backed array: the upload could belong to any of them, - // and re-querying get_dir() per recipient is what mg_dash_send_change() - // does anyway (directories can be user-specific) - struct mg_field_set *set; - for (set = up->dash->sets; set != NULL; set = set->next) { - if (set->get_dir == NULL) continue; - *set->index = -1; // Signal mg_dash_send_change() to broadcast new size - mg_dash_send_change(up->mgr, set); - } - } - mg_free(up); - *p = NULL; - return ok; -} - -// Opens dir/name and starts streaming the request body into it. Returns true -// if streaming started, otherwise the error reply has been sent already -static bool mg_dash_start_upload(struct mg_connection *c, - struct mg_http_message *hm, - struct mg_dash *dash, struct mg_fs *fs, - const char *dir, struct mg_str name) { - char path[MG_PATH_MAX]; - struct mg_dash_upload *up = NULL; - struct mg_fd *fd = NULL; - size_t n = mg_snprintf(path, sizeof(path), "%s%c%.*s", dir, MG_DIRSEP, - (int) name.len, name.buf); - if (n < sizeof(path)) fd = mg_fs_open(fs, path, MG_FS_WRITE); - if (fd != NULL) up = (struct mg_dash_upload *) mg_calloc(1, sizeof(*up)); - if (up == NULL) { - if (fd != NULL) mg_fs_close(fd); - mg_http_reply(c, 500, MG_JSON_HEADERS, "Open failed\n"); - return false; - } - up->mgr = c->mgr; - up->dash = dash; - up->fd = fd; - up->expected = hm->body.len; - if (mg_http_stream_body(c, hm, mg_dash_upload_cb, up)) { - return true; // The callback owns up and fd now - } - mg_fs_close(fd); // Chunked body or unexpected method: not streamed - mg_free(up); - mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad request\n"); - return false; -} - static uint64_t mg_dash_make_expiration_time(struct mg_dash *dash) { unsigned t = (unsigned) dash->session_auto_expiration_seconds; if (t == 0) t = 3600; // Default expiration time in seconds @@ -691,6 +548,25 @@ static bool otacb(struct mg_http_message *hm, struct mg_str *data, void **p) { return mg_ota_end(); } +static void mg_serve_fs(struct mg_connection *c, struct mg_http_message *hm, + struct mg_dash_user *u, bool stream) { + struct mg_dash *dash = (struct mg_dash *) c->fn_data; + char root[128] = "/fs/="; + if (u == NULL) { + mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); + } else if (dash->files_dir(u, root + 5, sizeof(root) - 5) == false) { + mg_http_reply(c, 500, MG_JSON_HEADERS, "Upload dir error\n"); + } else { + struct mg_http_serve_opts o; + memset(&o, 0, sizeof(o)); + o.root_dir = root; + o.fs = dash->upload_fs ? dash->upload_fs : &mg_fs_posix; + o.allow_delete = o.allow_upload = true; + if (stream) mg_http_serve_upload(c, hm, &o); + else mg_http_serve_dir(c, hm, &o); + } +} + void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { struct mg_dash *dash = (struct mg_dash *) c->fn_data; struct mg_dash_cdata *d = (struct mg_dash_cdata *) c->data; @@ -704,8 +580,6 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { // Received headers - check authentication and possibly start uploads/ota struct mg_http_message *hm = (struct mg_http_message *) ev_data; struct mg_dash_user *u = mg_dash_authenticate(c, hm, dash); - struct mg_str parts[3]; - memset(parts, 0, sizeof(parts)); if (mg_match(hm->uri, mg_str("/api/hi"), NULL) || mg_match(hm->uri, mg_str("/api/logout"), NULL)) { @@ -716,45 +590,14 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { } else if (mg_match(hm->uri, mg_str("/api/login"), NULL) && u != NULL) { mg_handle_login(c, u); d->marker = CONN_HANDLED; + } else if (u != NULL && dash->files_dir != NULL && + mg_match(hm->uri, mg_str("/fs/#"), NULL)) { + mg_serve_fs(c, hm, u, true); + return; // Important - the serve_fs() call can invalidate hm } else if (mg_match(hm->uri, mg_str("/api/ota"), NULL)) { // Authenticated above. If streaming started, the stream handler owns // the connection and c->data, so skip the marker check below if (mg_http_stream_body(c, hm, otacb, NULL)) return; - } else if (mg_match(hm->uri, mg_str("/fs/*/*"), parts) && - (mg_strcasecmp(hm->method, mg_str("POST")) == 0 || - mg_strcasecmp(hm->method, mg_str("PUT")) == 0)) { - struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]); - struct mg_str name = parts[1]; - int len = - mg_url_decode(name.buf, name.len, (char *) name.buf, name.len + 1, 0); - if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len; - if (set == NULL || set->get_dir == NULL) { - mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found\n"); - d->marker = CONN_HANDLED; - } else if (u == NULL) { - mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); - d->marker = CONN_HANDLED; - } else if (!mg_path_is_sane(name)) { - mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad file name\n"); - d->marker = CONN_HANDLED; - } else { - mg_dash_set_file_name(set, name); - if (set->fn != NULL && !set->fn(MG_DASH_WRITE, u)) { - mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); - d->marker = CONN_HANDLED; - } else { - char dir[256]; - struct mg_fs *fs = dash->upload_fs ? dash->upload_fs : &mg_fs_posix; - if (!set->get_dir(u, dir, sizeof(dir))) { - mg_http_reply(c, 500, MG_JSON_HEADERS, "Upload dir error\n"); - d->marker = CONN_HANDLED; - } else if (mg_dash_start_upload(c, hm, dash, fs, dir, name)) { - return; // The stream handler owns the connection and c->data - } else { - d->marker = CONN_HANDLED; // Error reply has been sent - } - } - } } if (d->marker != '\0') mg_log_http_req(c, hm); } else if (ev == MG_EV_HTTP_MSG && d->marker != '\0') { @@ -777,42 +620,9 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { } else if (mg_match(hm->uri, mg_str("/api/websocket"), NULL)) { d->u = u; mg_ws_upgrade(c, hm, NULL); - } else if (mg_match(hm->uri, mg_str("/fs/*/*"), parts)) { - struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]); - if (set == NULL || set->get_dir == NULL) { - mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found"); - } else if (u == NULL) { - mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); - } else { - char dir[256], path[512]; - struct mg_fs *fs = dash->upload_fs ? dash->upload_fs : &mg_fs_posix; - struct mg_str name = parts[1]; - int len = mg_url_decode(name.buf, name.len, (char *) name.buf, - name.len + 1, 0); - if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len; - if (!mg_path_is_sane(name)) { - mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad file name\n"); - return; - } - if (!set->get_dir(u, dir, sizeof(dir))) { - mg_http_reply(c, 500, MG_JSON_HEADERS, "Upload dir error\n"); - return; - } - mg_snprintf(path, sizeof(path), "%s/%.*s", dir, name.len, name.buf); - if (mg_strcasecmp(hm->method, mg_str("DELETE")) == 0) { - mg_dash_set_file_name(set, name); - if (set->fn != NULL && !set->fn(MG_DASH_DELETE, u)) { - mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); - } else { - fs->rm(path); - *set->index = -1; // Signal mg_dash_send_change() to send new size - mg_dash_send_change(c->mgr, set); - mg_http_reply(c, 200, NULL, "true"); - } - } else { - mg_http_serve_file(c, hm, path, NULL); - } - } + } else if (dash->files_dir != NULL && + mg_match(hm->uri, mg_str("/fs/#"), NULL)) { + mg_serve_fs(c, hm, u, false); } else if (mg_match(hm->uri, mg_str("/api/del/*/*/*"), parts) || mg_match(hm->uri, mg_str("/api/del/*/*"), parts)) { mg_dash_handle_del(c, dash, u, parts); diff --git a/src/dash.h b/src/dash.h index d3f81faf..aef9fbf8 100644 --- a/src/dash.h +++ b/src/dash.h @@ -73,9 +73,7 @@ struct mg_field_set { // fn sets *index = -1 to signal end of iteration. // Size query: framework sets *index = -1 before fn(READ); fn // sets *index = total size - bool (*get_dir)(const struct mg_dash_user *u, char *buf, - size_t len); // Non-NULL: file array - struct mg_field_set *next; // Next set in the list + struct mg_field_set *next; // Next set in the list }; // Custom URI handler, registered with MG_DASH_REGISTER_CUSTOM_HANDLER() @@ -96,6 +94,9 @@ struct mg_dash { int session_auto_expiration_seconds; // Session lifetime, 0: 1 hour default struct mg_dash_user *guest; // Used when authenticate == NULL struct mg_fs *upload_fs; // FS for uploads/listing, default &mg_fs_posix + // Resolves the built-in file manager's root dir for user u, e.g. per-user + // upload directories. NULL: file manager disabled + bool (*files_dir)(const struct mg_dash_user *u, char *buf, size_t len); }; #define MG_DASH_ADD_FIELD_SET(dash_, set_) \ @@ -121,43 +122,6 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data); // array size if *set->index < 0, or a single element update otherwise void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *); -// Default reader for file-backed array sets (set->get_dir != NULL) -// Usage example - this is a complete implemenation of the file manager: -// ```c -// static struct file { -// int index; -// char name[64]; -// size_t size; -// uint64_t checksum; -// } s_file; -// static struct mg_field_set set_files; -// -// static bool get_dir(const struct mg_dash_user *u, char *buf, size_t len) { -// (void) u; -// mkdir("/tmp/dashboard", 0755); -// mg_snprintf(buf, len, "%s", "/tmp/dashboard"); -// return true; -// } -// -// static struct mg_field fields_files[] = { -// {"name", MG_VAL_STR, s_file.name, sizeof(s_file.name)}, -// {"size", MG_VAL_UINT64, &s_file.size, 0}, -// {"checksum", MG_VAL_UINT64, &s_file.checksum, sizeof(s_file.checksum)}, -// {NULL, MG_VAL_INT, NULL, 0}, -// }; -// -// static bool files_fn(enum mg_dash_op op, struct mg_dash_user *u) { -// if (op == MG_DASH_WRITE) return s_uploads_enabled && u->level >= 7; -// if (op != MG_DASH_READ) return false; -// if (!mg_dash_dir_read(&set_files, u)) return false; -// s_file.checksum = 0; // Mock to show how to set custom fields -// return true; -// } -// -// static struct mg_field_set set_files = {"files", fields_files, files_fn, &s_file.index, get_dir, NULL}; -// ``` -bool mg_dash_dir_read(struct mg_field_set *set, struct mg_dash_user *u); - // Helper forward declarations for Mongoose CMSIS pack modules extern struct mg_mgr g_mgr; extern void mg_dash_init(struct mg_mgr *); diff --git a/test/unit_test.c b/test/unit_test.c index 9e129a93..69d2fa1d 100644 --- a/test/unit_test.c +++ b/test/unit_test.c @@ -4798,23 +4798,7 @@ static struct mg_field test_files_fields[] = { {NULL, MG_VAL_INT, NULL, 0}, }; static struct mg_field_set test_files_set = { - "files", test_files_fields, NULL, &s_test_file_index, test_upload_dir, NULL}; -static struct test_file_entry s_delete_file; -static char s_deleted_file[sizeof(s_delete_file.name)]; -static char s_uploaded_file[sizeof(s_delete_file.name)]; -static struct mg_field_set *s_delete_file_set; -static bool delete_file_fn(enum mg_dash_op op, struct mg_dash_user *u) { - if (op == MG_DASH_READ) return mg_dash_dir_read(s_delete_file_set, u); - if (op == MG_DASH_WRITE) { - mg_snprintf(s_uploaded_file, sizeof(s_uploaded_file), "%s", - s_delete_file.name); - } - if (op == MG_DASH_DELETE) { - mg_snprintf(s_deleted_file, sizeof(s_deleted_file), "%s", - s_delete_file.name); - } - return true; -} + "files", test_files_fields, NULL, &s_test_file_index, NULL}; static bool arr_fn(enum mg_dash_op op, struct mg_dash_user *u) { (void) u; if (op == MG_DASH_READ) { @@ -4861,20 +4845,18 @@ static void test_dash(void) { {"admin", MG_VAL_INT, &admin, sizeof(admin)}, {NULL, MG_VAL_INT, NULL, 0}, }; - struct mg_field_set set1 = {"set1", fields1, set1_fn, NULL, NULL, NULL}; - struct mg_field_set set2 = {"set2", fields2, set2_fn, NULL, NULL, NULL}; - struct mg_field_set set3 = {"set3", fields3, set3_fn, NULL, NULL, NULL}; + struct mg_field_set set1 = {"set1", fields1, set1_fn, NULL, NULL}; + struct mg_field_set set2 = {"set2", fields2, set2_fn, NULL, NULL}; + struct mg_field_set set3 = {"set3", fields3, set3_fn, NULL, NULL}; struct mg_field arr_fields[] = { {"val", MG_VAL_INT, &s_arr_val, sizeof(s_arr_val)}, {NULL, MG_VAL_INT, NULL, 0}, }; - struct mg_field_set arr_set = {"arr", arr_fields, arr_fn, &s_arr_index, NULL, - NULL}; + struct mg_field_set arr_set = {"arr", arr_fields, arr_fn, &s_arr_index, NULL}; const char *get_all_expected = "{\"arr\":17,\"set3\":{\"admin\":7}," "\"set2\":{\"two\":false}," - "\"set1\":{\"one\":1,\"three\":\"t: 1\"}," - "\"files\":0}\n"; + "\"set1\":{\"one\":1,\"three\":\"t: 1\"}}\n"; const char *set1_req = "POST /api/set HTTP/1.0\nContent-Length: 18\n\n" "{\"set1\":{\"one\":2}}"; const char *set1_set3_req = "POST /api/set HTTP/1.0\n" @@ -5227,51 +5209,40 @@ static void test_dash(void) { ASSERT(mg_http_listen(&mgr2, url2, mg_dash_ev_handler, &dash2) != NULL); ASSERT(fetch(&mgr2, buf, url2, "GET /api/get HTTP/1.0\n\n") == 200); ASSERT(strstr(buf, files_probe) == NULL); // no files fieldset - ASSERT(fetch(&mgr2, buf, url2, "GET /fs/files/test.txt HTTP/1.0\n\n") == 404); + ASSERT(fetch(&mgr2, buf, url2, "GET /fs/test.txt HTTP/1.0\n\n") == 404); ASSERT(fetch(&mgr2, buf, url2, - "POST /fs/files/test.txt HTTP/1.0\nContent-Length: 5\n\nhello") == + "POST /fs/test.txt HTTP/1.0\nContent-Length: 5\n\nhello") == 404); mg_mgr_free(&mgr2); } - // Test files fieldset with get_dir: /fs/ endpoints active + // Test the built-in file manager (struct mg_dash::files_dir): /fs/ active { const char *url3 = "http://localhost:26354"; struct mg_mgr mgr3; struct mg_dash dash3; - static int idx3; - static struct mg_field ff3[] = { - {"name", MG_VAL_STR, s_delete_file.name, sizeof(s_delete_file.name)}, - {"size", MG_VAL_UINT64, &s_delete_file.size, 0}, - {NULL, MG_VAL_INT, NULL, 0}, - }; - static struct mg_field_set fset3 = {"files", ff3, delete_file_fn, &idx3, - test_upload_dir, NULL}; + struct mg_str uploaded; memset(&dash3, 0, sizeof(dash3)); - s_deleted_file[0] = '\0'; - s_uploaded_file[0] = '\0'; - s_delete_file_set = &fset3; - MG_DASH_ADD_FIELD_SET(&dash3, &fset3); + dash3.files_dir = test_upload_dir; mg_mgr_init(&mgr3); mg_mem_files = mg_packed_files; ASSERT(mg_http_listen(&mgr3, url3, mg_dash_ev_handler, &dash3) != NULL); ASSERT(fetch(&mgr3, buf, url3, "GET /api/get HTTP/1.0\n\n") == 200); - ASSERT(strstr(buf, files_probe) != NULL); // files fieldset present + ASSERT(strstr(buf, files_probe) == NULL); // files manager is not a fieldset ASSERT(fetch(&mgr3, buf, url3, - "DELETE /fs/files/nofile.txt HTTP/1.0\n\n") == 200); - ASSERT(strcmp(s_deleted_file, "nofile.txt") == 0); + "DELETE /fs/nofile.txt HTTP/1.0\n\n") == 404); ASSERT(fetch(&mgr3, buf, url3, - "DELETE /fs/files/..%2Fevil.txt HTTP/1.0\n\n") == 400); - ASSERT(strcmp(s_deleted_file, "nofile.txt") == 0); + "DELETE /fs/..%2Fevil.txt HTTP/1.0\n\n") == 400); ASSERT(fetch(&mgr3, buf, url3, - "PUT /fs/files/test.bin HTTP/1.0\nContent-Length: 4\n\ndata") == + "PUT /fs/test.bin HTTP/1.0\nContent-Length: 4\n\ndata") == 200); - ASSERT(strcmp(s_uploaded_file, "test.bin") == 0); + uploaded = mg_file_read(&mg_fs_posix, "mongoose_dash_test/test.bin"); + ASSERT(mg_strcmp(uploaded, mg_str("data")) == 0); + mg_free((void *) uploaded.buf); ASSERT(fetch(&mgr3, buf, url3, - "PUT /fs/files/..%2Fevil.bin HTTP/1.0\nContent-Length: " + "PUT /fs/..%2Fevil.bin HTTP/1.0\nContent-Length: " "4\n\ndata") == 400); - ASSERT(strcmp(s_uploaded_file, "test.bin") == 0); - ASSERT(fetch(&mgr3, buf, url3, "GET /api/get/files/0 HTTP/1.0\n\n") == 200); + ASSERT(fetch(&mgr3, buf, url3, "GET /fs/ HTTP/1.0\n\n") == 200); ASSERT(strstr(buf, test_bin_probe) != NULL); mg_mgr_free(&mgr3); } diff --git a/tutorials/device-dashboard/array/dashboard.c b/tutorials/device-dashboard/array/dashboard.c index 7d4f86b8..81540a15 100644 --- a/tutorials/device-dashboard/array/dashboard.c +++ b/tutorials/device-dashboard/array/dashboard.c @@ -75,7 +75,7 @@ static bool arr_fn(enum mg_dash_op op, struct mg_dash_user *u) { } static struct mg_field_set field_set_arr = { - "event", fields_arr, arr_fn, &s_index, NULL, NULL, + "event", fields_arr, arr_fn, &s_index, NULL, }; void mg_dash_init(struct mg_mgr *mgr) { diff --git a/tutorials/device-dashboard/full/dashboard.c b/tutorials/device-dashboard/full/dashboard.c index 9c2ad53d..fff14bca 100644 --- a/tutorials/device-dashboard/full/dashboard.c +++ b/tutorials/device-dashboard/full/dashboard.c @@ -27,7 +27,7 @@ static struct mg_field fields_actions[] = { {NULL, MG_VAL_INT, NULL, 0}, }; static struct mg_field_set set_actions = {"actions", fields_actions, actions_fn, - NULL, NULL, NULL}; + NULL, NULL}; // Control panel // s_led1, s_led2, s_led3 are used to communicate LED status @@ -43,7 +43,7 @@ static struct mg_field fields_leds[] = { {NULL, MG_VAL_INT, NULL, 0}, }; static struct mg_field_set set_leds = {"leds", fields_leds, leds_fn, - NULL, NULL, NULL}; + NULL, NULL}; // Read-only device Metrics static int s_ram = 32, s_cpu = 7; @@ -66,7 +66,7 @@ static struct mg_field fields_metrics[] = { }; static struct mg_field_set set_metrics = {"metrics", fields_metrics, metrics_fn, - NULL, NULL, NULL}; + NULL, NULL}; // Read-write device settings static bool s_enable_login = false; @@ -105,7 +105,7 @@ static struct mg_field fields_settings[] = { }; static struct mg_field_set set_settings = { - "settings", fields_settings, settings_fn, NULL, NULL, NULL}; + "settings", fields_settings, settings_fn, NULL, NULL}; #define NUM_POINTS_GRAPH1 7 // How many graph1 data points to send @@ -150,7 +150,7 @@ static bool graph1_fn(enum mg_dash_op op, struct mg_dash_user *u) { return true; } static struct mg_field_set set_graph1 = {"graph1", fields_graph1, graph1_fn, - NULL, NULL, NULL}; + NULL, NULL}; #define NUM_POINTS_GRAPH2 100 // How many graph2 data points to send static char s_graph2_data[NUM_POINTS_GRAPH2 * 4 + 2 + 1]; @@ -187,7 +187,7 @@ static bool graph2_fn(enum mg_dash_op op, struct mg_dash_user *u) { return op == MG_DASH_READ || op == MG_DASH_WRITE; } static struct mg_field_set set_graph2 = {"graph2", fields_graph2, graph2_fn, - NULL, NULL, NULL}; + NULL, NULL}; static int authenticate(char *user, size_t userlen, const char *pass) { int level = 0; // Authentication failure @@ -201,42 +201,14 @@ static int authenticate(char *user, size_t userlen, const char *pass) { return level; } -static struct file { - int index; - char name[64]; - size_t size; - uint64_t checksum; -} s_file; -static struct mg_field_set set_files; - -static bool get_dir(const struct mg_dash_user *u, char *buf, size_t len) { - (void) u; +static bool files_dir(const struct mg_dash_user *u, char *buf, size_t len) { + (void) u; // Same dir for every user. Key off u->name for per-user dirs mg_snprintf(buf, len, "%s", "/tmp/dashboard"); return true; } -static struct mg_field fields_files[] = { - {"name", MG_VAL_STR, s_file.name, sizeof(s_file.name)}, - {"size", MG_VAL_UINT64, &s_file.size, 0}, - {"checksum", MG_VAL_UINT64, &s_file.checksum, sizeof(s_file.checksum)}, - {NULL, MG_VAL_INT, NULL, 0}, -}; - -// Custom reader: let the framework fill "name" and "size", then add our -// own field. A real implementation would hash the file instead of -// hardcoding zero -static bool files_fn(enum mg_dash_op op, struct mg_dash_user *u) { - if (op != MG_DASH_READ) return false; - if (!mg_dash_dir_read(&set_files, u)) return false; - s_file.checksum = 0; - return true; -} - -static struct mg_field_set set_files = {"files", fields_files, files_fn, - &s_file.index, get_dir, NULL}; - void mg_dash_init(struct mg_mgr *mgr) { - MG_DASH_ADD_FIELD_SET(&s_dash, &set_files); + s_dash.files_dir = files_dir; // Built-in file manager, see mg_dash.h MG_DASH_ADD_FIELD_SET(&s_dash, &set_leds); MG_DASH_ADD_FIELD_SET(&s_dash, &set_metrics); MG_DASH_ADD_FIELD_SET(&s_dash, &set_settings); diff --git a/tutorials/device-dashboard/full/dashboard.html b/tutorials/device-dashboard/full/dashboard.html index d70318f5..3fb3562b 100644 --- a/tutorials/device-dashboard/full/dashboard.html +++ b/tutorials/device-dashboard/full/dashboard.html @@ -16,7 +16,6 @@ leds: { led1: true, led2: false, led3: false }, settings: { volume: 12, log_level: 3, name: 'Galway', enable_login: true, ota_version: '1.2.3', ota_status: 'No checks yet', ota_url: 'http://my-product.com/ota.json', ota_interval: 30 }, metrics: { temperature: 1.2, cpu: 4, ram: 5, version: '1.2.3' }, - files: {start: 0, end: 100, data: [{name: 'device-log-2026-04-25.txt', size: 8625563}]}, }, }); @@ -110,6 +109,7 @@ Dashboard.on('ready', function() { setupAutoLogout(); document.body.style.visibility = 'visible'; + refreshFiles(); }); Dashboard.login().catch(err => showLogin(true)); @@ -170,13 +170,80 @@ ]); }; - // Intercept change notifications: re-plot graphs, reload the file - // list whenever its size changes (file uploaded or deleted) + // Intercept change notifications: re-plot graphs Dashboard.on('change', args => { args?.graph1?.data && updateGraph1(args?.graph1?.data); args?.graph2?.data && updateGraph2(args?.graph2?.data); - typeof args?.files === 'number' && Dashboard.call('get', { name: 'files', start: 0, end: 100 }); }); + + // File manager: served directly by the built-in mg_dash file manager + // (see struct mg_dash::files_dir), not by the generic data-repeat + // binding, since the listing endpoint returns a plain JSON array + const filesListEl = document.getElementById('files-list'); + const filesCountEl = document.getElementById('files-count'); + const filesUploadEl = document.getElementById('files-upload-input'); + + function renderFiles(list) { + filesCountEl.textContent = list.length + ' Files'; + filesListEl.innerHTML = list.map(f => ` +
+
+ +
${f.name}
+
+
${f.size}
+
+ + + + + + + + +
+
`).join(''); + filesListEl.querySelectorAll('[data-del]').forEach(btn => { + btn.addEventListener('click', ev => { + ev.preventDefault(); + fetch('fs/' + btn.dataset.del, { method: 'DELETE' }).then(refreshFiles); + }); + }); + } + + function refreshFiles() { + fetch('fs/') + .then(r => r.ok ? r.json() : Promise.reject(r.statusText)) + .then(list => renderFiles(list.filter(f => !f.dir))) + .catch(err => console.log('Files list failed', err)); + } + + filesUploadEl.addEventListener('change', ev => { + const f = ev.target.files[0]; + ev.target.value = ''; + if (!f) return; + const reader = new FileReader(); + reader.readAsArrayBuffer(f); + reader.onload = () => { + fetch('fs/' + encodeURIComponent(f.name), { method: 'POST', body: reader.result }) + .then(refreshFiles) + .catch(err => console.log('Upload failed', err)); + }; + }); + });