Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.

Commit d605729

Browse files
committed
file discovery/metadata API
1 parent c81c807 commit d605729

12 files changed

Lines changed: 595 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ The format follows Keep a Changelog and the project adheres to Semantic Versioni
2121
- `writeFileFromPath(path, sourceFsPath, opts)` to avoid manual source `File` management.
2222
- New `examples/LargeFileStreaming` sketch showing chunked large-binary streaming with streaming hash verification.
2323
- New `examples/AsyncLargeFileUpload` sketch showing background large-binary upload with progress polling and hash verification.
24+
- File discovery APIs for persisted uploads:
25+
- `getFileInfo(path)` for per-path metadata lookup
26+
- `listFiles(prefix, recursive)` for directory-style discovery under `/_files`
2427

2528
### Changed
2629
- `init()` now skips collections listed in `delayedCollectionSyncArray` during eager preload; deferred collections are loaded on first periodic autosync tick (or first `syncNow()` when `autosync=false`) and still load immediately on first explicit `collection(name)` access.
@@ -37,6 +40,11 @@ The format follows Keep a Changelog and the project adheres to Semantic Versioni
3740
- Async upload state retention is now bounded: terminal upload states are kept only for a recent window of upload IDs.
3841
- Updated `examples/FileStreaming` and file storage tests to cover callback/path convenience write flows.
3942

