#include "services/file_ops.h" #include #include #include #include #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(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(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('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(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(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(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(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(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 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(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(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(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