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:
139
src/store/fileStore.js
Normal file
139
src/store/fileStore.js
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user