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 }