@ultrst-armada/kloks 1.3.0 → 1.4.1
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 +45 -2
- package/packages/audio-midi/dist/index.js +1 -2
- package/packages/audio-midi/package.json +1 -1
- package/packages/backend/dist/{chunk-IPP4VURQ.js → chunk-7KNGX522.js} +1 -2
- package/packages/backend/dist/chunk-DZ4ZGIZM.js +1323 -0
- package/packages/backend/dist/{chunk-CU7RZBMF.js → chunk-FO6WYLW4.js} +1 -1
- package/packages/backend/dist/{chunk-BM4QMVPA.js → chunk-XTWHEZWS.js} +1 -1
- package/packages/backend/dist/index.d.ts +1 -1
- package/packages/backend/dist/index.js +390 -1372
- package/packages/backend/dist/krnl-services.js +2 -2
- package/packages/backend/dist/services/index.js +2 -2
- package/packages/backend/dist/workspace/index.d.ts +119 -1
- package/packages/backend/dist/workspace/index.js +11 -3
- package/packages/backend/package.json +1 -1
- 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/contracts/package.json +1 -1
- package/packages/functions/dist/index.js +1 -2
- package/packages/functions/package.json +1 -1
- package/packages/orchestrations/package.json +1 -1
- package/packages/services/dist/index.js +1 -2
- package/packages/services/package.json +1 -1
- package/packages/state-graph/dist/index.js +11 -12
- package/packages/state-graph/package.json +1 -1
- package/packages/backend/dist/chunk-KTLD7Z4L.js +0 -124
|
@@ -0,0 +1,1323 @@
|
|
|
1
|
+
// src/workspace/WorkspaceFS.ts
|
|
2
|
+
import { SkipoBase } from "@ultrst-armada/kloks/functions";
|
|
3
|
+
var WorkspaceFS = class extends SkipoBase {
|
|
4
|
+
primary;
|
|
5
|
+
syncEngine;
|
|
6
|
+
config;
|
|
7
|
+
constructor(primary, config, syncEngine) {
|
|
8
|
+
super();
|
|
9
|
+
this.primary = primary;
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.syncEngine = syncEngine;
|
|
12
|
+
}
|
|
13
|
+
setPrimary(newPrimary) {
|
|
14
|
+
this.primary = newPrimary;
|
|
15
|
+
}
|
|
16
|
+
setSyncEngine(engine) {
|
|
17
|
+
this.syncEngine = engine;
|
|
18
|
+
}
|
|
19
|
+
async exists(pathStr) {
|
|
20
|
+
return this.primary.exists(pathStr);
|
|
21
|
+
}
|
|
22
|
+
async stat(pathStr) {
|
|
23
|
+
return this.primary.stat(pathStr);
|
|
24
|
+
}
|
|
25
|
+
async list(pathStr = "") {
|
|
26
|
+
return this.primary.list(pathStr);
|
|
27
|
+
}
|
|
28
|
+
async read(pathStr) {
|
|
29
|
+
return this.primary.read(pathStr);
|
|
30
|
+
}
|
|
31
|
+
async readText(pathStr) {
|
|
32
|
+
const data = await this.read(pathStr);
|
|
33
|
+
return Buffer.from(data).toString("utf8");
|
|
34
|
+
}
|
|
35
|
+
async write(pathStr, data) {
|
|
36
|
+
await this.primary.write(pathStr, data);
|
|
37
|
+
if (this.syncEngine && this.config.sync?.enabled && this.config.sync.mode === "on_write") {
|
|
38
|
+
this.syncEngine.syncFile(pathStr).catch((err) => {
|
|
39
|
+
console.warn(`[WorkspaceFS] Async backup sync warning for '${pathStr}':`, err.message);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async mkdir(pathStr) {
|
|
44
|
+
await this.primary.mkdir(pathStr);
|
|
45
|
+
}
|
|
46
|
+
async delete(pathStr) {
|
|
47
|
+
await this.primary.delete(pathStr);
|
|
48
|
+
if (this.syncEngine && this.config.sync?.enabled && this.config.sync.deletePropagation) {
|
|
49
|
+
this.syncEngine.deleteFile(pathStr).catch((err) => {
|
|
50
|
+
console.warn(`[WorkspaceFS] Async delete sync warning for '${pathStr}':`, err.message);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// src/workspace/SyncEngine.ts
|
|
57
|
+
import { SkipoBase as SkipoBase2 } from "@ultrst-armada/kloks/functions";
|
|
58
|
+
var SyncEngine = class extends SkipoBase2 {
|
|
59
|
+
primary;
|
|
60
|
+
backups;
|
|
61
|
+
config;
|
|
62
|
+
constructor(primary, backups, config) {
|
|
63
|
+
super({ name: "SyncEngine" });
|
|
64
|
+
this.primary = primary;
|
|
65
|
+
this.backups = backups;
|
|
66
|
+
this.config = config;
|
|
67
|
+
}
|
|
68
|
+
async syncFile(pathStr) {
|
|
69
|
+
const data = await this.primary.read(pathStr);
|
|
70
|
+
for (const backup of this.backups) {
|
|
71
|
+
try {
|
|
72
|
+
await backup.write(pathStr, data);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.error(`[SyncEngine] Failed to sync ${pathStr} to backup:`, e.message);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async deleteFile(pathStr) {
|
|
79
|
+
for (const backup of this.backups) {
|
|
80
|
+
try {
|
|
81
|
+
await backup.delete(pathStr);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.error(`[SyncEngine] Failed to delete ${pathStr} on backup:`, e.message);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// src/workspace/WorkspaceCloneSyncEngine.ts
|
|
90
|
+
import { SkipoBase as SkipoBase3 } from "@ultrst-armada/kloks/functions";
|
|
91
|
+
var WorkspaceCloneSyncEngine = class extends SkipoBase3 {
|
|
92
|
+
constructor() {
|
|
93
|
+
super({ name: "WorkspaceCloneSyncEngine" });
|
|
94
|
+
}
|
|
95
|
+
async replicateToClones(files, clones, uploadFn) {
|
|
96
|
+
const results = [];
|
|
97
|
+
let totalSynced = 0;
|
|
98
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
99
|
+
for (const clone of clones) {
|
|
100
|
+
try {
|
|
101
|
+
let count = 0;
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
await uploadFn(clone, file.relPath, file.content, file.contentType);
|
|
104
|
+
count++;
|
|
105
|
+
}
|
|
106
|
+
clone.lastSyncedAt = nowIso;
|
|
107
|
+
clone.filesSynced = count;
|
|
108
|
+
clone.status = "SYNCED";
|
|
109
|
+
results.push({ cloneId: clone.id, label: clone.label, provider: clone.provider, filesSynced: count, status: "SYNCED" });
|
|
110
|
+
totalSynced += count;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
clone.status = "ERROR";
|
|
113
|
+
results.push({ cloneId: clone.id, label: clone.label, provider: clone.provider, filesSynced: 0, status: "ERROR", error: err.message });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { results, totalSynced };
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/core/unifiedDb.ts
|
|
121
|
+
import { neon } from "@neondatabase/serverless";
|
|
122
|
+
function cleanDbUrl(url) {
|
|
123
|
+
if (!url || typeof url !== "string") return "";
|
|
124
|
+
return url.trim().replace(/^["']|["']$/g, "");
|
|
125
|
+
}
|
|
126
|
+
function getUnifiedDbUrl(env) {
|
|
127
|
+
const fromEnv = cleanDbUrl(env?.SYSTEM_DATABASE_URL_1) || cleanDbUrl(env?.DATABASE_URL) || cleanDbUrl(env?.SYSTEM_DATABASE_URL_2);
|
|
128
|
+
if (fromEnv) return fromEnv;
|
|
129
|
+
if (typeof process !== "undefined" && process.env) {
|
|
130
|
+
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) || "";
|
|
131
|
+
}
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
134
|
+
async function queryUnifiedDb(sqlText, params = [], env) {
|
|
135
|
+
const dbUrl = getUnifiedDbUrl(env);
|
|
136
|
+
if (!dbUrl) {
|
|
137
|
+
throw new Error("[UnifiedDB] No database URL configured in environment.");
|
|
138
|
+
}
|
|
139
|
+
const sql = neon(dbUrl);
|
|
140
|
+
try {
|
|
141
|
+
const rows = await (sql.query ? sql.query(sqlText, params) : sql(sqlText, params));
|
|
142
|
+
return {
|
|
143
|
+
rows: Array.isArray(rows) ? rows : [],
|
|
144
|
+
rowCount: Array.isArray(rows) ? rows.length : 0
|
|
145
|
+
};
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error("[UnifiedDB] Query execution error:", err.message);
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/core/b2DirectCore.ts
|
|
153
|
+
var authCache = /* @__PURE__ */ new Map();
|
|
154
|
+
var bucketIdCache = /* @__PURE__ */ new Map();
|
|
155
|
+
async function authorizeB2(keyId, applicationKey) {
|
|
156
|
+
const cacheKey = `${keyId.trim()}:${applicationKey.trim()}`;
|
|
157
|
+
const cached = authCache.get(cacheKey);
|
|
158
|
+
if (cached && Date.now() < cached.expiresAt) return cached.auth;
|
|
159
|
+
const authHeader = typeof Buffer !== "undefined" ? Buffer.from(`${keyId.trim()}:${applicationKey.trim()}`).toString("base64") : btoa(`${keyId.trim()}:${applicationKey.trim()}`);
|
|
160
|
+
let res = await fetch("https://api.backblazeb2.com/b2api/v2/b2_authorize_account", {
|
|
161
|
+
headers: { Authorization: `Basic ${authHeader}` },
|
|
162
|
+
signal: AbortSignal.timeout(1e4)
|
|
163
|
+
});
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
res = await fetch("https://api.backblazeb2.com/b2api/v4/b2_authorize_account", {
|
|
166
|
+
headers: { Authorization: `Basic ${authHeader}` },
|
|
167
|
+
signal: AbortSignal.timeout(1e4)
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
const errText = await res.text();
|
|
172
|
+
throw new Error(`Backblaze B2 authorization failed (${res.status}): ${errText}`);
|
|
173
|
+
}
|
|
174
|
+
const data = await res.json();
|
|
175
|
+
const auth = {
|
|
176
|
+
accountId: data.accountId,
|
|
177
|
+
authorizationToken: data.authorizationToken,
|
|
178
|
+
apiUrl: (data.apiInfo?.storageApi?.apiUrl || data.apiUrl || "https://api.backblazeb2.com").replace(/\/+$/, ""),
|
|
179
|
+
downloadUrl: (data.apiInfo?.storageApi?.downloadUrl || data.downloadUrl || data.apiUrl || "https://f003.backblazeb2.com").replace(/\/+$/, ""),
|
|
180
|
+
s3ApiUrl: data.apiInfo?.storageApi?.s3ApiUrl || data.s3ApiUrl,
|
|
181
|
+
allowed: data.apiInfo?.storageApi?.allowed || data.allowed
|
|
182
|
+
};
|
|
183
|
+
authCache.set(cacheKey, { auth, expiresAt: Date.now() + 20 * 60 * 1e3 });
|
|
184
|
+
return auth;
|
|
185
|
+
}
|
|
186
|
+
async function b2ListBuckets(keyId, applicationKey, bucketId, bucketName) {
|
|
187
|
+
const auth = await authorizeB2(keyId, applicationKey);
|
|
188
|
+
const rBucketId = bucketId || auth.allowed?.bucketId || auth.allowed?.buckets?.[0]?.bucketId;
|
|
189
|
+
const rBucketName = bucketName || auth.allowed?.bucketName || auth.allowed?.buckets?.[0]?.bucketName;
|
|
190
|
+
if (!auth.allowed?.bucketId && !auth.allowed?.bucketName) {
|
|
191
|
+
try {
|
|
192
|
+
const res = await fetch(`${auth.apiUrl}/b2api/v2/b2_list_buckets`, {
|
|
193
|
+
method: "POST",
|
|
194
|
+
headers: { Authorization: auth.authorizationToken, "Content-Type": "application/json" },
|
|
195
|
+
body: JSON.stringify({ accountId: auth.accountId }),
|
|
196
|
+
signal: AbortSignal.timeout(8e3)
|
|
197
|
+
});
|
|
198
|
+
if (res.ok) {
|
|
199
|
+
const data = await res.json();
|
|
200
|
+
if (Array.isArray(data.buckets) && data.buckets.length > 0) return data.buckets;
|
|
201
|
+
}
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.warn("[B2DirectCore] list_buckets query failed:", err.message);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (rBucketId || rBucketName) {
|
|
207
|
+
const body = { accountId: auth.accountId };
|
|
208
|
+
if (rBucketId) body.bucketId = rBucketId;
|
|
209
|
+
else if (rBucketName) body.bucketName = rBucketName;
|
|
210
|
+
try {
|
|
211
|
+
const res = await fetch(`${auth.apiUrl}/b2api/v2/b2_list_buckets`, {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers: { Authorization: auth.authorizationToken, "Content-Type": "application/json" },
|
|
214
|
+
body: JSON.stringify(body),
|
|
215
|
+
signal: AbortSignal.timeout(8e3)
|
|
216
|
+
});
|
|
217
|
+
if (res.ok) {
|
|
218
|
+
const data = await res.json();
|
|
219
|
+
if (Array.isArray(data.buckets) && data.buckets.length > 0) return data.buckets;
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
return [{
|
|
224
|
+
accountId: auth.accountId,
|
|
225
|
+
bucketId: rBucketId || "restricted",
|
|
226
|
+
bucketName: rBucketName || (bucketName || "default-bucket"),
|
|
227
|
+
bucketType: "allPrivate"
|
|
228
|
+
}];
|
|
229
|
+
}
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
async function getB2BucketId(keyId, applicationKey, bucketName) {
|
|
233
|
+
const cacheKey = `${keyId.trim()}:${bucketName.trim()}`;
|
|
234
|
+
if (bucketIdCache.has(cacheKey)) return bucketIdCache.get(cacheKey);
|
|
235
|
+
const buckets = await b2ListBuckets(keyId, applicationKey, void 0, bucketName);
|
|
236
|
+
const match = buckets.find((b) => b.bucketName === bucketName);
|
|
237
|
+
if (!match) throw new Error(`B2 bucket "${bucketName}" not found in account`);
|
|
238
|
+
bucketIdCache.set(cacheKey, match.bucketId);
|
|
239
|
+
return match.bucketId;
|
|
240
|
+
}
|
|
241
|
+
async function b2UploadFile(keyId, applicationKey, bucketName, fileName, buffer, contentType = "b2/x-auto") {
|
|
242
|
+
const auth = await authorizeB2(keyId, applicationKey);
|
|
243
|
+
const bucketId = await getB2BucketId(keyId, applicationKey, bucketName);
|
|
244
|
+
const urlRes = await fetch(`${auth.apiUrl}/b2api/v2/b2_get_upload_url`, {
|
|
245
|
+
method: "POST",
|
|
246
|
+
headers: { Authorization: auth.authorizationToken, "Content-Type": "application/json" },
|
|
247
|
+
body: JSON.stringify({ bucketId })
|
|
248
|
+
});
|
|
249
|
+
if (!urlRes.ok) throw new Error(`B2 get upload url failed (${urlRes.status})`);
|
|
250
|
+
const { uploadUrl, authorizationToken } = await urlRes.json();
|
|
251
|
+
const encodedFileName = encodeURIComponent(fileName).replace(/%2F/g, "/");
|
|
252
|
+
const uploadRes = await fetch(uploadUrl, {
|
|
253
|
+
method: "POST",
|
|
254
|
+
headers: {
|
|
255
|
+
Authorization: authorizationToken,
|
|
256
|
+
"X-Bz-File-Name": encodedFileName,
|
|
257
|
+
"Content-Type": contentType,
|
|
258
|
+
"Content-Length": String(buffer.byteLength),
|
|
259
|
+
"X-Bz-Content-Sha1": "do_not_verify"
|
|
260
|
+
},
|
|
261
|
+
body: buffer
|
|
262
|
+
});
|
|
263
|
+
if (!uploadRes.ok) throw new Error(`B2 upload failed for ${fileName} (${uploadRes.status}): ${await uploadRes.text()}`);
|
|
264
|
+
return uploadRes.json();
|
|
265
|
+
}
|
|
266
|
+
async function b2DownloadFile(keyId, applicationKey, bucketName, fileName) {
|
|
267
|
+
const auth = await authorizeB2(keyId, applicationKey);
|
|
268
|
+
const encodedName = encodeURIComponent(fileName).replace(/%2F/g, "/");
|
|
269
|
+
const res = await fetch(`${auth.downloadUrl}/file/${bucketName}/${encodedName}`, { headers: { Authorization: auth.authorizationToken } });
|
|
270
|
+
if (res.status === 404) return null;
|
|
271
|
+
if (!res.ok) throw new Error(`B2 download failed for ${fileName} (${res.status})`);
|
|
272
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
273
|
+
}
|
|
274
|
+
async function b2ListFileNames(keyId, applicationKey, bucketName, prefix = "") {
|
|
275
|
+
const auth = await authorizeB2(keyId, applicationKey);
|
|
276
|
+
const bucketId = await getB2BucketId(keyId, applicationKey, bucketName);
|
|
277
|
+
const res = await fetch(`${auth.apiUrl}/b2api/v2/b2_list_file_names`, {
|
|
278
|
+
method: "POST",
|
|
279
|
+
headers: { Authorization: auth.authorizationToken, "Content-Type": "application/json" },
|
|
280
|
+
body: JSON.stringify({ bucketId, prefix, maxFileCount: 1e3 })
|
|
281
|
+
});
|
|
282
|
+
if (!res.ok) throw new Error(`B2 list files failed (${res.status})`);
|
|
283
|
+
const data = await res.json();
|
|
284
|
+
return data.files || [];
|
|
285
|
+
}
|
|
286
|
+
async function b2DeleteFileVersion(keyId, applicationKey, fileName, fileId) {
|
|
287
|
+
const auth = await authorizeB2(keyId, applicationKey);
|
|
288
|
+
const res = await fetch(`${auth.apiUrl}/b2api/v2/b2_delete_file_version`, {
|
|
289
|
+
method: "POST",
|
|
290
|
+
headers: { Authorization: auth.authorizationToken, "Content-Type": "application/json" },
|
|
291
|
+
body: JSON.stringify({ fileName, fileId })
|
|
292
|
+
});
|
|
293
|
+
if (!res.ok) throw new Error(`B2 delete file version failed (${res.status})`);
|
|
294
|
+
return res.json();
|
|
295
|
+
}
|
|
296
|
+
async function b2DeleteFolder(keyId, applicationKey, bucketName, folderPrefix) {
|
|
297
|
+
const cleanPrefix = folderPrefix.replace(/\/+$/, "") + "/";
|
|
298
|
+
const files = await b2ListFileNames(keyId, applicationKey, bucketName, cleanPrefix);
|
|
299
|
+
let deletedCount = 0;
|
|
300
|
+
for (const file of files) {
|
|
301
|
+
await b2DeleteFileVersion(keyId, applicationKey, file.fileName, file.fileId);
|
|
302
|
+
deletedCount++;
|
|
303
|
+
}
|
|
304
|
+
return { success: true, deletedCount };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/core/workspaceFileCore.ts
|
|
308
|
+
async function getFs() {
|
|
309
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
310
|
+
try {
|
|
311
|
+
const mod = await import("fs");
|
|
312
|
+
return mod.promises || mod.default?.promises || mod;
|
|
313
|
+
} catch {
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
async function getPath() {
|
|
319
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
320
|
+
try {
|
|
321
|
+
const mod = await import("path");
|
|
322
|
+
return mod.default || mod;
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
function getWorkspaceFolderPrefix(workspace) {
|
|
329
|
+
return workspace.localFolderKey || workspace.localFolderName || (workspace.workspacePath ? workspace.workspacePath.replace(/^\.\//, "") : workspace.id);
|
|
330
|
+
}
|
|
331
|
+
function isB2Workspace(workspace, connection) {
|
|
332
|
+
return connection?.provider === "backblaze_b2" || connection?.provider === "BACKBLAZE_B2" || workspace?.storageProvider === "backblaze_b2";
|
|
333
|
+
}
|
|
334
|
+
async function writeWorkspaceFile(workspace, connection, relativePath, content, contentType = "application/json") {
|
|
335
|
+
const cleanRel = relativePath.replace(/^\/+/, "");
|
|
336
|
+
const prefix = getWorkspaceFolderPrefix(workspace);
|
|
337
|
+
const buffer = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
338
|
+
if (isB2Workspace(workspace, connection)) {
|
|
339
|
+
const keyId = connection.b2KeyId || connection.keyId;
|
|
340
|
+
const appKey = connection.b2ApplicationKey || connection.appKey;
|
|
341
|
+
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
342
|
+
const fullKey = `${prefix}/${cleanRel}`;
|
|
343
|
+
await b2UploadFile(keyId, appKey, bucketName, fullKey, buffer, contentType);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const fs3 = await getFs();
|
|
347
|
+
const path3 = await getPath();
|
|
348
|
+
if (!fs3 || !path3) throw new Error("Local filesystem operations not supported in this runtime.");
|
|
349
|
+
const localDir = path3.resolve(process.cwd(), prefix, path3.dirname(cleanRel));
|
|
350
|
+
await fs3.mkdir(localDir, { recursive: true });
|
|
351
|
+
await fs3.writeFile(path3.resolve(process.cwd(), prefix, cleanRel), buffer);
|
|
352
|
+
}
|
|
353
|
+
async function readWorkspaceFile(workspace, connection, relativePath) {
|
|
354
|
+
const cleanRel = relativePath.replace(/^\/+/, "");
|
|
355
|
+
const prefix = getWorkspaceFolderPrefix(workspace);
|
|
356
|
+
if (isB2Workspace(workspace, connection)) {
|
|
357
|
+
const keyId = connection.b2KeyId || connection.keyId;
|
|
358
|
+
const appKey = connection.b2ApplicationKey || connection.appKey;
|
|
359
|
+
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
360
|
+
const fullKey = `${prefix}/${cleanRel}`;
|
|
361
|
+
return await b2DownloadFile(keyId, appKey, bucketName, fullKey);
|
|
362
|
+
}
|
|
363
|
+
const fs3 = await getFs();
|
|
364
|
+
const path3 = await getPath();
|
|
365
|
+
if (!fs3 || !path3) return null;
|
|
366
|
+
const fullPath = path3.resolve(process.cwd(), prefix, cleanRel);
|
|
367
|
+
try {
|
|
368
|
+
const buf = await fs3.readFile(fullPath);
|
|
369
|
+
return new Uint8Array(buf);
|
|
370
|
+
} catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async function deleteWorkspaceFile(workspace, connection, relativePath) {
|
|
375
|
+
const cleanRel = relativePath.replace(/^\/+/, "");
|
|
376
|
+
const prefix = getWorkspaceFolderPrefix(workspace);
|
|
377
|
+
if (isB2Workspace(workspace, connection)) {
|
|
378
|
+
const keyId = connection.b2KeyId || connection.keyId;
|
|
379
|
+
const appKey = connection.b2ApplicationKey || connection.appKey;
|
|
380
|
+
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
381
|
+
const fullKey = `${prefix}/${cleanRel}`;
|
|
382
|
+
const files = await b2ListFileNames(keyId, appKey, bucketName, fullKey);
|
|
383
|
+
const match = files.find((f) => f.fileName === fullKey);
|
|
384
|
+
if (match) {
|
|
385
|
+
await b2DeleteFileVersion(keyId, appKey, match.fileName, match.fileId);
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
const fs3 = await getFs();
|
|
391
|
+
const path3 = await getPath();
|
|
392
|
+
if (!fs3 || !path3) return false;
|
|
393
|
+
const fullPath = path3.resolve(process.cwd(), prefix, cleanRel);
|
|
394
|
+
try {
|
|
395
|
+
await fs3.unlink(fullPath);
|
|
396
|
+
return true;
|
|
397
|
+
} catch {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function listWorkspaceFiles(workspace, connection, subPrefix = "") {
|
|
402
|
+
const folderPrefix = getWorkspaceFolderPrefix(workspace);
|
|
403
|
+
const targetPrefix = subPrefix ? `${folderPrefix}/${subPrefix.replace(/^\/+/, "")}` : `${folderPrefix}/`;
|
|
404
|
+
if (isB2Workspace(workspace, connection)) {
|
|
405
|
+
const keyId = connection.b2KeyId || connection.keyId;
|
|
406
|
+
const appKey = connection.b2ApplicationKey || connection.appKey;
|
|
407
|
+
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
408
|
+
const rawFiles = await b2ListFileNames(keyId, appKey, bucketName, targetPrefix);
|
|
409
|
+
return rawFiles.map((f) => ({
|
|
410
|
+
fileName: f.fileName.slice(folderPrefix.length + 1),
|
|
411
|
+
sizeBytes: f.contentLength,
|
|
412
|
+
uploadTimestamp: f.uploadTimestamp
|
|
413
|
+
}));
|
|
414
|
+
}
|
|
415
|
+
const fs3 = await getFs();
|
|
416
|
+
const path3 = await getPath();
|
|
417
|
+
if (!fs3 || !path3) return [];
|
|
418
|
+
const baseDir = path3.resolve(process.cwd(), folderPrefix, subPrefix);
|
|
419
|
+
try {
|
|
420
|
+
const entries = await fs3.readdir(baseDir, { withFileTypes: true, recursive: true });
|
|
421
|
+
const result = [];
|
|
422
|
+
for (const entry of entries) {
|
|
423
|
+
if (entry.isFile()) {
|
|
424
|
+
const full = path3.join(entry.parentPath || baseDir, entry.name);
|
|
425
|
+
const st = await fs3.stat(full);
|
|
426
|
+
result.push({
|
|
427
|
+
fileName: path3.relative(path3.resolve(process.cwd(), folderPrefix), full),
|
|
428
|
+
sizeBytes: st.size
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
433
|
+
} catch {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async function deleteWorkspaceFolder(workspace, connection) {
|
|
438
|
+
const prefix = getWorkspaceFolderPrefix(workspace);
|
|
439
|
+
if (isB2Workspace(workspace, connection)) {
|
|
440
|
+
const keyId = connection.b2KeyId || connection.keyId;
|
|
441
|
+
const appKey = connection.b2ApplicationKey || connection.appKey;
|
|
442
|
+
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
443
|
+
await b2DeleteFolder(keyId, appKey, bucketName, prefix);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const fs3 = await getFs();
|
|
447
|
+
const path3 = await getPath();
|
|
448
|
+
if (!fs3 || !path3) return;
|
|
449
|
+
const localDir = path3.resolve(process.cwd(), prefix);
|
|
450
|
+
try {
|
|
451
|
+
await fs3.rm(localDir, { recursive: true, force: true });
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/core/workspaceBucketSyncCore.ts
|
|
457
|
+
function resolveWorkspaceConnection(profiles, workspace) {
|
|
458
|
+
const depm = profiles.depm || {};
|
|
459
|
+
const conns = depm.storage_connections?.length ? depm.storage_connections : profiles.srkl?.storage_connections || profiles.docustack?.storage_connections || [];
|
|
460
|
+
return conns.find((c) => c.id === workspace.storageConnectionId) || conns.find((c) => c.role === "primary") || conns[0] || null;
|
|
461
|
+
}
|
|
462
|
+
async function syncWorkspaceCreateToBucket(workspace, connection, env) {
|
|
463
|
+
if (!connection || connection.provider !== "backblaze_b2" && connection.provider !== "BACKBLAZE_B2") return;
|
|
464
|
+
try {
|
|
465
|
+
const wsMeta = {
|
|
466
|
+
id: workspace.id,
|
|
467
|
+
name: workspace.name,
|
|
468
|
+
createdAt: workspace.createdAt,
|
|
469
|
+
updatedAt: workspace.updatedAt,
|
|
470
|
+
storageProvider: "backblaze_b2",
|
|
471
|
+
bucket: connection.b2BucketName || "ultrst-apps"
|
|
472
|
+
};
|
|
473
|
+
await writeWorkspaceFile(workspace, connection, ".bzEmpty", new Uint8Array(0), "application/x-empty");
|
|
474
|
+
await writeWorkspaceFile(workspace, connection, "workspace.json", JSON.stringify(wsMeta, null, 2), "application/json");
|
|
475
|
+
await writeWorkspaceFile(
|
|
476
|
+
workspace,
|
|
477
|
+
connection,
|
|
478
|
+
"ecosystem.json",
|
|
479
|
+
JSON.stringify({ apps: [], nodes: [], edges: [], ecosystems: [] }, null, 2),
|
|
480
|
+
"application/json"
|
|
481
|
+
);
|
|
482
|
+
} catch (err) {
|
|
483
|
+
console.warn("[WorkspaceBucketSync] Create sync error:", err.message);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function syncWorkspaceDeleteToBucket(workspace, connection, env) {
|
|
487
|
+
if (!connection || connection.provider !== "backblaze_b2" && connection.provider !== "BACKBLAZE_B2") return;
|
|
488
|
+
try {
|
|
489
|
+
await deleteWorkspaceFolder(workspace, connection);
|
|
490
|
+
} catch (err) {
|
|
491
|
+
console.warn("[WorkspaceBucketSync] Delete sync error:", err.message);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async function syncWorkspaceDataToBucket(workspace, connection, data, env) {
|
|
495
|
+
if (!connection || connection.provider !== "backblaze_b2" && connection.provider !== "BACKBLAZE_B2") return;
|
|
496
|
+
try {
|
|
497
|
+
await writeWorkspaceFile(workspace, connection, "ecosystem.json", JSON.stringify(data, null, 2), "application/json");
|
|
498
|
+
} catch (err) {
|
|
499
|
+
console.warn("[WorkspaceBucketSync] Data sync error:", err.message);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/core/workspaceDataCore.ts
|
|
504
|
+
async function resolveUserRow(userId, env) {
|
|
505
|
+
if (userId) {
|
|
506
|
+
const res = await queryUnifiedDb(`SELECT * FROM "ultrst-users" WHERE id = $1 OR email = $1 LIMIT 1`, [userId], env);
|
|
507
|
+
if (res.rows.length > 0) return res.rows[0];
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
async function loadWorkspaceData(userId, workspaceId, env) {
|
|
512
|
+
if (!workspaceId) return { success: false, error: "workspaceId required", workspace: null };
|
|
513
|
+
const user = await resolveUserRow(userId, env);
|
|
514
|
+
if (!user) return { success: false, error: "User not found", workspace: null };
|
|
515
|
+
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
516
|
+
const depm = profiles.depm || {};
|
|
517
|
+
const workspaceDataMap = depm.workspace_data || {};
|
|
518
|
+
let data = workspaceDataMap[workspaceId];
|
|
519
|
+
const isEmptyData = !data || (!data.nodes || data.nodes.length === 0) && (!data.apps || data.apps.length === 0);
|
|
520
|
+
if (isEmptyData) {
|
|
521
|
+
const ws = (depm.workspaces || []).find((w) => w.id === workspaceId);
|
|
522
|
+
if (ws) {
|
|
523
|
+
const conn = resolveWorkspaceConnection(profiles, ws);
|
|
524
|
+
if (conn) {
|
|
525
|
+
try {
|
|
526
|
+
const fileBuf = await readWorkspaceFile(ws, conn, "ecosystem.json");
|
|
527
|
+
if (fileBuf && fileBuf.length > 0) {
|
|
528
|
+
const parsed = JSON.parse(new TextDecoder().decode(fileBuf));
|
|
529
|
+
if (parsed.nodes && parsed.nodes.length > 0 || parsed.apps && parsed.apps.length > 0) {
|
|
530
|
+
if (!profiles.depm) profiles.depm = {};
|
|
531
|
+
if (!profiles.depm.workspace_data) profiles.depm.workspace_data = {};
|
|
532
|
+
profiles.depm.workspace_data[workspaceId] = parsed;
|
|
533
|
+
data = parsed;
|
|
534
|
+
await queryUnifiedDb(
|
|
535
|
+
`UPDATE "ultrst-users" SET app_profiles = $1, updated_at = NOW() WHERE id = $2`,
|
|
536
|
+
[JSON.stringify(profiles), user.id],
|
|
537
|
+
env
|
|
538
|
+
);
|
|
539
|
+
return { success: true, workspace: data, workspaceId };
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
} catch (err) {
|
|
543
|
+
console.warn("[WorkspaceDataCore] Storage recovery check:", err.message);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (!data) {
|
|
549
|
+
const ws = (depm.workspaces || []).find((w) => w.id === workspaceId);
|
|
550
|
+
const ecoName = ws?.ecosystemName || `${ws?.name || "Workspace"} Ecosystem`;
|
|
551
|
+
const fallback = {
|
|
552
|
+
apps: [],
|
|
553
|
+
accounts: [],
|
|
554
|
+
deployments: [],
|
|
555
|
+
nodes: [],
|
|
556
|
+
edges: [],
|
|
557
|
+
ecosystems: [
|
|
558
|
+
{
|
|
559
|
+
id: `eco_${workspaceId}`,
|
|
560
|
+
name: ecoName,
|
|
561
|
+
icon: "Globe",
|
|
562
|
+
health: "HEALTHY",
|
|
563
|
+
status: "SYSTEM ONLINE",
|
|
564
|
+
connectedAppsCount: 0,
|
|
565
|
+
meshLinkCount: 0,
|
|
566
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
567
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
568
|
+
}
|
|
569
|
+
],
|
|
570
|
+
ecosystemName: ecoName,
|
|
571
|
+
ecosystemNodes: [
|
|
572
|
+
{
|
|
573
|
+
id: `node_eco_root_${workspaceId}`,
|
|
574
|
+
type: "ECOSYSTEM",
|
|
575
|
+
name: ecoName,
|
|
576
|
+
x: 0,
|
|
577
|
+
y: 0,
|
|
578
|
+
appId: "__ecosystem__",
|
|
579
|
+
icon: "Globe",
|
|
580
|
+
ecosystemData: { connectedAppsCount: 0, health: "HEALTHY", status: "SYSTEM ONLINE" }
|
|
581
|
+
}
|
|
582
|
+
],
|
|
583
|
+
ecosystemEdges: []
|
|
584
|
+
};
|
|
585
|
+
return { success: true, workspace: fallback, workspaceId };
|
|
586
|
+
}
|
|
587
|
+
return { success: true, workspace: data, workspaceId };
|
|
588
|
+
}
|
|
589
|
+
async function saveWorkspaceData(userId, workspaceId, data, env) {
|
|
590
|
+
if (!workspaceId) return { success: false, error: "workspaceId required" };
|
|
591
|
+
const user = await resolveUserRow(userId, env);
|
|
592
|
+
if (!user) return { success: false, error: "User not found" };
|
|
593
|
+
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
594
|
+
if (!profiles.depm) profiles.depm = {};
|
|
595
|
+
if (!profiles.depm.workspace_data) profiles.depm.workspace_data = {};
|
|
596
|
+
const existing = profiles.depm.workspace_data[workspaceId];
|
|
597
|
+
const incomingEmpty = (!data.nodes || data.nodes.length === 0) && (!data.apps || data.apps.length === 0);
|
|
598
|
+
const existingHasData = existing && (existing.nodes && existing.nodes.length > 0 || existing.apps && existing.apps.length > 0);
|
|
599
|
+
if (incomingEmpty && existingHasData && !data._forceEmptySave) {
|
|
600
|
+
console.warn(`[WorkspaceDataCore] Blocked empty overwrite for workspace ${workspaceId}.`);
|
|
601
|
+
return { success: true, workspaceId, workspace: existing, protected: true };
|
|
602
|
+
}
|
|
603
|
+
profiles.depm.workspace_data[workspaceId] = data;
|
|
604
|
+
if (Array.isArray(profiles.depm.workspaces)) {
|
|
605
|
+
const ws2 = profiles.depm.workspaces.find((w) => w.id === workspaceId);
|
|
606
|
+
if (ws2) {
|
|
607
|
+
ws2.appsCount = Array.isArray(data.apps) ? data.apps.length : 0;
|
|
608
|
+
ws2.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
await queryUnifiedDb(
|
|
612
|
+
`UPDATE "ultrst-users" SET app_profiles = $1, updated_at = NOW() WHERE id = $2`,
|
|
613
|
+
[JSON.stringify(profiles), user.id],
|
|
614
|
+
env
|
|
615
|
+
);
|
|
616
|
+
const ws = Array.isArray(profiles.depm?.workspaces) ? profiles.depm.workspaces.find((w) => w.id === workspaceId) : null;
|
|
617
|
+
if (ws) {
|
|
618
|
+
const conn = resolveWorkspaceConnection(profiles, ws);
|
|
619
|
+
await syncWorkspaceDataToBucket(ws, conn, data, env);
|
|
620
|
+
}
|
|
621
|
+
return { success: true, workspaceId, workspace: data };
|
|
622
|
+
}
|
|
623
|
+
async function renameAppInWorkspaceData(userId, workspaceId, appId, newName, env) {
|
|
624
|
+
if (!workspaceId || !appId || !newName) {
|
|
625
|
+
return { success: false, error: "workspaceId, appId and newName required" };
|
|
626
|
+
}
|
|
627
|
+
const loadRes = await loadWorkspaceData(userId, workspaceId, env);
|
|
628
|
+
if (!loadRes.success || !loadRes.workspace) return loadRes;
|
|
629
|
+
const wsData = loadRes.workspace;
|
|
630
|
+
const trimmed = newName.trim();
|
|
631
|
+
const folderName = trimmed.replace(/[/\\?%*:|"<>]/g, "_");
|
|
632
|
+
if (Array.isArray(wsData.apps)) {
|
|
633
|
+
const targetApp = wsData.apps.find((a) => a.id === appId);
|
|
634
|
+
if (targetApp) {
|
|
635
|
+
targetApp.name = trimmed;
|
|
636
|
+
targetApp.folderName = folderName;
|
|
637
|
+
targetApp.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (Array.isArray(wsData.nodes)) {
|
|
641
|
+
wsData.nodes = wsData.nodes.map((n) => {
|
|
642
|
+
if (n.appId === appId && (n.type === "APP" || n.type === "APP_NODE" || n.type === "APP_GRAPH_NODE")) {
|
|
643
|
+
return { ...n, name: trimmed, appData: n.appData ? { ...n.appData, name: trimmed, folderName } : void 0 };
|
|
644
|
+
}
|
|
645
|
+
return n;
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
return await saveWorkspaceData(userId, workspaceId, wsData, env);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/core/workspaceStorageDetailsCore.ts
|
|
652
|
+
async function getWorkspaceStorageDetails(workspaceId, env) {
|
|
653
|
+
if (!workspaceId) return { success: false, error: "workspaceId is required" };
|
|
654
|
+
const { workspace, connection } = await findUserWorkspace(workspaceId, env);
|
|
655
|
+
const folderPrefix = workspace?.localFolderKey || workspace?.localFolderName || (workspace?.workspacePath ? workspace.workspacePath.replace(/^\.\//, "") : workspaceId);
|
|
656
|
+
if (!connection || connection.provider !== "backblaze_b2" && connection.provider !== "BACKBLAZE_B2") {
|
|
657
|
+
return {
|
|
658
|
+
success: true,
|
|
659
|
+
workspaceId,
|
|
660
|
+
storageProvider: workspace?.storageProvider || "local_fs",
|
|
661
|
+
connection: connection || null,
|
|
662
|
+
folderPrefix,
|
|
663
|
+
localStats: { exists: false, appsCount: 0, ecosystemsCount: 0 },
|
|
664
|
+
bucket: null,
|
|
665
|
+
files: []
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
const keyId = connection.b2KeyId || connection.keyId;
|
|
669
|
+
const applicationKey = connection.b2ApplicationKey || connection.appKey;
|
|
670
|
+
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
671
|
+
try {
|
|
672
|
+
const buckets = await b2ListBuckets(keyId, applicationKey, void 0, bucketName);
|
|
673
|
+
const bucketObj = buckets.find((b) => b.bucketName === bucketName);
|
|
674
|
+
if (!bucketObj) {
|
|
675
|
+
return {
|
|
676
|
+
success: true,
|
|
677
|
+
workspaceId,
|
|
678
|
+
storageProvider: "backblaze_b2",
|
|
679
|
+
connection: { id: connection.id, label: connection.label, provider: connection.provider },
|
|
680
|
+
folderPrefix,
|
|
681
|
+
bucket: { name: bucketName, status: "BUCKET_NOT_FOUND" },
|
|
682
|
+
files: []
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
const rawFiles = await b2ListFileNames(keyId, applicationKey, bucketName, `${folderPrefix}/`);
|
|
686
|
+
return {
|
|
687
|
+
success: true,
|
|
688
|
+
workspaceId,
|
|
689
|
+
storageProvider: "backblaze_b2",
|
|
690
|
+
connection: { id: connection.id, label: connection.label, provider: connection.provider },
|
|
691
|
+
folderPrefix,
|
|
692
|
+
bucket: {
|
|
693
|
+
id: bucketObj.bucketId,
|
|
694
|
+
name: bucketObj.bucketName,
|
|
695
|
+
type: bucketObj.bucketType,
|
|
696
|
+
endpoint: connection.b2Endpoint || "s3.eu-central-003.backblazeb2.com",
|
|
697
|
+
region: connection.b2Region || "eu-central-003",
|
|
698
|
+
status: "CONNECTED"
|
|
699
|
+
},
|
|
700
|
+
fileCount: rawFiles.length,
|
|
701
|
+
files: rawFiles.map((f) => ({
|
|
702
|
+
fileName: f.fileName,
|
|
703
|
+
sizeBytes: f.contentLength,
|
|
704
|
+
uploadTimestamp: f.uploadTimestamp
|
|
705
|
+
}))
|
|
706
|
+
};
|
|
707
|
+
} catch (err) {
|
|
708
|
+
return {
|
|
709
|
+
success: false,
|
|
710
|
+
workspaceId,
|
|
711
|
+
storageProvider: "backblaze_b2",
|
|
712
|
+
error: err.message,
|
|
713
|
+
folderPrefix
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/core/workspaceCore.ts
|
|
719
|
+
async function findUserWorkspace(workspaceId, env) {
|
|
720
|
+
const res = await queryUnifiedDb(`SELECT id, email, username, app_profiles FROM "ultrst-users"`, [], env);
|
|
721
|
+
for (const user of res.rows) {
|
|
722
|
+
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
723
|
+
const depm = profiles.depm || {};
|
|
724
|
+
const workspaces = Array.isArray(depm.workspaces) ? depm.workspaces : [];
|
|
725
|
+
const ws = workspaces.find((w) => w.id === workspaceId);
|
|
726
|
+
if (ws) {
|
|
727
|
+
const conns = depm.storage_connections || profiles.srkl?.storage_connections || profiles.docustack?.storage_connections || [];
|
|
728
|
+
const conn = conns.find((c) => c.id === ws.storageConnectionId) || conns.find((c) => c.role === "primary") || conns[0] || null;
|
|
729
|
+
return { user, workspace: ws, connection: conn };
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return { user: null, workspace: null, connection: null };
|
|
733
|
+
}
|
|
734
|
+
async function getWorkspaces(userId, env) {
|
|
735
|
+
if (!userId) {
|
|
736
|
+
return { success: true, workspaces: [], activeWorkspaceId: null, storage_connections: [] };
|
|
737
|
+
}
|
|
738
|
+
const res = await queryUnifiedDb(`SELECT * FROM "ultrst-users" WHERE id = $1 OR email = $1 LIMIT 1`, [userId], env);
|
|
739
|
+
const userRow = res.rows[0];
|
|
740
|
+
if (!userRow) {
|
|
741
|
+
return { success: true, workspaces: [], activeWorkspaceId: null, storage_connections: [] };
|
|
742
|
+
}
|
|
743
|
+
const profiles = typeof userRow.app_profiles === "string" ? JSON.parse(userRow.app_profiles) : userRow.app_profiles || {};
|
|
744
|
+
const depm = profiles.depm || {};
|
|
745
|
+
const workspaces = Array.isArray(depm.workspaces) ? depm.workspaces : [];
|
|
746
|
+
const activeWorkspaceId = depm.activeWorkspaceId || null;
|
|
747
|
+
const conns = depm.storage_connections?.length ? depm.storage_connections : profiles.srkl?.storage_connections || [];
|
|
748
|
+
return { success: true, workspaces, activeWorkspaceId, storage_connections: conns };
|
|
749
|
+
}
|
|
750
|
+
async function createWorkspace(userId, wsData, env) {
|
|
751
|
+
const uRes = await queryUnifiedDb(`SELECT * FROM "ultrst-users" WHERE id = $1`, [userId], env);
|
|
752
|
+
const user = uRes.rows[0];
|
|
753
|
+
if (!user) throw new Error("User account not found.");
|
|
754
|
+
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
755
|
+
if (!profiles.depm) profiles.depm = {};
|
|
756
|
+
const workspaces = Array.isArray(profiles.depm.workspaces) ? profiles.depm.workspaces : [];
|
|
757
|
+
const wsName = (wsData.name || "New Workspace").trim();
|
|
758
|
+
const wsId = wsData.id || `ws-depm-${Date.now()}`;
|
|
759
|
+
const userName = user.username || user.name || "user";
|
|
760
|
+
const folderKey = `ultrst_depm_${userName.toLowerCase().replace(/[^a-z0-9]/g, "_")}_${wsName.toLowerCase().replace(/[^a-z0-9]/g, "_")}`;
|
|
761
|
+
const newWs = {
|
|
762
|
+
...wsData,
|
|
763
|
+
id: wsId,
|
|
764
|
+
name: wsName,
|
|
765
|
+
localFolderName: folderKey,
|
|
766
|
+
localFolderKey: folderKey,
|
|
767
|
+
workspacePath: `./${folderKey}`,
|
|
768
|
+
storageProvider: wsData.storageProvider || "backblaze_b2",
|
|
769
|
+
b2BucketName: wsData.b2BucketName || "ultrst-apps",
|
|
770
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
771
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
772
|
+
};
|
|
773
|
+
workspaces.push(newWs);
|
|
774
|
+
profiles.depm.workspaces = workspaces;
|
|
775
|
+
profiles.depm.activeWorkspaceId = wsId;
|
|
776
|
+
await queryUnifiedDb(`UPDATE "ultrst-users" SET app_profiles = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(profiles), user.id], env);
|
|
777
|
+
const conn = resolveWorkspaceConnection(profiles, newWs);
|
|
778
|
+
await syncWorkspaceCreateToBucket(newWs, conn, env);
|
|
779
|
+
return { success: true, workspace: newWs, workspaces };
|
|
780
|
+
}
|
|
781
|
+
async function updateWorkspace(userId, params, env) {
|
|
782
|
+
const uRes = await queryUnifiedDb(`SELECT * FROM "ultrst-users" WHERE id = $1`, [userId], env);
|
|
783
|
+
const user = uRes.rows[0];
|
|
784
|
+
if (!user) throw new Error("User not found.");
|
|
785
|
+
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
786
|
+
if (!profiles.depm) profiles.depm = {};
|
|
787
|
+
const workspaces = Array.isArray(profiles.depm.workspaces) ? profiles.depm.workspaces : [];
|
|
788
|
+
const { action, workspaceId, updates } = params;
|
|
789
|
+
const targetId = workspaceId || updates?.id;
|
|
790
|
+
const match = workspaces.find((w) => w.id === targetId);
|
|
791
|
+
if (!match) throw new Error("Workspace not found.");
|
|
792
|
+
if (action === "switch") {
|
|
793
|
+
profiles.depm.activeWorkspaceId = targetId;
|
|
794
|
+
} else {
|
|
795
|
+
const safeUpdates = { ...updates || params };
|
|
796
|
+
delete safeUpdates.storageProvider;
|
|
797
|
+
delete safeUpdates.storageConnectionId;
|
|
798
|
+
delete safeUpdates.b2BucketName;
|
|
799
|
+
delete safeUpdates.b2Endpoint;
|
|
800
|
+
delete safeUpdates.b2Region;
|
|
801
|
+
delete safeUpdates.localFolderName;
|
|
802
|
+
delete safeUpdates.localFolderKey;
|
|
803
|
+
delete safeUpdates.workspacePath;
|
|
804
|
+
delete safeUpdates.primaryStorage;
|
|
805
|
+
delete safeUpdates.id;
|
|
806
|
+
delete safeUpdates.action;
|
|
807
|
+
delete safeUpdates.workspaceId;
|
|
808
|
+
Object.assign(match, safeUpdates, { updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
809
|
+
}
|
|
810
|
+
await queryUnifiedDb(`UPDATE "ultrst-users" SET app_profiles = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(profiles), user.id], env);
|
|
811
|
+
return { success: true, workspace: match, workspaces };
|
|
812
|
+
}
|
|
813
|
+
async function deleteWorkspace(userId, wsId, env) {
|
|
814
|
+
const uRes = await queryUnifiedDb(`SELECT * FROM "ultrst-users" WHERE id = $1`, [userId], env);
|
|
815
|
+
const user = uRes.rows[0];
|
|
816
|
+
if (!user) throw new Error("User not found.");
|
|
817
|
+
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
818
|
+
if (!profiles.depm?.workspaces) return { success: true, workspaces: [] };
|
|
819
|
+
const targetWs = profiles.depm.workspaces.find((w) => w.id === wsId);
|
|
820
|
+
const conn = targetWs ? resolveWorkspaceConnection(profiles, targetWs) : null;
|
|
821
|
+
profiles.depm.workspaces = profiles.depm.workspaces.filter((w) => w.id !== wsId);
|
|
822
|
+
if (profiles.depm.activeWorkspaceId === wsId) {
|
|
823
|
+
profiles.depm.activeWorkspaceId = profiles.depm.workspaces[0]?.id || null;
|
|
824
|
+
}
|
|
825
|
+
if (profiles.depm.workspace_data?.[wsId]) {
|
|
826
|
+
delete profiles.depm.workspace_data[wsId];
|
|
827
|
+
}
|
|
828
|
+
await queryUnifiedDb(`UPDATE "ultrst-users" SET app_profiles = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(profiles), user.id], env);
|
|
829
|
+
if (targetWs && conn) {
|
|
830
|
+
await syncWorkspaceDeleteToBucket(targetWs, conn, env);
|
|
831
|
+
}
|
|
832
|
+
return { success: true, workspaces: profiles.depm.workspaces };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/workspace/RemoteWorkspaceService.ts
|
|
836
|
+
var RemoteWorkspaceService = class {
|
|
837
|
+
type = "remote";
|
|
838
|
+
async findWorkspace(workspaceId, env) {
|
|
839
|
+
return await findUserWorkspace(workspaceId, env);
|
|
840
|
+
}
|
|
841
|
+
async listWorkspaces(userId, env) {
|
|
842
|
+
return await getWorkspaces(userId, env);
|
|
843
|
+
}
|
|
844
|
+
async createWorkspace(userId, data, env) {
|
|
845
|
+
return await createWorkspace(userId, data, env);
|
|
846
|
+
}
|
|
847
|
+
async updateWorkspace(userId, params, env) {
|
|
848
|
+
return await updateWorkspace(userId, params, env);
|
|
849
|
+
}
|
|
850
|
+
async deleteWorkspace(userId, workspaceId, env) {
|
|
851
|
+
return await deleteWorkspace(userId, workspaceId, env);
|
|
852
|
+
}
|
|
853
|
+
async readFile(workspaceId, filePath, env) {
|
|
854
|
+
const { workspace, connection } = await this.findWorkspace(workspaceId, env);
|
|
855
|
+
if (!workspace) throw new Error(`Workspace "${workspaceId}" not found.`);
|
|
856
|
+
const data = await readWorkspaceFile(workspace, connection, filePath);
|
|
857
|
+
if (!data) throw new Error(`File "${filePath}" not found in workspace "${workspaceId}".`);
|
|
858
|
+
return data;
|
|
859
|
+
}
|
|
860
|
+
async writeFile(workspaceId, filePath, content, env) {
|
|
861
|
+
const { workspace, connection } = await this.findWorkspace(workspaceId, env);
|
|
862
|
+
if (!workspace) throw new Error(`Workspace "${workspaceId}" not found.`);
|
|
863
|
+
await writeWorkspaceFile(workspace, connection, filePath, content);
|
|
864
|
+
const size = typeof content === "string" ? new TextEncoder().encode(content).length : content.byteLength;
|
|
865
|
+
return { success: true, path: filePath, sizeBytes: size };
|
|
866
|
+
}
|
|
867
|
+
async deleteFile(workspaceId, filePath, env) {
|
|
868
|
+
const { workspace, connection } = await this.findWorkspace(workspaceId, env);
|
|
869
|
+
if (!workspace) throw new Error(`Workspace "${workspaceId}" not found.`);
|
|
870
|
+
const deleted = await deleteWorkspaceFile(workspace, connection, filePath);
|
|
871
|
+
return { success: deleted };
|
|
872
|
+
}
|
|
873
|
+
async listFiles(workspaceId, subfolder = "", env) {
|
|
874
|
+
const { workspace, connection } = await this.findWorkspace(workspaceId, env);
|
|
875
|
+
if (!workspace) throw new Error(`Workspace "${workspaceId}" not found.`);
|
|
876
|
+
const files = await listWorkspaceFiles(workspace, connection, subfolder);
|
|
877
|
+
return files.map((f) => ({
|
|
878
|
+
path: f.fileName,
|
|
879
|
+
name: f.fileName.split("/").pop() || f.fileName,
|
|
880
|
+
sizeBytes: f.sizeBytes,
|
|
881
|
+
updatedAt: f.uploadTimestamp
|
|
882
|
+
}));
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
var remoteWorkspaceService = new RemoteWorkspaceService();
|
|
886
|
+
|
|
887
|
+
// src/workspace/WorkingWorkspaceService.ts
|
|
888
|
+
import fs2 from "fs";
|
|
889
|
+
import path2 from "path";
|
|
890
|
+
|
|
891
|
+
// src/core/workspaceSyncEngine.ts
|
|
892
|
+
import fs from "fs";
|
|
893
|
+
import path from "path";
|
|
894
|
+
|
|
895
|
+
// src/core/workspaceCanonical.ts
|
|
896
|
+
function formatCanonicalWorkspaceFolderName(appName, userName, workspaceName) {
|
|
897
|
+
const safeApp = (appName || "qwnsr").replace(/[^a-zA-Z0-9]/g, "_").toLowerCase();
|
|
898
|
+
const safeUser = (userName || "user").replace(/[^a-zA-Z0-9]/g, "_").toLowerCase();
|
|
899
|
+
const safeWs = (workspaceName || "workspace").replace(/[^a-zA-Z0-9]/g, "_").toLowerCase();
|
|
900
|
+
return `ultrst_${safeApp}_${safeUser}_${safeWs}`;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/core/workspaceSyncEngine.ts
|
|
904
|
+
var ACCOUNTS_FILE = path.resolve(".skipo/accounts.json");
|
|
905
|
+
var CONFIG_FILE = path.resolve(".skipo/workspace_config.json");
|
|
906
|
+
function getDefaultWorkingWorkspaceDir() {
|
|
907
|
+
if (process.env.VERCEL || process.env.AWS_LAMBDA_FUNCTION_NAME) {
|
|
908
|
+
return path.join("/tmp", "workspace");
|
|
909
|
+
}
|
|
910
|
+
return path.join(process.cwd(), "workspace");
|
|
911
|
+
}
|
|
912
|
+
function getWorkingWorkspaceConfig() {
|
|
913
|
+
try {
|
|
914
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
915
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
916
|
+
const parsed = JSON.parse(raw);
|
|
917
|
+
if (parsed.workingDirectory) {
|
|
918
|
+
return {
|
|
919
|
+
workingDirectory: parsed.workingDirectory,
|
|
920
|
+
autoSyncIntervalMinutes: parsed.autoSyncIntervalMinutes || 5
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
} catch {
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
workingDirectory: getDefaultWorkingWorkspaceDir(),
|
|
928
|
+
autoSyncIntervalMinutes: 5
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
function setWorkingWorkspaceConfig(cfg) {
|
|
932
|
+
const current = getWorkingWorkspaceConfig();
|
|
933
|
+
const updated = {
|
|
934
|
+
workingDirectory: (cfg.workingDirectory || current.workingDirectory).trim(),
|
|
935
|
+
autoSyncIntervalMinutes: cfg.autoSyncIntervalMinutes || current.autoSyncIntervalMinutes
|
|
936
|
+
};
|
|
937
|
+
try {
|
|
938
|
+
fs.mkdirSync(path.dirname(CONFIG_FILE), { recursive: true });
|
|
939
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2), "utf-8");
|
|
940
|
+
} catch (err) {
|
|
941
|
+
console.warn("[WorkspaceSync] Failed to persist working config:", err);
|
|
942
|
+
}
|
|
943
|
+
return updated;
|
|
944
|
+
}
|
|
945
|
+
function getWorkingWorkspaceDir() {
|
|
946
|
+
const dir = getWorkingWorkspaceConfig().workingDirectory;
|
|
947
|
+
if (!fs.existsSync(dir)) {
|
|
948
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
949
|
+
}
|
|
950
|
+
return dir;
|
|
951
|
+
}
|
|
952
|
+
async function findWorkspaceRecord(workspaceId) {
|
|
953
|
+
try {
|
|
954
|
+
if (fs.existsSync(ACCOUNTS_FILE)) {
|
|
955
|
+
const raw = fs.readFileSync(ACCOUNTS_FILE, "utf-8");
|
|
956
|
+
const accounts = JSON.parse(raw);
|
|
957
|
+
for (const u of Object.values(accounts)) {
|
|
958
|
+
if (Array.isArray(u.workspaces)) {
|
|
959
|
+
const ws = u.workspaces.find((w) => w.id === workspaceId);
|
|
960
|
+
if (ws) {
|
|
961
|
+
const conns = Array.isArray(u.storage_connections) ? u.storage_connections : [];
|
|
962
|
+
const conn = conns.find((c) => c.id === ws.storageConnectionId) || conns[0] || null;
|
|
963
|
+
return { user: u, workspace: ws, connection: conn };
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
} catch {
|
|
969
|
+
}
|
|
970
|
+
try {
|
|
971
|
+
const res = await queryUnifiedDb(`SELECT id, email, username, app_profiles FROM "ultrst-users"`);
|
|
972
|
+
for (const u of res.rows) {
|
|
973
|
+
const profiles = typeof u.app_profiles === "string" ? JSON.parse(u.app_profiles) : u.app_profiles || {};
|
|
974
|
+
const qwnsr = profiles.qwnsr || {};
|
|
975
|
+
const workspaces = Array.isArray(qwnsr.workspaces) ? qwnsr.workspaces : [];
|
|
976
|
+
const ws = workspaces.find((w) => w.id === workspaceId);
|
|
977
|
+
if (ws) {
|
|
978
|
+
const conns = qwnsr.storage_connections || profiles.srkl?.storage_connections || [];
|
|
979
|
+
const conn = conns.find((c) => c.id === ws.storageConnectionId) || conns[0] || null;
|
|
980
|
+
return { user: u, workspace: ws, connection: conn };
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
} catch {
|
|
984
|
+
}
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
function getWorkspaceFolderKey(workspace, user) {
|
|
988
|
+
if (workspace.localFolderKey) return workspace.localFolderKey;
|
|
989
|
+
if (workspace.localFolderName && workspace.localFolderName.startsWith("ultrst_")) return workspace.localFolderName;
|
|
990
|
+
const userName = user?.username || user?.name || (user?.email ? user.email.split("@")[0] : "user");
|
|
991
|
+
return formatCanonicalWorkspaceFolderName("qwnsr", userName, workspace.name);
|
|
992
|
+
}
|
|
993
|
+
async function collectLocalFiles(dir, baseDir = dir) {
|
|
994
|
+
if (!fs.existsSync(dir)) return [];
|
|
995
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
996
|
+
const results = [];
|
|
997
|
+
for (const entry of entries) {
|
|
998
|
+
if (entry.name.startsWith(".") && entry.name !== ".bzEmpty") continue;
|
|
999
|
+
const full = path.join(dir, entry.name);
|
|
1000
|
+
if (entry.isDirectory()) {
|
|
1001
|
+
const nested = await collectLocalFiles(full, baseDir);
|
|
1002
|
+
results.push(...nested);
|
|
1003
|
+
} else if (entry.isFile()) {
|
|
1004
|
+
const st = await fs.promises.stat(full);
|
|
1005
|
+
const rel = path.relative(baseDir, full).replace(/\\/g, "/");
|
|
1006
|
+
results.push({ relPath: rel, fullPath: full, mtimeMs: st.mtimeMs, sizeBytes: st.size });
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return results;
|
|
1010
|
+
}
|
|
1011
|
+
async function syncWorkspace(workspaceId) {
|
|
1012
|
+
const match = await findWorkspaceRecord(workspaceId);
|
|
1013
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1014
|
+
if (!match) {
|
|
1015
|
+
return {
|
|
1016
|
+
success: false,
|
|
1017
|
+
workspaceId,
|
|
1018
|
+
folderKey: workspaceId,
|
|
1019
|
+
workingDir: getWorkingWorkspaceDir(),
|
|
1020
|
+
persistenceType: "local_fs",
|
|
1021
|
+
targetLocation: "Unknown",
|
|
1022
|
+
syncedFiles: [],
|
|
1023
|
+
uploadedCount: 0,
|
|
1024
|
+
downloadedCount: 0,
|
|
1025
|
+
syncedAt: nowIso,
|
|
1026
|
+
error: `Workspace '${workspaceId}' not found.`
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
const { workspace, user, connection } = match;
|
|
1030
|
+
const folderKey = getWorkspaceFolderKey(workspace, user);
|
|
1031
|
+
const workingDir = getWorkingWorkspaceDir();
|
|
1032
|
+
const isB2 = workspace.storageProvider === "backblaze_b2" || workspace.storageConfig?.type === "b2";
|
|
1033
|
+
const syncedFiles = [];
|
|
1034
|
+
let uploadedCount = 0;
|
|
1035
|
+
let downloadedCount = 0;
|
|
1036
|
+
if (isB2) {
|
|
1037
|
+
const creds = workspace.storageConfig || connection?.credentials || connection || {};
|
|
1038
|
+
const keyId = creds.keyId || creds.b2KeyId;
|
|
1039
|
+
const appKey = creds.applicationKey || creds.b2ApplicationKey || creds.appKey;
|
|
1040
|
+
const bucketName = creds.bucketName || creds.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
1041
|
+
if (!keyId || !appKey) {
|
|
1042
|
+
return {
|
|
1043
|
+
success: false,
|
|
1044
|
+
workspaceId,
|
|
1045
|
+
folderKey,
|
|
1046
|
+
workingDir,
|
|
1047
|
+
persistenceType: "backblaze_b2",
|
|
1048
|
+
targetLocation: bucketName,
|
|
1049
|
+
syncedFiles: [],
|
|
1050
|
+
uploadedCount: 0,
|
|
1051
|
+
downloadedCount: 0,
|
|
1052
|
+
syncedAt: nowIso,
|
|
1053
|
+
error: "Missing Backblaze B2 credentials for sync."
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
try {
|
|
1057
|
+
const b2Prefix = `${folderKey}/`;
|
|
1058
|
+
const remoteFiles = await b2ListFileNames(keyId, appKey, bucketName, b2Prefix);
|
|
1059
|
+
const remoteMap = /* @__PURE__ */ new Map();
|
|
1060
|
+
for (const rf of remoteFiles) {
|
|
1061
|
+
const cleanName = rf.fileName.slice(b2Prefix.length);
|
|
1062
|
+
if (cleanName) remoteMap.set(cleanName, { fileId: rf.fileId, size: rf.contentLength, timestamp: rf.uploadTimestamp });
|
|
1063
|
+
}
|
|
1064
|
+
const localFiles = await collectLocalFiles(workingDir);
|
|
1065
|
+
const localMap = /* @__PURE__ */ new Map();
|
|
1066
|
+
for (const lf of localFiles) {
|
|
1067
|
+
localMap.set(lf.relPath, lf);
|
|
1068
|
+
}
|
|
1069
|
+
for (const [relPath, rInfo] of remoteMap.entries()) {
|
|
1070
|
+
const lFile = localMap.get(relPath);
|
|
1071
|
+
const shouldDownload = !lFile || rInfo.timestamp > lFile.mtimeMs + 2e3;
|
|
1072
|
+
if (shouldDownload) {
|
|
1073
|
+
const buf = await b2DownloadFile(keyId, appKey, bucketName, `${b2Prefix}${relPath}`);
|
|
1074
|
+
if (buf) {
|
|
1075
|
+
const destPath = path.join(workingDir, relPath);
|
|
1076
|
+
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
|
|
1077
|
+
await fs.promises.writeFile(destPath, buf);
|
|
1078
|
+
try {
|
|
1079
|
+
const fileTime = new Date(rInfo.timestamp);
|
|
1080
|
+
await fs.promises.utimes(destPath, fileTime, fileTime);
|
|
1081
|
+
} catch {
|
|
1082
|
+
}
|
|
1083
|
+
syncedFiles.push({ fileName: relPath, action: "DOWNLOADED", sizeBytes: buf.byteLength });
|
|
1084
|
+
downloadedCount++;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
for (const [relPath, lFile] of localMap.entries()) {
|
|
1089
|
+
const rInfo = remoteMap.get(relPath);
|
|
1090
|
+
const shouldUpload = !rInfo || lFile.mtimeMs > rInfo.timestamp + 2e3;
|
|
1091
|
+
if (shouldUpload) {
|
|
1092
|
+
const content = await fs.promises.readFile(lFile.fullPath);
|
|
1093
|
+
await b2UploadFile(keyId, appKey, bucketName, `${b2Prefix}${relPath}`, new Uint8Array(content));
|
|
1094
|
+
syncedFiles.push({ fileName: relPath, action: "UPLOADED", sizeBytes: lFile.sizeBytes });
|
|
1095
|
+
uploadedCount++;
|
|
1096
|
+
} else if (!syncedFiles.some((f) => f.fileName === relPath)) {
|
|
1097
|
+
syncedFiles.push({ fileName: relPath, action: "UP_TO_DATE", sizeBytes: lFile.sizeBytes });
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return {
|
|
1101
|
+
success: true,
|
|
1102
|
+
workspaceId,
|
|
1103
|
+
folderKey,
|
|
1104
|
+
workingDir,
|
|
1105
|
+
persistenceType: "backblaze_b2",
|
|
1106
|
+
targetLocation: `B2://${bucketName}/${folderKey}/`,
|
|
1107
|
+
syncedFiles,
|
|
1108
|
+
uploadedCount,
|
|
1109
|
+
downloadedCount,
|
|
1110
|
+
syncedAt: nowIso
|
|
1111
|
+
};
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
return {
|
|
1114
|
+
success: false,
|
|
1115
|
+
workspaceId,
|
|
1116
|
+
folderKey,
|
|
1117
|
+
workingDir,
|
|
1118
|
+
persistenceType: "backblaze_b2",
|
|
1119
|
+
targetLocation: bucketName,
|
|
1120
|
+
syncedFiles,
|
|
1121
|
+
uploadedCount,
|
|
1122
|
+
downloadedCount,
|
|
1123
|
+
syncedAt: nowIso,
|
|
1124
|
+
error: err.message || "Backblaze sync failed."
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const vaultDir = path.resolve(process.cwd(), folderKey);
|
|
1129
|
+
await fs.promises.mkdir(vaultDir, { recursive: true });
|
|
1130
|
+
try {
|
|
1131
|
+
const vaultFiles = await collectLocalFiles(vaultDir);
|
|
1132
|
+
const vaultMap = new Map(vaultFiles.map((f) => [f.relPath, f]));
|
|
1133
|
+
const workingFiles = await collectLocalFiles(workingDir);
|
|
1134
|
+
const workingMap = new Map(workingFiles.map((f) => [f.relPath, f]));
|
|
1135
|
+
for (const [relPath, vFile] of vaultMap.entries()) {
|
|
1136
|
+
const wFile = workingMap.get(relPath);
|
|
1137
|
+
const shouldDownload = !wFile || vFile.mtimeMs > wFile.mtimeMs + 1e3;
|
|
1138
|
+
if (shouldDownload) {
|
|
1139
|
+
const dest = path.join(workingDir, relPath);
|
|
1140
|
+
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
1141
|
+
await fs.promises.copyFile(vFile.fullPath, dest);
|
|
1142
|
+
try {
|
|
1143
|
+
const vTime = new Date(vFile.mtimeMs);
|
|
1144
|
+
await fs.promises.utimes(dest, vTime, vTime);
|
|
1145
|
+
} catch {
|
|
1146
|
+
}
|
|
1147
|
+
syncedFiles.push({ fileName: relPath, action: "DOWNLOADED", sizeBytes: vFile.sizeBytes });
|
|
1148
|
+
downloadedCount++;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
for (const [relPath, wFile] of workingMap.entries()) {
|
|
1152
|
+
const vFile = vaultMap.get(relPath);
|
|
1153
|
+
const shouldUpload = !vFile || wFile.mtimeMs > vFile.mtimeMs + 1e3;
|
|
1154
|
+
if (shouldUpload) {
|
|
1155
|
+
const dest = path.join(vaultDir, relPath);
|
|
1156
|
+
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
1157
|
+
await fs.promises.copyFile(wFile.fullPath, dest);
|
|
1158
|
+
try {
|
|
1159
|
+
const wTime = new Date(wFile.mtimeMs);
|
|
1160
|
+
await fs.promises.utimes(dest, wTime, wTime);
|
|
1161
|
+
} catch {
|
|
1162
|
+
}
|
|
1163
|
+
syncedFiles.push({ fileName: relPath, action: "UPLOADED", sizeBytes: wFile.sizeBytes });
|
|
1164
|
+
uploadedCount++;
|
|
1165
|
+
} else if (!syncedFiles.some((f) => f.fileName === relPath)) {
|
|
1166
|
+
syncedFiles.push({ fileName: relPath, action: "UP_TO_DATE", sizeBytes: wFile.sizeBytes });
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return {
|
|
1170
|
+
success: true,
|
|
1171
|
+
workspaceId,
|
|
1172
|
+
folderKey,
|
|
1173
|
+
workingDir,
|
|
1174
|
+
persistenceType: "local_fs",
|
|
1175
|
+
targetLocation: vaultDir,
|
|
1176
|
+
syncedFiles,
|
|
1177
|
+
uploadedCount,
|
|
1178
|
+
downloadedCount,
|
|
1179
|
+
syncedAt: nowIso
|
|
1180
|
+
};
|
|
1181
|
+
} catch (err) {
|
|
1182
|
+
return {
|
|
1183
|
+
success: false,
|
|
1184
|
+
workspaceId,
|
|
1185
|
+
folderKey,
|
|
1186
|
+
workingDir,
|
|
1187
|
+
persistenceType: "local_fs",
|
|
1188
|
+
targetLocation: vaultDir,
|
|
1189
|
+
syncedFiles,
|
|
1190
|
+
uploadedCount,
|
|
1191
|
+
downloadedCount,
|
|
1192
|
+
syncedAt: nowIso,
|
|
1193
|
+
error: err.message
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/workspace/WorkingWorkspaceService.ts
|
|
1199
|
+
var WorkingWorkspaceService = class {
|
|
1200
|
+
constructor(workingDirResolver) {
|
|
1201
|
+
this.workingDirResolver = workingDirResolver;
|
|
1202
|
+
}
|
|
1203
|
+
workingDirResolver;
|
|
1204
|
+
type = "working";
|
|
1205
|
+
getWorkingDir() {
|
|
1206
|
+
return this.workingDirResolver ? this.workingDirResolver() : getWorkingWorkspaceDir();
|
|
1207
|
+
}
|
|
1208
|
+
async findWorkspace(workspaceId, env) {
|
|
1209
|
+
return await findUserWorkspace(workspaceId, env);
|
|
1210
|
+
}
|
|
1211
|
+
async listWorkspaces(userId, env) {
|
|
1212
|
+
return await getWorkspaces(userId, env);
|
|
1213
|
+
}
|
|
1214
|
+
async createWorkspace(userId, data, env) {
|
|
1215
|
+
const res = await createWorkspace(userId, data, env);
|
|
1216
|
+
const dir = this.getWorkingDir();
|
|
1217
|
+
try {
|
|
1218
|
+
await fs2.promises.mkdir(dir, { recursive: true });
|
|
1219
|
+
} catch {
|
|
1220
|
+
}
|
|
1221
|
+
return res;
|
|
1222
|
+
}
|
|
1223
|
+
async updateWorkspace(userId, params, env) {
|
|
1224
|
+
return await updateWorkspace(userId, params, env);
|
|
1225
|
+
}
|
|
1226
|
+
async deleteWorkspace(userId, workspaceId, env) {
|
|
1227
|
+
return await deleteWorkspace(userId, workspaceId, env);
|
|
1228
|
+
}
|
|
1229
|
+
async readFile(_workspaceId, filePath) {
|
|
1230
|
+
const cleanRel = filePath.replace(/^\/+/, "");
|
|
1231
|
+
const fullPath = path2.resolve(this.getWorkingDir(), cleanRel);
|
|
1232
|
+
return await fs2.promises.readFile(fullPath);
|
|
1233
|
+
}
|
|
1234
|
+
async writeFile(_workspaceId, filePath, content) {
|
|
1235
|
+
const cleanRel = filePath.replace(/^\/+/, "");
|
|
1236
|
+
const fullPath = path2.resolve(this.getWorkingDir(), cleanRel);
|
|
1237
|
+
await fs2.promises.mkdir(path2.dirname(fullPath), { recursive: true });
|
|
1238
|
+
const buffer = typeof content === "string" ? Buffer.from(content) : Buffer.from(content.buffer, content.byteOffset, content.byteLength);
|
|
1239
|
+
await fs2.promises.writeFile(fullPath, buffer);
|
|
1240
|
+
return { success: true, path: filePath, sizeBytes: buffer.byteLength };
|
|
1241
|
+
}
|
|
1242
|
+
async deleteFile(_workspaceId, filePath) {
|
|
1243
|
+
const cleanRel = filePath.replace(/^\/+/, "");
|
|
1244
|
+
const fullPath = path2.resolve(this.getWorkingDir(), cleanRel);
|
|
1245
|
+
try {
|
|
1246
|
+
await fs2.promises.unlink(fullPath);
|
|
1247
|
+
return { success: true };
|
|
1248
|
+
} catch {
|
|
1249
|
+
return { success: false };
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
async listFiles(_workspaceId, subfolder = "") {
|
|
1253
|
+
const baseDir = path2.resolve(this.getWorkingDir(), subfolder.replace(/^\/+/, ""));
|
|
1254
|
+
if (!fs2.existsSync(baseDir)) return [];
|
|
1255
|
+
const entries = await fs2.promises.readdir(baseDir, { withFileTypes: true });
|
|
1256
|
+
return entries.map((e) => {
|
|
1257
|
+
const full = path2.join(baseDir, e.name);
|
|
1258
|
+
let sizeBytes = 0;
|
|
1259
|
+
let updatedAt = Date.now();
|
|
1260
|
+
try {
|
|
1261
|
+
const s = fs2.statSync(full);
|
|
1262
|
+
sizeBytes = s.size;
|
|
1263
|
+
updatedAt = s.mtimeMs;
|
|
1264
|
+
} catch {
|
|
1265
|
+
}
|
|
1266
|
+
return {
|
|
1267
|
+
path: path2.relative(this.getWorkingDir(), full),
|
|
1268
|
+
name: e.name,
|
|
1269
|
+
isFolder: e.isDirectory(),
|
|
1270
|
+
sizeBytes,
|
|
1271
|
+
updatedAt
|
|
1272
|
+
};
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
async sync(workspaceId, _options, _env) {
|
|
1276
|
+
return await syncWorkspace(workspaceId);
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
var workingWorkspaceService = new WorkingWorkspaceService();
|
|
1280
|
+
|
|
1281
|
+
export {
|
|
1282
|
+
WorkspaceFS,
|
|
1283
|
+
SyncEngine,
|
|
1284
|
+
WorkspaceCloneSyncEngine,
|
|
1285
|
+
cleanDbUrl,
|
|
1286
|
+
getUnifiedDbUrl,
|
|
1287
|
+
queryUnifiedDb,
|
|
1288
|
+
b2ListBuckets,
|
|
1289
|
+
getB2BucketId,
|
|
1290
|
+
b2DeleteFileVersion,
|
|
1291
|
+
b2DeleteFolder,
|
|
1292
|
+
getWorkspaceFolderPrefix,
|
|
1293
|
+
isB2Workspace,
|
|
1294
|
+
writeWorkspaceFile,
|
|
1295
|
+
readWorkspaceFile,
|
|
1296
|
+
deleteWorkspaceFile,
|
|
1297
|
+
listWorkspaceFiles,
|
|
1298
|
+
deleteWorkspaceFolder,
|
|
1299
|
+
resolveWorkspaceConnection,
|
|
1300
|
+
syncWorkspaceCreateToBucket,
|
|
1301
|
+
syncWorkspaceDeleteToBucket,
|
|
1302
|
+
syncWorkspaceDataToBucket,
|
|
1303
|
+
loadWorkspaceData,
|
|
1304
|
+
saveWorkspaceData,
|
|
1305
|
+
renameAppInWorkspaceData,
|
|
1306
|
+
getWorkspaceStorageDetails,
|
|
1307
|
+
findUserWorkspace,
|
|
1308
|
+
getWorkspaces,
|
|
1309
|
+
createWorkspace,
|
|
1310
|
+
updateWorkspace,
|
|
1311
|
+
deleteWorkspace,
|
|
1312
|
+
RemoteWorkspaceService,
|
|
1313
|
+
remoteWorkspaceService,
|
|
1314
|
+
getDefaultWorkingWorkspaceDir,
|
|
1315
|
+
getWorkingWorkspaceConfig,
|
|
1316
|
+
setWorkingWorkspaceConfig,
|
|
1317
|
+
getWorkingWorkspaceDir,
|
|
1318
|
+
findWorkspaceRecord,
|
|
1319
|
+
getWorkspaceFolderKey,
|
|
1320
|
+
syncWorkspace,
|
|
1321
|
+
WorkingWorkspaceService,
|
|
1322
|
+
workingWorkspaceService
|
|
1323
|
+
};
|