按建议,继续完成后续工作
结论:已新增 OPFS host-side 路径模型,覆盖 INI、tool table、parameter、G-code、preview cache 与 session snapshot 存储目标,并让 file-service 和浏览器 smoke 复用该模型;聚合 smoke 验证通过。
This commit is contained in:
@@ -1,17 +1,4 @@
|
||||
function splitPath(path) {
|
||||
if (typeof path !== "string" || path.length === 0) {
|
||||
throw new Error("OPFS path must be a non-empty relative path.");
|
||||
}
|
||||
if (path.startsWith("/") || path.includes("//")) {
|
||||
throw new Error(`Invalid OPFS path: ${path}`);
|
||||
}
|
||||
|
||||
const parts = path.split("/");
|
||||
if (parts.some((part) => part === "." || part === ".." || part === "")) {
|
||||
throw new Error(`Invalid OPFS path: ${path}`);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
import { splitOpfsPath } from "./path-model.js";
|
||||
|
||||
export async function getOpfsRoot(storage = globalThis.navigator?.storage) {
|
||||
if (!storage?.getDirectory) {
|
||||
@@ -21,7 +8,7 @@ export async function getOpfsRoot(storage = globalThis.navigator?.storage) {
|
||||
}
|
||||
|
||||
export async function ensureParentDir(root, path) {
|
||||
const parts = splitPath(path);
|
||||
const parts = splitOpfsPath(path);
|
||||
let current = root;
|
||||
for (const part of parts.slice(0, -1)) {
|
||||
current = await current.getDirectoryHandle(part, { create: true });
|
||||
@@ -32,7 +19,7 @@ export async function ensureParentDir(root, path) {
|
||||
export async function saveTextFile(path, text, storage) {
|
||||
const root = await getOpfsRoot(storage);
|
||||
const dir = await ensureParentDir(root, path);
|
||||
const filename = splitPath(path).at(-1);
|
||||
const filename = splitOpfsPath(path).at(-1);
|
||||
const fileHandle = await dir.getFileHandle(filename, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(text);
|
||||
@@ -41,7 +28,7 @@ export async function saveTextFile(path, text, storage) {
|
||||
|
||||
export async function loadTextFile(path, storage) {
|
||||
const root = await getOpfsRoot(storage);
|
||||
const parts = splitPath(path);
|
||||
const parts = splitOpfsPath(path);
|
||||
let current = root;
|
||||
for (const part of parts.slice(0, -1)) {
|
||||
current = await current.getDirectoryHandle(part);
|
||||
|
||||
101
wasm-port/runtime/opfs/path-model.js
Normal file
101
wasm-port/runtime/opfs/path-model.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const ROOT = "linuxcnc";
|
||||
|
||||
function assertSegment(segment, label = "path segment") {
|
||||
if (typeof segment !== "string" || segment.length === 0) {
|
||||
throw new Error(`${label} must be a non-empty string.`);
|
||||
}
|
||||
if (
|
||||
segment === "." ||
|
||||
segment === ".." ||
|
||||
segment.includes("/") ||
|
||||
segment.includes("\\") ||
|
||||
segment.includes("\0")
|
||||
) {
|
||||
throw new Error(`Invalid ${label}: ${segment}`);
|
||||
}
|
||||
return segment;
|
||||
}
|
||||
|
||||
export function splitOpfsPath(path) {
|
||||
if (typeof path !== "string" || path.length === 0) {
|
||||
throw new Error("OPFS path must be a non-empty relative path.");
|
||||
}
|
||||
if (path.startsWith("/") || path.includes("//")) {
|
||||
throw new Error(`Invalid OPFS path: ${path}`);
|
||||
}
|
||||
|
||||
const parts = path.split("/");
|
||||
if (parts.some((part) => part === "." || part === ".." || part === "")) {
|
||||
throw new Error(`Invalid OPFS path: ${path}`);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
export function normalizeOpfsPath(path) {
|
||||
return splitOpfsPath(path).join("/");
|
||||
}
|
||||
|
||||
export function opfsPath(...segments) {
|
||||
return segments.map((segment) => assertSegment(segment)).join("/");
|
||||
}
|
||||
|
||||
export function machineRoot(machineId) {
|
||||
return opfsPath(ROOT, "machines", assertSegment(machineId, "machine id"));
|
||||
}
|
||||
|
||||
export function machineIniPath(machineId, filename = "machine.ini") {
|
||||
return opfsPath(
|
||||
ROOT,
|
||||
"machines",
|
||||
assertSegment(machineId, "machine id"),
|
||||
assertSegment(filename, "INI filename"),
|
||||
);
|
||||
}
|
||||
|
||||
export function toolTablePath(machineId, filename = "tool.tbl") {
|
||||
return opfsPath(
|
||||
ROOT,
|
||||
"machines",
|
||||
assertSegment(machineId, "machine id"),
|
||||
assertSegment(filename, "tool table filename"),
|
||||
);
|
||||
}
|
||||
|
||||
export function parameterFilePath(machineId, filename = "linuxcnc.var") {
|
||||
return opfsPath(
|
||||
ROOT,
|
||||
"machines",
|
||||
assertSegment(machineId, "machine id"),
|
||||
assertSegment(filename, "parameter filename"),
|
||||
);
|
||||
}
|
||||
|
||||
export function gcodeProgramPath(filename) {
|
||||
return opfsPath(ROOT, "gcode", assertSegment(filename, "G-code filename"));
|
||||
}
|
||||
|
||||
export function previewCachePath(cacheKey, filename = "preview.json") {
|
||||
return opfsPath(
|
||||
ROOT,
|
||||
"preview-cache",
|
||||
assertSegment(cacheKey, "preview cache key"),
|
||||
assertSegment(filename, "preview cache filename"),
|
||||
);
|
||||
}
|
||||
|
||||
export function sessionSnapshotPath(sessionId, filename = "snapshot.json") {
|
||||
return opfsPath(
|
||||
ROOT,
|
||||
"sessions",
|
||||
assertSegment(sessionId, "session id"),
|
||||
assertSegment(filename, "session snapshot filename"),
|
||||
);
|
||||
}
|
||||
|
||||
export function defaultMachinePaths(machineId) {
|
||||
return {
|
||||
ini: machineIniPath(machineId),
|
||||
toolTable: toolTablePath(machineId),
|
||||
parameters: parameterFilePath(machineId),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user