Public Access
M1 step 3: Log writing and Storage Clean-up
- lib/storage_model (host-tested): FAT-safe names, daily Log paths, dates from Log/Capture file names, CleanupPlan by category and age, byte formatting - StorageService does all card I/O on its task: queued Log lines are written in batches each second (dropped while Logs are paused), plus file listing and deletion jobs - Settings > Storage moves into StoragePage: usage, Clean up (category, age with size preview, confirmation), Erase SD card - Clock: local date for Log names - Serial: log <text>, sd list Verified on the device: lines land in /irc/dev/#test/2026-10-02.log with folders created as needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -34,6 +34,15 @@ std::string ClockModel::formatLocalTime(int64_t utcSeconds) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
std::string ClockModel::formatLocalDate(int64_t utcSeconds) {
|
||||
time_t t = static_cast<time_t>(utcSeconds);
|
||||
struct tm local;
|
||||
localtime_r(&t, &local);
|
||||
char buf[12];
|
||||
std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", local.tm_year + 1900, local.tm_mon + 1, local.tm_mday);
|
||||
return buf;
|
||||
}
|
||||
|
||||
std::string ClockModel::formatAge(int64_t seconds) {
|
||||
char buf[24];
|
||||
if (seconds < 60) return "now";
|
||||
|
||||
@@ -21,6 +21,7 @@ class ClockModel {
|
||||
|
||||
static void applyTimezone(const char* posixTz);
|
||||
static std::string formatLocalTime(int64_t utcSeconds); // "HH:MM"
|
||||
static std::string formatLocalDate(int64_t utcSeconds); // "YYYY-MM-DD"
|
||||
static std::string formatAge(int64_t seconds); // "now", "5 min ago", "3 h ago", "2 d ago"
|
||||
|
||||
private:
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "cleanup_plan.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include "storage_paths.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
int maxAgeDays(CleanupAge age) {
|
||||
switch (age) {
|
||||
case CleanupAge::OneMonth: return 30;
|
||||
case CleanupAge::ThreeMonths: return 91;
|
||||
case CleanupAge::SixMonths: return 182;
|
||||
case CleanupAge::OneYear: return 365;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CleanupPlan CleanupPlan::make(const std::vector<StoredFile>& files, CleanupAge age, int todayDay) {
|
||||
CleanupPlan plan;
|
||||
for (auto& f : files) {
|
||||
if (f.path.rfind("/notes", 0) == 0) continue; // Notes are never cleaned up
|
||||
bool selected = age == CleanupAge::Everything;
|
||||
if (!selected) {
|
||||
size_t slash = f.path.find_last_of('/');
|
||||
int day = storage::fileDay(slash == std::string::npos ? f.path : f.path.substr(slash + 1));
|
||||
selected = day >= 0 && todayDay - day > maxAgeDays(age);
|
||||
}
|
||||
if (selected) {
|
||||
plan.paths.push_back(f.path);
|
||||
plan.bytes += f.bytes;
|
||||
}
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
std::string formatBytes(uint64_t bytes) {
|
||||
const char* units[] = {"B", "KB", "MB", "GB"};
|
||||
double v = static_cast<double>(bytes);
|
||||
int u = 0;
|
||||
while (v >= 1024 && u < 3) {
|
||||
v /= 1024;
|
||||
u++;
|
||||
}
|
||||
char buf[16];
|
||||
if (u == 0 || v >= 10) std::snprintf(buf, sizeof(buf), "%.0f %s", v, units[u]);
|
||||
else std::snprintf(buf, sizeof(buf), "%.1f %s", v, units[u]);
|
||||
return buf;
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace roro {
|
||||
|
||||
struct StoredFile {
|
||||
std::string path;
|
||||
uint64_t bytes;
|
||||
};
|
||||
|
||||
// What Storage Clean-up offers (see CONTEXT.md). Notes are deliberately absent.
|
||||
struct CleanupCategory {
|
||||
const char* label;
|
||||
const char* folder;
|
||||
};
|
||||
inline const CleanupCategory kCleanupCategories[] = {
|
||||
{"IRC logs", "/irc"},
|
||||
{"Probe-request logs", "/wifi/probes"},
|
||||
{"Wi-Fi captures", "/captures/wifi"},
|
||||
};
|
||||
|
||||
enum class CleanupAge { OneMonth, ThreeMonths, SixMonths, OneYear, Everything };
|
||||
inline const char* const kCleanupAgeLabels[] = {"Older than 1 month", "Older than 3 months", "Older than 6 months",
|
||||
"Older than 1 year", "Everything"};
|
||||
|
||||
// The files a clean-up would delete, and how much space that frees. Dates come from file names;
|
||||
// undated files are only removed by Everything.
|
||||
struct CleanupPlan {
|
||||
std::vector<std::string> paths;
|
||||
uint64_t bytes = 0;
|
||||
|
||||
static CleanupPlan make(const std::vector<StoredFile>& files, CleanupAge age, int todayDay);
|
||||
};
|
||||
|
||||
std::string formatBytes(uint64_t bytes);
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "storage_paths.h"
|
||||
|
||||
#include <cctype>
|
||||
|
||||
namespace roro::storage {
|
||||
|
||||
namespace {
|
||||
|
||||
// Howard Hinnant's days_from_civil.
|
||||
int daysFromCivil(int y, int m, int d) {
|
||||
y -= m <= 2;
|
||||
int era = (y >= 0 ? y : y - 399) / 400;
|
||||
int yoe = y - era * 400;
|
||||
int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
|
||||
int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||
return era * 146097 + doe - 719468;
|
||||
}
|
||||
|
||||
bool digits(const std::string& s, size_t pos, size_t n, int& out) {
|
||||
if (pos + n > s.size()) return false;
|
||||
out = 0;
|
||||
for (size_t i = pos; i < pos + n; i++) {
|
||||
if (!std::isdigit(static_cast<unsigned char>(s[i]))) return false;
|
||||
out = out * 10 + (s[i] - '0');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int validDay(int y, int m, int d) {
|
||||
static const int kDays[] = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
||||
if (y < 1970 || m < 1 || m > 12 || d < 1 || d > kDays[m - 1]) return -1;
|
||||
return daysFromCivil(y, m, d);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string sanitize(const std::string& name) {
|
||||
std::string out;
|
||||
for (char c : name) {
|
||||
bool forbidden = c == '\\' || c == '/' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' ||
|
||||
c == '>' || c == '|' || static_cast<unsigned char>(c) < 0x20;
|
||||
out += forbidden ? '_' : c;
|
||||
}
|
||||
if (out.empty() || out == ".") return "_";
|
||||
if (out == "..") return "_.";
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string dailyLogPath(std::initializer_list<std::string> folders, const std::string& date) {
|
||||
std::string path;
|
||||
for (auto& f : folders) path += "/" + sanitize(f);
|
||||
return path + "/" + (date.empty() ? "undated" : date) + ".log";
|
||||
}
|
||||
|
||||
int fileDay(const std::string& name) {
|
||||
int y, m, d;
|
||||
if (digits(name, 0, 4, y) && name.size() > 10 && name[4] == '-' && digits(name, 5, 2, m) && name[7] == '-' &&
|
||||
digits(name, 8, 2, d))
|
||||
return validDay(y, m, d);
|
||||
if (digits(name, 0, 4, y) && digits(name, 4, 2, m) && digits(name, 6, 2, d) && name.size() > 8 && name[8] == '-')
|
||||
return validDay(y, m, d);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int dayFromUtc(int64_t utcSeconds) { return static_cast<int>(utcSeconds / 86400); }
|
||||
|
||||
} // namespace roro::storage
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
|
||||
namespace roro::storage {
|
||||
|
||||
// Makes one path component safe for FAT: forbidden characters become '_', never empty or a dot name.
|
||||
std::string sanitize(const std::string& name);
|
||||
|
||||
// "/irc/libera/#roro/2026-10-02.log" from {"irc", "libera", "#roro"} and a local date
|
||||
// ("YYYY-MM-DD"); an empty date (clock not set yet) gives "undated.log".
|
||||
std::string dailyLogPath(std::initializer_list<std::string> folders, const std::string& date);
|
||||
|
||||
// Day number (days since 1970-01-01) encoded in a Log ("2026-10-02.log") or Capture
|
||||
// ("20261002-144500.pcap") file name, or -1 if the name carries no valid date.
|
||||
int fileDay(const std::string& fileName);
|
||||
|
||||
int dayFromUtc(int64_t utcSeconds);
|
||||
|
||||
} // namespace roro::storage
|
||||
Reference in New Issue
Block a user