/ firmware / src / services / http / api / filesystem.cpp
filesystem.cpp
  1  #include "api.h"
  2  #include <config.h>
  3  #include "filesystems/api.h"
  4  #include <storage.h>
  5  #include <led.h>
  6  
  7  #include <Arduino.h>
  8  #include <AsyncJson.h>
  9  #include <ArduinoJson.h>
 10  #include <LittleFS.h>
 11  #include <SD.h>
 12  
 13  #include <memory>
 14  
 15  namespace {
 16  
 17  struct FileUploadState {
 18    bool ok = false;
 19  };
 20  
 21  bool ensure_parent_dirs(fs::FS &fs, const String &path) {
 22    int idx = 1;
 23    while ((idx = path.indexOf('/', idx)) > 0) {
 24      String dir = path.substring(0, idx);
 25      if (!fs.exists(dir)) fs.mkdir(dir);
 26      idx++;
 27    }
 28    return true;
 29  }
 30  
 31  bool write_with_retry(File &file, const uint8_t *data, size_t length) {
 32    size_t written = 0;
 33    uint32_t start = millis();
 34    while (written < length) {
 35      size_t n = file.write(data + written, length - written);
 36      if (n > 0) {
 37        written += n;
 38        start = millis();
 39        continue;
 40      }
 41      delay(1);
 42      if (millis() - start > 5000) return false;
 43    }
 44    return true;
 45  }
 46  
 47  void handle_legacy_files(AsyncWebServerRequest *request) {
 48    if (!hardware::storage::ensureSD()) {
 49      request->send(503, asyncsrv::T_application_json, "{\"error\":\"no SD card\"}");
 50      return;
 51    }
 52  
 53    AsyncJsonResponse *response = new AsyncJsonResponse(true);
 54    JsonArray root = response->getRoot().to<JsonArray>();
 55    filesystems::api::listDirectory(SD, "/", root);
 56  
 57    response->setLength();
 58    request->send(response);
 59  }
 60  
 61  void handle_root(AsyncWebServerRequest *request) {
 62    AsyncJsonResponse *response = new AsyncJsonResponse();
 63    JsonObject root = response->getRoot().to<JsonObject>();
 64    root["ok"] = true;
 65  
 66    JsonArray sd_entries = root["sd"].to<JsonArray>();
 67    if (hardware::storage::ensureSD()) filesystems::api::listDirectory(SD, "/", sd_entries);
 68  
 69    JsonArray littlefs_entries = root["littlefs"].to<JsonArray>();
 70    if (hardware::storage::ensureLittleFS()) filesystems::api::listDirectory(LittleFS, "/", littlefs_entries);
 71  
 72    response->setLength();
 73    request->send(response);
 74  }
 75  
 76  void handle_get(AsyncWebServerRequest *request) {
 77    FilesystemResolveCommand command = {
 78      .url = request->url(),
 79      .target = {},
 80    };
 81    filesystems::api::resolveTarget(&command);
 82    FilesystemTarget &target = command.target;
 83    if (!target.ok) {
 84      request->send(400, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"invalid filesystem prefix\"}");
 85      return;
 86    }
 87    if (filesystems::api::isSensitivePath(target.path)) {
 88      request->send(403, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"forbidden path\"}");
 89      return;
 90    }
 91  
 92    File entry = target.fs->open(target.path);
 93    if (!entry) {
 94      request->send(404, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"not found\"}");
 95      return;
 96    }
 97  
 98    if (entry.isDirectory()) {
 99      entry.close();
100      AsyncJsonResponse *response = new AsyncJsonResponse(true);
101      JsonArray root = response->getRoot().to<JsonArray>();
102      filesystems::api::listDirectory(*target.fs, target.path, root);
103      response->setLength();
104      request->send(response);
105      return;
106    }
107  
108    entry.close();
109    request->send(*target.fs, target.path, "application/octet-stream");
110  }
111  
112  void handle_mkdir(AsyncWebServerRequest *request) {
113    FilesystemResolveCommand command = {
114      .url = request->url(),
115      .target = {},
116    };
117    filesystems::api::resolveTarget(&command);
118    FilesystemTarget &target = command.target;
119    if (!target.ok) {
120      request->send(400, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"invalid filesystem prefix\"}");
121      return;
122    }
123    if (filesystems::api::isSensitivePath(target.path)) {
124      request->send(403, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"forbidden path\"}");
125      return;
126    }
127  
128    bool created = target.fs->mkdir(target.path);
129    AsyncJsonResponse *response = new AsyncJsonResponse();
130    response->setCode(created ? 201 : 500);
131    JsonObject root = response->getRoot().to<JsonObject>();
132    root["ok"] = created;
133    response->setLength();
134    request->send(response);
135  }
136  
137  void handle_format(AsyncWebServerRequest *request) {
138    AsyncWebServerRequestPtr weak = request->pause();
139  
140    xTaskCreate([](void *arg) {
141      AsyncWebServerRequestPtr *wp = (AsyncWebServerRequestPtr *)arg;
142      bool formatted = hardware::storage::ensureLittleFS() && LittleFS.format();
143  
144      auto request = wp->lock();
145      delete wp;
146      if (request) {
147        AsyncJsonResponse *response = new AsyncJsonResponse();
148        response->setCode(formatted ? 200 : 500);
149        JsonObject root = response->getRoot().to<JsonObject>();
150        root["ok"] = formatted;
151        response->setLength();
152        request->send(response);
153      }
154      vTaskDelete(nullptr);
155    }, "fs-format", 4096, new AsyncWebServerRequestPtr(weak), 1, nullptr);
156  }
157  
158  void handle_upload(AsyncWebServerRequest *request, String filename,
159                     size_t index, uint8_t *data, size_t len, bool final) {
160    (void)filename;
161    FileUploadState *state = reinterpret_cast<FileUploadState *>(request->_tempObject);
162  
163    if (!index) {
164      delete state;
165      state = new FileUploadState();
166      request->_tempObject = state;
167  
168      FilesystemResolveCommand command = {
169        .url = request->url(),
170        .target = {},
171      };
172      filesystems::api::resolveTarget(&command);
173      FilesystemTarget &target = command.target;
174      if (!target.ok) {
175        request->send(400, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"invalid filesystem prefix\"}");
176        return;
177      }
178      if (filesystems::api::isSensitivePath(target.path)) {
179        request->send(403, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"forbidden path\"}");
180        return;
181      }
182  
183      ensure_parent_dirs(*target.fs, target.path);
184  
185      if (target.fs->exists(target.path)) target.fs->remove(target.path);
186  
187      request->_tempFile = target.fs->open(target.path, FILE_WRITE, true);
188      if (!request->_tempFile) {
189        request->send(500, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"open failed\"}");
190        return;
191      }
192  
193      state->ok = true;
194      Serial.printf("[http] upload: %s\n", target.path.c_str());
195    }
196  
197    if (request->getResponse()) return;
198  
199    if (state && state->ok) {
200      float t = (float)(millis() % 1000) / 1000.0f;
201      uint8_t b = (uint8_t)((sinf(t * 6.2832f) + 1.0f) * 0.5f * 200.0f) + 10;
202      LED.setBrightness(b);
203      LED.set(colors::White);
204    }
205  
206    if (state && state->ok && request->_tempFile && len) {
207      if (!write_with_retry(request->_tempFile, data, len)) {
208        state->ok = false;
209        request->_tempFile.close();
210        request->abort();
211      }
212    }
213  
214    if (final) {
215      if (request->_tempFile) request->_tempFile.close();
216      if (state && state->ok) {
217        Serial.printf("[http] upload complete (%u bytes)\n", (unsigned)(index + len));
218      }
219      LED.setBrightness(config::led::BRIGHTNESS);
220      LED.set(colors::Green);
221    }
222  }
223  
224  void handle_upload_complete(AsyncWebServerRequest *request) {
225    std::unique_ptr<FileUploadState> state(
226        reinterpret_cast<FileUploadState *>(request->_tempObject));
227    request->_tempObject = nullptr;
228  
229    if (request->getResponse()) return;
230  
231    AsyncJsonResponse *response = new AsyncJsonResponse();
232    response->setCode((state && state->ok) ? 201 : 500);
233    JsonObject root = response->getRoot().to<JsonObject>();
234    root["ok"] = (state && state->ok);
235    response->setLength();
236    request->send(response);
237  }
238  
239  void handle_delete(AsyncWebServerRequest *request) {
240    FilesystemResolveCommand command = {
241      .url = request->url(),
242      .target = {},
243    };
244    filesystems::api::resolveTarget(&command);
245    FilesystemTarget &target = command.target;
246    if (!target.ok) {
247      request->send(400, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"invalid filesystem prefix\"}");
248      return;
249    }
250    if (filesystems::api::isSensitivePath(target.path)) {
251      request->send(403, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"forbidden path\"}");
252      return;
253    }
254  
255    bool removed = filesystems::api::removeRecursive(*target.fs, target.path);
256    AsyncJsonResponse *response = new AsyncJsonResponse();
257    response->setCode(removed ? 200 : 404);
258    JsonObject root = response->getRoot().to<JsonObject>();
259    root["ok"] = removed;
260    response->setLength();
261    request->send(response);
262  }
263  
264  }
265  
266  void services::http::api::filesystem::registerRoutes(AsyncWebServer &server,
267                                                       AsyncRateLimitMiddleware &format_limit) {
268    server.on("/api/files", HTTP_GET, handle_legacy_files);
269  
270    server.on(AsyncURIMatcher::exact("/api/filesystem"), HTTP_GET, handle_root);
271    server.on("/api/filesystem/littlefs/format", HTTP_POST, handle_format)
272      .addMiddleware(&format_limit);
273    server.on(AsyncURIMatcher::dir("/api/filesystem"), HTTP_GET, handle_get);
274    server.on(AsyncURIMatcher::dir("/api/filesystem"), HTTP_POST, handle_mkdir);
275    server.on(AsyncURIMatcher::dir("/api/filesystem"), HTTP_PUT,
276              handle_upload_complete, handle_upload);
277    server.on(AsyncURIMatcher::dir("/api/filesystem"), HTTP_DELETE, handle_delete);
278  
279    AsyncCallbackJsonWebHandler &rename_handler =
280        server.on(AsyncURIMatcher::dir("/api/filesystem"), HTTP_PATCH,
281            [](AsyncWebServerRequest *request, JsonVariant &json) {
282      FilesystemResolveCommand command = {
283        .url = request->url(),
284        .target = {},
285      };
286      filesystems::api::resolveTarget(&command);
287      FilesystemTarget &target = command.target;
288      if (!target.ok) {
289        request->send(400, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"invalid filesystem prefix\"}");
290        return;
291      }
292      if (filesystems::api::isSensitivePath(target.path)) {
293        request->send(403, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"forbidden path\"}");
294        return;
295      }
296  
297      JsonObject body = json.as<JsonObject>();
298      String new_name = body["name"] | "";
299      if (new_name.isEmpty()) {
300        request->send(400, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"missing name in body\"}");
301        return;
302      }
303  
304      int last_slash = target.path.lastIndexOf('/');
305      String dir = (last_slash > 0) ? target.path.substring(0, last_slash) : "";
306      String new_path = dir + "/" + new_name;
307  
308      if (filesystems::api::isSensitivePath(new_path)) {
309        request->send(403, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"forbidden path\"}");
310        return;
311      }
312  
313      bool ok = target.fs->rename(target.path, new_path);
314      AsyncJsonResponse *response = new AsyncJsonResponse();
315      response->setCode(ok ? 200 : 500);
316      JsonObject root = response->getRoot().to<JsonObject>();
317      root["ok"] = ok;
318      if (ok) {
319        root["from"] = target.path;
320        root["to"] = new_path;
321      }
322      response->setLength();
323      request->send(response);
324    });
325    rename_handler.setMaxContentLength(256);
326  }