Public Access
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
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include "clock_model.h"
|
||||
#include "event_bus.h"
|
||||
@@ -20,7 +21,16 @@ class ClockService : public Service {
|
||||
const char* name() const override { return "clock"; }
|
||||
void start() override { applyTimezone(); }
|
||||
|
||||
bool set(int64_t utcSeconds, TimeSource source) { return model_.set(utcSeconds, source, millis()); }
|
||||
bool set(int64_t utcSeconds, TimeSource source) {
|
||||
if (!model_.set(utcSeconds, source, millis())) return false;
|
||||
// The system's own clock too (F1, Q137): the card's files are dated from it, and NTP was
|
||||
// the only thing setting it. NTP has just done so itself.
|
||||
if (source != TimeSource::Ntp) {
|
||||
timeval now = {static_cast<time_t>(utcSeconds), 0};
|
||||
settimeofday(&now, nullptr);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const ClockModel& model() const { return model_; }
|
||||
|
||||
// UTC seconds, or -1 if the clock isn't set.
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
#include "services/file_ops.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
|
||||
#include <ctime>
|
||||
#include <memory>
|
||||
|
||||
#include "file_names.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
using files::baseName;
|
||||
using files::joinPath;
|
||||
using files::parentOf;
|
||||
|
||||
namespace {
|
||||
// The FatFs drive the card is mounted as: SDFS keeps it to itself.
|
||||
struct SdDrive : fs::SDFS {
|
||||
static uint8_t of(fs::SDFS& sd) { return sd.*(&SdDrive::_pdrv); }
|
||||
};
|
||||
|
||||
// A FAT date and time (local, as the firmware wrote them) as Unix time; 0 if there's none.
|
||||
uint32_t fatTime(uint16_t date, uint16_t time) {
|
||||
if (date == 0) return 0;
|
||||
struct tm t = {};
|
||||
t.tm_year = (date >> 9) + 80;
|
||||
t.tm_mon = ((date >> 5) & 15) - 1;
|
||||
t.tm_mday = date & 31;
|
||||
t.tm_hour = time >> 11;
|
||||
t.tm_min = (time >> 5) & 63;
|
||||
t.tm_sec = (time & 31) * 2;
|
||||
t.tm_isdst = -1;
|
||||
time_t at = mktime(&t);
|
||||
return at < 0 ? 0 : static_cast<uint32_t>(at);
|
||||
}
|
||||
|
||||
struct Guard {
|
||||
explicit Guard(SemaphoreHandle_t l) : l_(l) { xSemaphoreTake(l_, portMAX_DELAY); }
|
||||
~Guard() { xSemaphoreGive(l_); }
|
||||
SemaphoreHandle_t l_;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
FileOps::FileOps(StorageService& storage, InUse inUse)
|
||||
: storage_(storage), inUse_(std::move(inUse)), lock_(xSemaphoreCreateMutex()) {}
|
||||
|
||||
FileOps::Status FileOps::status() const {
|
||||
Guard g(lock_);
|
||||
Status s = status_;
|
||||
s.running = busy_;
|
||||
s.doneBytes = doneBytes_;
|
||||
s.files = files_;
|
||||
return s;
|
||||
}
|
||||
|
||||
bool FileOps::finished(Status& out) {
|
||||
// The storage task drops its jobs when the card goes: nothing would ever end this one.
|
||||
if (busy_ && !storage_.state().present) {
|
||||
fail("The card went away");
|
||||
busy_ = false;
|
||||
done_ = true;
|
||||
}
|
||||
if (!done_.exchange(false)) return false;
|
||||
out = status();
|
||||
out.running = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void FileOps::takeListing(files::FileList& out) {
|
||||
Guard g(lock_);
|
||||
out = std::move(listing_);
|
||||
listing_.clear();
|
||||
}
|
||||
|
||||
bool FileOps::isFolder(const std::string& path, bool& exists) {
|
||||
bool folder = false;
|
||||
exists = false;
|
||||
storage_.runAndWait([&]() {
|
||||
fs::File f = SD.open(path.c_str());
|
||||
exists = static_cast<bool>(f);
|
||||
folder = f && f.isDirectory();
|
||||
});
|
||||
return folder;
|
||||
}
|
||||
|
||||
std::string FileOps::whyReadOnly(const std::string& path, bool folder) const {
|
||||
return files::whyReadOnly(path, inUse_(path, folder));
|
||||
}
|
||||
|
||||
std::string FileOps::start(Op op, const std::string& path) {
|
||||
if (busy_) return "The card is busy with something else";
|
||||
if (!storage_.state().present) return "No SD card";
|
||||
{
|
||||
Guard g(lock_);
|
||||
status_ = Status();
|
||||
status_.op = op;
|
||||
status_.path = path;
|
||||
}
|
||||
closeAll(); // only handles a vanished card left behind
|
||||
listOpen_ = false;
|
||||
op_ = op;
|
||||
phase_ = 0;
|
||||
undo_ = false;
|
||||
cancel_ = false;
|
||||
done_ = false;
|
||||
doneBytes_ = 0;
|
||||
files_ = 0;
|
||||
countBytes_ = 0;
|
||||
startMs_ = millis();
|
||||
busy_ = true;
|
||||
queue();
|
||||
return "";
|
||||
}
|
||||
|
||||
void FileOps::queue() {
|
||||
storage_.runJob([this]() { slice(); });
|
||||
}
|
||||
|
||||
std::string FileOps::list(const std::string& folder) {
|
||||
from_ = folder;
|
||||
return start(Op::List, folder);
|
||||
}
|
||||
|
||||
std::string FileOps::count(const std::string& path) {
|
||||
from_ = path;
|
||||
return start(Op::Count, path);
|
||||
}
|
||||
|
||||
std::string FileOps::copy(const std::string& from, bool folder, const std::string& intoFolder, const std::string& name, bool replace) {
|
||||
if (busy_) return "The card is busy with something else";
|
||||
std::string why = files::whyNotInto(from, intoFolder);
|
||||
// A copy next to its original is fine: only the name must differ.
|
||||
if (why == "It's already there" && name != baseName(from)) why.clear();
|
||||
if (why.empty()) why = files::checkName(name);
|
||||
if (!why.empty()) return why;
|
||||
from_ = from;
|
||||
to_ = joinPath(intoFolder, name);
|
||||
replace_ = replace;
|
||||
sourceIsFolder_ = folder;
|
||||
return start(Op::Copy, from);
|
||||
}
|
||||
|
||||
std::string FileOps::move(const std::string& from, bool folder, const std::string& to, bool replace) {
|
||||
if (busy_) return "The card is busy with something else";
|
||||
std::string why = files::whyReadOnly(from, inUse_(from, folder));
|
||||
if (why.empty() && from == to) why = "It's already there";
|
||||
if (why.empty() && parentOf(to) != parentOf(from)) why = files::whyNotInto(from, parentOf(to));
|
||||
if (why.empty()) why = files::checkName(baseName(to));
|
||||
if (!why.empty()) return why;
|
||||
from_ = from;
|
||||
to_ = to;
|
||||
replace_ = replace;
|
||||
sourceIsFolder_ = folder;
|
||||
return start(Op::Move, from);
|
||||
}
|
||||
|
||||
std::string FileOps::remove(const std::string& path, bool folder) {
|
||||
if (busy_) return "The card is busy with something else";
|
||||
std::string why = files::whyReadOnly(path, inUse_(path, folder));
|
||||
if (!why.empty()) return why;
|
||||
from_ = path;
|
||||
sourceIsFolder_ = folder;
|
||||
return start(Op::Delete, path);
|
||||
}
|
||||
|
||||
std::string FileOps::makeFolder(const std::string& path) {
|
||||
if (busy_) return "The card is busy with something else";
|
||||
std::string why = files::checkName(baseName(path));
|
||||
if (why.empty() && files::isInside(path, files::kGeminiCache)) why = std::string(files::kGeminiCache) + " is the Gemini App's working space";
|
||||
if (!why.empty()) return why;
|
||||
from_ = path;
|
||||
return start(Op::MakeFolder, path);
|
||||
}
|
||||
|
||||
void FileOps::fail(const std::string& why) {
|
||||
Guard g(lock_);
|
||||
if (status_.error.empty()) status_.error = why;
|
||||
}
|
||||
|
||||
void FileOps::closeAll() {
|
||||
if (in_) in_.close();
|
||||
if (out_) out_.close();
|
||||
for (auto& d : dirs_)
|
||||
if (d) d.close();
|
||||
dirs_.clear();
|
||||
paths_.clear();
|
||||
}
|
||||
|
||||
void FileOps::finish() {
|
||||
if (listOpen_) f_closedir(&listDir_);
|
||||
listOpen_ = false;
|
||||
closeAll();
|
||||
{
|
||||
Guard g(lock_);
|
||||
status_.cancelled = cancel_ && status_.error.empty() && (op_ == Op::Copy || op_ == Op::Delete || op_ == Op::Count);
|
||||
status_.bytes = countBytes_;
|
||||
status_.ms = millis() - startMs_;
|
||||
}
|
||||
op_ = Op::None;
|
||||
busy_ = false;
|
||||
done_ = true;
|
||||
}
|
||||
|
||||
bool FileOps::enter(const std::string& path) {
|
||||
fs::File d = SD.open(path.c_str());
|
||||
if (!d || !d.isDirectory()) return false;
|
||||
dirs_.push_back(d);
|
||||
paths_.push_back(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
// One slice on the storage task, then back in the queue behind whatever else is waiting.
|
||||
void FileOps::slice() {
|
||||
if (!busy_) return; // given up on: the card went away (finished())
|
||||
bool over = true;
|
||||
switch (op_) {
|
||||
case Op::List: over = sliceList(); break;
|
||||
case Op::Count: over = sliceCount(); break;
|
||||
case Op::Copy: over = sliceCopy(); break;
|
||||
case Op::Delete: over = sliceDelete(); break;
|
||||
case Op::Move: {
|
||||
if (!SD.exists(from_.c_str())) fail("It isn't there any more");
|
||||
else if (SD.exists(to_.c_str()) && !replace_) {
|
||||
Guard g(lock_);
|
||||
status_.exists = true;
|
||||
status_.error = baseName(to_) + " exists already";
|
||||
} else {
|
||||
if (SD.exists(to_.c_str())) SD.remove(to_.c_str()); // a file: a folder in the way makes the rename fail
|
||||
if (!SD.rename(from_.c_str(), to_.c_str())) fail("The card refused to move it");
|
||||
else files_ = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Op::MakeFolder:
|
||||
if (SD.exists(from_.c_str())) fail(baseName(from_) + " exists already");
|
||||
else if (!SD.mkdir(from_.c_str())) fail("The card refused to make the folder");
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
if (over) finish();
|
||||
else queue();
|
||||
}
|
||||
|
||||
// Straight from FatFs, one pass over the folder: the Arduino File looks every entry up by name
|
||||
// again for its size and its date, which made 300 entries take over two seconds.
|
||||
bool FileOps::sliceList() {
|
||||
uint32_t t0 = millis();
|
||||
if (phase_ == 0) {
|
||||
Guard g(lock_);
|
||||
listing_.clear();
|
||||
std::string path = std::string(1, static_cast<char>('0' + SdDrive::of(SD))) + ":" + from_;
|
||||
if (f_opendir(&listDir_, path.c_str()) != FR_OK) {
|
||||
status_.error = from_ + " isn't a folder";
|
||||
return true;
|
||||
}
|
||||
listOpen_ = true;
|
||||
phase_ = 1;
|
||||
}
|
||||
while (millis() - t0 < kSliceMs) {
|
||||
if (cancel_) return true;
|
||||
FILINFO info;
|
||||
if (f_readdir(&listDir_, &info) != FR_OK || info.fname[0] == 0) return true;
|
||||
bool folder = info.fattrib & AM_DIR;
|
||||
Guard g(lock_);
|
||||
listing_.add(info.fname, folder ? 0 : static_cast<uint32_t>(info.fsize), fatTime(info.fdate, info.ftime), folder);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// What's inside, for "Delete saved and its 42 files?" (Q132) and for a copy's total.
|
||||
bool FileOps::sliceCount() {
|
||||
uint32_t t0 = millis();
|
||||
if (phase_ == 0) {
|
||||
phase_ = 1;
|
||||
fs::File f = SD.open(from_.c_str());
|
||||
if (!f) {
|
||||
fail("It isn't there any more");
|
||||
return true;
|
||||
}
|
||||
if (!f.isDirectory()) {
|
||||
files_ = 1;
|
||||
countBytes_ = static_cast<uint32_t>(f.size());
|
||||
return true;
|
||||
}
|
||||
f.close();
|
||||
enter(from_);
|
||||
}
|
||||
while (!dirs_.empty() && millis() - t0 < kSliceMs) {
|
||||
if (cancel_) return true;
|
||||
fs::File f = dirs_.back().openNextFile();
|
||||
if (!f) {
|
||||
dirs_.back().close();
|
||||
dirs_.pop_back();
|
||||
paths_.pop_back();
|
||||
} else if (f.isDirectory()) {
|
||||
std::string sub = joinPath(paths_.back(), f.name());
|
||||
f.close();
|
||||
enter(sub);
|
||||
} else {
|
||||
files_++;
|
||||
countBytes_ += static_cast<uint32_t>(f.size());
|
||||
}
|
||||
}
|
||||
return dirs_.empty();
|
||||
}
|
||||
|
||||
// Deletes files as the walk meets them and each folder once it's empty. Also how a cancelled or
|
||||
// failed copy takes back what it had written (undo_).
|
||||
bool FileOps::sliceDelete() {
|
||||
uint32_t t0 = millis();
|
||||
if (phase_ == 0) {
|
||||
phase_ = 1;
|
||||
fs::File f = SD.open(from_.c_str());
|
||||
if (!f) {
|
||||
if (!undo_) fail("It isn't there any more");
|
||||
return true;
|
||||
}
|
||||
bool folder = f.isDirectory();
|
||||
f.close();
|
||||
if (!folder) {
|
||||
if (!SD.remove(from_.c_str())) fail("The card refused to delete it");
|
||||
else if (!undo_) files_ = 1;
|
||||
return true;
|
||||
}
|
||||
enter(from_);
|
||||
}
|
||||
while (!dirs_.empty() && millis() - t0 < kSliceMs) {
|
||||
if (cancel_ && !undo_) return true; // what's deleted stays deleted
|
||||
fs::File f = dirs_.back().openNextFile();
|
||||
if (!f) {
|
||||
std::string done = paths_.back();
|
||||
dirs_.back().close();
|
||||
dirs_.pop_back();
|
||||
paths_.pop_back();
|
||||
if (!SD.rmdir(done.c_str())) {
|
||||
fail("The card refused to delete " + baseName(done));
|
||||
return true;
|
||||
}
|
||||
} else if (f.isDirectory()) {
|
||||
std::string sub = joinPath(paths_.back(), f.name());
|
||||
f.close();
|
||||
if (!enter(sub)) {
|
||||
fail("The card refused to open " + baseName(sub));
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
std::string path = joinPath(paths_.back(), f.name());
|
||||
f.close();
|
||||
if (!SD.remove(path.c_str())) {
|
||||
fail("The card refused to delete " + baseName(path));
|
||||
return true;
|
||||
}
|
||||
if (!undo_) files_++;
|
||||
}
|
||||
}
|
||||
return dirs_.empty();
|
||||
}
|
||||
|
||||
// Phases 0 and 1 count the source (the walk of Count), 2 checks and prepares, 3 copies, 4 takes
|
||||
// a failed or cancelled copy back.
|
||||
bool FileOps::sliceCopy() {
|
||||
uint32_t t0 = millis();
|
||||
if (phase_ <= 1) {
|
||||
if (!sliceCount()) return false;
|
||||
phase_ = 2;
|
||||
}
|
||||
if (phase_ == 2) {
|
||||
{
|
||||
Guard g(lock_);
|
||||
if (!status_.error.empty()) return true;
|
||||
status_.totalBytes = countBytes_;
|
||||
}
|
||||
if (cancel_) return true;
|
||||
files_ = 0;
|
||||
StorageState card = storage_.state();
|
||||
if (card.totalBytes - card.usedBytes < static_cast<uint64_t>(countBytes_) + 64 * 1024) {
|
||||
fail("Not enough room on the card");
|
||||
return true;
|
||||
}
|
||||
if (SD.exists(to_.c_str())) {
|
||||
if (!replace_) {
|
||||
Guard g(lock_);
|
||||
status_.exists = true;
|
||||
status_.error = baseName(to_) + " exists already";
|
||||
return true;
|
||||
}
|
||||
if (sourceIsFolder_) {
|
||||
fail("A folder with that name is in the way");
|
||||
return true;
|
||||
}
|
||||
SD.remove(to_.c_str());
|
||||
}
|
||||
if (sourceIsFolder_) {
|
||||
if (!SD.mkdir(to_.c_str()) || !enter(from_)) {
|
||||
fail("The card refused to make the folder");
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
in_ = SD.open(from_.c_str(), FILE_READ);
|
||||
outPath_ = to_;
|
||||
out_ = SD.open(outPath_.c_str(), FILE_WRITE);
|
||||
fileBytes_ = 0;
|
||||
fileSize_ = in_ ? static_cast<uint32_t>(in_.size()) : 0;
|
||||
if (!in_ || !out_) {
|
||||
fail("The card refused to open the files");
|
||||
phase_ = 4;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
phase_ = 3;
|
||||
}
|
||||
if (phase_ == 3) {
|
||||
std::unique_ptr<uint8_t[]> piece(new uint8_t[kPiece]);
|
||||
while (millis() - t0 < kSliceMs) {
|
||||
if (cancel_) {
|
||||
phase_ = 4;
|
||||
return false;
|
||||
}
|
||||
if (in_) { // a file in progress
|
||||
int n = in_.read(piece.get(), kPiece);
|
||||
if (n > 0 && out_.write(piece.get(), n) != static_cast<size_t>(n)) n = -1;
|
||||
if (n < 0) {
|
||||
fail("The card refused a write");
|
||||
phase_ = 4;
|
||||
return false;
|
||||
}
|
||||
if (n > 0) {
|
||||
doneBytes_ += n;
|
||||
fileBytes_ += n;
|
||||
continue;
|
||||
}
|
||||
in_.close();
|
||||
out_.close();
|
||||
// Q133: the sizes are compared. A Log that grew meanwhile is copied as far as it went.
|
||||
fs::File check = SD.open(outPath_.c_str(), FILE_READ);
|
||||
bool same = check && static_cast<uint32_t>(check.size()) == fileBytes_ && fileBytes_ >= fileSize_;
|
||||
if (check) check.close();
|
||||
if (!same) {
|
||||
fail("The copy of " + baseName(outPath_) + " isn't the size of the original");
|
||||
phase_ = 4;
|
||||
return false;
|
||||
}
|
||||
files_++;
|
||||
continue;
|
||||
}
|
||||
if (dirs_.empty()) return true; // a single file, or the whole folder: done
|
||||
fs::File f = dirs_.back().openNextFile();
|
||||
// The copy sits where the original does, relative to the two roots.
|
||||
auto target = [&](const std::string& source) { return to_ + source.substr(from_.size()); };
|
||||
if (!f) {
|
||||
dirs_.back().close();
|
||||
dirs_.pop_back();
|
||||
paths_.pop_back();
|
||||
} else if (f.isDirectory()) {
|
||||
std::string sub = joinPath(paths_.back(), f.name());
|
||||
f.close();
|
||||
if (!SD.mkdir(target(sub).c_str()) || !enter(sub)) {
|
||||
fail("The card refused to make " + baseName(sub));
|
||||
phase_ = 4;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
std::string source = joinPath(paths_.back(), f.name());
|
||||
f.close();
|
||||
in_ = SD.open(source.c_str(), FILE_READ);
|
||||
outPath_ = target(source);
|
||||
out_ = SD.open(outPath_.c_str(), FILE_WRITE);
|
||||
fileBytes_ = 0;
|
||||
fileSize_ = in_ ? static_cast<uint32_t>(in_.size()) : 0;
|
||||
if (!in_ || !out_) {
|
||||
fail("The card refused to open " + baseName(source));
|
||||
phase_ = 4;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Phase 4: take it back. The copy never replaced a folder, so everything under to_ is ours.
|
||||
if (!undo_) {
|
||||
closeAll();
|
||||
undo_ = true;
|
||||
from_ = to_;
|
||||
phase_ = 0;
|
||||
op_ = Op::Delete; // finish() reports it as the copy it was: status_.op stays Copy
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,102 @@
|
||||
#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
|
||||
@@ -53,6 +53,7 @@ class GnssService : public Service {
|
||||
std::string startTrack(uint32_t nowMs);
|
||||
void stopTrack();
|
||||
bool tracking() const { return !trackPath_.empty(); }
|
||||
const std::string& trackPath() const { return trackPath_; }
|
||||
int trackPoints() const { return trackPoints_; }
|
||||
uint32_t trackStartMs() const { return trackStartMs_; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user