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
103 lines
4.3 KiB
C++
103 lines
4.3 KiB
C++
#pragma once
|
|
|
|
#include <FS.h>
|
|
#include <ff.h>
|
|
#include <freertos/FreeRTOS.h>
|
|
#include <freertos/semphr.h>
|
|
|
|
#include <atomic>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "file_list.h"
|
|
#include "services/storage_service.h"
|
|
|
|
namespace roro {
|
|
|
|
// What the Storage App does to the card (docs/milestones/F1.md): list a folder, count what's in
|
|
// one, copy, move, delete, make a folder. One operation at a time, on the storage task like every
|
|
// card access, **in slices of about 150 ms**: a long copy re-queues itself between slices, so IRC
|
|
// Logs, a Gemini page streaming to the card or a Capture are written in between and nobody waits
|
|
// long enough to give up. The read-only rules of Q130 are checked here, whoever asks.
|
|
class FileOps {
|
|
public:
|
|
enum class Op : uint8_t { None, List, Count, Copy, Move, Delete, MakeFolder };
|
|
// The files the firmware has open right now that matter for `path` (a folder or not).
|
|
using InUse = std::function<std::vector<std::string>(const std::string& path, bool folder)>;
|
|
|
|
struct Status {
|
|
Op op = Op::None;
|
|
bool running = false;
|
|
bool cancelled = false;
|
|
bool exists = false; // a copy or move found something with that name: ask, then `replace`
|
|
std::string error; // why it failed, for a human; empty when it worked
|
|
uint32_t doneBytes = 0, totalBytes = 0; // a copy's progress
|
|
uint32_t files = 0; // files copied or deleted so far; counted, for Count
|
|
uint32_t bytes = 0; // Count: their total size
|
|
std::string path; // what it was about
|
|
uint32_t ms = 0; // how long it took, once it has ended
|
|
};
|
|
|
|
FileOps(StorageService& storage, InUse inUse);
|
|
|
|
// Each returns "" when the operation started, or why it can't. One at a time.
|
|
std::string list(const std::string& folder);
|
|
std::string count(const std::string& path);
|
|
std::string copy(const std::string& from, bool folder, const std::string& intoFolder, const std::string& name, bool replace = false);
|
|
std::string move(const std::string& from, bool folder, const std::string& to, bool replace = false);
|
|
std::string remove(const std::string& path, bool folder);
|
|
std::string makeFolder(const std::string& path);
|
|
void cancel() { cancel_ = true; }
|
|
// Why `path` can't be renamed, moved or deleted, or "": asked before the confirmation.
|
|
std::string whyReadOnly(const std::string& path, bool folder) const;
|
|
|
|
bool busy() const { return busy_; }
|
|
Status status() const; // while it runs
|
|
bool finished(Status& out); // true once, when it has ended
|
|
void takeListing(files::FileList& out); // after a List finished without error
|
|
|
|
// For the console, which has no listing to know from: is `path` a folder? Waits for the card.
|
|
bool isFolder(const std::string& path, bool& exists);
|
|
|
|
private:
|
|
static constexpr uint32_t kSliceMs = 150;
|
|
static constexpr size_t kPiece = 4096; // Q133
|
|
|
|
std::string start(Op op, const std::string& path);
|
|
void queue();
|
|
void slice(); // on the storage task
|
|
bool sliceList(); // each: true when the operation is over
|
|
bool sliceCount();
|
|
bool sliceCopy();
|
|
bool sliceDelete();
|
|
void fail(const std::string& why);
|
|
void finish();
|
|
bool enter(const std::string& path); // push a folder onto the walk
|
|
void closeAll();
|
|
|
|
StorageService& storage_;
|
|
InUse inUse_;
|
|
mutable SemaphoreHandle_t lock_; // status_, listing_
|
|
Status status_;
|
|
files::FileList listing_;
|
|
std::atomic<bool> busy_{false}, done_{false}, cancel_{false};
|
|
std::atomic<uint32_t> doneBytes_{0}, files_{0};
|
|
|
|
// The operation in hand: only the storage task touches these while it runs.
|
|
Op op_ = Op::None;
|
|
std::string from_, to_;
|
|
bool replace_ = false, sourceIsFolder_ = false, undo_ = false;
|
|
int phase_ = 0;
|
|
std::vector<fs::File> dirs_; // the folders being walked, outermost first
|
|
std::vector<std::string> paths_; // their paths
|
|
FF_DIR listDir_; // List reads the folder straight from FatFs
|
|
bool listOpen_ = false;
|
|
fs::File in_, out_; // the file being copied
|
|
std::string outPath_;
|
|
uint32_t countBytes_ = 0, startMs_ = 0;
|
|
uint32_t fileBytes_ = 0, fileSize_ = 0; // copied of the file in hand; its size when opened
|
|
};
|
|
|
|
} // namespace roro
|