ima2-gen 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/nodeStore.js +13 -12
- package/lib/storageMigration.js +63 -13
- package/package.json +1 -1
- package/routes/nodes.js +10 -4
- package/routes/sessions.js +1 -7
package/lib/nodeStore.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeFile, readFile, access } from "fs/promises";
|
|
1
|
+
import { writeFile, readFile, access, mkdir } from "fs/promises";
|
|
2
2
|
import { join, resolve, sep } from "path";
|
|
3
3
|
import { randomBytes } from "crypto";
|
|
4
4
|
import { config } from "../config.js";
|
|
@@ -7,16 +7,17 @@ export function newNodeId() {
|
|
|
7
7
|
return "n_" + randomBytes(config.ids.nodeHexBytes).toString("hex");
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export async function saveNode(rootDir, { nodeId, b64, meta, ext = "png" }) {
|
|
10
|
+
export async function saveNode(rootDir, { nodeId, b64, meta, ext = "png", generatedDir = config.storage.generatedDir }) {
|
|
11
11
|
void rootDir;
|
|
12
12
|
const filename = `${nodeId}.${ext}`;
|
|
13
|
-
await
|
|
14
|
-
await writeFile(join(
|
|
13
|
+
await mkdir(generatedDir, { recursive: true });
|
|
14
|
+
await writeFile(join(generatedDir, filename), Buffer.from(b64, "base64"));
|
|
15
|
+
await writeFile(join(generatedDir, filename + ".json"), JSON.stringify(meta, null, 2));
|
|
15
16
|
return { filename };
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
export async function loadNodeB64(rootDir, filename) {
|
|
19
|
-
const p = resolveGeneratedPath(rootDir, filename);
|
|
19
|
+
export async function loadNodeB64(rootDir, filename, generatedDir = config.storage.generatedDir) {
|
|
20
|
+
const p = resolveGeneratedPath(rootDir, filename, generatedDir);
|
|
20
21
|
try { await access(p); } catch {
|
|
21
22
|
const err = new Error(`Node file not found: ${filename}`);
|
|
22
23
|
err.code = "NODE_NOT_FOUND";
|
|
@@ -27,17 +28,17 @@ export async function loadNodeB64(rootDir, filename) {
|
|
|
27
28
|
return buf.toString("base64");
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
export async function loadNodeMeta(rootDir, nodeId, ext = "png") {
|
|
31
|
+
export async function loadNodeMeta(rootDir, nodeId, ext = "png", generatedDir = config.storage.generatedDir) {
|
|
31
32
|
void rootDir;
|
|
32
33
|
try {
|
|
33
|
-
return JSON.parse(await readFile(join(
|
|
34
|
+
return JSON.parse(await readFile(join(generatedDir, `${nodeId}.${ext}.json`), "utf-8"));
|
|
34
35
|
} catch {
|
|
35
36
|
return null;
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
export async function loadAssetB64(rootDir, externalSrc) {
|
|
40
|
-
const p = resolveGeneratedPath(rootDir, externalSrc);
|
|
40
|
+
export async function loadAssetB64(rootDir, externalSrc, generatedDir = config.storage.generatedDir) {
|
|
41
|
+
const p = resolveGeneratedPath(rootDir, externalSrc, generatedDir);
|
|
41
42
|
try { await access(p); } catch {
|
|
42
43
|
const err = new Error(`Asset file not found: ${externalSrc}`);
|
|
43
44
|
err.code = "NODE_NOT_FOUND";
|
|
@@ -48,7 +49,7 @@ export async function loadAssetB64(rootDir, externalSrc) {
|
|
|
48
49
|
return buf.toString("base64");
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
function resolveGeneratedPath(rootDir, relPath) {
|
|
52
|
+
function resolveGeneratedPath(rootDir, relPath, generatedDir = config.storage.generatedDir) {
|
|
52
53
|
void rootDir;
|
|
53
54
|
if (typeof relPath !== "string" || relPath.length === 0) {
|
|
54
55
|
const err = new Error("Asset path is required");
|
|
@@ -56,7 +57,7 @@ function resolveGeneratedPath(rootDir, relPath) {
|
|
|
56
57
|
err.status = 400;
|
|
57
58
|
throw err;
|
|
58
59
|
}
|
|
59
|
-
const baseDir = resolve(
|
|
60
|
+
const baseDir = resolve(generatedDir);
|
|
60
61
|
const target = resolve(baseDir, relPath);
|
|
61
62
|
if (target !== baseDir && !target.startsWith(baseDir + sep)) {
|
|
62
63
|
const err = new Error(`Asset path escapes generated/: ${relPath}`);
|
package/lib/storageMigration.js
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
import { mkdir, readdir, copyFile, stat } from "node:fs/promises";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { join, resolve, sep } from "node:path";
|
|
3
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
|
|
6
|
+
const PACKAGE_NAME = "ima2-gen";
|
|
7
|
+
|
|
8
|
+
function addStats(a, b) {
|
|
9
|
+
return {
|
|
10
|
+
copied: a.copied + b.copied,
|
|
11
|
+
skippedExisting: a.skippedExisting + b.skippedExisting,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
4
14
|
|
|
5
15
|
async function copyMissingTree(srcDir, dstDir) {
|
|
6
16
|
await mkdir(dstDir, { recursive: true });
|
|
7
17
|
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
18
|
+
let stats = { copied: 0, skippedExisting: 0 };
|
|
8
19
|
for (const entry of entries) {
|
|
9
20
|
const src = join(srcDir, entry.name);
|
|
10
21
|
const dst = join(dstDir, entry.name);
|
|
11
22
|
if (entry.isDirectory()) {
|
|
12
|
-
await copyMissingTree(src, dst);
|
|
23
|
+
stats = addStats(stats, await copyMissingTree(src, dst));
|
|
13
24
|
continue;
|
|
14
25
|
}
|
|
15
26
|
if (!entry.isFile()) continue;
|
|
16
|
-
if (existsSync(dst))
|
|
27
|
+
if (existsSync(dst)) {
|
|
28
|
+
stats.skippedExisting += 1;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
17
31
|
await copyFile(src, dst);
|
|
32
|
+
stats.copied += 1;
|
|
18
33
|
}
|
|
34
|
+
return stats;
|
|
19
35
|
}
|
|
20
36
|
|
|
21
37
|
function isSameOrInside(child, parent) {
|
|
@@ -24,18 +40,52 @@ function isSameOrInside(child, parent) {
|
|
|
24
40
|
return a === b || a.startsWith(b + sep);
|
|
25
41
|
}
|
|
26
42
|
|
|
27
|
-
export async function migrateGeneratedStorage(ctx) {
|
|
28
|
-
const legacyDir = join(ctx.rootDir, "generated");
|
|
43
|
+
export async function migrateGeneratedStorage(ctx, options = {}) {
|
|
29
44
|
const targetDir = ctx.config.storage.generatedDir;
|
|
30
|
-
|
|
45
|
+
const candidates = options.legacyDirs || getLegacyGeneratedCandidates(ctx, options.env);
|
|
46
|
+
const result = {
|
|
47
|
+
copied: 0,
|
|
48
|
+
skippedExisting: 0,
|
|
49
|
+
sourcesScanned: 0,
|
|
50
|
+
sourcesSkipped: 0,
|
|
51
|
+
};
|
|
31
52
|
try {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
for (const legacyDir of candidates) {
|
|
54
|
+
if (isSameOrInside(legacyDir, targetDir) || isSameOrInside(targetDir, legacyDir)) {
|
|
55
|
+
result.sourcesSkipped += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const legacyStat = await stat(legacyDir);
|
|
60
|
+
if (!legacyStat.isDirectory()) continue;
|
|
61
|
+
result.sourcesScanned += 1;
|
|
62
|
+
const copyStats = await copyMissingTree(legacyDir, targetDir);
|
|
63
|
+
result.copied += copyStats.copied;
|
|
64
|
+
result.skippedExisting += copyStats.skippedExisting;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (err?.code !== "ENOENT") {
|
|
67
|
+
console.warn("[storage] generated asset migration source skipped:", legacyDir, err.message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
39
70
|
}
|
|
71
|
+
if (result.copied > 0) console.log(`[storage] migrated ${result.copied} generated assets to ${targetDir}`);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.warn("[storage] generated asset migration skipped:", err.message);
|
|
40
74
|
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getLegacyGeneratedCandidates(ctx, env = process.env) {
|
|
79
|
+
const home = homedir();
|
|
80
|
+
const nodePrefix = dirname(dirname(process.execPath));
|
|
81
|
+
const npmPrefix = env.npm_config_prefix || nodePrefix;
|
|
82
|
+
const appData = env.APPDATA || join(home, "AppData", "Roaming");
|
|
83
|
+
return Array.from(new Set([
|
|
84
|
+
join(ctx.rootDir, "generated"),
|
|
85
|
+
join(npmPrefix, "lib", "node_modules", PACKAGE_NAME, "generated"),
|
|
86
|
+
join(npmPrefix, "node_modules", PACKAGE_NAME, "generated"),
|
|
87
|
+
join(appData, "npm", "node_modules", PACKAGE_NAME, "generated"),
|
|
88
|
+
join(home, ".npm-global", "lib", "node_modules", PACKAGE_NAME, "generated"),
|
|
89
|
+
join(home, ".nvm", "versions", "node", process.version, "lib", "node_modules", PACKAGE_NAME, "generated"),
|
|
90
|
+
].map((p) => resolve(p))));
|
|
41
91
|
}
|
package/package.json
CHANGED
package/routes/nodes.js
CHANGED
|
@@ -156,9 +156,9 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
156
156
|
const startTime = Date.now();
|
|
157
157
|
let parentB64 = null;
|
|
158
158
|
if (parentNodeId) {
|
|
159
|
-
parentB64 = await loadNodeB64(ctx.rootDir, `${parentNodeId}.png
|
|
159
|
+
parentB64 = await loadNodeB64(ctx.rootDir, `${parentNodeId}.png`, ctx.config.storage.generatedDir);
|
|
160
160
|
} else if (typeof externalSrc === "string" && externalSrc.length > 0) {
|
|
161
|
-
parentB64 = await loadAssetB64(ctx.rootDir, externalSrc);
|
|
161
|
+
parentB64 = await loadAssetB64(ctx.rootDir, externalSrc, ctx.config.storage.generatedDir);
|
|
162
162
|
}
|
|
163
163
|
logEvent("node", "request", {
|
|
164
164
|
requestId,
|
|
@@ -270,7 +270,13 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
270
270
|
moderation,
|
|
271
271
|
};
|
|
272
272
|
await mkdir(ctx.config.storage.generatedDir, { recursive: true });
|
|
273
|
-
const { filename } = await saveNode(ctx.rootDir, {
|
|
273
|
+
const { filename } = await saveNode(ctx.rootDir, {
|
|
274
|
+
nodeId,
|
|
275
|
+
b64,
|
|
276
|
+
meta,
|
|
277
|
+
ext: format,
|
|
278
|
+
generatedDir: ctx.config.storage.generatedDir,
|
|
279
|
+
});
|
|
274
280
|
finishMeta = { nodeId, filename, imageChars: b64.length };
|
|
275
281
|
finishHttpStatus = 200;
|
|
276
282
|
logEvent("node", "saved", {
|
|
@@ -325,7 +331,7 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
325
331
|
app.get("/api/node/:nodeId", async (req, res) => {
|
|
326
332
|
try {
|
|
327
333
|
const { nodeId } = req.params;
|
|
328
|
-
const meta = await loadNodeMeta(ctx.rootDir, nodeId);
|
|
334
|
+
const meta = await loadNodeMeta(ctx.rootDir, nodeId, "png", ctx.config.storage.generatedDir);
|
|
329
335
|
if (!meta) {
|
|
330
336
|
return res.status(404).json({ error: { code: "NODE_NOT_FOUND", message: "Node metadata missing" } });
|
|
331
337
|
}
|
package/routes/sessions.js
CHANGED
|
@@ -266,13 +266,7 @@ export function registerSessionRoutes(app, ctx) {
|
|
|
266
266
|
const code = err.code || "DB_ERROR";
|
|
267
267
|
const payload = { error: { code, message: err.message } };
|
|
268
268
|
if (typeof err.currentVersion === "number") payload.currentVersion = err.currentVersion;
|
|
269
|
-
if (code
|
|
270
|
-
logEvent("session", "graph_conflict", {
|
|
271
|
-
sessionId: req.params.id,
|
|
272
|
-
code,
|
|
273
|
-
currentVersion: err.currentVersion,
|
|
274
|
-
});
|
|
275
|
-
} else {
|
|
269
|
+
if (code !== "GRAPH_VERSION_CONFLICT") {
|
|
276
270
|
logError("session", "graph_error", err, { sessionId: req.params.id, code });
|
|
277
271
|
}
|
|
278
272
|
res.status(err.status || 500).json(payload);
|