43+
### Fixed
44+
- Collection cleanup on sync now reports filesystem removal errors instead of silently succeeding when a collection directory or document file cannot be deleted.
45+
- Recursive collection and `dropAll()` cleanup now removes directories consistently across Arduino-ESP32 and ESP-IDF builds.
46+
- Added hardware tests that verify collection directories and document files actually disappear from LittleFS after sync-driven cleanup.
47+
4048
### Removed
4149
- Legacy `onSync(std::function<void()>)` callback API (breaking change). Use `onSyncStatus(...)` instead.
4250

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ db.writeFileStream(
115115
},
116116
fileOpts
117117
);
118+
119+
auto fileInfo = db.getFileInfo("notes/readme.txt");
120+
auto fileTree = db.listFiles("firmware", true);
118121
```
119122
120123
## Gotchas
@@ -125,6 +128,8 @@ db.writeFileStream(
125128
- `writeFileStream()` and `readFileStream()` hold the filesystem lock while processing the stream; use reasonable chunk sizes and avoid blocking stream sources/sinks.
126129
- `writeFileStreamAsync()` runs producer callbacks on a background task; callbacks must be short and thread-safe.
127130
- `getFileUploadState(uploadId)` retains terminal states for a bounded number of recent uploads; older upload IDs eventually return `NotFound`.
131+
- Uploaded files are not surfaced as collections or snapshots; use `getFileInfo()` / `listFiles()` to inspect persisted file storage under `/_files`.
132+
- `dropCollection()` only schedules on-disk removal; the collection directory and document files are deleted on the next autosync pass or `syncNow()`.
128133
- `/_files` is an internal reserved directory used for file storage and cannot be used as a collection name.
129134
- `getSnapshot()` and `restoreFromSnapshot()` currently cover document collections only; file storage under `/_files` is not included.
130135
- `usePSRAMBuffers` affects ESPJsonDB-owned byte buffers, decoded `DocView` `JsonDocument` pools on ArduinoJson v7, and long-lived internal DB containers (collection/schema/upload/diag maps and queues). Public return containers like `readFile()` still use the existing API types.
@@ -137,6 +142,7 @@ db.writeFileStream(
137142
- `void onSyncStatus(std::function<void(const DBSyncStatus&)>)` – observe cold preload and `syncNow()` progress with stage/source/current collection counters.
138143
- `onSync(std::function<void()>)` was removed; migrate to `onSyncStatus(...)`.
139144
- Collection management: `collection(name)`, `dropCollection(name)`, `dropAll()`, `getAllCollectionName()`.
145+
- `dropCollection(name)` removes in-memory state immediately and deletes the corresponding filesystem directory on the next autosync pass or explicit `syncNow()`.
140146
- Document helpers:
141147
- Create: `create`, `createMany` (JSON array) plus direct `Collection::create*` variants.
142148
- Read: `findById`, `findOne`, `findMany` (predicate or JSON filter) returning `DocView` so you can read/write lazily.
@@ -153,6 +159,8 @@ db.writeFileStream(
153159
- `cancelFileUpload(uploadId)`, `getFileUploadState(uploadId)` for async job control (terminal states are retained for a bounded recent window).
154160
- `writeFile(path, data, size)` / `readFile(path)` for direct byte buffers.
155161
- `writeTextFile(path, text)` / `readTextFile(path)` for UTF-8 or plain text payloads.
162+
- `getFileInfo(path)` returns a JSON object with `path`, `name`, `exists`, `isDirectory`, and `size`.
163+
- `listFiles(prefix, recursive)` returns a JSON document with `prefix`, `recursive`, and an `entries` array of file/directory metadata objects.
156164
- `fileExists(path)`, `fileSize(path)`, `removeFile(path)` for file lifecycle utilities.
157165
- File paths are relative to `/<baseDir>/_files` and path traversal segments are rejected.
158166
- `ESPJsonDBFileOptions`: `overwrite` and `chunkSize` controls for stream writes.

src/esp_jsondb/collection/collection.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -918,7 +918,9 @@ DbStatus Collection::flushDirtyToFs(const std::string &baseDir, bool &didWork) {
918918
{
919919
FrLock fs(g_fsMutex);
920920
if (_fs->exists(path.c_str())) {
921-
_fs->remove(path.c_str());
921+
if (!_fs->remove(path.c_str())) {
922+
return recordStatus({DbStatusCode::IoError, "document delete failed"});
923+
}
922924
}
923925
}
924926
}

src/esp_jsondb/db.cpp

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
namespace {
99
constexpr uint32_t kTaskStopTimeoutMs = 200;
10-
static void removeTree(fs::FS &fsImpl, const std::string &path);
10+
static DbStatus removeTree(fs::FS &fsImpl, const std::string &path);
1111
using DirEntry = std::pair<std::string, bool>;
1212
using DirEntryVector = JsonDbVector<DirEntry>;
1313
} // namespace
@@ -888,7 +888,10 @@ DbStatus ESPJsonDB::runSyncPass() {
888888
DbStatus finalStatus{DbStatusCode::Ok, ""};
889889
if (dropAll) {
890890
if (_fs) {
891-
removeTree(*_fs, _baseDir);
891+
auto st = removeTree(*_fs, _baseDir);
892+
if (!st.ok()) {
893+
return setLastError(st);
894+
}
892895
}
893896
auto st = ensureFsReady();
894897
if (!st.ok()) {
@@ -1045,42 +1048,52 @@ static void listDirEntries(
10451048
d.close();
10461049
}
10471050

1048-
static void removeTree(fs::FS &fsImpl, const std::string &path) {
1051+
static DbStatus removeTree(fs::FS &fsImpl, const std::string &path) {
10491052
// Check if path is a directory
10501053
bool isDir = false;
10511054
{
10521055
FrLock fs(g_fsMutex);
10531056
if (!fsImpl.exists(path.c_str()))
1054-
return;
1057+
return {DbStatusCode::Ok, ""};
10551058
File f = fsImpl.open(path.c_str());
10561059
if (f) {
10571060
isDir = f.isDirectory();
10581061
f.close();
1062+
} else {
1063+
return {DbStatusCode::IoError, "open path failed during recursive remove"};
10591064
}
10601065
}
10611066
if (!isDir) {
10621067
FrLock fs(g_fsMutex);
1063-
fsImpl.remove(path.c_str());
1064-
return;
1068+
if (!fsImpl.remove(path.c_str())) {
1069+
return {DbStatusCode::IoError, "remove file failed during recursive remove"};
1070+
}
1071+
return {DbStatusCode::Ok, ""};
10651072
}
10661073
// List children first without holding lock during recursion
10671074
DirEntryVector entries{JsonDbAllocator<DirEntry>(false)};
10681075
listDirEntries(fsImpl, path, entries);
10691076
for (auto &e : entries) {
10701077
if (e.second) {
1071-
removeTree(fsImpl, e.first);
1078+
auto st = removeTree(fsImpl, e.first);
1079+
if (!st.ok()) {
1080+
return st;
1081+
}
10721082
} else {
10731083
FrLock fs(g_fsMutex);
1074-
fsImpl.remove(e.first.c_str());
1084+
if (!fsImpl.remove(e.first.c_str())) {
1085+
return {DbStatusCode::IoError, "remove child file failed during recursive remove"};
1086+
}
10751087
}
10761088
}
10771089
// Finally remove the directory itself
10781090
{
10791091
FrLock fs(g_fsMutex);
1080-
#ifdef ARDUINO_ARCH_ESP32
1081-
fsImpl.rmdir(path.c_str());
1082-
#endif
1092+
if (!fsImpl.rmdir(path.c_str())) {
1093+
return {DbStatusCode::IoError, "remove directory failed during recursive remove"};
1094+
}
10831095
}
1096+
return {DbStatusCode::Ok, ""};
10841097
}
10851098
} // namespace
10861099

@@ -1089,9 +1102,11 @@ DbStatus ESPJsonDB::removeCollectionDir(const std::string &name) {
10891102
if (!dir.empty() && dir.back() != '/')
10901103
dir += '/';
10911104
dir += name;
1092-
if (_fs)
1093-
removeTree(*_fs, dir);
1094-
return setLastError({DbStatusCode::Ok, ""});
1105+
if (!_fs) {
1106+
return setLastError({DbStatusCode::InvalidArgument, "filesystem handle is null"});
1107+
}
1108+
auto st = removeTree(*_fs, dir);
1109+
return setLastError(st);
10951110
}
10961111

10971112
void ESPJsonDB::emitEvent(DBEventType ev) {

src/esp_jsondb/db.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ class ESPJsonDB {
184184
readFileStream(const std::string &relativePath, Stream &out, size_t chunkSize = 512);
185185
DbResult<std::vector<uint8_t>> readFile(const std::string &relativePath);
186186
DbResult<std::string> readTextFile(const std::string &relativePath);
187+
DbResult<JsonDocument> getFileInfo(const std::string &relativePath);
188+
DbResult<JsonDocument> listFiles(const std::string &relativePrefix = "", bool recursive = true);
187189

188190
DbStatus removeFile(const std::string &relativePath);
189191
DbResult<bool> fileExists(const std::string &relativePath);

src/esp_jsondb/db_files.cpp

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,108 @@ std::string parentDirOf(const std::string &path) {
1717
return path.substr(0, pos);
1818
}
1919

20+
std::string fileNameOf(const std::string &path) {
21+
auto pos = path.find_last_of('/');
22+
if (pos == std::string::npos)
23+
return path;
24+
return path.substr(pos + 1);
25+
}
26+
27+
struct FileEntryInfo {
28+
std::string path;
29+
bool isDirectory = false;
30+
size_t size = 0;
31+
};
32+
33+
DbStatus statFileEntry(
34+
fs::FS &filesystem,
35+
const std::string &absolutePath,
36+
const std::string &relativePath,
37+
FileEntryInfo &out
38+
) {
39+
FrLock fs(g_fsMutex);
40+
if (!filesystem.exists(absolutePath.c_str())) {
41+
return {DbStatusCode::NotFound, "file not found"};
42+
}
43+
File file = filesystem.open(absolutePath.c_str(), FILE_READ);
44+
if (!file) {
45+
return {DbStatusCode::IoError, "open file info failed"};
46+
}
47+
out.path = relativePath;
48+
out.isDirectory = file.isDirectory();
49+
out.size = out.isDirectory ? 0 : file.size();
50+
file.close();
51+
return {DbStatusCode::Ok, ""};
52+
}
53+
54+
void appendFileInfoJson(JsonArray entries, const FileEntryInfo &info) {
55+
JsonObject entry = entries.add<JsonObject>();
56+
entry["path"] = info.path.c_str();
57+
entry["name"] = fileNameOf(info.path).c_str();
58+
entry["exists"] = true;
59+
entry["isDirectory"] = info.isDirectory;
60+
entry["size"] = info.size;
61+
}
62+
63+
DbStatus collectDirectoryEntries(
64+
fs::FS &filesystem,
65+
const std::string &absoluteDir,
66+
const std::string &relativeDir,
67+
bool recursive,
68+
std::vector<FileEntryInfo> &entries
69+
) {
70+
std::vector<std::pair<std::string, std::string>> pendingDirs;
71+
{
72+
FrLock fs(g_fsMutex);
73+
File dir = filesystem.open(absoluteDir.c_str(), FILE_READ);
74+
if (!dir || !dir.isDirectory()) {
75+
if (dir)
76+
dir.close();
77+
return {DbStatusCode::NotFound, "file not found"};
78+
}
79+
for (File child = dir.openNextFile(); child; child = dir.openNextFile()) {
80+
const bool isDirectory = child.isDirectory();
81+
String rawName = child.name();
82+
child.close();
83+
std::string segment = rawName.c_str();
84+
auto slash = segment.find_last_of('/');
85+
if (slash != std::string::npos)
86+
segment = segment.substr(slash + 1);
87+
if (segment.empty())
88+
continue;
89+
90+
FileEntryInfo info;
91+
info.path = relativeDir.empty() ? segment : joinPath(relativeDir, segment);
92+
info.isDirectory = isDirectory;
93+
info.size = 0;
94+
if (!isDirectory) {
95+
const std::string childPath = joinPath(absoluteDir, segment);
96+
File childFile = filesystem.open(childPath.c_str(), FILE_READ);
97+
if (!childFile) {
98+
dir.close();
99+
return {DbStatusCode::IoError, "open child file info failed"};
100+
}
101+
info.size = childFile.size();
102+
childFile.close();
103+
}
104+
entries.push_back(info);
105+
106+
if (recursive && isDirectory) {
107+
pendingDirs.emplace_back(joinPath(absoluteDir, segment), info.path);
108+
}
109+
}
110+
dir.close();
111+
}
112+
113+
for (const auto &pending : pendingDirs) {
114+
auto st = collectDirectoryEntries(filesystem, pending.first, pending.second, true, entries);
115+
if (!st.ok())
116+
return st;
117+
}
118+
119+
return {DbStatusCode::Ok, ""};
120+
}
121+
20122
DbStatus writeFromPullCb(
21123
fs::FS &filesystem,
22124
const std::string &finalPath,
@@ -426,6 +528,92 @@ DbResult<std::string> ESPJsonDB::readTextFile(const std::string &relativePath) {
426528
return res;
427529
}
428530

531+
DbResult<JsonDocument> ESPJsonDB::getFileInfo(const std::string &relativePath) {
532+
DbResult<JsonDocument> res{};
533+
auto ready = ensureReady();
534+
if (!ready.ok()) {
535+
res.status = setLastError(ready);
536+
return res;
537+
}
538+
539+
std::string normalized;
540+
auto nst = normalizeFilePath(relativePath, normalized);
541+
if (!nst.ok()) {
542+
res.status = setLastError(nst);
543+
return res;
544+
}
545+
546+
FileEntryInfo info;
547+
const auto st = statFileEntry(*_fs, joinPath(fileRootDir(), normalized), normalized, info);
548+
if (!st.ok()) {
549+
res.status = setLastError(st);
550+
return res;
551+
}
552+
553+
res.value["path"] = info.path.c_str();
554+
res.value["name"] = fileNameOf(info.path).c_str();
555+
res.value["exists"] = true;
556+
res.value["isDirectory"] = info.isDirectory;
557+
res.value["size"] = info.size;
558+
res.status = setLastError({DbStatusCode::Ok, ""});
559+
return res;
560+
}
561+
562+
DbResult<JsonDocument>
563+
ESPJsonDB::listFiles(const std::string &relativePrefix, bool recursive) {
564+
DbResult<JsonDocument> res{};
565+
auto ready = ensureReady();
566+
if (!ready.ok()) {
567+
res.status = setLastError(ready);
568+
return res;
569+
}
570+
571+
std::string normalizedPrefix;
572+
if (!relativePrefix.empty()) {
573+
auto nst = normalizeFilePath(relativePrefix, normalizedPrefix);
574+
if (!nst.ok()) {
575+
res.status = setLastError(nst);
576+
return res;
577+
}
578+
}
579+
580+
const std::string rootPath = fileRootDir();
581+
const std::string targetPath =
582+
normalizedPrefix.empty() ? rootPath : joinPath(rootPath, normalizedPrefix);
583+
584+
FileEntryInfo targetInfo;
585+
auto st = statFileEntry(*_fs, targetPath, normalizedPrefix, targetInfo);
586+
if (!st.ok()) {
587+
res.status = setLastError(st);
588+
return res;
589+
}
590+
591+
std::vector<FileEntryInfo> entries;
592+
if (targetInfo.isDirectory) {
593+
st = collectDirectoryEntries(*_fs, targetPath, normalizedPrefix, recursive, entries);
594+
if (!st.ok()) {
595+
res.status = setLastError(st);
596+
return res;
597+
}
598+
} else {
599+
entries.push_back(targetInfo);
600+
}
601+
602+
std::sort(entries.begin(), entries.end(), [](const FileEntryInfo &lhs, const FileEntryInfo &rhs) {
603+
return lhs.path < rhs.path;
604+
});
605+
606+
res.value["prefix"] = normalizedPrefix.c_str();
607+
res.value["recursive"] = recursive;
608+
JsonArray entriesJson = res.value["entries"].to<JsonArray>();
609+
for (const auto &entry : entries) {
610+
appendFileInfoJson(entriesJson, entry);
611+
}
612+
613+
res.status = setLastError({DbStatusCode::Ok, ""});
614+
return res;
615+
}
616+
429617
DbStatus ESPJsonDB::removeFile(const std::string &relativePath) {
430618
auto ready = ensureReady();
431619
if (!ready.ok())

0 commit comments

Comments
 (0)