Initiales Projekt-Skelett appRobotFileservice

Ausgelagertes Programm-/File-Handling (vormals GCode.receiveFC im appRobotDriver,
ToDo_4 / ToDo_6b). Express-Service mit .gcode + .json-Storage, aktivem Programm +
Cursor, Teaching (FPoint) und Playback. Speicherung in Grad, driver-nativ (Radian)
zum Driver. Konzept/API unter doc/draft_filehandeling*.md. Tests: jest (13 gruen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chk
2026-06-14 10:12:41 +02:00
commit b68bdfa9b4
20 changed files with 6085 additions and 0 deletions

139
src/store/fileStore.js Normal file
View File

@@ -0,0 +1,139 @@
/**
* Datei-basierte Persistenz der Programme:
* <id>.<ext> — G-Code (Grad), standardnah, Zeitstempel/Cursor im Kommentar
* <id>.json — Sidecar mit Metadaten (Name, Zeiten, lineCount, angleUnit)
*
* Nach außen werden Programme NUR über die id angesprochen — niemals über Pfade.
* Storage-Details bleiben hier gekapselt (Konzept §8/§10).
*/
const fsp = require('fs/promises');
const path = require('path');
const cfg = require('../config');
const { ApiError } = require('../errors');
const ID_RE = /^[a-z0-9_]+$/;
/** Wandelt einen Anzeigenamen in eine sichere id (keine Pfade, kein '../'). */
function slugify(name) {
return String(name || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '')
.slice(0, 100);
}
/** Stellt sicher, dass eine id keine Pfad-Trenner o. Ä. enthält. */
function assertValidId(id) {
if (!ID_RE.test(String(id || ''))) {
throw new ApiError(400, 'INVALID_NAME', `invalid program id: ${id}`);
}
return id;
}
const gcodePath = (id) => path.join(cfg.storageDir, `${id}.${cfg.fileExt}`);
const jsonPath = (id) => path.join(cfg.storageDir, `${id}.json`);
async function ensureDir() {
await fsp.mkdir(cfg.storageDir, { recursive: true });
}
async function exists(id) {
try {
await fsp.access(gcodePath(id));
return true;
} catch {
return false;
}
}
/** Zerlegt Datei-Text in nicht-leere Zeilen (CR/LF-tolerant). */
function splitLines(text) {
return String(text)
.split(/\r?\n/)
.map((l) => l.replace(/\s+$/, ''))
.filter((l) => l.trim().length > 0);
}
/** Liest ein Programm: { id, name, lines (Grad, mit Kommentaren), meta }. */
async function read(id) {
assertValidId(id);
if (!(await exists(id))) {
throw new ApiError(404, 'PROGRAM_NOT_FOUND', `no program '${id}'`);
}
const text = await fsp.readFile(gcodePath(id), 'utf8');
const lines = splitLines(text);
let meta = {};
try {
meta = JSON.parse(await fsp.readFile(jsonPath(id), 'utf8'));
} catch {
/* Sidecar ist optional */
}
return { id, name: meta.name || id, lines, meta };
}
/** Schreibt .gcode + .json. lines = gespeicherte Zeilen (Grad, inkl. Kommentar/Cursor). */
async function write(id, { name, lines }) {
assertValidId(id);
await ensureDir();
const now = new Date().toISOString();
let createdAt = now;
try {
const prev = JSON.parse(await fsp.readFile(jsonPath(id), 'utf8'));
createdAt = prev.createdAt || now;
} catch {
/* neues Programm */
}
const meta = {
id,
name: name || id,
lineCount: lines.length,
angleUnit: cfg.storeAngleUnit,
createdAt,
updatedAt: now,
};
const body = lines.join('\n') + (lines.length ? '\n' : '');
await fsp.writeFile(gcodePath(id), body, 'utf8');
await fsp.writeFile(jsonPath(id), JSON.stringify(meta, null, 2) + '\n', 'utf8');
return meta;
}
async function remove(id) {
assertValidId(id);
if (!(await exists(id))) {
throw new ApiError(404, 'PROGRAM_NOT_FOUND', `no program '${id}'`);
}
await fsp.rm(gcodePath(id), { force: true });
await fsp.rm(jsonPath(id), { force: true });
}
/** Liste aller Programme (id, name, lineCount). */
async function list() {
await ensureDir();
const entries = await fsp.readdir(cfg.storageDir);
const ext = `.${cfg.fileExt}`;
const ids = entries.filter((f) => f.endsWith(ext)).map((f) => f.slice(0, -ext.length));
const out = [];
for (const id of ids) {
try {
const { name, lines, meta } = await read(id);
out.push({ id, name, lineCount: meta.lineCount ?? lines.length });
} catch {
/* defekte Einträge überspringen */
}
}
return out;
}
module.exports = {
slugify,
assertValidId,
exists,
read,
write,
remove,
list,
ensureDir,
gcodePath,
jsonPath,
};