Public Access
The Storage App browses the SD card: folders first with sizes and dates, three sorts, one item at a time with a clipboard (c, x, v), rename, delete after counting what's inside, new folder, details. A listing holds 256 entries and says when a folder has more. FileOps does the card's work for the App and the console alike, one operation at a time on the storage task in turns of about 150 ms, so Logs and Captures are still written during a long copy. A copy shows progress, can be cancelled (what it wrote is taken back) and compares sizes after. The read-only rules are checked there: the firmware's top-level folders, /gemini/cache, and files being written (a Track, a Capture, an upload, today's IRC Logs). A listing reads the folder straight from FatFs: through the Arduino File, 329 entries took over two seconds. Viewers by type: text read a screen at a time whatever the file's size (logs open at the end), a hex dump, a Capture's packets as the LoRa Scanner lists them, a Track's summary, and an Update File checked as an install would check it, without writing anything. Tab shows any file as hex or text. Settings > Storage is gone: usage, Storage Clean-up and Erase are the App's Maintenance, behind a warning. The Storage Warning points there. The Clock sets the system time whatever its source, so files are dated correctly with a GNSS Fix alone (Q137). Console: cp, mv, mkdir, du, cancel; rm takes folders and follows the rules; ls shows dates; Debug Builds get `sd fill`. 424 host tests. Checked on the device: docs/milestones/F1.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
138 lines
5.0 KiB
C++
138 lines
5.0 KiB
C++
#include "file_list.h"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
|
|
namespace roro::files {
|
|
|
|
namespace {
|
|
|
|
// Names compare letters only, whatever the case; ties by the bytes, so the order is total.
|
|
int compareNames(const char* a, const char* b) {
|
|
for (const char *x = a, *y = b;; ++x, ++y) {
|
|
int cx = std::tolower(static_cast<unsigned char>(*x)), cy = std::tolower(static_cast<unsigned char>(*y));
|
|
if (cx != cy) return cx < cy ? -1 : 1;
|
|
if (!cx) break;
|
|
}
|
|
return std::strcmp(a, b);
|
|
}
|
|
|
|
constexpr uint32_t kYear2020 = 1577836800;
|
|
|
|
std::string sizeText(uint32_t bytes) {
|
|
char s[16];
|
|
if (bytes < 1024) std::snprintf(s, sizeof s, "%u B", static_cast<unsigned>(bytes));
|
|
else if (bytes < 10 * 1024) std::snprintf(s, sizeof s, "%.1f KB", bytes / 1024.0);
|
|
else if (bytes < 1024 * 1024) std::snprintf(s, sizeof s, "%u KB", static_cast<unsigned>(bytes / 1024));
|
|
else if (bytes < 10u * 1024 * 1024) std::snprintf(s, sizeof s, "%.1f MB", bytes / 1048576.0);
|
|
else if (bytes < 1024u * 1024 * 1024) std::snprintf(s, sizeof s, "%u MB", static_cast<unsigned>(bytes / 1048576));
|
|
else std::snprintf(s, sizeof s, "%.1f GB", bytes / 1073741824.0);
|
|
return s;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void FileList::clear() {
|
|
std::vector<Entry>().swap(entries_);
|
|
std::vector<uint16_t>().swap(order_);
|
|
std::vector<char>().swap(names_);
|
|
more_ = wasted_ = 0;
|
|
}
|
|
|
|
bool FileList::before(const Entry& a, const Entry& b, FileSort by) const {
|
|
if (a.folder != b.folder) return a.folder;
|
|
if (!a.folder) {
|
|
if (by == FileSort::Date && a.modified != b.modified) return a.modified > b.modified;
|
|
if (by == FileSort::Size && a.size != b.size) return a.size > b.size;
|
|
}
|
|
return compareNames(names_.data() + a.name, names_.data() + b.name) < 0;
|
|
}
|
|
|
|
void FileList::add(const char* name, uint32_t size, uint32_t modified, bool folder) {
|
|
size_t len = std::strlen(name) + 1;
|
|
if (entries_.size() >= kMax) {
|
|
// Full: the new entry takes the place of the one that sorts last by name, if it sorts
|
|
// before it. The old name's bytes stay in the buffer until there's enough waste to pack.
|
|
more_++;
|
|
size_t last = 0;
|
|
for (size_t i = 1; i < entries_.size(); i++)
|
|
if (before(entries_[last], entries_[i], FileSort::Name)) last = i;
|
|
Entry candidate{static_cast<uint32_t>(names_.size()), size, modified, folder};
|
|
names_.insert(names_.end(), name, name + len);
|
|
if (!before(candidate, entries_[last], FileSort::Name)) {
|
|
names_.resize(names_.size() - len);
|
|
return;
|
|
}
|
|
wasted_ += std::strlen(names_.data() + entries_[last].name) + 1;
|
|
entries_[last] = candidate;
|
|
if (wasted_ > 2048) compact();
|
|
return;
|
|
}
|
|
if (entries_.empty()) {
|
|
entries_.reserve(32);
|
|
names_.reserve(512);
|
|
}
|
|
entries_.push_back({static_cast<uint32_t>(names_.size()), size, modified, folder});
|
|
names_.insert(names_.end(), name, name + len);
|
|
order_.push_back(static_cast<uint16_t>(order_.size()));
|
|
}
|
|
|
|
void FileList::compact() {
|
|
std::vector<char> packed;
|
|
packed.reserve(names_.size() - wasted_);
|
|
for (Entry& e : entries_) {
|
|
const char* n = names_.data() + e.name;
|
|
e.name = static_cast<uint32_t>(packed.size());
|
|
packed.insert(packed.end(), n, n + std::strlen(n) + 1);
|
|
}
|
|
names_.swap(packed);
|
|
wasted_ = 0;
|
|
}
|
|
|
|
void FileList::sort(FileSort by) {
|
|
if (wasted_) compact();
|
|
names_.shrink_to_fit();
|
|
entries_.shrink_to_fit();
|
|
order_.resize(entries_.size());
|
|
for (size_t i = 0; i < order_.size(); i++) order_[i] = static_cast<uint16_t>(i);
|
|
std::sort(order_.begin(), order_.end(), [&](uint16_t a, uint16_t b) { return before(entries_[a], entries_[b], by); });
|
|
}
|
|
|
|
int FileList::find(const std::string& name) const {
|
|
for (size_t i = 0; i < order_.size(); i++)
|
|
if (name == this->name(i)) return static_cast<int>(i);
|
|
return -1;
|
|
}
|
|
|
|
size_t FileList::bytes() const {
|
|
return entries_.capacity() * sizeof(Entry) + order_.capacity() * sizeof(uint16_t) + names_.capacity();
|
|
}
|
|
|
|
std::string formatStamp(uint32_t modified) {
|
|
if (modified < kYear2020) return "-";
|
|
time_t t = static_cast<time_t>(modified);
|
|
struct tm local;
|
|
localtime_r(&t, &local);
|
|
char s[20];
|
|
std::snprintf(s, sizeof s, "%04d-%02d-%02d %02d:%02d", local.tm_year + 1900, local.tm_mon + 1, local.tm_mday, local.tm_hour,
|
|
local.tm_min);
|
|
return s;
|
|
}
|
|
|
|
std::string rowDetail(bool folder, uint32_t size, uint32_t modified) {
|
|
if (folder) return "folder";
|
|
std::string stamp = formatStamp(modified);
|
|
return sizeText(size) + " " + (stamp == "-" ? stamp : stamp.substr(0, 10));
|
|
}
|
|
|
|
std::string fitName(const std::string& name, size_t maxChars) {
|
|
if (name.size() <= maxChars || maxChars < 8) return name.substr(0, maxChars);
|
|
size_t tail = std::min<size_t>(6, maxChars / 3), head = maxChars - tail - 2;
|
|
return name.substr(0, head) + ".." + name.substr(name.size() - tail);
|
|
}
|
|
|
|
} // namespace roro::files
|