Files
roro9stack/src/services/file_ops.h
T
twislaandClaude Opus 5.5 b7aed8e91c F1 steps 2-6: the Storage App, its viewers, and Maintenance moved in (#3)
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
2026-10-06 08:45:43 +02:00

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