@ultrst-armada/kloks 1.3.0 → 1.4.0
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/package.json +44 -1
- package/packages/backend/dist/chunk-DZ4ZGIZM.js +1323 -0
- package/packages/backend/dist/index.d.ts +1 -1
- package/packages/backend/dist/index.js +399 -1381
- package/packages/backend/dist/workspace/index.d.ts +119 -1
- package/packages/backend/dist/workspace/index.js +11 -3
- package/packages/client/dist/index.d.ts +154 -0
- package/packages/client/dist/index.js +62 -0
- package/packages/client/package.json +41 -0
- package/packages/state-graph/dist/index.js +25 -25
- package/packages/backend/dist/chunk-KTLD7Z4L.js +0 -124
|
@@ -37,10 +37,48 @@ import {
|
|
|
37
37
|
b2UploadFile
|
|
38
38
|
} from "./chunk-4QEMALXF.js";
|
|
39
39
|
import {
|
|
40
|
+
RemoteWorkspaceService,
|
|
40
41
|
SyncEngine,
|
|
42
|
+
WorkingWorkspaceService,
|
|
41
43
|
WorkspaceCloneSyncEngine,
|
|
42
|
-
WorkspaceFS
|
|
43
|
-
|
|
44
|
+
WorkspaceFS,
|
|
45
|
+
b2DeleteFileVersion,
|
|
46
|
+
b2DeleteFolder,
|
|
47
|
+
b2ListBuckets as b2ListBuckets2,
|
|
48
|
+
cleanDbUrl,
|
|
49
|
+
createWorkspace,
|
|
50
|
+
deleteWorkspace,
|
|
51
|
+
deleteWorkspaceFile,
|
|
52
|
+
deleteWorkspaceFolder,
|
|
53
|
+
findUserWorkspace,
|
|
54
|
+
findWorkspaceRecord,
|
|
55
|
+
getB2BucketId,
|
|
56
|
+
getDefaultWorkingWorkspaceDir,
|
|
57
|
+
getUnifiedDbUrl,
|
|
58
|
+
getWorkingWorkspaceConfig,
|
|
59
|
+
getWorkingWorkspaceDir,
|
|
60
|
+
getWorkspaceFolderKey,
|
|
61
|
+
getWorkspaceFolderPrefix,
|
|
62
|
+
getWorkspaceStorageDetails,
|
|
63
|
+
getWorkspaces,
|
|
64
|
+
isB2Workspace,
|
|
65
|
+
listWorkspaceFiles,
|
|
66
|
+
loadWorkspaceData,
|
|
67
|
+
queryUnifiedDb,
|
|
68
|
+
readWorkspaceFile,
|
|
69
|
+
remoteWorkspaceService,
|
|
70
|
+
renameAppInWorkspaceData,
|
|
71
|
+
resolveWorkspaceConnection,
|
|
72
|
+
saveWorkspaceData,
|
|
73
|
+
setWorkingWorkspaceConfig,
|
|
74
|
+
syncWorkspace,
|
|
75
|
+
syncWorkspaceCreateToBucket,
|
|
76
|
+
syncWorkspaceDataToBucket,
|
|
77
|
+
syncWorkspaceDeleteToBucket,
|
|
78
|
+
updateWorkspace,
|
|
79
|
+
workingWorkspaceService,
|
|
80
|
+
writeWorkspaceFile
|
|
81
|
+
} from "./chunk-DZ4ZGIZM.js";
|
|
44
82
|
import {
|
|
45
83
|
AuthServiceDefinition,
|
|
46
84
|
AuthServiceFacade,
|
|
@@ -83,6 +121,11 @@ import {
|
|
|
83
121
|
withAuth,
|
|
84
122
|
withCors
|
|
85
123
|
} from "./chunk-TWCEVES2.js";
|
|
124
|
+
import {
|
|
125
|
+
BackendProvider,
|
|
126
|
+
useBackend,
|
|
127
|
+
useBackendClient
|
|
128
|
+
} from "./chunk-G3ZOSXUR.js";
|
|
86
129
|
import {
|
|
87
130
|
AdminGuard,
|
|
88
131
|
BaseAccountProvider,
|
|
@@ -96,10 +139,13 @@ import {
|
|
|
96
139
|
SessionManager
|
|
97
140
|
} from "./chunk-25Y7AVTP.js";
|
|
98
141
|
import {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
142
|
+
AuditLogger,
|
|
143
|
+
ControlPlaneManager,
|
|
144
|
+
DatabaseClusterManager,
|
|
145
|
+
detectDatabaseEngine,
|
|
146
|
+
getSystemDatabaseUrls,
|
|
147
|
+
resolveSystemDatabaseClusterNodes
|
|
148
|
+
} from "./chunk-RCCQ7IRM.js";
|
|
103
149
|
import {
|
|
104
150
|
BackendRegistry,
|
|
105
151
|
ConcurrencyConflictError,
|
|
@@ -122,14 +168,6 @@ import {
|
|
|
122
168
|
WebSocketTransport,
|
|
123
169
|
createBackendClient
|
|
124
170
|
} from "./chunk-NIP22V2G.js";
|
|
125
|
-
import {
|
|
126
|
-
AuditLogger,
|
|
127
|
-
ControlPlaneManager,
|
|
128
|
-
DatabaseClusterManager,
|
|
129
|
-
detectDatabaseEngine,
|
|
130
|
-
getSystemDatabaseUrls,
|
|
131
|
-
resolveSystemDatabaseClusterNodes
|
|
132
|
-
} from "./chunk-RCCQ7IRM.js";
|
|
133
171
|
import {
|
|
134
172
|
JobDependencyGraph,
|
|
135
173
|
ResultFlowBridge,
|
|
@@ -218,40 +256,6 @@ function validateServiceDefinition(def) {
|
|
|
218
256
|
// src/core/authCore.ts
|
|
219
257
|
import crypto2 from "crypto";
|
|
220
258
|
import bcrypt from "bcryptjs";
|
|
221
|
-
|
|
222
|
-
// src/core/unifiedDb.ts
|
|
223
|
-
import { neon } from "@neondatabase/serverless";
|
|
224
|
-
function cleanDbUrl(url) {
|
|
225
|
-
if (!url || typeof url !== "string") return "";
|
|
226
|
-
return url.trim().replace(/^["']|["']$/g, "");
|
|
227
|
-
}
|
|
228
|
-
function getUnifiedDbUrl(env) {
|
|
229
|
-
const fromEnv = cleanDbUrl(env?.SYSTEM_DATABASE_URL_1) || cleanDbUrl(env?.DATABASE_URL) || cleanDbUrl(env?.SYSTEM_DATABASE_URL_2);
|
|
230
|
-
if (fromEnv) return fromEnv;
|
|
231
|
-
if (typeof process !== "undefined" && process.env) {
|
|
232
|
-
return cleanDbUrl(process.env.SYSTEM_DATABASE_URL_1) || cleanDbUrl(process.env.DATABASE_URL) || cleanDbUrl(process.env.SYSTEM_DATABASE_URL_2) || cleanDbUrl(process.env.SYSTEM_DATABASE_URL_3) || "";
|
|
233
|
-
}
|
|
234
|
-
return "";
|
|
235
|
-
}
|
|
236
|
-
async function queryUnifiedDb(sqlText, params = [], env) {
|
|
237
|
-
const dbUrl = getUnifiedDbUrl(env);
|
|
238
|
-
if (!dbUrl) {
|
|
239
|
-
throw new Error("[UnifiedDB] No database URL configured in environment.");
|
|
240
|
-
}
|
|
241
|
-
const sql = neon(dbUrl);
|
|
242
|
-
try {
|
|
243
|
-
const rows = await (sql.query ? sql.query(sqlText, params) : sql(sqlText, params));
|
|
244
|
-
return {
|
|
245
|
-
rows: Array.isArray(rows) ? rows : [],
|
|
246
|
-
rowCount: Array.isArray(rows) ? rows.length : 0
|
|
247
|
-
};
|
|
248
|
-
} catch (err) {
|
|
249
|
-
console.error("[UnifiedDB] Query execution error:", err.message);
|
|
250
|
-
throw err;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// src/core/authCore.ts
|
|
255
259
|
async function loginUser(email, password, isAdmin = false, env) {
|
|
256
260
|
const normEmail = (email || "").trim().toLowerCase();
|
|
257
261
|
const rawPassword = (password || "").trim();
|
|
@@ -327,473 +331,11 @@ async function getUsers(env) {
|
|
|
327
331
|
}
|
|
328
332
|
}
|
|
329
333
|
|
|
330
|
-
// src/core/b2DirectCore.ts
|
|
331
|
-
var authCache = /* @__PURE__ */ new Map();
|
|
332
|
-
var bucketIdCache = /* @__PURE__ */ new Map();
|
|
333
|
-
async function authorizeB22(keyId, applicationKey) {
|
|
334
|
-
const cacheKey = `${keyId.trim()}:${applicationKey.trim()}`;
|
|
335
|
-
const cached = authCache.get(cacheKey);
|
|
336
|
-
if (cached && Date.now() < cached.expiresAt) return cached.auth;
|
|
337
|
-
const authHeader = typeof Buffer !== "undefined" ? Buffer.from(`${keyId.trim()}:${applicationKey.trim()}`).toString("base64") : btoa(`${keyId.trim()}:${applicationKey.trim()}`);
|
|
338
|
-
let res = await fetch("https://api.backblazeb2.com/b2api/v2/b2_authorize_account", {
|
|
339
|
-
headers: { Authorization: `Basic ${authHeader}` },
|
|
340
|
-
signal: AbortSignal.timeout(1e4)
|
|
341
|
-
});
|
|
342
|
-
if (!res.ok) {
|
|
343
|
-
res = await fetch("https://api.backblazeb2.com/b2api/v4/b2_authorize_account", {
|
|
344
|
-
headers: { Authorization: `Basic ${authHeader}` },
|
|
345
|
-
signal: AbortSignal.timeout(1e4)
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
if (!res.ok) {
|
|
349
|
-
const errText = await res.text();
|
|
350
|
-
throw new Error(`Backblaze B2 authorization failed (${res.status}): ${errText}`);
|
|
351
|
-
}
|
|
352
|
-
const data = await res.json();
|
|
353
|
-
const auth = {
|
|
354
|
-
accountId: data.accountId,
|
|
355
|
-
authorizationToken: data.authorizationToken,
|
|
356
|
-
apiUrl: (data.apiInfo?.storageApi?.apiUrl || data.apiUrl || "https://api.backblazeb2.com").replace(/\/+$/, ""),
|
|
357
|
-
downloadUrl: (data.apiInfo?.storageApi?.downloadUrl || data.downloadUrl || data.apiUrl || "https://f003.backblazeb2.com").replace(/\/+$/, ""),
|
|
358
|
-
s3ApiUrl: data.apiInfo?.storageApi?.s3ApiUrl || data.s3ApiUrl,
|
|
359
|
-
allowed: data.apiInfo?.storageApi?.allowed || data.allowed
|
|
360
|
-
};
|
|
361
|
-
authCache.set(cacheKey, { auth, expiresAt: Date.now() + 20 * 60 * 1e3 });
|
|
362
|
-
return auth;
|
|
363
|
-
}
|
|
364
|
-
async function b2ListBuckets2(keyId, applicationKey, bucketId, bucketName) {
|
|
365
|
-
const auth = await authorizeB22(keyId, applicationKey);
|
|
366
|
-
const rBucketId = bucketId || auth.allowed?.bucketId || auth.allowed?.buckets?.[0]?.bucketId;
|
|
367
|
-
const rBucketName = bucketName || auth.allowed?.bucketName || auth.allowed?.buckets?.[0]?.bucketName;
|
|
368
|
-
if (!auth.allowed?.bucketId && !auth.allowed?.bucketName) {
|
|
369
|
-
try {
|
|
370
|
-
const res = await fetch(`${auth.apiUrl}/b2api/v2/b2_list_buckets`, {
|
|
371
|
-
method: "POST",
|
|
372
|
-
headers: { Authorization: auth.authorizationToken, "Content-Type": "application/json" },
|
|
373
|
-
body: JSON.stringify({ accountId: auth.accountId }),
|
|
374
|
-
signal: AbortSignal.timeout(8e3)
|
|
375
|
-
});
|
|
376
|
-
if (res.ok) {
|
|
377
|
-
const data = await res.json();
|
|
378
|
-
if (Array.isArray(data.buckets) && data.buckets.length > 0) return data.buckets;
|
|
379
|
-
}
|
|
380
|
-
} catch (err) {
|
|
381
|
-
console.warn("[B2DirectCore] list_buckets query failed:", err.message);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
if (rBucketId || rBucketName) {
|
|
385
|
-
const body = { accountId: auth.accountId };
|
|
386
|
-
if (rBucketId) body.bucketId = rBucketId;
|
|
387
|
-
else if (rBucketName) body.bucketName = rBucketName;
|
|
388
|
-
try {
|
|
389
|
-
const res = await fetch(`${auth.apiUrl}/b2api/v2/b2_list_buckets`, {
|
|
390
|
-
method: "POST",
|
|
391
|
-
headers: { Authorization: auth.authorizationToken, "Content-Type": "application/json" },
|
|
392
|
-
body: JSON.stringify(body),
|
|
393
|
-
signal: AbortSignal.timeout(8e3)
|
|
394
|
-
});
|
|
395
|
-
if (res.ok) {
|
|
396
|
-
const data = await res.json();
|
|
397
|
-
if (Array.isArray(data.buckets) && data.buckets.length > 0) return data.buckets;
|
|
398
|
-
}
|
|
399
|
-
} catch {
|
|
400
|
-
}
|
|
401
|
-
return [{
|
|
402
|
-
accountId: auth.accountId,
|
|
403
|
-
bucketId: rBucketId || "restricted",
|
|
404
|
-
bucketName: rBucketName || (bucketName || "default-bucket"),
|
|
405
|
-
bucketType: "allPrivate"
|
|
406
|
-
}];
|
|
407
|
-
}
|
|
408
|
-
return [];
|
|
409
|
-
}
|
|
410
|
-
async function getB2BucketId(keyId, applicationKey, bucketName) {
|
|
411
|
-
const cacheKey = `${keyId.trim()}:${bucketName.trim()}`;
|
|
412
|
-
if (bucketIdCache.has(cacheKey)) return bucketIdCache.get(cacheKey);
|
|
413
|
-
const buckets = await b2ListBuckets2(keyId, applicationKey, void 0, bucketName);
|
|
414
|
-
const match = buckets.find((b) => b.bucketName === bucketName);
|
|
415
|
-
if (!match) throw new Error(`B2 bucket "${bucketName}" not found in account`);
|
|
416
|
-
bucketIdCache.set(cacheKey, match.bucketId);
|
|
417
|
-
return match.bucketId;
|
|
418
|
-
}
|
|
419
|
-
async function b2UploadFile2(keyId, applicationKey, bucketName, fileName, buffer, contentType = "b2/x-auto") {
|
|
420
|
-
const auth = await authorizeB22(keyId, applicationKey);
|
|
421
|
-
const bucketId = await getB2BucketId(keyId, applicationKey, bucketName);
|
|
422
|
-
const urlRes = await fetch(`${auth.apiUrl}/b2api/v2/b2_get_upload_url`, {
|
|
423
|
-
method: "POST",
|
|
424
|
-
headers: { Authorization: auth.authorizationToken, "Content-Type": "application/json" },
|
|
425
|
-
body: JSON.stringify({ bucketId })
|
|
426
|
-
});
|
|
427
|
-
if (!urlRes.ok) throw new Error(`B2 get upload url failed (${urlRes.status})`);
|
|
428
|
-
const { uploadUrl, authorizationToken } = await urlRes.json();
|
|
429
|
-
const encodedFileName = encodeURIComponent(fileName).replace(/%2F/g, "/");
|
|
430
|
-
const uploadRes = await fetch(uploadUrl, {
|
|
431
|
-
method: "POST",
|
|
432
|
-
headers: {
|
|
433
|
-
Authorization: authorizationToken,
|
|
434
|
-
"X-Bz-File-Name": encodedFileName,
|
|
435
|
-
"Content-Type": contentType,
|
|
436
|
-
"Content-Length": String(buffer.byteLength),
|
|
437
|
-
"X-Bz-Content-Sha1": "do_not_verify"
|
|
438
|
-
},
|
|
439
|
-
body: buffer
|
|
440
|
-
});
|
|
441
|
-
if (!uploadRes.ok) throw new Error(`B2 upload failed for ${fileName} (${uploadRes.status}): ${await uploadRes.text()}`);
|
|
442
|
-
return uploadRes.json();
|
|
443
|
-
}
|
|
444
|
-
async function b2DownloadFile2(keyId, applicationKey, bucketName, fileName) {
|
|
445
|
-
const auth = await authorizeB22(keyId, applicationKey);
|
|
446
|
-
const encodedName = encodeURIComponent(fileName).replace(/%2F/g, "/");
|
|
447
|
-
const res = await fetch(`${auth.downloadUrl}/file/${bucketName}/${encodedName}`, { headers: { Authorization: auth.authorizationToken } });
|
|
448
|
-
if (res.status === 404) return null;
|
|
449
|
-
if (!res.ok) throw new Error(`B2 download failed for ${fileName} (${res.status})`);
|
|
450
|
-
return new Uint8Array(await res.arrayBuffer());
|
|
451
|
-
}
|
|
452
|
-
async function b2ListFileNames2(keyId, applicationKey, bucketName, prefix = "") {
|
|
453
|
-
const auth = await authorizeB22(keyId, applicationKey);
|
|
454
|
-
const bucketId = await getB2BucketId(keyId, applicationKey, bucketName);
|
|
455
|
-
const res = await fetch(`${auth.apiUrl}/b2api/v2/b2_list_file_names`, {
|
|
456
|
-
method: "POST",
|
|
457
|
-
headers: { Authorization: auth.authorizationToken, "Content-Type": "application/json" },
|
|
458
|
-
body: JSON.stringify({ bucketId, prefix, maxFileCount: 1e3 })
|
|
459
|
-
});
|
|
460
|
-
if (!res.ok) throw new Error(`B2 list files failed (${res.status})`);
|
|
461
|
-
const data = await res.json();
|
|
462
|
-
return data.files || [];
|
|
463
|
-
}
|
|
464
|
-
async function b2DeleteFileVersion(keyId, applicationKey, fileName, fileId) {
|
|
465
|
-
const auth = await authorizeB22(keyId, applicationKey);
|
|
466
|
-
const res = await fetch(`${auth.apiUrl}/b2api/v2/b2_delete_file_version`, {
|
|
467
|
-
method: "POST",
|
|
468
|
-
headers: { Authorization: auth.authorizationToken, "Content-Type": "application/json" },
|
|
469
|
-
body: JSON.stringify({ fileName, fileId })
|
|
470
|
-
});
|
|
471
|
-
if (!res.ok) throw new Error(`B2 delete file version failed (${res.status})`);
|
|
472
|
-
return res.json();
|
|
473
|
-
}
|
|
474
|
-
async function b2DeleteFolder(keyId, applicationKey, bucketName, folderPrefix) {
|
|
475
|
-
const cleanPrefix = folderPrefix.replace(/\/+$/, "") + "/";
|
|
476
|
-
const files = await b2ListFileNames2(keyId, applicationKey, bucketName, cleanPrefix);
|
|
477
|
-
let deletedCount = 0;
|
|
478
|
-
for (const file of files) {
|
|
479
|
-
await b2DeleteFileVersion(keyId, applicationKey, file.fileName, file.fileId);
|
|
480
|
-
deletedCount++;
|
|
481
|
-
}
|
|
482
|
-
return { success: true, deletedCount };
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// src/core/workspaceSyncEngine.ts
|
|
486
|
-
import fs from "fs";
|
|
487
|
-
import path from "path";
|
|
488
|
-
|
|
489
|
-
// src/core/workspaceCanonical.ts
|
|
490
|
-
function formatCanonicalWorkspaceFolderName2(appName, userName, workspaceName) {
|
|
491
|
-
const safeApp = (appName || "qwnsr").replace(/[^a-zA-Z0-9]/g, "_").toLowerCase();
|
|
492
|
-
const safeUser = (userName || "user").replace(/[^a-zA-Z0-9]/g, "_").toLowerCase();
|
|
493
|
-
const safeWs = (workspaceName || "workspace").replace(/[^a-zA-Z0-9]/g, "_").toLowerCase();
|
|
494
|
-
return `ultrst_${safeApp}_${safeUser}_${safeWs}`;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// src/core/workspaceSyncEngine.ts
|
|
498
|
-
var ACCOUNTS_FILE = path.resolve(".skipo/accounts.json");
|
|
499
|
-
var CONFIG_FILE = path.resolve(".skipo/workspace_config.json");
|
|
500
|
-
function getDefaultWorkingWorkspaceDir() {
|
|
501
|
-
if (process.env.VERCEL || process.env.AWS_LAMBDA_FUNCTION_NAME) {
|
|
502
|
-
return path.join("/tmp", "workspace");
|
|
503
|
-
}
|
|
504
|
-
return path.join(process.cwd(), "workspace");
|
|
505
|
-
}
|
|
506
|
-
function getWorkingWorkspaceConfig() {
|
|
507
|
-
try {
|
|
508
|
-
if (fs.existsSync(CONFIG_FILE)) {
|
|
509
|
-
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
510
|
-
const parsed = JSON.parse(raw);
|
|
511
|
-
if (parsed.workingDirectory) {
|
|
512
|
-
return {
|
|
513
|
-
workingDirectory: parsed.workingDirectory,
|
|
514
|
-
autoSyncIntervalMinutes: parsed.autoSyncIntervalMinutes || 5
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
} catch {
|
|
519
|
-
}
|
|
520
|
-
return {
|
|
521
|
-
workingDirectory: getDefaultWorkingWorkspaceDir(),
|
|
522
|
-
autoSyncIntervalMinutes: 5
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
function setWorkingWorkspaceConfig(cfg) {
|
|
526
|
-
const current = getWorkingWorkspaceConfig();
|
|
527
|
-
const updated = {
|
|
528
|
-
workingDirectory: (cfg.workingDirectory || current.workingDirectory).trim(),
|
|
529
|
-
autoSyncIntervalMinutes: cfg.autoSyncIntervalMinutes || current.autoSyncIntervalMinutes
|
|
530
|
-
};
|
|
531
|
-
try {
|
|
532
|
-
fs.mkdirSync(path.dirname(CONFIG_FILE), { recursive: true });
|
|
533
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2), "utf-8");
|
|
534
|
-
} catch (err) {
|
|
535
|
-
console.warn("[WorkspaceSync] Failed to persist working config:", err);
|
|
536
|
-
}
|
|
537
|
-
return updated;
|
|
538
|
-
}
|
|
539
|
-
function getWorkingWorkspaceDir() {
|
|
540
|
-
const dir = getWorkingWorkspaceConfig().workingDirectory;
|
|
541
|
-
if (!fs.existsSync(dir)) {
|
|
542
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
543
|
-
}
|
|
544
|
-
return dir;
|
|
545
|
-
}
|
|
546
|
-
async function findWorkspaceRecord(workspaceId) {
|
|
547
|
-
try {
|
|
548
|
-
if (fs.existsSync(ACCOUNTS_FILE)) {
|
|
549
|
-
const raw = fs.readFileSync(ACCOUNTS_FILE, "utf-8");
|
|
550
|
-
const accounts = JSON.parse(raw);
|
|
551
|
-
for (const u of Object.values(accounts)) {
|
|
552
|
-
if (Array.isArray(u.workspaces)) {
|
|
553
|
-
const ws = u.workspaces.find((w) => w.id === workspaceId);
|
|
554
|
-
if (ws) {
|
|
555
|
-
const conns = Array.isArray(u.storage_connections) ? u.storage_connections : [];
|
|
556
|
-
const conn = conns.find((c) => c.id === ws.storageConnectionId) || conns[0] || null;
|
|
557
|
-
return { user: u, workspace: ws, connection: conn };
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
} catch {
|
|
563
|
-
}
|
|
564
|
-
try {
|
|
565
|
-
const res = await queryUnifiedDb(`SELECT id, email, username, app_profiles FROM "ultrst-users"`);
|
|
566
|
-
for (const u of res.rows) {
|
|
567
|
-
const profiles = typeof u.app_profiles === "string" ? JSON.parse(u.app_profiles) : u.app_profiles || {};
|
|
568
|
-
const qwnsr = profiles.qwnsr || {};
|
|
569
|
-
const workspaces = Array.isArray(qwnsr.workspaces) ? qwnsr.workspaces : [];
|
|
570
|
-
const ws = workspaces.find((w) => w.id === workspaceId);
|
|
571
|
-
if (ws) {
|
|
572
|
-
const conns = qwnsr.storage_connections || profiles.srkl?.storage_connections || [];
|
|
573
|
-
const conn = conns.find((c) => c.id === ws.storageConnectionId) || conns[0] || null;
|
|
574
|
-
return { user: u, workspace: ws, connection: conn };
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
} catch {
|
|
578
|
-
}
|
|
579
|
-
return null;
|
|
580
|
-
}
|
|
581
|
-
function getWorkspaceFolderKey(workspace, user) {
|
|
582
|
-
if (workspace.localFolderKey) return workspace.localFolderKey;
|
|
583
|
-
if (workspace.localFolderName && workspace.localFolderName.startsWith("ultrst_")) return workspace.localFolderName;
|
|
584
|
-
const userName = user?.username || user?.name || (user?.email ? user.email.split("@")[0] : "user");
|
|
585
|
-
return formatCanonicalWorkspaceFolderName2("qwnsr", userName, workspace.name);
|
|
586
|
-
}
|
|
587
|
-
async function collectLocalFiles(dir, baseDir = dir) {
|
|
588
|
-
if (!fs.existsSync(dir)) return [];
|
|
589
|
-
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
590
|
-
const results = [];
|
|
591
|
-
for (const entry of entries) {
|
|
592
|
-
if (entry.name.startsWith(".") && entry.name !== ".bzEmpty") continue;
|
|
593
|
-
const full = path.join(dir, entry.name);
|
|
594
|
-
if (entry.isDirectory()) {
|
|
595
|
-
const nested = await collectLocalFiles(full, baseDir);
|
|
596
|
-
results.push(...nested);
|
|
597
|
-
} else if (entry.isFile()) {
|
|
598
|
-
const st = await fs.promises.stat(full);
|
|
599
|
-
const rel = path.relative(baseDir, full).replace(/\\/g, "/");
|
|
600
|
-
results.push({ relPath: rel, fullPath: full, mtimeMs: st.mtimeMs, sizeBytes: st.size });
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
return results;
|
|
604
|
-
}
|
|
605
|
-
async function syncWorkspace(workspaceId) {
|
|
606
|
-
const match = await findWorkspaceRecord(workspaceId);
|
|
607
|
-
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
608
|
-
if (!match) {
|
|
609
|
-
return {
|
|
610
|
-
success: false,
|
|
611
|
-
workspaceId,
|
|
612
|
-
folderKey: workspaceId,
|
|
613
|
-
workingDir: getWorkingWorkspaceDir(),
|
|
614
|
-
persistenceType: "local_fs",
|
|
615
|
-
targetLocation: "Unknown",
|
|
616
|
-
syncedFiles: [],
|
|
617
|
-
uploadedCount: 0,
|
|
618
|
-
downloadedCount: 0,
|
|
619
|
-
syncedAt: nowIso,
|
|
620
|
-
error: `Workspace '${workspaceId}' not found.`
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
const { workspace, user, connection } = match;
|
|
624
|
-
const folderKey = getWorkspaceFolderKey(workspace, user);
|
|
625
|
-
const workingDir = getWorkingWorkspaceDir();
|
|
626
|
-
const isB2 = workspace.storageProvider === "backblaze_b2" || workspace.storageConfig?.type === "b2";
|
|
627
|
-
const syncedFiles = [];
|
|
628
|
-
let uploadedCount = 0;
|
|
629
|
-
let downloadedCount = 0;
|
|
630
|
-
if (isB2) {
|
|
631
|
-
const creds = workspace.storageConfig || connection?.credentials || connection || {};
|
|
632
|
-
const keyId = creds.keyId || creds.b2KeyId;
|
|
633
|
-
const appKey = creds.applicationKey || creds.b2ApplicationKey || creds.appKey;
|
|
634
|
-
const bucketName = creds.bucketName || creds.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
635
|
-
if (!keyId || !appKey) {
|
|
636
|
-
return {
|
|
637
|
-
success: false,
|
|
638
|
-
workspaceId,
|
|
639
|
-
folderKey,
|
|
640
|
-
workingDir,
|
|
641
|
-
persistenceType: "backblaze_b2",
|
|
642
|
-
targetLocation: bucketName,
|
|
643
|
-
syncedFiles: [],
|
|
644
|
-
uploadedCount: 0,
|
|
645
|
-
downloadedCount: 0,
|
|
646
|
-
syncedAt: nowIso,
|
|
647
|
-
error: "Missing Backblaze B2 credentials for sync."
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
try {
|
|
651
|
-
const b2Prefix = `${folderKey}/`;
|
|
652
|
-
const remoteFiles = await b2ListFileNames2(keyId, appKey, bucketName, b2Prefix);
|
|
653
|
-
const remoteMap = /* @__PURE__ */ new Map();
|
|
654
|
-
for (const rf of remoteFiles) {
|
|
655
|
-
const cleanName = rf.fileName.slice(b2Prefix.length);
|
|
656
|
-
if (cleanName) remoteMap.set(cleanName, { fileId: rf.fileId, size: rf.contentLength, timestamp: rf.uploadTimestamp });
|
|
657
|
-
}
|
|
658
|
-
const localFiles = await collectLocalFiles(workingDir);
|
|
659
|
-
const localMap = /* @__PURE__ */ new Map();
|
|
660
|
-
for (const lf of localFiles) {
|
|
661
|
-
localMap.set(lf.relPath, lf);
|
|
662
|
-
}
|
|
663
|
-
for (const [relPath, rInfo] of remoteMap.entries()) {
|
|
664
|
-
const lFile = localMap.get(relPath);
|
|
665
|
-
const shouldDownload = !lFile || rInfo.timestamp > lFile.mtimeMs + 2e3;
|
|
666
|
-
if (shouldDownload) {
|
|
667
|
-
const buf = await b2DownloadFile2(keyId, appKey, bucketName, `${b2Prefix}${relPath}`);
|
|
668
|
-
if (buf) {
|
|
669
|
-
const destPath = path.join(workingDir, relPath);
|
|
670
|
-
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
|
|
671
|
-
await fs.promises.writeFile(destPath, buf);
|
|
672
|
-
try {
|
|
673
|
-
const fileTime = new Date(rInfo.timestamp);
|
|
674
|
-
await fs.promises.utimes(destPath, fileTime, fileTime);
|
|
675
|
-
} catch {
|
|
676
|
-
}
|
|
677
|
-
syncedFiles.push({ fileName: relPath, action: "DOWNLOADED", sizeBytes: buf.byteLength });
|
|
678
|
-
downloadedCount++;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
for (const [relPath, lFile] of localMap.entries()) {
|
|
683
|
-
const rInfo = remoteMap.get(relPath);
|
|
684
|
-
const shouldUpload = !rInfo || lFile.mtimeMs > rInfo.timestamp + 2e3;
|
|
685
|
-
if (shouldUpload) {
|
|
686
|
-
const content = await fs.promises.readFile(lFile.fullPath);
|
|
687
|
-
await b2UploadFile2(keyId, appKey, bucketName, `${b2Prefix}${relPath}`, new Uint8Array(content));
|
|
688
|
-
syncedFiles.push({ fileName: relPath, action: "UPLOADED", sizeBytes: lFile.sizeBytes });
|
|
689
|
-
uploadedCount++;
|
|
690
|
-
} else if (!syncedFiles.some((f) => f.fileName === relPath)) {
|
|
691
|
-
syncedFiles.push({ fileName: relPath, action: "UP_TO_DATE", sizeBytes: lFile.sizeBytes });
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
return {
|
|
695
|
-
success: true,
|
|
696
|
-
workspaceId,
|
|
697
|
-
folderKey,
|
|
698
|
-
workingDir,
|
|
699
|
-
persistenceType: "backblaze_b2",
|
|
700
|
-
targetLocation: `B2://${bucketName}/${folderKey}/`,
|
|
701
|
-
syncedFiles,
|
|
702
|
-
uploadedCount,
|
|
703
|
-
downloadedCount,
|
|
704
|
-
syncedAt: nowIso
|
|
705
|
-
};
|
|
706
|
-
} catch (err) {
|
|
707
|
-
return {
|
|
708
|
-
success: false,
|
|
709
|
-
workspaceId,
|
|
710
|
-
folderKey,
|
|
711
|
-
workingDir,
|
|
712
|
-
persistenceType: "backblaze_b2",
|
|
713
|
-
targetLocation: bucketName,
|
|
714
|
-
syncedFiles,
|
|
715
|
-
uploadedCount,
|
|
716
|
-
downloadedCount,
|
|
717
|
-
syncedAt: nowIso,
|
|
718
|
-
error: err.message || "Backblaze sync failed."
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
const vaultDir = path.resolve(process.cwd(), folderKey);
|
|
723
|
-
await fs.promises.mkdir(vaultDir, { recursive: true });
|
|
724
|
-
try {
|
|
725
|
-
const vaultFiles = await collectLocalFiles(vaultDir);
|
|
726
|
-
const vaultMap = new Map(vaultFiles.map((f) => [f.relPath, f]));
|
|
727
|
-
const workingFiles = await collectLocalFiles(workingDir);
|
|
728
|
-
const workingMap = new Map(workingFiles.map((f) => [f.relPath, f]));
|
|
729
|
-
for (const [relPath, vFile] of vaultMap.entries()) {
|
|
730
|
-
const wFile = workingMap.get(relPath);
|
|
731
|
-
const shouldDownload = !wFile || vFile.mtimeMs > wFile.mtimeMs + 1e3;
|
|
732
|
-
if (shouldDownload) {
|
|
733
|
-
const dest = path.join(workingDir, relPath);
|
|
734
|
-
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
735
|
-
await fs.promises.copyFile(vFile.fullPath, dest);
|
|
736
|
-
try {
|
|
737
|
-
const vTime = new Date(vFile.mtimeMs);
|
|
738
|
-
await fs.promises.utimes(dest, vTime, vTime);
|
|
739
|
-
} catch {
|
|
740
|
-
}
|
|
741
|
-
syncedFiles.push({ fileName: relPath, action: "DOWNLOADED", sizeBytes: vFile.sizeBytes });
|
|
742
|
-
downloadedCount++;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
for (const [relPath, wFile] of workingMap.entries()) {
|
|
746
|
-
const vFile = vaultMap.get(relPath);
|
|
747
|
-
const shouldUpload = !vFile || wFile.mtimeMs > vFile.mtimeMs + 1e3;
|
|
748
|
-
if (shouldUpload) {
|
|
749
|
-
const dest = path.join(vaultDir, relPath);
|
|
750
|
-
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
751
|
-
await fs.promises.copyFile(wFile.fullPath, dest);
|
|
752
|
-
try {
|
|
753
|
-
const wTime = new Date(wFile.mtimeMs);
|
|
754
|
-
await fs.promises.utimes(dest, wTime, wTime);
|
|
755
|
-
} catch {
|
|
756
|
-
}
|
|
757
|
-
syncedFiles.push({ fileName: relPath, action: "UPLOADED", sizeBytes: wFile.sizeBytes });
|
|
758
|
-
uploadedCount++;
|
|
759
|
-
} else if (!syncedFiles.some((f) => f.fileName === relPath)) {
|
|
760
|
-
syncedFiles.push({ fileName: relPath, action: "UP_TO_DATE", sizeBytes: wFile.sizeBytes });
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
return {
|
|
764
|
-
success: true,
|
|
765
|
-
workspaceId,
|
|
766
|
-
folderKey,
|
|
767
|
-
workingDir,
|
|
768
|
-
persistenceType: "local_fs",
|
|
769
|
-
targetLocation: vaultDir,
|
|
770
|
-
syncedFiles,
|
|
771
|
-
uploadedCount,
|
|
772
|
-
downloadedCount,
|
|
773
|
-
syncedAt: nowIso
|
|
774
|
-
};
|
|
775
|
-
} catch (err) {
|
|
776
|
-
return {
|
|
777
|
-
success: false,
|
|
778
|
-
workspaceId,
|
|
779
|
-
folderKey,
|
|
780
|
-
workingDir,
|
|
781
|
-
persistenceType: "local_fs",
|
|
782
|
-
targetLocation: vaultDir,
|
|
783
|
-
syncedFiles,
|
|
784
|
-
uploadedCount,
|
|
785
|
-
downloadedCount,
|
|
786
|
-
syncedAt: nowIso,
|
|
787
|
-
error: err.message
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
334
|
// src/core/SrklStorageService.ts
|
|
793
335
|
import { S3Client, ListBucketsCommand, HeadBucketCommand } from "@aws-sdk/client-s3";
|
|
794
|
-
import { promises as
|
|
795
|
-
import
|
|
796
|
-
var
|
|
336
|
+
import { promises as fs } from "fs";
|
|
337
|
+
import path from "path";
|
|
338
|
+
var ACCOUNTS_FILE = path.resolve(".skipo/accounts.json");
|
|
797
339
|
var SrklStorageService = class {
|
|
798
340
|
srklBaseUrl;
|
|
799
341
|
constructor(srklBaseUrl) {
|
|
@@ -906,10 +448,10 @@ var SrklStorageService = class {
|
|
|
906
448
|
}
|
|
907
449
|
async persistWorkspaceConfig(wsId, config) {
|
|
908
450
|
try {
|
|
909
|
-
await
|
|
451
|
+
await fs.mkdir(path.dirname(ACCOUNTS_FILE), { recursive: true });
|
|
910
452
|
let accounts = {};
|
|
911
453
|
try {
|
|
912
|
-
accounts = JSON.parse(await
|
|
454
|
+
accounts = JSON.parse(await fs.readFile(ACCOUNTS_FILE, "utf-8"));
|
|
913
455
|
} catch {
|
|
914
456
|
accounts = {};
|
|
915
457
|
}
|
|
@@ -920,7 +462,7 @@ var SrklStorageService = class {
|
|
|
920
462
|
ws.storageConfig = config;
|
|
921
463
|
ws.storageProvider = "backblaze_b2";
|
|
922
464
|
ws.updatedAt = Date.now();
|
|
923
|
-
await
|
|
465
|
+
await fs.writeFile(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), "utf-8");
|
|
924
466
|
return;
|
|
925
467
|
}
|
|
926
468
|
}
|
|
@@ -945,7 +487,7 @@ var SrklStorageService = class {
|
|
|
945
487
|
});
|
|
946
488
|
}
|
|
947
489
|
try {
|
|
948
|
-
const raw = await
|
|
490
|
+
const raw = await fs.readFile(ACCOUNTS_FILE, "utf-8");
|
|
949
491
|
const accounts = JSON.parse(raw);
|
|
950
492
|
for (const user of Object.values(accounts)) {
|
|
951
493
|
const conns = Array.isArray(user?.storage_connections) ? user.storage_connections : [];
|
|
@@ -1074,10 +616,10 @@ async function getSrklAccounts2(userId, service, env) {
|
|
|
1074
616
|
}
|
|
1075
617
|
}
|
|
1076
618
|
try {
|
|
1077
|
-
const
|
|
1078
|
-
const
|
|
1079
|
-
const accPath =
|
|
1080
|
-
const raw = await
|
|
619
|
+
const fs9 = await import("fs/promises");
|
|
620
|
+
const path9 = await import("path");
|
|
621
|
+
const accPath = path9.resolve(".skipo/accounts.json");
|
|
622
|
+
const raw = await fs9.readFile(accPath, "utf-8");
|
|
1081
623
|
const localUsers = JSON.parse(raw);
|
|
1082
624
|
for (const u of Object.values(localUsers)) {
|
|
1083
625
|
const conns = Array.isArray(u.storage_connections) ? u.storage_connections : [];
|
|
@@ -1184,155 +726,6 @@ async function getSrklStatus(env) {
|
|
|
1184
726
|
}
|
|
1185
727
|
}
|
|
1186
728
|
|
|
1187
|
-
// src/core/workspaceFileCore.ts
|
|
1188
|
-
async function getFs() {
|
|
1189
|
-
if (typeof process !== "undefined" && process.versions?.node) {
|
|
1190
|
-
try {
|
|
1191
|
-
const mod = await import("fs");
|
|
1192
|
-
return mod.promises || mod.default?.promises || mod;
|
|
1193
|
-
} catch {
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
return null;
|
|
1197
|
-
}
|
|
1198
|
-
async function getPath() {
|
|
1199
|
-
if (typeof process !== "undefined" && process.versions?.node) {
|
|
1200
|
-
try {
|
|
1201
|
-
const mod = await import("path");
|
|
1202
|
-
return mod.default || mod;
|
|
1203
|
-
} catch {
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
return null;
|
|
1207
|
-
}
|
|
1208
|
-
function getWorkspaceFolderPrefix(workspace) {
|
|
1209
|
-
return workspace.localFolderKey || workspace.localFolderName || (workspace.workspacePath ? workspace.workspacePath.replace(/^\.\//, "") : workspace.id);
|
|
1210
|
-
}
|
|
1211
|
-
function isB2Workspace(workspace, connection) {
|
|
1212
|
-
return connection?.provider === "backblaze_b2" || connection?.provider === "BACKBLAZE_B2" || workspace?.storageProvider === "backblaze_b2";
|
|
1213
|
-
}
|
|
1214
|
-
async function writeWorkspaceFile(workspace, connection, relativePath, content, contentType = "application/json") {
|
|
1215
|
-
const cleanRel = relativePath.replace(/^\/+/, "");
|
|
1216
|
-
const prefix = getWorkspaceFolderPrefix(workspace);
|
|
1217
|
-
const buffer = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
1218
|
-
if (isB2Workspace(workspace, connection)) {
|
|
1219
|
-
const keyId = connection.b2KeyId || connection.keyId;
|
|
1220
|
-
const appKey = connection.b2ApplicationKey || connection.appKey;
|
|
1221
|
-
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
1222
|
-
const fullKey = `${prefix}/${cleanRel}`;
|
|
1223
|
-
await b2UploadFile2(keyId, appKey, bucketName, fullKey, buffer, contentType);
|
|
1224
|
-
return;
|
|
1225
|
-
}
|
|
1226
|
-
const fs10 = await getFs();
|
|
1227
|
-
const path10 = await getPath();
|
|
1228
|
-
if (!fs10 || !path10) throw new Error("Local filesystem operations not supported in this runtime.");
|
|
1229
|
-
const localDir = path10.resolve(process.cwd(), prefix, path10.dirname(cleanRel));
|
|
1230
|
-
await fs10.mkdir(localDir, { recursive: true });
|
|
1231
|
-
await fs10.writeFile(path10.resolve(process.cwd(), prefix, cleanRel), buffer);
|
|
1232
|
-
}
|
|
1233
|
-
async function readWorkspaceFile(workspace, connection, relativePath) {
|
|
1234
|
-
const cleanRel = relativePath.replace(/^\/+/, "");
|
|
1235
|
-
const prefix = getWorkspaceFolderPrefix(workspace);
|
|
1236
|
-
if (isB2Workspace(workspace, connection)) {
|
|
1237
|
-
const keyId = connection.b2KeyId || connection.keyId;
|
|
1238
|
-
const appKey = connection.b2ApplicationKey || connection.appKey;
|
|
1239
|
-
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
1240
|
-
const fullKey = `${prefix}/${cleanRel}`;
|
|
1241
|
-
return await b2DownloadFile2(keyId, appKey, bucketName, fullKey);
|
|
1242
|
-
}
|
|
1243
|
-
const fs10 = await getFs();
|
|
1244
|
-
const path10 = await getPath();
|
|
1245
|
-
if (!fs10 || !path10) return null;
|
|
1246
|
-
const fullPath = path10.resolve(process.cwd(), prefix, cleanRel);
|
|
1247
|
-
try {
|
|
1248
|
-
const buf = await fs10.readFile(fullPath);
|
|
1249
|
-
return new Uint8Array(buf);
|
|
1250
|
-
} catch {
|
|
1251
|
-
return null;
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
async function deleteWorkspaceFile(workspace, connection, relativePath) {
|
|
1255
|
-
const cleanRel = relativePath.replace(/^\/+/, "");
|
|
1256
|
-
const prefix = getWorkspaceFolderPrefix(workspace);
|
|
1257
|
-
if (isB2Workspace(workspace, connection)) {
|
|
1258
|
-
const keyId = connection.b2KeyId || connection.keyId;
|
|
1259
|
-
const appKey = connection.b2ApplicationKey || connection.appKey;
|
|
1260
|
-
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
1261
|
-
const fullKey = `${prefix}/${cleanRel}`;
|
|
1262
|
-
const files = await b2ListFileNames2(keyId, appKey, bucketName, fullKey);
|
|
1263
|
-
const match = files.find((f) => f.fileName === fullKey);
|
|
1264
|
-
if (match) {
|
|
1265
|
-
await b2DeleteFileVersion(keyId, appKey, match.fileName, match.fileId);
|
|
1266
|
-
return true;
|
|
1267
|
-
}
|
|
1268
|
-
return false;
|
|
1269
|
-
}
|
|
1270
|
-
const fs10 = await getFs();
|
|
1271
|
-
const path10 = await getPath();
|
|
1272
|
-
if (!fs10 || !path10) return false;
|
|
1273
|
-
const fullPath = path10.resolve(process.cwd(), prefix, cleanRel);
|
|
1274
|
-
try {
|
|
1275
|
-
await fs10.unlink(fullPath);
|
|
1276
|
-
return true;
|
|
1277
|
-
} catch {
|
|
1278
|
-
return false;
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
async function listWorkspaceFiles(workspace, connection, subPrefix = "") {
|
|
1282
|
-
const folderPrefix = getWorkspaceFolderPrefix(workspace);
|
|
1283
|
-
const targetPrefix = subPrefix ? `${folderPrefix}/${subPrefix.replace(/^\/+/, "")}` : `${folderPrefix}/`;
|
|
1284
|
-
if (isB2Workspace(workspace, connection)) {
|
|
1285
|
-
const keyId = connection.b2KeyId || connection.keyId;
|
|
1286
|
-
const appKey = connection.b2ApplicationKey || connection.appKey;
|
|
1287
|
-
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
1288
|
-
const rawFiles = await b2ListFileNames2(keyId, appKey, bucketName, targetPrefix);
|
|
1289
|
-
return rawFiles.map((f) => ({
|
|
1290
|
-
fileName: f.fileName.slice(folderPrefix.length + 1),
|
|
1291
|
-
sizeBytes: f.contentLength,
|
|
1292
|
-
uploadTimestamp: f.uploadTimestamp
|
|
1293
|
-
}));
|
|
1294
|
-
}
|
|
1295
|
-
const fs10 = await getFs();
|
|
1296
|
-
const path10 = await getPath();
|
|
1297
|
-
if (!fs10 || !path10) return [];
|
|
1298
|
-
const baseDir = path10.resolve(process.cwd(), folderPrefix, subPrefix);
|
|
1299
|
-
try {
|
|
1300
|
-
const entries = await fs10.readdir(baseDir, { withFileTypes: true, recursive: true });
|
|
1301
|
-
const result = [];
|
|
1302
|
-
for (const entry of entries) {
|
|
1303
|
-
if (entry.isFile()) {
|
|
1304
|
-
const full = path10.join(entry.parentPath || baseDir, entry.name);
|
|
1305
|
-
const st = await fs10.stat(full);
|
|
1306
|
-
result.push({
|
|
1307
|
-
fileName: path10.relative(path10.resolve(process.cwd(), folderPrefix), full),
|
|
1308
|
-
sizeBytes: st.size
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
return result;
|
|
1313
|
-
} catch {
|
|
1314
|
-
return [];
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
async function deleteWorkspaceFolder(workspace, connection) {
|
|
1318
|
-
const prefix = getWorkspaceFolderPrefix(workspace);
|
|
1319
|
-
if (isB2Workspace(workspace, connection)) {
|
|
1320
|
-
const keyId = connection.b2KeyId || connection.keyId;
|
|
1321
|
-
const appKey = connection.b2ApplicationKey || connection.appKey;
|
|
1322
|
-
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
1323
|
-
await b2DeleteFolder(keyId, appKey, bucketName, prefix);
|
|
1324
|
-
return;
|
|
1325
|
-
}
|
|
1326
|
-
const fs10 = await getFs();
|
|
1327
|
-
const path10 = await getPath();
|
|
1328
|
-
if (!fs10 || !path10) return;
|
|
1329
|
-
const localDir = path10.resolve(process.cwd(), prefix);
|
|
1330
|
-
try {
|
|
1331
|
-
await fs10.rm(localDir, { recursive: true, force: true });
|
|
1332
|
-
} catch {
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
729
|
// src/core/credentialVaultCore.ts
|
|
1337
730
|
var MANIFEST_PATH = "credentials/manifest.json";
|
|
1338
731
|
var SECRET_PREFIX = "credentials/secrets";
|
|
@@ -2697,385 +2090,6 @@ async function matchGatewayIngressAsync(urlPath, search, env) {
|
|
|
2697
2090
|
return null;
|
|
2698
2091
|
}
|
|
2699
2092
|
|
|
2700
|
-
// src/core/workspaceBucketSyncCore.ts
|
|
2701
|
-
function resolveWorkspaceConnection(profiles, workspace) {
|
|
2702
|
-
const depm = profiles.depm || {};
|
|
2703
|
-
const conns = depm.storage_connections?.length ? depm.storage_connections : profiles.srkl?.storage_connections || profiles.docustack?.storage_connections || [];
|
|
2704
|
-
return conns.find((c) => c.id === workspace.storageConnectionId) || conns.find((c) => c.role === "primary") || conns[0] || null;
|
|
2705
|
-
}
|
|
2706
|
-
async function syncWorkspaceCreateToBucket(workspace, connection, env) {
|
|
2707
|
-
if (!connection || connection.provider !== "backblaze_b2" && connection.provider !== "BACKBLAZE_B2") return;
|
|
2708
|
-
try {
|
|
2709
|
-
const wsMeta = {
|
|
2710
|
-
id: workspace.id,
|
|
2711
|
-
name: workspace.name,
|
|
2712
|
-
createdAt: workspace.createdAt,
|
|
2713
|
-
updatedAt: workspace.updatedAt,
|
|
2714
|
-
storageProvider: "backblaze_b2",
|
|
2715
|
-
bucket: connection.b2BucketName || "ultrst-apps"
|
|
2716
|
-
};
|
|
2717
|
-
await writeWorkspaceFile(workspace, connection, ".bzEmpty", new Uint8Array(0), "application/x-empty");
|
|
2718
|
-
await writeWorkspaceFile(workspace, connection, "workspace.json", JSON.stringify(wsMeta, null, 2), "application/json");
|
|
2719
|
-
await writeWorkspaceFile(
|
|
2720
|
-
workspace,
|
|
2721
|
-
connection,
|
|
2722
|
-
"ecosystem.json",
|
|
2723
|
-
JSON.stringify({ apps: [], nodes: [], edges: [], ecosystems: [] }, null, 2),
|
|
2724
|
-
"application/json"
|
|
2725
|
-
);
|
|
2726
|
-
} catch (err) {
|
|
2727
|
-
console.warn("[WorkspaceBucketSync] Create sync error:", err.message);
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
async function syncWorkspaceDeleteToBucket(workspace, connection, env) {
|
|
2731
|
-
if (!connection || connection.provider !== "backblaze_b2" && connection.provider !== "BACKBLAZE_B2") return;
|
|
2732
|
-
try {
|
|
2733
|
-
await deleteWorkspaceFolder(workspace, connection);
|
|
2734
|
-
} catch (err) {
|
|
2735
|
-
console.warn("[WorkspaceBucketSync] Delete sync error:", err.message);
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2738
|
-
async function syncWorkspaceDataToBucket(workspace, connection, data, env) {
|
|
2739
|
-
if (!connection || connection.provider !== "backblaze_b2" && connection.provider !== "BACKBLAZE_B2") return;
|
|
2740
|
-
try {
|
|
2741
|
-
await writeWorkspaceFile(workspace, connection, "ecosystem.json", JSON.stringify(data, null, 2), "application/json");
|
|
2742
|
-
} catch (err) {
|
|
2743
|
-
console.warn("[WorkspaceBucketSync] Data sync error:", err.message);
|
|
2744
|
-
}
|
|
2745
|
-
}
|
|
2746
|
-
|
|
2747
|
-
// src/core/workspaceDataCore.ts
|
|
2748
|
-
async function resolveUserRow(userId, env) {
|
|
2749
|
-
if (userId) {
|
|
2750
|
-
const res = await queryUnifiedDb(`SELECT * FROM "ultrst-users" WHERE id = $1 OR email = $1 LIMIT 1`, [userId], env);
|
|
2751
|
-
if (res.rows.length > 0) return res.rows[0];
|
|
2752
|
-
}
|
|
2753
|
-
return null;
|
|
2754
|
-
}
|
|
2755
|
-
async function loadWorkspaceData(userId, workspaceId, env) {
|
|
2756
|
-
if (!workspaceId) return { success: false, error: "workspaceId required", workspace: null };
|
|
2757
|
-
const user = await resolveUserRow(userId, env);
|
|
2758
|
-
if (!user) return { success: false, error: "User not found", workspace: null };
|
|
2759
|
-
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
2760
|
-
const depm = profiles.depm || {};
|
|
2761
|
-
const workspaceDataMap = depm.workspace_data || {};
|
|
2762
|
-
let data = workspaceDataMap[workspaceId];
|
|
2763
|
-
const isEmptyData = !data || (!data.nodes || data.nodes.length === 0) && (!data.apps || data.apps.length === 0);
|
|
2764
|
-
if (isEmptyData) {
|
|
2765
|
-
const ws = (depm.workspaces || []).find((w) => w.id === workspaceId);
|
|
2766
|
-
if (ws) {
|
|
2767
|
-
const conn = resolveWorkspaceConnection(profiles, ws);
|
|
2768
|
-
if (conn) {
|
|
2769
|
-
try {
|
|
2770
|
-
const fileBuf = await readWorkspaceFile(ws, conn, "ecosystem.json");
|
|
2771
|
-
if (fileBuf && fileBuf.length > 0) {
|
|
2772
|
-
const parsed = JSON.parse(new TextDecoder().decode(fileBuf));
|
|
2773
|
-
if (parsed.nodes && parsed.nodes.length > 0 || parsed.apps && parsed.apps.length > 0) {
|
|
2774
|
-
if (!profiles.depm) profiles.depm = {};
|
|
2775
|
-
if (!profiles.depm.workspace_data) profiles.depm.workspace_data = {};
|
|
2776
|
-
profiles.depm.workspace_data[workspaceId] = parsed;
|
|
2777
|
-
data = parsed;
|
|
2778
|
-
await queryUnifiedDb(
|
|
2779
|
-
`UPDATE "ultrst-users" SET app_profiles = $1, updated_at = NOW() WHERE id = $2`,
|
|
2780
|
-
[JSON.stringify(profiles), user.id],
|
|
2781
|
-
env
|
|
2782
|
-
);
|
|
2783
|
-
return { success: true, workspace: data, workspaceId };
|
|
2784
|
-
}
|
|
2785
|
-
}
|
|
2786
|
-
} catch (err) {
|
|
2787
|
-
console.warn("[WorkspaceDataCore] Storage recovery check:", err.message);
|
|
2788
|
-
}
|
|
2789
|
-
}
|
|
2790
|
-
}
|
|
2791
|
-
}
|
|
2792
|
-
if (!data) {
|
|
2793
|
-
const ws = (depm.workspaces || []).find((w) => w.id === workspaceId);
|
|
2794
|
-
const ecoName = ws?.ecosystemName || `${ws?.name || "Workspace"} Ecosystem`;
|
|
2795
|
-
const fallback = {
|
|
2796
|
-
apps: [],
|
|
2797
|
-
accounts: [],
|
|
2798
|
-
deployments: [],
|
|
2799
|
-
nodes: [],
|
|
2800
|
-
edges: [],
|
|
2801
|
-
ecosystems: [
|
|
2802
|
-
{
|
|
2803
|
-
id: `eco_${workspaceId}`,
|
|
2804
|
-
name: ecoName,
|
|
2805
|
-
icon: "Globe",
|
|
2806
|
-
health: "HEALTHY",
|
|
2807
|
-
status: "SYSTEM ONLINE",
|
|
2808
|
-
connectedAppsCount: 0,
|
|
2809
|
-
meshLinkCount: 0,
|
|
2810
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2811
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2812
|
-
}
|
|
2813
|
-
],
|
|
2814
|
-
ecosystemName: ecoName,
|
|
2815
|
-
ecosystemNodes: [
|
|
2816
|
-
{
|
|
2817
|
-
id: `node_eco_root_${workspaceId}`,
|
|
2818
|
-
type: "ECOSYSTEM",
|
|
2819
|
-
name: ecoName,
|
|
2820
|
-
x: 0,
|
|
2821
|
-
y: 0,
|
|
2822
|
-
appId: "__ecosystem__",
|
|
2823
|
-
icon: "Globe",
|
|
2824
|
-
ecosystemData: { connectedAppsCount: 0, health: "HEALTHY", status: "SYSTEM ONLINE" }
|
|
2825
|
-
}
|
|
2826
|
-
],
|
|
2827
|
-
ecosystemEdges: []
|
|
2828
|
-
};
|
|
2829
|
-
return { success: true, workspace: fallback, workspaceId };
|
|
2830
|
-
}
|
|
2831
|
-
return { success: true, workspace: data, workspaceId };
|
|
2832
|
-
}
|
|
2833
|
-
async function saveWorkspaceData(userId, workspaceId, data, env) {
|
|
2834
|
-
if (!workspaceId) return { success: false, error: "workspaceId required" };
|
|
2835
|
-
const user = await resolveUserRow(userId, env);
|
|
2836
|
-
if (!user) return { success: false, error: "User not found" };
|
|
2837
|
-
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
2838
|
-
if (!profiles.depm) profiles.depm = {};
|
|
2839
|
-
if (!profiles.depm.workspace_data) profiles.depm.workspace_data = {};
|
|
2840
|
-
const existing = profiles.depm.workspace_data[workspaceId];
|
|
2841
|
-
const incomingEmpty = (!data.nodes || data.nodes.length === 0) && (!data.apps || data.apps.length === 0);
|
|
2842
|
-
const existingHasData = existing && (existing.nodes && existing.nodes.length > 0 || existing.apps && existing.apps.length > 0);
|
|
2843
|
-
if (incomingEmpty && existingHasData && !data._forceEmptySave) {
|
|
2844
|
-
console.warn(`[WorkspaceDataCore] Blocked empty overwrite for workspace ${workspaceId}.`);
|
|
2845
|
-
return { success: true, workspaceId, workspace: existing, protected: true };
|
|
2846
|
-
}
|
|
2847
|
-
profiles.depm.workspace_data[workspaceId] = data;
|
|
2848
|
-
if (Array.isArray(profiles.depm.workspaces)) {
|
|
2849
|
-
const ws2 = profiles.depm.workspaces.find((w) => w.id === workspaceId);
|
|
2850
|
-
if (ws2) {
|
|
2851
|
-
ws2.appsCount = Array.isArray(data.apps) ? data.apps.length : 0;
|
|
2852
|
-
ws2.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2853
|
-
}
|
|
2854
|
-
}
|
|
2855
|
-
await queryUnifiedDb(
|
|
2856
|
-
`UPDATE "ultrst-users" SET app_profiles = $1, updated_at = NOW() WHERE id = $2`,
|
|
2857
|
-
[JSON.stringify(profiles), user.id],
|
|
2858
|
-
env
|
|
2859
|
-
);
|
|
2860
|
-
const ws = Array.isArray(profiles.depm?.workspaces) ? profiles.depm.workspaces.find((w) => w.id === workspaceId) : null;
|
|
2861
|
-
if (ws) {
|
|
2862
|
-
const conn = resolveWorkspaceConnection(profiles, ws);
|
|
2863
|
-
await syncWorkspaceDataToBucket(ws, conn, data, env);
|
|
2864
|
-
}
|
|
2865
|
-
return { success: true, workspaceId, workspace: data };
|
|
2866
|
-
}
|
|
2867
|
-
async function renameAppInWorkspaceData(userId, workspaceId, appId, newName, env) {
|
|
2868
|
-
if (!workspaceId || !appId || !newName) {
|
|
2869
|
-
return { success: false, error: "workspaceId, appId and newName required" };
|
|
2870
|
-
}
|
|
2871
|
-
const loadRes = await loadWorkspaceData(userId, workspaceId, env);
|
|
2872
|
-
if (!loadRes.success || !loadRes.workspace) return loadRes;
|
|
2873
|
-
const wsData = loadRes.workspace;
|
|
2874
|
-
const trimmed = newName.trim();
|
|
2875
|
-
const folderName = trimmed.replace(/[/\\?%*:|"<>]/g, "_");
|
|
2876
|
-
if (Array.isArray(wsData.apps)) {
|
|
2877
|
-
const targetApp = wsData.apps.find((a) => a.id === appId);
|
|
2878
|
-
if (targetApp) {
|
|
2879
|
-
targetApp.name = trimmed;
|
|
2880
|
-
targetApp.folderName = folderName;
|
|
2881
|
-
targetApp.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2882
|
-
}
|
|
2883
|
-
}
|
|
2884
|
-
if (Array.isArray(wsData.nodes)) {
|
|
2885
|
-
wsData.nodes = wsData.nodes.map((n) => {
|
|
2886
|
-
if (n.appId === appId && (n.type === "APP" || n.type === "APP_NODE" || n.type === "APP_GRAPH_NODE")) {
|
|
2887
|
-
return { ...n, name: trimmed, appData: n.appData ? { ...n.appData, name: trimmed, folderName } : void 0 };
|
|
2888
|
-
}
|
|
2889
|
-
return n;
|
|
2890
|
-
});
|
|
2891
|
-
}
|
|
2892
|
-
return await saveWorkspaceData(userId, workspaceId, wsData, env);
|
|
2893
|
-
}
|
|
2894
|
-
|
|
2895
|
-
// src/core/workspaceStorageDetailsCore.ts
|
|
2896
|
-
async function getWorkspaceStorageDetails(workspaceId, env) {
|
|
2897
|
-
if (!workspaceId) return { success: false, error: "workspaceId is required" };
|
|
2898
|
-
const { workspace, connection } = await findUserWorkspace(workspaceId, env);
|
|
2899
|
-
const folderPrefix = workspace?.localFolderKey || workspace?.localFolderName || (workspace?.workspacePath ? workspace.workspacePath.replace(/^\.\//, "") : workspaceId);
|
|
2900
|
-
if (!connection || connection.provider !== "backblaze_b2" && connection.provider !== "BACKBLAZE_B2") {
|
|
2901
|
-
return {
|
|
2902
|
-
success: true,
|
|
2903
|
-
workspaceId,
|
|
2904
|
-
storageProvider: workspace?.storageProvider || "local_fs",
|
|
2905
|
-
connection: connection || null,
|
|
2906
|
-
folderPrefix,
|
|
2907
|
-
localStats: { exists: false, appsCount: 0, ecosystemsCount: 0 },
|
|
2908
|
-
bucket: null,
|
|
2909
|
-
files: []
|
|
2910
|
-
};
|
|
2911
|
-
}
|
|
2912
|
-
const keyId = connection.b2KeyId || connection.keyId;
|
|
2913
|
-
const applicationKey = connection.b2ApplicationKey || connection.appKey;
|
|
2914
|
-
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
2915
|
-
try {
|
|
2916
|
-
const buckets = await b2ListBuckets2(keyId, applicationKey, void 0, bucketName);
|
|
2917
|
-
const bucketObj = buckets.find((b) => b.bucketName === bucketName);
|
|
2918
|
-
if (!bucketObj) {
|
|
2919
|
-
return {
|
|
2920
|
-
success: true,
|
|
2921
|
-
workspaceId,
|
|
2922
|
-
storageProvider: "backblaze_b2",
|
|
2923
|
-
connection: { id: connection.id, label: connection.label, provider: connection.provider },
|
|
2924
|
-
folderPrefix,
|
|
2925
|
-
bucket: { name: bucketName, status: "BUCKET_NOT_FOUND" },
|
|
2926
|
-
files: []
|
|
2927
|
-
};
|
|
2928
|
-
}
|
|
2929
|
-
const rawFiles = await b2ListFileNames2(keyId, applicationKey, bucketName, `${folderPrefix}/`);
|
|
2930
|
-
return {
|
|
2931
|
-
success: true,
|
|
2932
|
-
workspaceId,
|
|
2933
|
-
storageProvider: "backblaze_b2",
|
|
2934
|
-
connection: { id: connection.id, label: connection.label, provider: connection.provider },
|
|
2935
|
-
folderPrefix,
|
|
2936
|
-
bucket: {
|
|
2937
|
-
id: bucketObj.bucketId,
|
|
2938
|
-
name: bucketObj.bucketName,
|
|
2939
|
-
type: bucketObj.bucketType,
|
|
2940
|
-
endpoint: connection.b2Endpoint || "s3.eu-central-003.backblazeb2.com",
|
|
2941
|
-
region: connection.b2Region || "eu-central-003",
|
|
2942
|
-
status: "CONNECTED"
|
|
2943
|
-
},
|
|
2944
|
-
fileCount: rawFiles.length,
|
|
2945
|
-
files: rawFiles.map((f) => ({
|
|
2946
|
-
fileName: f.fileName,
|
|
2947
|
-
sizeBytes: f.contentLength,
|
|
2948
|
-
uploadTimestamp: f.uploadTimestamp
|
|
2949
|
-
}))
|
|
2950
|
-
};
|
|
2951
|
-
} catch (err) {
|
|
2952
|
-
return {
|
|
2953
|
-
success: false,
|
|
2954
|
-
workspaceId,
|
|
2955
|
-
storageProvider: "backblaze_b2",
|
|
2956
|
-
error: err.message,
|
|
2957
|
-
folderPrefix
|
|
2958
|
-
};
|
|
2959
|
-
}
|
|
2960
|
-
}
|
|
2961
|
-
|
|
2962
|
-
// src/core/workspaceCore.ts
|
|
2963
|
-
async function findUserWorkspace(workspaceId, env) {
|
|
2964
|
-
const res = await queryUnifiedDb(`SELECT id, email, username, app_profiles FROM "ultrst-users"`, [], env);
|
|
2965
|
-
for (const user of res.rows) {
|
|
2966
|
-
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
2967
|
-
const depm = profiles.depm || {};
|
|
2968
|
-
const workspaces = Array.isArray(depm.workspaces) ? depm.workspaces : [];
|
|
2969
|
-
const ws = workspaces.find((w) => w.id === workspaceId);
|
|
2970
|
-
if (ws) {
|
|
2971
|
-
const conns = depm.storage_connections || profiles.srkl?.storage_connections || profiles.docustack?.storage_connections || [];
|
|
2972
|
-
const conn = conns.find((c) => c.id === ws.storageConnectionId) || conns.find((c) => c.role === "primary") || conns[0] || null;
|
|
2973
|
-
return { user, workspace: ws, connection: conn };
|
|
2974
|
-
}
|
|
2975
|
-
}
|
|
2976
|
-
return { user: null, workspace: null, connection: null };
|
|
2977
|
-
}
|
|
2978
|
-
async function getWorkspaces(userId, env) {
|
|
2979
|
-
if (!userId) {
|
|
2980
|
-
return { success: true, workspaces: [], activeWorkspaceId: null, storage_connections: [] };
|
|
2981
|
-
}
|
|
2982
|
-
const res = await queryUnifiedDb(`SELECT * FROM "ultrst-users" WHERE id = $1 OR email = $1 LIMIT 1`, [userId], env);
|
|
2983
|
-
const userRow = res.rows[0];
|
|
2984
|
-
if (!userRow) {
|
|
2985
|
-
return { success: true, workspaces: [], activeWorkspaceId: null, storage_connections: [] };
|
|
2986
|
-
}
|
|
2987
|
-
const profiles = typeof userRow.app_profiles === "string" ? JSON.parse(userRow.app_profiles) : userRow.app_profiles || {};
|
|
2988
|
-
const depm = profiles.depm || {};
|
|
2989
|
-
const workspaces = Array.isArray(depm.workspaces) ? depm.workspaces : [];
|
|
2990
|
-
const activeWorkspaceId = depm.activeWorkspaceId || null;
|
|
2991
|
-
const conns = depm.storage_connections?.length ? depm.storage_connections : profiles.srkl?.storage_connections || [];
|
|
2992
|
-
return { success: true, workspaces, activeWorkspaceId, storage_connections: conns };
|
|
2993
|
-
}
|
|
2994
|
-
async function createWorkspace(userId, wsData, env) {
|
|
2995
|
-
const uRes = await queryUnifiedDb(`SELECT * FROM "ultrst-users" WHERE id = $1`, [userId], env);
|
|
2996
|
-
const user = uRes.rows[0];
|
|
2997
|
-
if (!user) throw new Error("User account not found.");
|
|
2998
|
-
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
2999
|
-
if (!profiles.depm) profiles.depm = {};
|
|
3000
|
-
const workspaces = Array.isArray(profiles.depm.workspaces) ? profiles.depm.workspaces : [];
|
|
3001
|
-
const wsName = (wsData.name || "New Workspace").trim();
|
|
3002
|
-
const wsId = wsData.id || `ws-depm-${Date.now()}`;
|
|
3003
|
-
const userName = user.username || user.name || "user";
|
|
3004
|
-
const folderKey = `ultrst_depm_${userName.toLowerCase().replace(/[^a-z0-9]/g, "_")}_${wsName.toLowerCase().replace(/[^a-z0-9]/g, "_")}`;
|
|
3005
|
-
const newWs = {
|
|
3006
|
-
...wsData,
|
|
3007
|
-
id: wsId,
|
|
3008
|
-
name: wsName,
|
|
3009
|
-
localFolderName: folderKey,
|
|
3010
|
-
localFolderKey: folderKey,
|
|
3011
|
-
workspacePath: `./${folderKey}`,
|
|
3012
|
-
storageProvider: wsData.storageProvider || "backblaze_b2",
|
|
3013
|
-
b2BucketName: wsData.b2BucketName || "ultrst-apps",
|
|
3014
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3015
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3016
|
-
};
|
|
3017
|
-
workspaces.push(newWs);
|
|
3018
|
-
profiles.depm.workspaces = workspaces;
|
|
3019
|
-
profiles.depm.activeWorkspaceId = wsId;
|
|
3020
|
-
await queryUnifiedDb(`UPDATE "ultrst-users" SET app_profiles = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(profiles), user.id], env);
|
|
3021
|
-
const conn = resolveWorkspaceConnection(profiles, newWs);
|
|
3022
|
-
await syncWorkspaceCreateToBucket(newWs, conn, env);
|
|
3023
|
-
return { success: true, workspace: newWs, workspaces };
|
|
3024
|
-
}
|
|
3025
|
-
async function updateWorkspace(userId, params, env) {
|
|
3026
|
-
const uRes = await queryUnifiedDb(`SELECT * FROM "ultrst-users" WHERE id = $1`, [userId], env);
|
|
3027
|
-
const user = uRes.rows[0];
|
|
3028
|
-
if (!user) throw new Error("User not found.");
|
|
3029
|
-
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
3030
|
-
if (!profiles.depm) profiles.depm = {};
|
|
3031
|
-
const workspaces = Array.isArray(profiles.depm.workspaces) ? profiles.depm.workspaces : [];
|
|
3032
|
-
const { action, workspaceId, updates } = params;
|
|
3033
|
-
const targetId = workspaceId || updates?.id;
|
|
3034
|
-
const match = workspaces.find((w) => w.id === targetId);
|
|
3035
|
-
if (!match) throw new Error("Workspace not found.");
|
|
3036
|
-
if (action === "switch") {
|
|
3037
|
-
profiles.depm.activeWorkspaceId = targetId;
|
|
3038
|
-
} else {
|
|
3039
|
-
const safeUpdates = { ...updates || params };
|
|
3040
|
-
delete safeUpdates.storageProvider;
|
|
3041
|
-
delete safeUpdates.storageConnectionId;
|
|
3042
|
-
delete safeUpdates.b2BucketName;
|
|
3043
|
-
delete safeUpdates.b2Endpoint;
|
|
3044
|
-
delete safeUpdates.b2Region;
|
|
3045
|
-
delete safeUpdates.localFolderName;
|
|
3046
|
-
delete safeUpdates.localFolderKey;
|
|
3047
|
-
delete safeUpdates.workspacePath;
|
|
3048
|
-
delete safeUpdates.primaryStorage;
|
|
3049
|
-
delete safeUpdates.id;
|
|
3050
|
-
delete safeUpdates.action;
|
|
3051
|
-
delete safeUpdates.workspaceId;
|
|
3052
|
-
Object.assign(match, safeUpdates, { updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3053
|
-
}
|
|
3054
|
-
await queryUnifiedDb(`UPDATE "ultrst-users" SET app_profiles = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(profiles), user.id], env);
|
|
3055
|
-
return { success: true, workspace: match, workspaces };
|
|
3056
|
-
}
|
|
3057
|
-
async function deleteWorkspace(userId, wsId, env) {
|
|
3058
|
-
const uRes = await queryUnifiedDb(`SELECT * FROM "ultrst-users" WHERE id = $1`, [userId], env);
|
|
3059
|
-
const user = uRes.rows[0];
|
|
3060
|
-
if (!user) throw new Error("User not found.");
|
|
3061
|
-
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
3062
|
-
if (!profiles.depm?.workspaces) return { success: true, workspaces: [] };
|
|
3063
|
-
const targetWs = profiles.depm.workspaces.find((w) => w.id === wsId);
|
|
3064
|
-
const conn = targetWs ? resolveWorkspaceConnection(profiles, targetWs) : null;
|
|
3065
|
-
profiles.depm.workspaces = profiles.depm.workspaces.filter((w) => w.id !== wsId);
|
|
3066
|
-
if (profiles.depm.activeWorkspaceId === wsId) {
|
|
3067
|
-
profiles.depm.activeWorkspaceId = profiles.depm.workspaces[0]?.id || null;
|
|
3068
|
-
}
|
|
3069
|
-
if (profiles.depm.workspace_data?.[wsId]) {
|
|
3070
|
-
delete profiles.depm.workspace_data[wsId];
|
|
3071
|
-
}
|
|
3072
|
-
await queryUnifiedDb(`UPDATE "ultrst-users" SET app_profiles = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(profiles), user.id], env);
|
|
3073
|
-
if (targetWs && conn) {
|
|
3074
|
-
await syncWorkspaceDeleteToBucket(targetWs, conn, env);
|
|
3075
|
-
}
|
|
3076
|
-
return { success: true, workspaces: profiles.depm.workspaces };
|
|
3077
|
-
}
|
|
3078
|
-
|
|
3079
2093
|
// src/core/cloudflareIntegrationCore.ts
|
|
3080
2094
|
var cfFetcher = new CloudflareResourceFetcher();
|
|
3081
2095
|
function buildHeaders(token, authKey, authEmail) {
|
|
@@ -3542,8 +2556,8 @@ function createWavBuffer(sampleRate, numChannels, samples) {
|
|
|
3542
2556
|
}
|
|
3543
2557
|
|
|
3544
2558
|
// src/server/sampleGenerator.ts
|
|
3545
|
-
import
|
|
3546
|
-
import
|
|
2559
|
+
import fs2 from "fs";
|
|
2560
|
+
import path2 from "path";
|
|
3547
2561
|
var GLOBAL_SAMPLE_DEFS = [
|
|
3548
2562
|
{
|
|
3549
2563
|
filename: "808_sub_boom.wav",
|
|
@@ -3640,13 +2654,13 @@ var GLOBAL_SAMPLE_DEFS = [
|
|
|
3640
2654
|
}
|
|
3641
2655
|
];
|
|
3642
2656
|
async function ensureGlobalSamples(globalSamplesDir) {
|
|
3643
|
-
if (!
|
|
3644
|
-
|
|
2657
|
+
if (!fs2.existsSync(globalSamplesDir)) {
|
|
2658
|
+
fs2.mkdirSync(globalSamplesDir, { recursive: true });
|
|
3645
2659
|
}
|
|
3646
2660
|
const sampleRate = 44100;
|
|
3647
2661
|
for (const def of GLOBAL_SAMPLE_DEFS) {
|
|
3648
|
-
const filePath =
|
|
3649
|
-
if (!
|
|
2662
|
+
const filePath = path2.join(globalSamplesDir, def.filename);
|
|
2663
|
+
if (!fs2.existsSync(filePath)) {
|
|
3650
2664
|
const numSamples = Math.floor(sampleRate * def.durationSec);
|
|
3651
2665
|
const chL = new Float32Array(numSamples);
|
|
3652
2666
|
const chR = new Float32Array(numSamples);
|
|
@@ -3658,16 +2672,16 @@ async function ensureGlobalSamples(globalSamplesDir) {
|
|
|
3658
2672
|
chR[i] = r;
|
|
3659
2673
|
}
|
|
3660
2674
|
const wavBuf = createWavBuffer(sampleRate, 2, [chL, chR]);
|
|
3661
|
-
await
|
|
2675
|
+
await fs2.promises.writeFile(filePath, wavBuf);
|
|
3662
2676
|
}
|
|
3663
2677
|
}
|
|
3664
2678
|
}
|
|
3665
2679
|
|
|
3666
2680
|
// src/server/services/assetPoolService.ts
|
|
3667
|
-
import
|
|
3668
|
-
import
|
|
3669
|
-
var WORKSPACE_ASSETS_ROOT =
|
|
3670
|
-
var PROJECTS_ROOT =
|
|
2681
|
+
import fs3 from "fs";
|
|
2682
|
+
import path3 from "path";
|
|
2683
|
+
var WORKSPACE_ASSETS_ROOT = path3.join(process.cwd(), "assets");
|
|
2684
|
+
var PROJECTS_ROOT = path3.join(process.cwd(), "projects");
|
|
3671
2685
|
var SUPPORTED_AUDIO_EXTS = [
|
|
3672
2686
|
".wav",
|
|
3673
2687
|
".mp3",
|
|
@@ -3685,27 +2699,27 @@ var SUPPORTED_AUDIO_EXTS = [
|
|
|
3685
2699
|
var SUPPORTED_MIDI_EXTS = [".mid", ".midi", ".json"];
|
|
3686
2700
|
async function ensureAssetPoolDirectories(rootDir) {
|
|
3687
2701
|
const dirs = [
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
2702
|
+
path3.join(rootDir, "audio", "stereo", "default"),
|
|
2703
|
+
path3.join(rootDir, "audio", "stereo", "userdefined"),
|
|
2704
|
+
path3.join(rootDir, "audio", "mono", "default"),
|
|
2705
|
+
path3.join(rootDir, "audio", "mono", "userdefined"),
|
|
2706
|
+
path3.join(rootDir, "midi", "default"),
|
|
2707
|
+
path3.join(rootDir, "midi", "userdefined")
|
|
3694
2708
|
];
|
|
3695
2709
|
for (const dir of dirs) {
|
|
3696
|
-
if (!
|
|
3697
|
-
await
|
|
2710
|
+
if (!fs3.existsSync(dir)) {
|
|
2711
|
+
await fs3.promises.mkdir(dir, { recursive: true });
|
|
3698
2712
|
}
|
|
3699
2713
|
}
|
|
3700
2714
|
}
|
|
3701
2715
|
async function seedDefaultAssets() {
|
|
3702
2716
|
await ensureAssetPoolDirectories(WORKSPACE_ASSETS_ROOT);
|
|
3703
|
-
const stereoDefaultDir =
|
|
3704
|
-
const monoDefaultDir =
|
|
3705
|
-
const midiDefaultDir =
|
|
2717
|
+
const stereoDefaultDir = path3.join(WORKSPACE_ASSETS_ROOT, "audio", "stereo", "default");
|
|
2718
|
+
const monoDefaultDir = path3.join(WORKSPACE_ASSETS_ROOT, "audio", "mono", "default");
|
|
2719
|
+
const midiDefaultDir = path3.join(WORKSPACE_ASSETS_ROOT, "midi", "default");
|
|
3706
2720
|
const sampleRate = 44100;
|
|
3707
|
-
const subBoomPath =
|
|
3708
|
-
if (!
|
|
2721
|
+
const subBoomPath = path3.join(stereoDefaultDir, "808_sub_boom.wav");
|
|
2722
|
+
if (!fs3.existsSync(subBoomPath)) {
|
|
3709
2723
|
const numSamples = Math.floor(sampleRate * 1.8);
|
|
3710
2724
|
const chL = new Float32Array(numSamples);
|
|
3711
2725
|
const chR = new Float32Array(numSamples);
|
|
@@ -3716,10 +2730,10 @@ async function seedDefaultAssets() {
|
|
|
3716
2730
|
chL[i] = s;
|
|
3717
2731
|
chR[i] = s;
|
|
3718
2732
|
}
|
|
3719
|
-
await
|
|
2733
|
+
await fs3.promises.writeFile(subBoomPath, createWavBuffer(sampleRate, 2, [chL, chR]));
|
|
3720
2734
|
}
|
|
3721
|
-
const leadPath =
|
|
3722
|
-
if (!
|
|
2735
|
+
const leadPath = path3.join(stereoDefaultDir, "celestial_lead.wav");
|
|
2736
|
+
if (!fs3.existsSync(leadPath)) {
|
|
3723
2737
|
const numSamples = Math.floor(sampleRate * 2.5);
|
|
3724
2738
|
const chL = new Float32Array(numSamples);
|
|
3725
2739
|
const chR = new Float32Array(numSamples);
|
|
@@ -3733,10 +2747,10 @@ async function seedDefaultAssets() {
|
|
|
3733
2747
|
chL[i] = s1 * env;
|
|
3734
2748
|
chR[i] = (s1 + s2 * 0.5) * env;
|
|
3735
2749
|
}
|
|
3736
|
-
await
|
|
2750
|
+
await fs3.promises.writeFile(leadPath, createWavBuffer(sampleRate, 2, [chL, chR]));
|
|
3737
2751
|
}
|
|
3738
|
-
const padPath =
|
|
3739
|
-
if (!
|
|
2752
|
+
const padPath = path3.join(stereoDefaultDir, "ethereal_pad.wav");
|
|
2753
|
+
if (!fs3.existsSync(padPath)) {
|
|
3740
2754
|
const numSamples = Math.floor(sampleRate * 3);
|
|
3741
2755
|
const chL = new Float32Array(numSamples);
|
|
3742
2756
|
const chR = new Float32Array(numSamples);
|
|
@@ -3755,10 +2769,10 @@ async function seedDefaultAssets() {
|
|
|
3755
2769
|
chL[i] = left * env;
|
|
3756
2770
|
chR[i] = right * env;
|
|
3757
2771
|
}
|
|
3758
|
-
await
|
|
2772
|
+
await fs3.promises.writeFile(padPath, createWavBuffer(sampleRate, 2, [chL, chR]));
|
|
3759
2773
|
}
|
|
3760
|
-
const snarePath =
|
|
3761
|
-
if (!
|
|
2774
|
+
const snarePath = path3.join(monoDefaultDir, "snare_snappy.wav");
|
|
2775
|
+
if (!fs3.existsSync(snarePath)) {
|
|
3762
2776
|
const numSamples = Math.floor(sampleRate * 0.8);
|
|
3763
2777
|
const chM = new Float32Array(numSamples);
|
|
3764
2778
|
for (let i = 0; i < numSamples; i++) {
|
|
@@ -3768,10 +2782,10 @@ async function seedDefaultAssets() {
|
|
|
3768
2782
|
const noise = (Math.random() * 2 - 1) * Math.exp(-p * 4.5) * 0.6;
|
|
3769
2783
|
chM[i] = tone + noise;
|
|
3770
2784
|
}
|
|
3771
|
-
await
|
|
2785
|
+
await fs3.promises.writeFile(snarePath, createWavBuffer(sampleRate, 1, [chM]));
|
|
3772
2786
|
}
|
|
3773
|
-
const hihatPath =
|
|
3774
|
-
if (!
|
|
2787
|
+
const hihatPath = path3.join(monoDefaultDir, "hihat_closed.wav");
|
|
2788
|
+
if (!fs3.existsSync(hihatPath)) {
|
|
3775
2789
|
const numSamples = Math.floor(sampleRate * 0.25);
|
|
3776
2790
|
const chM = new Float32Array(numSamples);
|
|
3777
2791
|
for (let i = 0; i < numSamples; i++) {
|
|
@@ -3779,10 +2793,10 @@ async function seedDefaultAssets() {
|
|
|
3779
2793
|
const noise = (Math.random() * 2 - 1) * Math.exp(-p * 20);
|
|
3780
2794
|
chM[i] = noise * 0.5;
|
|
3781
2795
|
}
|
|
3782
|
-
await
|
|
2796
|
+
await fs3.promises.writeFile(hihatPath, createWavBuffer(sampleRate, 1, [chM]));
|
|
3783
2797
|
}
|
|
3784
|
-
const midiChordPath =
|
|
3785
|
-
if (!
|
|
2798
|
+
const midiChordPath = path3.join(midiDefaultDir, "lydian_celestial_pad.json");
|
|
2799
|
+
if (!fs3.existsSync(midiChordPath)) {
|
|
3786
2800
|
const starterMidiPattern = {
|
|
3787
2801
|
id: "pat-default-lydian",
|
|
3788
2802
|
name: "Lydian Celestial Pad",
|
|
@@ -3795,31 +2809,31 @@ async function seedDefaultAssets() {
|
|
|
3795
2809
|
{ note: 71, name: "B4", velocity: 0.75, time: 0, duration: 3.8 }
|
|
3796
2810
|
]
|
|
3797
2811
|
};
|
|
3798
|
-
await
|
|
2812
|
+
await fs3.promises.writeFile(midiChordPath, JSON.stringify(starterMidiPattern, null, 2), "utf-8");
|
|
3799
2813
|
}
|
|
3800
2814
|
await ensureProjectAssets("default-kingdom");
|
|
3801
2815
|
}
|
|
3802
2816
|
async function ensureProjectAssets(projectId) {
|
|
3803
|
-
const safeId =
|
|
3804
|
-
const projectAssetsDir =
|
|
2817
|
+
const safeId = path3.basename(projectId);
|
|
2818
|
+
const projectAssetsDir = path3.join(PROJECTS_ROOT, safeId, "assets");
|
|
3805
2819
|
await ensureAssetPoolDirectories(projectAssetsDir);
|
|
3806
2820
|
return projectAssetsDir;
|
|
3807
2821
|
}
|
|
3808
2822
|
function getAssetPoolLocationPath(scope, projectId, dataType, channelType, subfolder = "userdefined") {
|
|
3809
|
-
const root = scope === "project" && projectId ?
|
|
3810
|
-
const safeSub =
|
|
2823
|
+
const root = scope === "project" && projectId ? path3.join(PROJECTS_ROOT, path3.basename(projectId), "assets") : WORKSPACE_ASSETS_ROOT;
|
|
2824
|
+
const safeSub = path3.basename(subfolder).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3811
2825
|
if (dataType === "audio") {
|
|
3812
2826
|
const safeChannel = channelType === "mono" ? "mono" : "stereo";
|
|
3813
|
-
return
|
|
2827
|
+
return path3.join(root, "audio", safeChannel, safeSub);
|
|
3814
2828
|
} else {
|
|
3815
|
-
return
|
|
2829
|
+
return path3.join(root, "midi", safeSub);
|
|
3816
2830
|
}
|
|
3817
2831
|
}
|
|
3818
2832
|
function getAssetPoolPublicUrl(scope, projectId, dataType, channelType, subfolder, filename) {
|
|
3819
|
-
const safeFilename =
|
|
3820
|
-
const safeSub =
|
|
2833
|
+
const safeFilename = path3.basename(filename);
|
|
2834
|
+
const safeSub = path3.basename(subfolder);
|
|
3821
2835
|
if (scope === "project" && projectId) {
|
|
3822
|
-
const safeProj =
|
|
2836
|
+
const safeProj = path3.basename(projectId);
|
|
3823
2837
|
if (dataType === "audio") {
|
|
3824
2838
|
const channel = channelType === "mono" ? "mono" : "stereo";
|
|
3825
2839
|
return `/projects/${safeProj}/assets/audio/${channel}/${safeSub}/${safeFilename}`;
|
|
@@ -3841,31 +2855,31 @@ async function scanAssetPool(scope, projectId) {
|
|
|
3841
2855
|
} else {
|
|
3842
2856
|
await ensureAssetPoolDirectories(WORKSPACE_ASSETS_ROOT);
|
|
3843
2857
|
}
|
|
3844
|
-
const rootDir = scope === "project" && projectId ?
|
|
2858
|
+
const rootDir = scope === "project" && projectId ? path3.join(PROJECTS_ROOT, path3.basename(projectId), "assets") : WORKSPACE_ASSETS_ROOT;
|
|
3845
2859
|
const folders = [];
|
|
3846
2860
|
const items = [];
|
|
3847
2861
|
async function scanCategoryFolder(dataType, channelType) {
|
|
3848
|
-
const catDir = dataType === "audio" ?
|
|
3849
|
-
if (!
|
|
3850
|
-
await
|
|
2862
|
+
const catDir = dataType === "audio" ? path3.join(rootDir, "audio", channelType === "mono" ? "mono" : "stereo") : path3.join(rootDir, "midi");
|
|
2863
|
+
if (!fs3.existsSync(catDir)) {
|
|
2864
|
+
await fs3.promises.mkdir(catDir, { recursive: true });
|
|
3851
2865
|
}
|
|
3852
|
-
const subentries = await
|
|
2866
|
+
const subentries = await fs3.promises.readdir(catDir, { withFileTypes: true });
|
|
3853
2867
|
for (const entry of subentries) {
|
|
3854
2868
|
if (!entry.isDirectory()) continue;
|
|
3855
2869
|
const subfolderName = entry.name;
|
|
3856
|
-
const subfolderDir =
|
|
3857
|
-
const files = await
|
|
2870
|
+
const subfolderDir = path3.join(catDir, subfolderName);
|
|
2871
|
+
const files = await fs3.promises.readdir(subfolderDir);
|
|
3858
2872
|
let validFileCount = 0;
|
|
3859
2873
|
for (const file of files) {
|
|
3860
2874
|
if (file.startsWith(".")) continue;
|
|
3861
|
-
const ext =
|
|
2875
|
+
const ext = path3.extname(file).toLowerCase();
|
|
3862
2876
|
const isAudio = SUPPORTED_AUDIO_EXTS.includes(ext);
|
|
3863
2877
|
const isMidi = SUPPORTED_MIDI_EXTS.includes(ext);
|
|
3864
2878
|
if (dataType === "audio" && !isAudio || dataType === "midi" && !isMidi) continue;
|
|
3865
2879
|
validFileCount++;
|
|
3866
|
-
const filePath =
|
|
3867
|
-
const stat = await
|
|
3868
|
-
const cleanName =
|
|
2880
|
+
const filePath = path3.join(subfolderDir, file);
|
|
2881
|
+
const stat = await fs3.promises.stat(filePath);
|
|
2882
|
+
const cleanName = path3.basename(file, ext).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
3869
2883
|
const relPath = dataType === "audio" ? `audio/${channelType === "mono" ? "mono" : "stereo"}/${subfolderName}/${file}` : `midi/${subfolderName}/${file}`;
|
|
3870
2884
|
const url = getAssetPoolPublicUrl(
|
|
3871
2885
|
scope,
|
|
@@ -3906,7 +2920,7 @@ async function scanAssetPool(scope, projectId) {
|
|
|
3906
2920
|
return { folders, items };
|
|
3907
2921
|
}
|
|
3908
2922
|
async function createAssetSubfolder(params) {
|
|
3909
|
-
const safeName =
|
|
2923
|
+
const safeName = path3.basename(params.folderName).trim().replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3910
2924
|
if (!safeName) throw new Error("Invalid subfolder name");
|
|
3911
2925
|
const dirPath = getAssetPoolLocationPath(
|
|
3912
2926
|
params.scope,
|
|
@@ -3915,8 +2929,8 @@ async function createAssetSubfolder(params) {
|
|
|
3915
2929
|
params.channelType,
|
|
3916
2930
|
safeName
|
|
3917
2931
|
);
|
|
3918
|
-
if (!
|
|
3919
|
-
await
|
|
2932
|
+
if (!fs3.existsSync(dirPath)) {
|
|
2933
|
+
await fs3.promises.mkdir(dirPath, { recursive: true });
|
|
3920
2934
|
}
|
|
3921
2935
|
return {
|
|
3922
2936
|
name: safeName,
|
|
@@ -3935,16 +2949,16 @@ async function saveAssetToPool(params) {
|
|
|
3935
2949
|
params.channelType,
|
|
3936
2950
|
subfolder
|
|
3937
2951
|
);
|
|
3938
|
-
if (!
|
|
3939
|
-
await
|
|
2952
|
+
if (!fs3.existsSync(targetDir)) {
|
|
2953
|
+
await fs3.promises.mkdir(targetDir, { recursive: true });
|
|
3940
2954
|
}
|
|
3941
|
-
const safeFilename =
|
|
3942
|
-
const ext =
|
|
3943
|
-
const filePath =
|
|
2955
|
+
const safeFilename = path3.basename(params.filename).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2956
|
+
const ext = path3.extname(safeFilename).toLowerCase() || (params.dataType === "audio" ? ".wav" : ".json");
|
|
2957
|
+
const filePath = path3.join(targetDir, safeFilename);
|
|
3944
2958
|
const buffer = Buffer.from(params.base64Content, "base64");
|
|
3945
|
-
await
|
|
3946
|
-
const stat = await
|
|
3947
|
-
const cleanName =
|
|
2959
|
+
await fs3.promises.writeFile(filePath, buffer);
|
|
2960
|
+
const stat = await fs3.promises.stat(filePath);
|
|
2961
|
+
const cleanName = path3.basename(safeFilename, ext).replace(/[-_]/g, " ");
|
|
3948
2962
|
const relPath = params.dataType === "audio" ? `audio/${params.channelType === "mono" ? "mono" : "stereo"}/${subfolder}/${safeFilename}` : `midi/${subfolder}/${safeFilename}`;
|
|
3949
2963
|
const url = getAssetPoolPublicUrl(
|
|
3950
2964
|
params.scope,
|
|
@@ -3978,19 +2992,19 @@ async function deleteAssetFromPool(params) {
|
|
|
3978
2992
|
params.channelType,
|
|
3979
2993
|
params.subfolder
|
|
3980
2994
|
);
|
|
3981
|
-
const filePath =
|
|
3982
|
-
if (
|
|
3983
|
-
await
|
|
2995
|
+
const filePath = path3.join(targetDir, path3.basename(params.filename));
|
|
2996
|
+
if (fs3.existsSync(filePath)) {
|
|
2997
|
+
await fs3.promises.unlink(filePath);
|
|
3984
2998
|
return true;
|
|
3985
2999
|
}
|
|
3986
3000
|
return false;
|
|
3987
3001
|
}
|
|
3988
3002
|
|
|
3989
3003
|
// src/server/services/graphAssetService.ts
|
|
3990
|
-
import
|
|
3991
|
-
import
|
|
3992
|
-
var WORKSPACE_ROOT =
|
|
3993
|
-
var GRAPHS_ROOT =
|
|
3004
|
+
import fs4 from "fs";
|
|
3005
|
+
import path4 from "path";
|
|
3006
|
+
var WORKSPACE_ROOT = path4.join(process.cwd(), "workspaces", "default");
|
|
3007
|
+
var GRAPHS_ROOT = path4.join(WORKSPACE_ROOT, "graphs");
|
|
3994
3008
|
var SUPPORTED_AUDIO_EXTS2 = [
|
|
3995
3009
|
".wav",
|
|
3996
3010
|
".mp3",
|
|
@@ -4008,26 +3022,26 @@ var SUPPORTED_AUDIO_EXTS2 = [
|
|
|
4008
3022
|
var SUPPORTED_MIDI_EXTS2 = [".mid", ".midi", ".json"];
|
|
4009
3023
|
function ensureGraphAssetDirectories(graphDir) {
|
|
4010
3024
|
const dirs = [
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
3025
|
+
path4.join(graphDir, "assets", "audio", "stereo", "default"),
|
|
3026
|
+
path4.join(graphDir, "assets", "audio", "stereo", "userdefined"),
|
|
3027
|
+
path4.join(graphDir, "assets", "audio", "mono", "default"),
|
|
3028
|
+
path4.join(graphDir, "assets", "audio", "mono", "userdefined"),
|
|
3029
|
+
path4.join(graphDir, "assets", "midi", "default"),
|
|
3030
|
+
path4.join(graphDir, "assets", "midi", "userdefined")
|
|
4017
3031
|
];
|
|
4018
3032
|
for (const dir of dirs) {
|
|
4019
|
-
if (!
|
|
4020
|
-
|
|
3033
|
+
if (!fs4.existsSync(dir)) {
|
|
3034
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
4021
3035
|
}
|
|
4022
3036
|
}
|
|
4023
3037
|
}
|
|
4024
3038
|
function seedStarterGraphAssets(graphDir) {
|
|
4025
|
-
const stereoDefaultDir =
|
|
4026
|
-
const monoDefaultDir =
|
|
4027
|
-
const midiDefaultDir =
|
|
3039
|
+
const stereoDefaultDir = path4.join(graphDir, "assets", "audio", "stereo", "default");
|
|
3040
|
+
const monoDefaultDir = path4.join(graphDir, "assets", "audio", "mono", "default");
|
|
3041
|
+
const midiDefaultDir = path4.join(graphDir, "assets", "midi", "default");
|
|
4028
3042
|
const sampleRate = 44100;
|
|
4029
|
-
const subBoomPath =
|
|
4030
|
-
if (!
|
|
3043
|
+
const subBoomPath = path4.join(stereoDefaultDir, "808_sub_boom.wav");
|
|
3044
|
+
if (!fs4.existsSync(subBoomPath)) {
|
|
4031
3045
|
const numSamples = Math.floor(sampleRate * 1.8);
|
|
4032
3046
|
const chL = new Float32Array(numSamples);
|
|
4033
3047
|
const chR = new Float32Array(numSamples);
|
|
@@ -4038,10 +3052,10 @@ function seedStarterGraphAssets(graphDir) {
|
|
|
4038
3052
|
chL[i] = s;
|
|
4039
3053
|
chR[i] = s;
|
|
4040
3054
|
}
|
|
4041
|
-
|
|
3055
|
+
fs4.writeFileSync(subBoomPath, createWavBuffer(sampleRate, 2, [chL, chR]));
|
|
4042
3056
|
}
|
|
4043
|
-
const leadPath =
|
|
4044
|
-
if (!
|
|
3057
|
+
const leadPath = path4.join(stereoDefaultDir, "celestial_lead.wav");
|
|
3058
|
+
if (!fs4.existsSync(leadPath)) {
|
|
4045
3059
|
const numSamples = Math.floor(sampleRate * 2.5);
|
|
4046
3060
|
const chL = new Float32Array(numSamples);
|
|
4047
3061
|
const chR = new Float32Array(numSamples);
|
|
@@ -4055,10 +3069,10 @@ function seedStarterGraphAssets(graphDir) {
|
|
|
4055
3069
|
chL[i] = s1 * env;
|
|
4056
3070
|
chR[i] = (s1 + s2 * 0.5) * env;
|
|
4057
3071
|
}
|
|
4058
|
-
|
|
3072
|
+
fs4.writeFileSync(leadPath, createWavBuffer(sampleRate, 2, [chL, chR]));
|
|
4059
3073
|
}
|
|
4060
|
-
const padPath =
|
|
4061
|
-
if (!
|
|
3074
|
+
const padPath = path4.join(stereoDefaultDir, "ethereal_pad.wav");
|
|
3075
|
+
if (!fs4.existsSync(padPath)) {
|
|
4062
3076
|
const numSamples = Math.floor(sampleRate * 3);
|
|
4063
3077
|
const chL = new Float32Array(numSamples);
|
|
4064
3078
|
const chR = new Float32Array(numSamples);
|
|
@@ -4077,10 +3091,10 @@ function seedStarterGraphAssets(graphDir) {
|
|
|
4077
3091
|
chL[i] = left * env;
|
|
4078
3092
|
chR[i] = right * env;
|
|
4079
3093
|
}
|
|
4080
|
-
|
|
3094
|
+
fs4.writeFileSync(padPath, createWavBuffer(sampleRate, 2, [chL, chR]));
|
|
4081
3095
|
}
|
|
4082
|
-
const snarePath =
|
|
4083
|
-
if (!
|
|
3096
|
+
const snarePath = path4.join(monoDefaultDir, "snare_snappy.wav");
|
|
3097
|
+
if (!fs4.existsSync(snarePath)) {
|
|
4084
3098
|
const numSamples = Math.floor(sampleRate * 0.8);
|
|
4085
3099
|
const chM = new Float32Array(numSamples);
|
|
4086
3100
|
for (let i = 0; i < numSamples; i++) {
|
|
@@ -4090,10 +3104,10 @@ function seedStarterGraphAssets(graphDir) {
|
|
|
4090
3104
|
const noise = (Math.random() * 2 - 1) * Math.exp(-p * 4.5) * 0.6;
|
|
4091
3105
|
chM[i] = tone + noise;
|
|
4092
3106
|
}
|
|
4093
|
-
|
|
3107
|
+
fs4.writeFileSync(snarePath, createWavBuffer(sampleRate, 1, [chM]));
|
|
4094
3108
|
}
|
|
4095
|
-
const hihatPath =
|
|
4096
|
-
if (!
|
|
3109
|
+
const hihatPath = path4.join(monoDefaultDir, "hihat_closed.wav");
|
|
3110
|
+
if (!fs4.existsSync(hihatPath)) {
|
|
4097
3111
|
const numSamples = Math.floor(sampleRate * 0.25);
|
|
4098
3112
|
const chM = new Float32Array(numSamples);
|
|
4099
3113
|
for (let i = 0; i < numSamples; i++) {
|
|
@@ -4101,10 +3115,10 @@ function seedStarterGraphAssets(graphDir) {
|
|
|
4101
3115
|
const noise = (Math.random() * 2 - 1) * Math.exp(-p * 20);
|
|
4102
3116
|
chM[i] = noise * 0.5;
|
|
4103
3117
|
}
|
|
4104
|
-
|
|
3118
|
+
fs4.writeFileSync(hihatPath, createWavBuffer(sampleRate, 1, [chM]));
|
|
4105
3119
|
}
|
|
4106
|
-
const midiChordPath =
|
|
4107
|
-
if (!
|
|
3120
|
+
const midiChordPath = path4.join(midiDefaultDir, "lydian_celestial_pad.json");
|
|
3121
|
+
if (!fs4.existsSync(midiChordPath)) {
|
|
4108
3122
|
const starterMidiPattern = {
|
|
4109
3123
|
id: "pat-default-lydian",
|
|
4110
3124
|
name: "Lydian Celestial Pad",
|
|
@@ -4117,43 +3131,43 @@ function seedStarterGraphAssets(graphDir) {
|
|
|
4117
3131
|
{ note: 71, name: "B4", velocity: 0.75, time: 0, duration: 3.8 }
|
|
4118
3132
|
]
|
|
4119
3133
|
};
|
|
4120
|
-
|
|
3134
|
+
fs4.writeFileSync(midiChordPath, JSON.stringify(starterMidiPattern, null, 2), "utf-8");
|
|
4121
3135
|
}
|
|
4122
3136
|
}
|
|
4123
3137
|
async function scanGraphAssets(graphId, includeImported = true) {
|
|
4124
|
-
const safeGraphId =
|
|
4125
|
-
const graphDir =
|
|
4126
|
-
if (!
|
|
4127
|
-
await
|
|
3138
|
+
const safeGraphId = path4.basename(graphId);
|
|
3139
|
+
const graphDir = path4.join(GRAPHS_ROOT, safeGraphId);
|
|
3140
|
+
if (!fs4.existsSync(graphDir)) {
|
|
3141
|
+
await fs4.promises.mkdir(graphDir, { recursive: true });
|
|
4128
3142
|
}
|
|
4129
3143
|
ensureGraphAssetDirectories(graphDir);
|
|
4130
3144
|
const folders = [];
|
|
4131
3145
|
const items = [];
|
|
4132
3146
|
const importedItems = [];
|
|
4133
3147
|
async function scanSingleGraph(gId, isImported, ownerName) {
|
|
4134
|
-
const currentGraphDir =
|
|
4135
|
-
const assetsDir =
|
|
4136
|
-
if (!
|
|
3148
|
+
const currentGraphDir = path4.join(GRAPHS_ROOT, path4.basename(gId));
|
|
3149
|
+
const assetsDir = path4.join(currentGraphDir, "assets");
|
|
3150
|
+
if (!fs4.existsSync(assetsDir)) return;
|
|
4137
3151
|
async function scanCategoryFolder(dataType, channelType) {
|
|
4138
|
-
const catDir = dataType === "audio" ?
|
|
4139
|
-
if (!
|
|
4140
|
-
const subentries = await
|
|
3152
|
+
const catDir = dataType === "audio" ? path4.join(assetsDir, "audio", channelType === "mono" ? "mono" : "stereo") : path4.join(assetsDir, "midi");
|
|
3153
|
+
if (!fs4.existsSync(catDir)) return;
|
|
3154
|
+
const subentries = await fs4.promises.readdir(catDir, { withFileTypes: true });
|
|
4141
3155
|
for (const entry of subentries) {
|
|
4142
3156
|
if (!entry.isDirectory()) continue;
|
|
4143
3157
|
const subfolderName = entry.name;
|
|
4144
|
-
const subfolderDir =
|
|
4145
|
-
const files = await
|
|
3158
|
+
const subfolderDir = path4.join(catDir, subfolderName);
|
|
3159
|
+
const files = await fs4.promises.readdir(subfolderDir);
|
|
4146
3160
|
let validFileCount = 0;
|
|
4147
3161
|
for (const file of files) {
|
|
4148
3162
|
if (file.startsWith(".")) continue;
|
|
4149
|
-
const ext =
|
|
3163
|
+
const ext = path4.extname(file).toLowerCase();
|
|
4150
3164
|
const isAudio = SUPPORTED_AUDIO_EXTS2.includes(ext);
|
|
4151
3165
|
const isMidi = SUPPORTED_MIDI_EXTS2.includes(ext);
|
|
4152
3166
|
if (dataType === "audio" && !isAudio || dataType === "midi" && !isMidi) continue;
|
|
4153
3167
|
validFileCount++;
|
|
4154
|
-
const filePath =
|
|
4155
|
-
const stat = await
|
|
4156
|
-
const cleanName =
|
|
3168
|
+
const filePath = path4.join(subfolderDir, file);
|
|
3169
|
+
const stat = await fs4.promises.stat(filePath);
|
|
3170
|
+
const cleanName = path4.basename(file, ext).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
4157
3171
|
const relPath = dataType === "audio" ? `audio/${channelType === "mono" ? "mono" : "stereo"}/${subfolderName}/${file}` : `midi/${subfolderName}/${file}`;
|
|
4158
3172
|
const url = dataType === "audio" ? `/graphs/${gId}/assets/audio/${channelType === "mono" ? "mono" : "stereo"}/${subfolderName}/${file}` : `/graphs/${gId}/assets/midi/${subfolderName}/${file}`;
|
|
4159
3173
|
const item = {
|
|
@@ -4196,18 +3210,18 @@ async function scanGraphAssets(graphId, includeImported = true) {
|
|
|
4196
3210
|
}
|
|
4197
3211
|
await scanSingleGraph(safeGraphId, false);
|
|
4198
3212
|
if (includeImported) {
|
|
4199
|
-
const qmsFile =
|
|
4200
|
-
if (
|
|
3213
|
+
const qmsFile = path4.join(graphDir, "graph.qms");
|
|
3214
|
+
if (fs4.existsSync(qmsFile)) {
|
|
4201
3215
|
try {
|
|
4202
|
-
const raw = await
|
|
3216
|
+
const raw = await fs4.promises.readFile(qmsFile, "utf-8");
|
|
4203
3217
|
const pkg = JSON.parse(raw);
|
|
4204
3218
|
const importedIds = pkg?.metadata?.importedGraphIds || [];
|
|
4205
3219
|
for (const impId of importedIds) {
|
|
4206
|
-
const impQms =
|
|
3220
|
+
const impQms = path4.join(GRAPHS_ROOT, impId, "graph.qms");
|
|
4207
3221
|
let impName = impId;
|
|
4208
|
-
if (
|
|
3222
|
+
if (fs4.existsSync(impQms)) {
|
|
4209
3223
|
try {
|
|
4210
|
-
const impRaw = await
|
|
3224
|
+
const impRaw = await fs4.promises.readFile(impQms, "utf-8");
|
|
4211
3225
|
const impPkg = JSON.parse(impRaw);
|
|
4212
3226
|
impName = impPkg?.metadata?.name || impId;
|
|
4213
3227
|
} catch {
|
|
@@ -4223,14 +3237,14 @@ async function scanGraphAssets(graphId, includeImported = true) {
|
|
|
4223
3237
|
return { folders, items, importedItems };
|
|
4224
3238
|
}
|
|
4225
3239
|
async function createGraphAssetFolder(params) {
|
|
4226
|
-
const safeGraphId =
|
|
4227
|
-
const safeFolderName =
|
|
3240
|
+
const safeGraphId = path4.basename(params.graphId);
|
|
3241
|
+
const safeFolderName = path4.basename(params.folderName).trim().replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
4228
3242
|
if (!safeFolderName) throw new Error("Invalid subfolder name");
|
|
4229
|
-
const graphDir =
|
|
3243
|
+
const graphDir = path4.join(GRAPHS_ROOT, safeGraphId);
|
|
4230
3244
|
ensureGraphAssetDirectories(graphDir);
|
|
4231
|
-
const targetDir = params.dataType === "audio" ?
|
|
4232
|
-
if (!
|
|
4233
|
-
await
|
|
3245
|
+
const targetDir = params.dataType === "audio" ? path4.join(graphDir, "assets", "audio", params.channelType === "mono" ? "mono" : "stereo", safeFolderName) : path4.join(graphDir, "assets", "midi", safeFolderName);
|
|
3246
|
+
if (!fs4.existsSync(targetDir)) {
|
|
3247
|
+
await fs4.promises.mkdir(targetDir, { recursive: true });
|
|
4234
3248
|
}
|
|
4235
3249
|
return {
|
|
4236
3250
|
name: safeFolderName,
|
|
@@ -4242,21 +3256,21 @@ async function createGraphAssetFolder(params) {
|
|
|
4242
3256
|
};
|
|
4243
3257
|
}
|
|
4244
3258
|
async function saveAssetToGraph(params) {
|
|
4245
|
-
const safeGraphId =
|
|
3259
|
+
const safeGraphId = path4.basename(params.graphId);
|
|
4246
3260
|
const subfolder = params.subfolder || "userdefined";
|
|
4247
|
-
const graphDir =
|
|
3261
|
+
const graphDir = path4.join(GRAPHS_ROOT, safeGraphId);
|
|
4248
3262
|
ensureGraphAssetDirectories(graphDir);
|
|
4249
|
-
const targetDir = params.dataType === "audio" ?
|
|
4250
|
-
if (!
|
|
4251
|
-
await
|
|
3263
|
+
const targetDir = params.dataType === "audio" ? path4.join(graphDir, "assets", "audio", params.channelType === "mono" ? "mono" : "stereo", subfolder) : path4.join(graphDir, "assets", "midi", subfolder);
|
|
3264
|
+
if (!fs4.existsSync(targetDir)) {
|
|
3265
|
+
await fs4.promises.mkdir(targetDir, { recursive: true });
|
|
4252
3266
|
}
|
|
4253
|
-
const safeFilename =
|
|
4254
|
-
const ext =
|
|
4255
|
-
const filePath =
|
|
3267
|
+
const safeFilename = path4.basename(params.filename).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3268
|
+
const ext = path4.extname(safeFilename).toLowerCase() || (params.dataType === "audio" ? ".wav" : ".json");
|
|
3269
|
+
const filePath = path4.join(targetDir, safeFilename);
|
|
4256
3270
|
const buffer = Buffer.from(params.base64Content, "base64");
|
|
4257
|
-
await
|
|
4258
|
-
const stat = await
|
|
4259
|
-
const cleanName =
|
|
3271
|
+
await fs4.promises.writeFile(filePath, buffer);
|
|
3272
|
+
const stat = await fs4.promises.stat(filePath);
|
|
3273
|
+
const cleanName = path4.basename(safeFilename, ext).replace(/[-_]/g, " ");
|
|
4260
3274
|
const relPath = params.dataType === "audio" ? `audio/${params.channelType === "mono" ? "mono" : "stereo"}/${subfolder}/${safeFilename}` : `midi/${subfolder}/${safeFilename}`;
|
|
4261
3275
|
const url = params.dataType === "audio" ? `/graphs/${safeGraphId}/assets/audio/${params.channelType === "mono" ? "mono" : "stereo"}/${subfolder}/${safeFilename}` : `/graphs/${safeGraphId}/assets/midi/${subfolder}/${safeFilename}`;
|
|
4262
3276
|
return {
|
|
@@ -4275,32 +3289,32 @@ async function saveAssetToGraph(params) {
|
|
|
4275
3289
|
};
|
|
4276
3290
|
}
|
|
4277
3291
|
async function deleteAssetFromGraph(params) {
|
|
4278
|
-
const safeGraphId =
|
|
4279
|
-
const graphDir =
|
|
4280
|
-
const targetDir = params.dataType === "audio" ?
|
|
4281
|
-
const filePath =
|
|
4282
|
-
if (
|
|
4283
|
-
await
|
|
3292
|
+
const safeGraphId = path4.basename(params.graphId);
|
|
3293
|
+
const graphDir = path4.join(GRAPHS_ROOT, safeGraphId);
|
|
3294
|
+
const targetDir = params.dataType === "audio" ? path4.join(graphDir, "assets", "audio", params.channelType === "mono" ? "mono" : "stereo", params.subfolder) : path4.join(graphDir, "assets", "midi", params.subfolder);
|
|
3295
|
+
const filePath = path4.join(targetDir, path4.basename(params.filename));
|
|
3296
|
+
if (fs4.existsSync(filePath)) {
|
|
3297
|
+
await fs4.promises.unlink(filePath);
|
|
4284
3298
|
return true;
|
|
4285
3299
|
}
|
|
4286
3300
|
return false;
|
|
4287
3301
|
}
|
|
4288
3302
|
|
|
4289
3303
|
// src/server/services/graphPackageService.ts
|
|
4290
|
-
import
|
|
4291
|
-
import
|
|
3304
|
+
import fs5 from "fs";
|
|
3305
|
+
import path5 from "path";
|
|
4292
3306
|
import { validateGraphImport } from "@ultrst-armada/kloks/state-graph";
|
|
4293
|
-
var WORKSPACE_ROOT2 =
|
|
4294
|
-
var GRAPHS_ROOT2 =
|
|
4295
|
-
var LEGACY_PROJECTS_ROOT =
|
|
3307
|
+
var WORKSPACE_ROOT2 = path5.join(process.cwd(), "workspaces", "default");
|
|
3308
|
+
var GRAPHS_ROOT2 = path5.join(WORKSPACE_ROOT2, "graphs");
|
|
3309
|
+
var LEGACY_PROJECTS_ROOT = path5.join(process.cwd(), "projects");
|
|
4296
3310
|
function ensureWorkspaceStructure() {
|
|
4297
|
-
if (!
|
|
4298
|
-
|
|
3311
|
+
if (!fs5.existsSync(GRAPHS_ROOT2)) {
|
|
3312
|
+
fs5.mkdirSync(GRAPHS_ROOT2, { recursive: true });
|
|
4299
3313
|
}
|
|
4300
3314
|
seedStarterGraphsIfEmpty();
|
|
4301
3315
|
}
|
|
4302
3316
|
function seedStarterGraphsIfEmpty() {
|
|
4303
|
-
const existing =
|
|
3317
|
+
const existing = fs5.existsSync(GRAPHS_ROOT2) ? fs5.readdirSync(GRAPHS_ROOT2) : [];
|
|
4304
3318
|
if (existing.length > 0) return;
|
|
4305
3319
|
const introId = "intro-motif-starter";
|
|
4306
3320
|
createInitialGraphPackageSync(
|
|
@@ -4569,9 +3583,9 @@ function seedStarterGraphsIfEmpty() {
|
|
|
4569
3583
|
);
|
|
4570
3584
|
}
|
|
4571
3585
|
function createInitialGraphPackageSync(id, name, description, type, bpm, masterVolume, currentBiome, nodes, connections, importedGraphIds = []) {
|
|
4572
|
-
const graphDir =
|
|
4573
|
-
if (!
|
|
4574
|
-
|
|
3586
|
+
const graphDir = path5.join(GRAPHS_ROOT2, id);
|
|
3587
|
+
if (!fs5.existsSync(graphDir)) {
|
|
3588
|
+
fs5.mkdirSync(graphDir, { recursive: true });
|
|
4575
3589
|
}
|
|
4576
3590
|
ensureGraphAssetDirectories(graphDir);
|
|
4577
3591
|
seedStarterGraphAssets(graphDir);
|
|
@@ -4604,29 +3618,29 @@ function createInitialGraphPackageSync(id, name, description, type, bpm, masterV
|
|
|
4604
3618
|
connections
|
|
4605
3619
|
}
|
|
4606
3620
|
};
|
|
4607
|
-
|
|
3621
|
+
fs5.writeFileSync(path5.join(graphDir, "graph.qms"), JSON.stringify(graphPackage, null, 2), "utf-8");
|
|
4608
3622
|
}
|
|
4609
3623
|
async function listAllGraphPackages() {
|
|
4610
3624
|
ensureWorkspaceStructure();
|
|
4611
|
-
const dirs = await
|
|
3625
|
+
const dirs = await fs5.promises.readdir(GRAPHS_ROOT2);
|
|
4612
3626
|
const packages = [];
|
|
4613
3627
|
for (const dir of dirs) {
|
|
4614
|
-
const graphDir =
|
|
4615
|
-
const stat = await
|
|
3628
|
+
const graphDir = path5.join(GRAPHS_ROOT2, dir);
|
|
3629
|
+
const stat = await fs5.promises.stat(graphDir);
|
|
4616
3630
|
if (!stat.isDirectory()) continue;
|
|
4617
|
-
const qmsFile =
|
|
4618
|
-
const legacyProjectFile =
|
|
3631
|
+
const qmsFile = path5.join(graphDir, "graph.qms");
|
|
3632
|
+
const legacyProjectFile = path5.join(graphDir, "project.json");
|
|
4619
3633
|
let packageData = null;
|
|
4620
|
-
if (
|
|
3634
|
+
if (fs5.existsSync(qmsFile)) {
|
|
4621
3635
|
try {
|
|
4622
|
-
const raw = await
|
|
3636
|
+
const raw = await fs5.promises.readFile(qmsFile, "utf-8");
|
|
4623
3637
|
packageData = JSON.parse(raw);
|
|
4624
3638
|
} catch (err) {
|
|
4625
3639
|
console.warn(`Corrupted graph.qms in ${dir}:`, err);
|
|
4626
3640
|
}
|
|
4627
|
-
} else if (
|
|
3641
|
+
} else if (fs5.existsSync(legacyProjectFile)) {
|
|
4628
3642
|
try {
|
|
4629
|
-
const raw = await
|
|
3643
|
+
const raw = await fs5.promises.readFile(legacyProjectFile, "utf-8");
|
|
4630
3644
|
const legacy = JSON.parse(raw);
|
|
4631
3645
|
packageData = {
|
|
4632
3646
|
format: "qms_graph_package",
|
|
@@ -4656,7 +3670,7 @@ async function listAllGraphPackages() {
|
|
|
4656
3670
|
mapIoConfig: legacy.mapIoConfig
|
|
4657
3671
|
}
|
|
4658
3672
|
};
|
|
4659
|
-
await
|
|
3673
|
+
await fs5.promises.writeFile(qmsFile, JSON.stringify(packageData, null, 2), "utf-8");
|
|
4660
3674
|
} catch (err) {
|
|
4661
3675
|
console.warn(`Failed to auto-migrate legacy project ${dir}:`, err);
|
|
4662
3676
|
}
|
|
@@ -4678,13 +3692,13 @@ async function listAllGraphPackages() {
|
|
|
4678
3692
|
}
|
|
4679
3693
|
async function getGraphPackageById(id) {
|
|
4680
3694
|
ensureWorkspaceStructure();
|
|
4681
|
-
const safeId =
|
|
4682
|
-
const graphDir =
|
|
4683
|
-
const qmsFile =
|
|
4684
|
-
if (!
|
|
4685
|
-
const legacyProject =
|
|
4686
|
-
if (
|
|
4687
|
-
const raw2 = await
|
|
3695
|
+
const safeId = path5.basename(id);
|
|
3696
|
+
const graphDir = path5.join(GRAPHS_ROOT2, safeId);
|
|
3697
|
+
const qmsFile = path5.join(graphDir, "graph.qms");
|
|
3698
|
+
if (!fs5.existsSync(qmsFile)) {
|
|
3699
|
+
const legacyProject = path5.join(LEGACY_PROJECTS_ROOT, safeId, "project.json");
|
|
3700
|
+
if (fs5.existsSync(legacyProject)) {
|
|
3701
|
+
const raw2 = await fs5.promises.readFile(legacyProject, "utf-8");
|
|
4688
3702
|
const legacy = JSON.parse(raw2);
|
|
4689
3703
|
return {
|
|
4690
3704
|
format: "qms_graph_package",
|
|
@@ -4717,7 +3731,7 @@ async function getGraphPackageById(id) {
|
|
|
4717
3731
|
}
|
|
4718
3732
|
return null;
|
|
4719
3733
|
}
|
|
4720
|
-
const raw = await
|
|
3734
|
+
const raw = await fs5.promises.readFile(qmsFile, "utf-8");
|
|
4721
3735
|
return JSON.parse(raw);
|
|
4722
3736
|
}
|
|
4723
3737
|
async function createGraphPackage(params) {
|
|
@@ -4725,8 +3739,8 @@ async function createGraphPackage(params) {
|
|
|
4725
3739
|
const name = params.name.trim();
|
|
4726
3740
|
const safeSlug = name.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-");
|
|
4727
3741
|
const id = `${safeSlug}-${Date.now().toString(36)}`;
|
|
4728
|
-
const graphDir =
|
|
4729
|
-
await
|
|
3742
|
+
const graphDir = path5.join(GRAPHS_ROOT2, id);
|
|
3743
|
+
await fs5.promises.mkdir(graphDir, { recursive: true });
|
|
4730
3744
|
await ensureGraphAssetDirectories(graphDir);
|
|
4731
3745
|
seedStarterGraphAssets(graphDir);
|
|
4732
3746
|
const now = Date.now();
|
|
@@ -4761,8 +3775,8 @@ async function createGraphPackage(params) {
|
|
|
4761
3775
|
connections: params.connections || []
|
|
4762
3776
|
}
|
|
4763
3777
|
};
|
|
4764
|
-
await
|
|
4765
|
-
|
|
3778
|
+
await fs5.promises.writeFile(
|
|
3779
|
+
path5.join(graphDir, "graph.qms"),
|
|
4766
3780
|
JSON.stringify(graphPackage, null, 2),
|
|
4767
3781
|
"utf-8"
|
|
4768
3782
|
);
|
|
@@ -4770,10 +3784,10 @@ async function createGraphPackage(params) {
|
|
|
4770
3784
|
}
|
|
4771
3785
|
async function saveGraphPackage(id, payload) {
|
|
4772
3786
|
ensureWorkspaceStructure();
|
|
4773
|
-
const safeId =
|
|
4774
|
-
const graphDir =
|
|
4775
|
-
if (!
|
|
4776
|
-
await
|
|
3787
|
+
const safeId = path5.basename(id);
|
|
3788
|
+
const graphDir = path5.join(GRAPHS_ROOT2, safeId);
|
|
3789
|
+
if (!fs5.existsSync(graphDir)) {
|
|
3790
|
+
await fs5.promises.mkdir(graphDir, { recursive: true });
|
|
4777
3791
|
await ensureGraphAssetDirectories(graphDir);
|
|
4778
3792
|
}
|
|
4779
3793
|
const existing = await getGraphPackageById(safeId);
|
|
@@ -4808,18 +3822,18 @@ async function saveGraphPackage(id, payload) {
|
|
|
4808
3822
|
mapIoConfig: payload.mapIoConfig || existing?.graph?.mapIoConfig
|
|
4809
3823
|
}
|
|
4810
3824
|
};
|
|
4811
|
-
await
|
|
4812
|
-
|
|
3825
|
+
await fs5.promises.writeFile(
|
|
3826
|
+
path5.join(graphDir, "graph.qms"),
|
|
4813
3827
|
JSON.stringify(graphPackage, null, 2),
|
|
4814
3828
|
"utf-8"
|
|
4815
3829
|
);
|
|
4816
3830
|
return graphPackage;
|
|
4817
3831
|
}
|
|
4818
3832
|
async function deleteGraphPackage(id) {
|
|
4819
|
-
const safeId =
|
|
4820
|
-
const graphDir =
|
|
4821
|
-
if (!
|
|
4822
|
-
await
|
|
3833
|
+
const safeId = path5.basename(id);
|
|
3834
|
+
const graphDir = path5.join(GRAPHS_ROOT2, safeId);
|
|
3835
|
+
if (!fs5.existsSync(graphDir)) return false;
|
|
3836
|
+
await fs5.promises.rm(graphDir, { recursive: true, force: true });
|
|
4823
3837
|
return true;
|
|
4824
3838
|
}
|
|
4825
3839
|
async function importGraphDependency(targetGraphId, candidateImportId) {
|
|
@@ -4861,26 +3875,26 @@ async function removeGraphDependency(targetGraphId, importIdToRemove) {
|
|
|
4861
3875
|
}
|
|
4862
3876
|
|
|
4863
3877
|
// src/server/services/projectService.ts
|
|
4864
|
-
import
|
|
4865
|
-
import
|
|
4866
|
-
var PROJECTS_ROOT2 =
|
|
3878
|
+
import fs6 from "fs";
|
|
3879
|
+
import path6 from "path";
|
|
3880
|
+
var PROJECTS_ROOT2 = path6.join(process.cwd(), "projects");
|
|
4867
3881
|
var customStarterProjectTemplate = null;
|
|
4868
3882
|
function configureStarterProjectTemplate(template) {
|
|
4869
3883
|
customStarterProjectTemplate = template;
|
|
4870
3884
|
}
|
|
4871
3885
|
function ensureProjectsRoot(starterTemplate) {
|
|
4872
|
-
if (!
|
|
4873
|
-
|
|
3886
|
+
if (!fs6.existsSync(PROJECTS_ROOT2)) {
|
|
3887
|
+
fs6.mkdirSync(PROJECTS_ROOT2, { recursive: true });
|
|
4874
3888
|
}
|
|
4875
3889
|
const template = starterTemplate || customStarterProjectTemplate;
|
|
4876
3890
|
if (template && template.id) {
|
|
4877
|
-
const defaultProjDir =
|
|
4878
|
-
if (!
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
3891
|
+
const defaultProjDir = path6.join(PROJECTS_ROOT2, String(template.id));
|
|
3892
|
+
if (!fs6.existsSync(defaultProjDir)) {
|
|
3893
|
+
fs6.mkdirSync(defaultProjDir, { recursive: true });
|
|
3894
|
+
fs6.mkdirSync(path6.join(defaultProjDir, "samples"), { recursive: true });
|
|
3895
|
+
fs6.mkdirSync(path6.join(defaultProjDir, "assets"), { recursive: true });
|
|
3896
|
+
fs6.writeFileSync(
|
|
3897
|
+
path6.join(defaultProjDir, "project.json"),
|
|
4884
3898
|
JSON.stringify({ ...template, createdAt: Date.now(), updatedAt: Date.now() }, null, 2),
|
|
4885
3899
|
"utf-8"
|
|
4886
3900
|
);
|
|
@@ -4889,22 +3903,22 @@ function ensureProjectsRoot(starterTemplate) {
|
|
|
4889
3903
|
}
|
|
4890
3904
|
async function listAllProjects() {
|
|
4891
3905
|
ensureProjectsRoot();
|
|
4892
|
-
const dirs = await
|
|
3906
|
+
const dirs = await fs6.promises.readdir(PROJECTS_ROOT2);
|
|
4893
3907
|
const projects = [];
|
|
4894
3908
|
for (const dir of dirs) {
|
|
4895
|
-
const projDir =
|
|
4896
|
-
const stat = await
|
|
3909
|
+
const projDir = path6.join(PROJECTS_ROOT2, dir);
|
|
3910
|
+
const stat = await fs6.promises.stat(projDir);
|
|
4897
3911
|
if (!stat.isDirectory()) continue;
|
|
4898
|
-
const projectFile =
|
|
4899
|
-
const samplesDir =
|
|
3912
|
+
const projectFile = path6.join(projDir, "project.json");
|
|
3913
|
+
const samplesDir = path6.join(projDir, "samples");
|
|
4900
3914
|
let sampleCount = 0;
|
|
4901
|
-
if (
|
|
4902
|
-
const samples = await
|
|
3915
|
+
if (fs6.existsSync(samplesDir)) {
|
|
3916
|
+
const samples = await fs6.promises.readdir(samplesDir);
|
|
4903
3917
|
sampleCount = samples.filter((f) => !f.startsWith(".")).length;
|
|
4904
3918
|
}
|
|
4905
|
-
if (
|
|
3919
|
+
if (fs6.existsSync(projectFile)) {
|
|
4906
3920
|
try {
|
|
4907
|
-
const raw = await
|
|
3921
|
+
const raw = await fs6.promises.readFile(projectFile, "utf-8");
|
|
4908
3922
|
const data = JSON.parse(raw);
|
|
4909
3923
|
projects.push({
|
|
4910
3924
|
id: data.id || dir,
|
|
@@ -4924,24 +3938,24 @@ async function listAllProjects() {
|
|
|
4924
3938
|
return projects.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
4925
3939
|
}
|
|
4926
3940
|
async function getProjectById(id) {
|
|
4927
|
-
const safeId =
|
|
4928
|
-
const projectFile =
|
|
4929
|
-
if (!
|
|
4930
|
-
const raw = await
|
|
3941
|
+
const safeId = path6.basename(id);
|
|
3942
|
+
const projectFile = path6.join(PROJECTS_ROOT2, safeId, "project.json");
|
|
3943
|
+
if (!fs6.existsSync(projectFile)) return null;
|
|
3944
|
+
const raw = await fs6.promises.readFile(projectFile, "utf-8");
|
|
4931
3945
|
return JSON.parse(raw);
|
|
4932
3946
|
}
|
|
4933
3947
|
async function saveProject(id, payload) {
|
|
4934
|
-
const safeId =
|
|
4935
|
-
const projDir =
|
|
4936
|
-
const projectFile =
|
|
4937
|
-
if (!
|
|
4938
|
-
await
|
|
4939
|
-
await
|
|
4940
|
-
await
|
|
3948
|
+
const safeId = path6.basename(id);
|
|
3949
|
+
const projDir = path6.join(PROJECTS_ROOT2, safeId);
|
|
3950
|
+
const projectFile = path6.join(projDir, "project.json");
|
|
3951
|
+
if (!fs6.existsSync(projDir)) {
|
|
3952
|
+
await fs6.promises.mkdir(projDir, { recursive: true });
|
|
3953
|
+
await fs6.promises.mkdir(path6.join(projDir, "samples"), { recursive: true });
|
|
3954
|
+
await fs6.promises.mkdir(path6.join(projDir, "assets"), { recursive: true });
|
|
4941
3955
|
}
|
|
4942
3956
|
let existingData = {};
|
|
4943
|
-
if (
|
|
4944
|
-
const raw = await
|
|
3957
|
+
if (fs6.existsSync(projectFile)) {
|
|
3958
|
+
const raw = await fs6.promises.readFile(projectFile, "utf-8");
|
|
4945
3959
|
existingData = JSON.parse(raw);
|
|
4946
3960
|
}
|
|
4947
3961
|
const updatedData = {
|
|
@@ -4950,22 +3964,22 @@ async function saveProject(id, payload) {
|
|
|
4950
3964
|
id: safeId,
|
|
4951
3965
|
updatedAt: Date.now()
|
|
4952
3966
|
};
|
|
4953
|
-
await
|
|
3967
|
+
await fs6.promises.writeFile(projectFile, JSON.stringify(updatedData, null, 2), "utf-8");
|
|
4954
3968
|
return updatedData;
|
|
4955
3969
|
}
|
|
4956
3970
|
async function deleteProject(id) {
|
|
4957
|
-
const safeId =
|
|
4958
|
-
const projDir =
|
|
4959
|
-
if (!
|
|
4960
|
-
await
|
|
3971
|
+
const safeId = path6.basename(id);
|
|
3972
|
+
const projDir = path6.join(PROJECTS_ROOT2, safeId);
|
|
3973
|
+
if (!fs6.existsSync(projDir)) return false;
|
|
3974
|
+
await fs6.promises.rm(projDir, { recursive: true, force: true });
|
|
4961
3975
|
return true;
|
|
4962
3976
|
}
|
|
4963
3977
|
|
|
4964
3978
|
// src/server/services/sampleService.ts
|
|
4965
|
-
import
|
|
4966
|
-
import
|
|
4967
|
-
var SAMPLES_GLOBAL_ROOT =
|
|
4968
|
-
var PROJECTS_ROOT3 =
|
|
3979
|
+
import fs7 from "fs";
|
|
3980
|
+
import path7 from "path";
|
|
3981
|
+
var SAMPLES_GLOBAL_ROOT = path7.join(process.cwd(), "samples", "global");
|
|
3982
|
+
var PROJECTS_ROOT3 = path7.join(process.cwd(), "projects");
|
|
4969
3983
|
var SUPPORTED_AUDIO_EXTS3 = [".wav", ".mp3", ".ogg", ".flac", ".m4a", ".webm", ".weba", ".aac", ".aiff", ".aif", ".opus", ".mp4"];
|
|
4970
3984
|
function initGlobalSamplesDirectory() {
|
|
4971
3985
|
ensureGlobalSamples(SAMPLES_GLOBAL_ROOT).catch((err) => {
|
|
@@ -4973,17 +3987,17 @@ function initGlobalSamplesDirectory() {
|
|
|
4973
3987
|
});
|
|
4974
3988
|
}
|
|
4975
3989
|
async function listGlobalSamples() {
|
|
4976
|
-
if (!
|
|
4977
|
-
await
|
|
3990
|
+
if (!fs7.existsSync(SAMPLES_GLOBAL_ROOT)) {
|
|
3991
|
+
await fs7.promises.mkdir(SAMPLES_GLOBAL_ROOT, { recursive: true });
|
|
4978
3992
|
}
|
|
4979
|
-
const files = await
|
|
3993
|
+
const files = await fs7.promises.readdir(SAMPLES_GLOBAL_ROOT);
|
|
4980
3994
|
const samples = [];
|
|
4981
3995
|
for (const file of files) {
|
|
4982
|
-
const ext =
|
|
3996
|
+
const ext = path7.extname(file).toLowerCase();
|
|
4983
3997
|
if (!SUPPORTED_AUDIO_EXTS3.includes(ext)) continue;
|
|
4984
|
-
const filePath =
|
|
4985
|
-
const stat = await
|
|
4986
|
-
const baseName =
|
|
3998
|
+
const filePath = path7.join(SAMPLES_GLOBAL_ROOT, file);
|
|
3999
|
+
const stat = await fs7.promises.stat(filePath);
|
|
4000
|
+
const baseName = path7.basename(file, ext);
|
|
4987
4001
|
const cleanName = baseName.replace(/[-_]/g, " ");
|
|
4988
4002
|
let category = "synth";
|
|
4989
4003
|
if (/808|kick|snare|hihat|drum|perc/i.test(baseName)) category = "drums";
|
|
@@ -5003,15 +4017,15 @@ async function listGlobalSamples() {
|
|
|
5003
4017
|
return samples;
|
|
5004
4018
|
}
|
|
5005
4019
|
async function saveGlobalSample(filename, base64Content, category) {
|
|
5006
|
-
const safeFilename =
|
|
5007
|
-
const ext =
|
|
5008
|
-
const filePath =
|
|
4020
|
+
const safeFilename = path7.basename(filename).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
4021
|
+
const ext = path7.extname(safeFilename).toLowerCase() || ".wav";
|
|
4022
|
+
const filePath = path7.join(SAMPLES_GLOBAL_ROOT, safeFilename);
|
|
5009
4023
|
const buffer = Buffer.from(base64Content, "base64");
|
|
5010
|
-
await
|
|
5011
|
-
const stat = await
|
|
4024
|
+
await fs7.promises.writeFile(filePath, buffer);
|
|
4025
|
+
const stat = await fs7.promises.stat(filePath);
|
|
5012
4026
|
return {
|
|
5013
4027
|
filename: safeFilename,
|
|
5014
|
-
name:
|
|
4028
|
+
name: path7.basename(safeFilename, ext).replace(/[-_]/g, " "),
|
|
5015
4029
|
category: category || "custom",
|
|
5016
4030
|
format: ext.replace(".", "").toUpperCase(),
|
|
5017
4031
|
url: `/samples/global/${safeFilename}`,
|
|
@@ -5021,28 +4035,28 @@ async function saveGlobalSample(filename, base64Content, category) {
|
|
|
5021
4035
|
};
|
|
5022
4036
|
}
|
|
5023
4037
|
async function deleteGlobalSample(filename) {
|
|
5024
|
-
const safeFilename =
|
|
5025
|
-
const filePath =
|
|
5026
|
-
if (
|
|
5027
|
-
await
|
|
4038
|
+
const safeFilename = path7.basename(filename);
|
|
4039
|
+
const filePath = path7.join(SAMPLES_GLOBAL_ROOT, safeFilename);
|
|
4040
|
+
if (fs7.existsSync(filePath)) {
|
|
4041
|
+
await fs7.promises.unlink(filePath);
|
|
5028
4042
|
return true;
|
|
5029
4043
|
}
|
|
5030
4044
|
return false;
|
|
5031
4045
|
}
|
|
5032
4046
|
async function listProjectSamples(projectId) {
|
|
5033
|
-
const safeId =
|
|
5034
|
-
const samplesDir =
|
|
5035
|
-
if (!
|
|
5036
|
-
await
|
|
4047
|
+
const safeId = path7.basename(projectId);
|
|
4048
|
+
const samplesDir = path7.join(PROJECTS_ROOT3, safeId, "samples");
|
|
4049
|
+
if (!fs7.existsSync(samplesDir)) {
|
|
4050
|
+
await fs7.promises.mkdir(samplesDir, { recursive: true });
|
|
5037
4051
|
}
|
|
5038
|
-
const files = await
|
|
4052
|
+
const files = await fs7.promises.readdir(samplesDir);
|
|
5039
4053
|
const samples = [];
|
|
5040
4054
|
for (const file of files) {
|
|
5041
|
-
const ext =
|
|
4055
|
+
const ext = path7.extname(file).toLowerCase();
|
|
5042
4056
|
if (!SUPPORTED_AUDIO_EXTS3.includes(ext)) continue;
|
|
5043
|
-
const filePath =
|
|
5044
|
-
const stat = await
|
|
5045
|
-
const baseName =
|
|
4057
|
+
const filePath = path7.join(samplesDir, file);
|
|
4058
|
+
const stat = await fs7.promises.stat(filePath);
|
|
4059
|
+
const baseName = path7.basename(file, ext);
|
|
5046
4060
|
samples.push({
|
|
5047
4061
|
filename: file,
|
|
5048
4062
|
name: baseName.replace(/[-_]/g, " "),
|
|
@@ -5058,21 +4072,21 @@ async function listProjectSamples(projectId) {
|
|
|
5058
4072
|
return samples;
|
|
5059
4073
|
}
|
|
5060
4074
|
async function saveProjectSample(projectId, filename, base64Content, isRecording) {
|
|
5061
|
-
const safeId =
|
|
5062
|
-
const samplesDir =
|
|
5063
|
-
if (!
|
|
5064
|
-
await
|
|
4075
|
+
const safeId = path7.basename(projectId);
|
|
4076
|
+
const samplesDir = path7.join(PROJECTS_ROOT3, safeId, "samples");
|
|
4077
|
+
if (!fs7.existsSync(samplesDir)) {
|
|
4078
|
+
await fs7.promises.mkdir(samplesDir, { recursive: true });
|
|
5065
4079
|
}
|
|
5066
4080
|
const defaultName = isRecording ? `recording_take_${Date.now()}.wav` : `clip_${Date.now()}.wav`;
|
|
5067
|
-
const targetFilename = filename ?
|
|
5068
|
-
const ext =
|
|
5069
|
-
const filePath =
|
|
4081
|
+
const targetFilename = filename ? path7.basename(filename).replace(/[^a-zA-Z0-9._-]/g, "_") : defaultName;
|
|
4082
|
+
const ext = path7.extname(targetFilename).toLowerCase() || ".wav";
|
|
4083
|
+
const filePath = path7.join(samplesDir, targetFilename);
|
|
5070
4084
|
const buffer = Buffer.from(base64Content, "base64");
|
|
5071
|
-
await
|
|
5072
|
-
const stat = await
|
|
4085
|
+
await fs7.promises.writeFile(filePath, buffer);
|
|
4086
|
+
const stat = await fs7.promises.stat(filePath);
|
|
5073
4087
|
return {
|
|
5074
4088
|
filename: targetFilename,
|
|
5075
|
-
name:
|
|
4089
|
+
name: path7.basename(targetFilename, ext).replace(/[-_]/g, " "),
|
|
5076
4090
|
category: isRecording ? "recording" : "import",
|
|
5077
4091
|
format: ext.replace(".", "").toUpperCase(),
|
|
5078
4092
|
url: `/projects/${safeId}/samples/${targetFilename}`,
|
|
@@ -5083,26 +4097,26 @@ async function saveProjectSample(projectId, filename, base64Content, isRecording
|
|
|
5083
4097
|
};
|
|
5084
4098
|
}
|
|
5085
4099
|
async function deleteProjectSample(projectId, filename) {
|
|
5086
|
-
const safeId =
|
|
5087
|
-
const safeFilename =
|
|
5088
|
-
const filePath =
|
|
5089
|
-
if (
|
|
5090
|
-
await
|
|
4100
|
+
const safeId = path7.basename(projectId);
|
|
4101
|
+
const safeFilename = path7.basename(filename);
|
|
4102
|
+
const filePath = path7.join(PROJECTS_ROOT3, safeId, "samples", safeFilename);
|
|
4103
|
+
if (fs7.existsSync(filePath)) {
|
|
4104
|
+
await fs7.promises.unlink(filePath);
|
|
5091
4105
|
return true;
|
|
5092
4106
|
}
|
|
5093
4107
|
return false;
|
|
5094
4108
|
}
|
|
5095
4109
|
|
|
5096
4110
|
// src/server/services/workspaceGraphService.ts
|
|
5097
|
-
import
|
|
5098
|
-
import
|
|
5099
|
-
var AUTH_FILE_PATH =
|
|
4111
|
+
import fs8 from "fs";
|
|
4112
|
+
import path8 from "path";
|
|
4113
|
+
var AUTH_FILE_PATH = path8.resolve(".skipo/accounts.json");
|
|
5100
4114
|
async function resolveWorkspaceDir(workspaceIdOrSlug) {
|
|
5101
4115
|
const workingDir = getWorkingWorkspaceDir();
|
|
5102
4116
|
let targetWs = null;
|
|
5103
4117
|
try {
|
|
5104
|
-
if (
|
|
5105
|
-
const raw = await
|
|
4118
|
+
if (fs8.existsSync(AUTH_FILE_PATH)) {
|
|
4119
|
+
const raw = await fs8.promises.readFile(AUTH_FILE_PATH, "utf-8");
|
|
5106
4120
|
const accounts = JSON.parse(raw);
|
|
5107
4121
|
for (const user of Object.values(accounts)) {
|
|
5108
4122
|
if (Array.isArray(user.workspaces)) {
|
|
@@ -5122,35 +4136,35 @@ async function resolveWorkspaceDir(workspaceIdOrSlug) {
|
|
|
5122
4136
|
if (targetWs) {
|
|
5123
4137
|
const rawFolderName = targetWs.localFolderName || targetWs.name.replace(/[/\\?%*:|"<>]/g, "_");
|
|
5124
4138
|
const folderName = rawFolderName.trim();
|
|
5125
|
-
await
|
|
4139
|
+
await fs8.promises.mkdir(workingDir, { recursive: true });
|
|
5126
4140
|
return { wsDir: workingDir, wsName: targetWs.name, wsFolder: folderName, wsId: targetWs.id };
|
|
5127
4141
|
}
|
|
5128
|
-
await
|
|
4142
|
+
await fs8.promises.mkdir(workingDir, { recursive: true });
|
|
5129
4143
|
return { wsDir: workingDir, wsName: "Active Workspace", wsFolder: "workspace", wsId: workspaceIdOrSlug || "default" };
|
|
5130
4144
|
}
|
|
5131
4145
|
async function listWorkspaceGraphs(workspaceIdOrSlug) {
|
|
5132
4146
|
const { wsDir, wsFolder } = await resolveWorkspaceDir(workspaceIdOrSlug);
|
|
5133
4147
|
const graphs = [];
|
|
5134
4148
|
const inspectDirForGraph = async (dirPath, folderName) => {
|
|
5135
|
-
const projectJson =
|
|
5136
|
-
const graphQms =
|
|
4149
|
+
const projectJson = path8.join(dirPath, "project.json");
|
|
4150
|
+
const graphQms = path8.join(dirPath, "graph.qms");
|
|
5137
4151
|
let data = null;
|
|
5138
|
-
if (
|
|
4152
|
+
if (fs8.existsSync(projectJson)) {
|
|
5139
4153
|
try {
|
|
5140
|
-
const raw = await
|
|
4154
|
+
const raw = await fs8.promises.readFile(projectJson, "utf-8");
|
|
5141
4155
|
data = JSON.parse(raw);
|
|
5142
4156
|
} catch {
|
|
5143
4157
|
}
|
|
5144
|
-
} else if (
|
|
4158
|
+
} else if (fs8.existsSync(graphQms)) {
|
|
5145
4159
|
try {
|
|
5146
|
-
const raw = await
|
|
4160
|
+
const raw = await fs8.promises.readFile(graphQms, "utf-8");
|
|
5147
4161
|
const pkg = JSON.parse(raw);
|
|
5148
4162
|
data = { ...pkg.metadata || {}, ...pkg.graph || {} };
|
|
5149
4163
|
} catch {
|
|
5150
4164
|
}
|
|
5151
4165
|
}
|
|
5152
4166
|
if (!data) return null;
|
|
5153
|
-
const stat = await
|
|
4167
|
+
const stat = await fs8.promises.stat(dirPath);
|
|
5154
4168
|
const nodeCount = Array.isArray(data.nodes) ? data.nodes.length : 0;
|
|
5155
4169
|
const connectionCount = Array.isArray(data.connections) ? data.connections.length : 0;
|
|
5156
4170
|
return {
|
|
@@ -5158,7 +4172,7 @@ async function listWorkspaceGraphs(workspaceIdOrSlug) {
|
|
|
5158
4172
|
name: data.name || folderName,
|
|
5159
4173
|
description: data.description || "",
|
|
5160
4174
|
folderName,
|
|
5161
|
-
folderPath:
|
|
4175
|
+
folderPath: path8.relative(process.cwd(), dirPath).replace(/\\/g, "/"),
|
|
5162
4176
|
createdAt: data.createdAt || stat.birthtimeMs,
|
|
5163
4177
|
updatedAt: data.updatedAt || stat.mtimeMs,
|
|
5164
4178
|
bpm: data.bpm || 120,
|
|
@@ -5171,18 +4185,18 @@ async function listWorkspaceGraphs(workspaceIdOrSlug) {
|
|
|
5171
4185
|
connectionCount
|
|
5172
4186
|
};
|
|
5173
4187
|
};
|
|
5174
|
-
const entries = await
|
|
4188
|
+
const entries = await fs8.promises.readdir(wsDir);
|
|
5175
4189
|
for (const entry of entries) {
|
|
5176
4190
|
if (entry.startsWith(".") || entry === "samples" || entry === "assets") continue;
|
|
5177
|
-
const fullPath =
|
|
5178
|
-
const stat = await
|
|
4191
|
+
const fullPath = path8.join(wsDir, entry);
|
|
4192
|
+
const stat = await fs8.promises.stat(fullPath);
|
|
5179
4193
|
if (!stat.isDirectory()) continue;
|
|
5180
4194
|
if (entry === "graphs") {
|
|
5181
|
-
const subEntries = await
|
|
4195
|
+
const subEntries = await fs8.promises.readdir(fullPath);
|
|
5182
4196
|
for (const sub of subEntries) {
|
|
5183
4197
|
if (sub.startsWith(".")) continue;
|
|
5184
|
-
const subPath =
|
|
5185
|
-
const subStat = await
|
|
4198
|
+
const subPath = path8.join(fullPath, sub);
|
|
4199
|
+
const subStat = await fs8.promises.stat(subPath);
|
|
5186
4200
|
if (subStat.isDirectory()) {
|
|
5187
4201
|
const g2 = await inspectDirForGraph(subPath, sub);
|
|
5188
4202
|
if (g2) graphs.push(g2);
|
|
@@ -5199,10 +4213,10 @@ async function createWorkspaceGraph(workspaceIdOrSlug, params) {
|
|
|
5199
4213
|
const { wsDir, wsFolder, wsId } = await resolveWorkspaceDir(workspaceIdOrSlug);
|
|
5200
4214
|
const rawName = params.name.trim();
|
|
5201
4215
|
const folderName = rawName.replace(/[/\\?%*:|"<>]/g, "_");
|
|
5202
|
-
const graphDir =
|
|
5203
|
-
await
|
|
5204
|
-
await
|
|
5205
|
-
await
|
|
4216
|
+
const graphDir = path8.join(wsDir, folderName);
|
|
4217
|
+
await fs8.promises.mkdir(graphDir, { recursive: true });
|
|
4218
|
+
await fs8.promises.mkdir(path8.join(graphDir, "samples"), { recursive: true });
|
|
4219
|
+
await fs8.promises.mkdir(path8.join(graphDir, "assets"), { recursive: true });
|
|
5206
4220
|
const now = Date.now();
|
|
5207
4221
|
const nodes = params.templateData?.nodes || [];
|
|
5208
4222
|
const connections = params.templateData?.connections || [];
|
|
@@ -5213,7 +4227,7 @@ async function createWorkspaceGraph(workspaceIdOrSlug, params) {
|
|
|
5213
4227
|
id: folderName,
|
|
5214
4228
|
name: rawName,
|
|
5215
4229
|
description: params.description || `Workspace graph in ${wsFolder}`,
|
|
5216
|
-
folderPath:
|
|
4230
|
+
folderPath: path8.relative(process.cwd(), graphDir).replace(/\\/g, "/"),
|
|
5217
4231
|
createdAt: now,
|
|
5218
4232
|
updatedAt: now,
|
|
5219
4233
|
bpm,
|
|
@@ -5222,7 +4236,7 @@ async function createWorkspaceGraph(workspaceIdOrSlug, params) {
|
|
|
5222
4236
|
nodes,
|
|
5223
4237
|
connections
|
|
5224
4238
|
};
|
|
5225
|
-
await
|
|
4239
|
+
await fs8.promises.writeFile(path8.join(graphDir, "project.json"), JSON.stringify(projectData, null, 2), "utf-8");
|
|
5226
4240
|
const graphPackage = {
|
|
5227
4241
|
format: "qms_graph_package",
|
|
5228
4242
|
version: "2.0.0",
|
|
@@ -5240,27 +4254,27 @@ async function createWorkspaceGraph(workspaceIdOrSlug, params) {
|
|
|
5240
4254
|
},
|
|
5241
4255
|
graph: { bpm, masterVolume, currentBiome, nodes, connections }
|
|
5242
4256
|
};
|
|
5243
|
-
await
|
|
4257
|
+
await fs8.promises.writeFile(path8.join(graphDir, "graph.qms"), JSON.stringify(graphPackage, null, 2), "utf-8");
|
|
5244
4258
|
return projectData;
|
|
5245
4259
|
}
|
|
5246
4260
|
async function getWorkspaceGraph(workspaceIdOrSlug, graphIdOrName) {
|
|
5247
4261
|
const { wsDir } = await resolveWorkspaceDir(workspaceIdOrSlug);
|
|
5248
4262
|
const safeTarget = graphIdOrName.replace(/[/\\?%*:|"<>]/g, "_");
|
|
5249
4263
|
const candidates = [
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
4264
|
+
path8.join(wsDir, safeTarget),
|
|
4265
|
+
path8.join(wsDir, graphIdOrName),
|
|
4266
|
+
path8.join(wsDir, "graphs", safeTarget)
|
|
5253
4267
|
];
|
|
5254
4268
|
for (const cand of candidates) {
|
|
5255
|
-
if (
|
|
5256
|
-
const pJson =
|
|
5257
|
-
if (
|
|
5258
|
-
const raw = await
|
|
4269
|
+
if (fs8.existsSync(cand)) {
|
|
4270
|
+
const pJson = path8.join(cand, "project.json");
|
|
4271
|
+
if (fs8.existsSync(pJson)) {
|
|
4272
|
+
const raw = await fs8.promises.readFile(pJson, "utf-8");
|
|
5259
4273
|
return JSON.parse(raw);
|
|
5260
4274
|
}
|
|
5261
|
-
const gQms =
|
|
5262
|
-
if (
|
|
5263
|
-
const raw = await
|
|
4275
|
+
const gQms = path8.join(cand, "graph.qms");
|
|
4276
|
+
if (fs8.existsSync(gQms)) {
|
|
4277
|
+
const raw = await fs8.promises.readFile(gQms, "utf-8");
|
|
5264
4278
|
const pkg = JSON.parse(raw);
|
|
5265
4279
|
return { ...pkg.metadata || {}, ...pkg.graph || {} };
|
|
5266
4280
|
}
|
|
@@ -5271,39 +4285,39 @@ async function getWorkspaceGraph(workspaceIdOrSlug, graphIdOrName) {
|
|
|
5271
4285
|
async function saveWorkspaceGraph(workspaceIdOrSlug, graphIdOrName, payload) {
|
|
5272
4286
|
const { wsDir } = await resolveWorkspaceDir(workspaceIdOrSlug);
|
|
5273
4287
|
const safeTarget = graphIdOrName.replace(/[/\\?%*:|"<>]/g, "_");
|
|
5274
|
-
let targetDir =
|
|
5275
|
-
if (!
|
|
5276
|
-
targetDir =
|
|
4288
|
+
let targetDir = path8.join(wsDir, safeTarget);
|
|
4289
|
+
if (!fs8.existsSync(targetDir) && fs8.existsSync(path8.join(wsDir, "graphs", safeTarget))) {
|
|
4290
|
+
targetDir = path8.join(wsDir, "graphs", safeTarget);
|
|
5277
4291
|
}
|
|
5278
|
-
await
|
|
5279
|
-
const pJson =
|
|
4292
|
+
await fs8.promises.mkdir(targetDir, { recursive: true });
|
|
4293
|
+
const pJson = path8.join(targetDir, "project.json");
|
|
5280
4294
|
let existing = {};
|
|
5281
|
-
if (
|
|
4295
|
+
if (fs8.existsSync(pJson)) {
|
|
5282
4296
|
try {
|
|
5283
|
-
existing = JSON.parse(await
|
|
4297
|
+
existing = JSON.parse(await fs8.promises.readFile(pJson, "utf-8"));
|
|
5284
4298
|
} catch {
|
|
5285
4299
|
}
|
|
5286
4300
|
}
|
|
5287
4301
|
const updated = {
|
|
5288
4302
|
...existing,
|
|
5289
4303
|
...payload,
|
|
5290
|
-
id: existing.id ||
|
|
4304
|
+
id: existing.id || path8.basename(targetDir),
|
|
5291
4305
|
updatedAt: Date.now()
|
|
5292
4306
|
};
|
|
5293
|
-
await
|
|
4307
|
+
await fs8.promises.writeFile(pJson, JSON.stringify(updated, null, 2), "utf-8");
|
|
5294
4308
|
return updated;
|
|
5295
4309
|
}
|
|
5296
4310
|
async function deleteWorkspaceGraph(workspaceIdOrSlug, graphIdOrName) {
|
|
5297
4311
|
const { wsDir } = await resolveWorkspaceDir(workspaceIdOrSlug);
|
|
5298
4312
|
const safeTarget = graphIdOrName.replace(/[/\\?%*:|"<>]/g, "_");
|
|
5299
4313
|
const candidates = [
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
4314
|
+
path8.join(wsDir, safeTarget),
|
|
4315
|
+
path8.join(wsDir, graphIdOrName),
|
|
4316
|
+
path8.join(wsDir, "graphs", safeTarget)
|
|
5303
4317
|
];
|
|
5304
4318
|
for (const cand of candidates) {
|
|
5305
|
-
if (
|
|
5306
|
-
await
|
|
4319
|
+
if (fs8.existsSync(cand)) {
|
|
4320
|
+
await fs8.promises.rm(cand, { recursive: true, force: true });
|
|
5307
4321
|
return true;
|
|
5308
4322
|
}
|
|
5309
4323
|
}
|
|
@@ -5363,6 +4377,7 @@ export {
|
|
|
5363
4377
|
PasswordHasher,
|
|
5364
4378
|
RateLimiter,
|
|
5365
4379
|
RemoteExecutor,
|
|
4380
|
+
RemoteWorkspaceService,
|
|
5366
4381
|
ResourceFetcherRegistry,
|
|
5367
4382
|
ResultFlowBridge,
|
|
5368
4383
|
S3Adapter,
|
|
@@ -5386,6 +4401,7 @@ export {
|
|
|
5386
4401
|
WORKSPACE_ROOT,
|
|
5387
4402
|
WebSocketTransport,
|
|
5388
4403
|
WorkerExecutionBoundary,
|
|
4404
|
+
WorkingWorkspaceService,
|
|
5389
4405
|
WorkspaceCloneSyncEngine,
|
|
5390
4406
|
WorkspaceFS,
|
|
5391
4407
|
WorkspaceServiceDefinition,
|
|
@@ -5510,6 +4526,7 @@ export {
|
|
|
5510
4526
|
queryUnifiedDb,
|
|
5511
4527
|
readWorkspaceFile,
|
|
5512
4528
|
recordExecution,
|
|
4529
|
+
remoteWorkspaceService,
|
|
5513
4530
|
removeGraphDependency,
|
|
5514
4531
|
renameAppInWorkspaceData,
|
|
5515
4532
|
renameS3Folder,
|
|
@@ -5554,5 +4571,6 @@ export {
|
|
|
5554
4571
|
validateServiceDefinition,
|
|
5555
4572
|
withAuth,
|
|
5556
4573
|
withCors,
|
|
4574
|
+
workingWorkspaceService,
|
|
5557
4575
|
writeWorkspaceFile
|
|
5558
4576
|
};
|