@rudderhq/server 0.7.18 → 0.7.19-canary.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/dist/agent-runtimes/registry.d.ts.map +1 -1
- package/dist/agent-runtimes/registry.js +2 -1
- package/dist/agent-runtimes/registry.js.map +1 -1
- package/dist/file-system-durability.d.ts +5 -0
- package/dist/file-system-durability.d.ts.map +1 -0
- package/dist/file-system-durability.js +39 -0
- package/dist/file-system-durability.js.map +1 -0
- package/dist/home-paths.d.ts +1 -0
- package/dist/home-paths.d.ts.map +1 -1
- package/dist/home-paths.js +705 -102
- package/dist/home-paths.js.map +1 -1
- package/dist/routes/chats.d.ts.map +1 -1
- package/dist/routes/chats.js +2 -0
- package/dist/routes/chats.js.map +1 -1
- package/dist/routes/issues.d.ts.map +1 -1
- package/dist/routes/issues.js +27 -5
- package/dist/routes/issues.js.map +1 -1
- package/dist/routes/issues.mutations.d.ts.map +1 -1
- package/dist/routes/issues.mutations.js +10 -6
- package/dist/routes/issues.mutations.js.map +1 -1
- package/dist/routes/orgs.d.ts.map +1 -1
- package/dist/routes/orgs.js +28 -7
- package/dist/routes/orgs.js.map +1 -1
- package/dist/routes/rudder-plugins.d.ts +5 -0
- package/dist/routes/rudder-plugins.d.ts.map +1 -1
- package/dist/routes/rudder-plugins.js +21 -1
- package/dist/routes/rudder-plugins.js.map +1 -1
- package/dist/routes/rudder-plugins.test.d.ts +2 -0
- package/dist/routes/rudder-plugins.test.d.ts.map +1 -0
- package/dist/routes/rudder-plugins.test.js +42 -0
- package/dist/routes/rudder-plugins.test.js.map +1 -0
- package/dist/routes/run-intelligence.d.ts.map +1 -1
- package/dist/routes/run-intelligence.js +8 -0
- package/dist/routes/run-intelligence.js.map +1 -1
- package/dist/services/agent-issue-creation.d.ts.map +1 -1
- package/dist/services/agent-issue-creation.js +2 -0
- package/dist/services/agent-issue-creation.js.map +1 -1
- package/dist/services/chat-assistant.d.ts.map +1 -1
- package/dist/services/chat-assistant.helpers.d.ts +4 -2
- package/dist/services/chat-assistant.helpers.d.ts.map +1 -1
- package/dist/services/chat-assistant.helpers.js +5 -0
- package/dist/services/chat-assistant.helpers.js.map +1 -1
- package/dist/services/chat-assistant.js +31 -8
- package/dist/services/chat-assistant.js.map +1 -1
- package/dist/services/chat-inline-annotations.test.js +6 -3
- package/dist/services/chat-inline-annotations.test.js.map +1 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js.map +1 -1
- package/dist/services/issue-references.d.ts +10 -0
- package/dist/services/issue-references.d.ts.map +1 -0
- package/dist/services/issue-references.js +141 -0
- package/dist/services/issue-references.js.map +1 -0
- package/dist/services/issues.d.ts +1 -1
- package/dist/services/issues.d.ts.map +1 -1
- package/dist/services/issues.helpers.d.ts +4 -0
- package/dist/services/issues.helpers.d.ts.map +1 -1
- package/dist/services/issues.helpers.js.map +1 -1
- package/dist/services/issues.js +164 -97
- package/dist/services/issues.js.map +1 -1
- package/dist/services/organization-workspace-browser.d.ts +6 -2
- package/dist/services/organization-workspace-browser.d.ts.map +1 -1
- package/dist/services/organization-workspace-browser.js +212 -47
- package/dist/services/organization-workspace-browser.js.map +1 -1
- package/dist/services/plugin-archive-native.d.ts +10 -0
- package/dist/services/plugin-archive-native.d.ts.map +1 -0
- package/dist/services/plugin-archive-native.js +170 -0
- package/dist/services/plugin-archive-native.js.map +1 -0
- package/dist/services/plugin-archive-node.d.ts +3 -0
- package/dist/services/plugin-archive-node.d.ts.map +1 -0
- package/dist/services/plugin-archive-node.js +97 -0
- package/dist/services/plugin-archive-node.js.map +1 -0
- package/dist/services/rudder-plugins.d.ts +1 -1
- package/dist/services/rudder-plugins.d.ts.map +1 -1
- package/dist/services/rudder-plugins.js +54 -93
- package/dist/services/rudder-plugins.js.map +1 -1
- package/dist/services/rudder-plugins.test.js +51 -0
- package/dist/services/rudder-plugins.test.js.map +1 -1
- package/dist/services/run-intelligence.d.ts +1 -0
- package/dist/services/run-intelligence.d.ts.map +1 -1
- package/dist/services/run-intelligence.js +1 -1
- package/dist/services/run-intelligence.js.map +1 -1
- package/dist/services/run-log-store.d.ts +1 -0
- package/dist/services/run-log-store.d.ts.map +1 -1
- package/dist/services/run-log-store.js +123 -4
- package/dist/services/run-log-store.js.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.d.ts +1 -0
- package/dist/services/runtime-kernel/heartbeat.d.ts.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.js.map +1 -1
- package/dist/services/runtime-kernel/model-fallback.d.ts.map +1 -1
- package/dist/services/runtime-kernel/model-fallback.js +56 -11
- package/dist/services/runtime-kernel/model-fallback.js.map +1 -1
- package/dist/services/workspace-backup-v2.d.ts +1 -0
- package/dist/services/workspace-backup-v2.d.ts.map +1 -1
- package/dist/services/workspace-backup-v2.js +16 -0
- package/dist/services/workspace-backup-v2.js.map +1 -1
- package/dist/services/workspace-backups.d.ts.map +1 -1
- package/dist/services/workspace-backups.js +4 -2
- package/dist/services/workspace-backups.js.map +1 -1
- package/dist/services/workspace-files-native.d.ts +16 -0
- package/dist/services/workspace-files-native.d.ts.map +1 -0
- package/dist/services/workspace-files-native.js +212 -0
- package/dist/services/workspace-files-native.js.map +1 -0
- package/dist/services/workspace-manifest-native.d.ts +1 -1
- package/dist/services/workspace-manifest-native.d.ts.map +1 -1
- package/dist/services/workspace-manifest-native.js +49 -6
- package/dist/services/workspace-manifest-native.js.map +1 -1
- package/dist/services/workspace-runtime.helpers.d.ts.map +1 -1
- package/dist/services/workspace-runtime.helpers.js +13 -11
- package/dist/services/workspace-runtime.helpers.js.map +1 -1
- package/package.json +14 -14
- package/resources/bundled-skills/rudder-docs/SKILL.md +12 -12
- package/resources/bundled-skills/rudder-docs/references/source-map.md +7 -31
- package/skills/rudder-docs/SKILL.md +12 -12
- package/skills/rudder-docs/references/source-map.md +7 -31
- package/ui-dist/assets/{LibraryLiveSurface-BHneB__U.js → LibraryLiveSurface-D-6Puw4N.js} +1 -1
- package/ui-dist/assets/{abnfDiagram-VRR7QNED-GUoY9rTt.js → abnfDiagram-VRR7QNED-DjgQ9iD_.js} +1 -1
- package/ui-dist/assets/{addon-fit-CqXXE0-1.js → addon-fit-OOCvZY14.js} +1 -1
- package/ui-dist/assets/{arc-BD7m_r6l.js → arc-D6yqUjH4.js} +1 -1
- package/ui-dist/assets/{architectureDiagram-ZJ3FMSHR-nEufXAY_.js → architectureDiagram-ZJ3FMSHR-eF-Es2a-.js} +1 -1
- package/ui-dist/assets/{blockDiagram-677ZJIJ3-D3qUEbL5.js → blockDiagram-677ZJIJ3-B7G2uxR7.js} +1 -1
- package/ui-dist/assets/{c4Diagram-LMCZKHZV-7SPUxKgx.js → c4Diagram-LMCZKHZV-YhY4bz85.js} +1 -1
- package/ui-dist/assets/channel-DISxdJMI.js +1 -0
- package/ui-dist/assets/{chunk-2Q5K7J3B-6o3ETzOY.js → chunk-2Q5K7J3B-D7QxbIUK.js} +1 -1
- package/ui-dist/assets/{chunk-32BRIVSS-Efd566e8.js → chunk-32BRIVSS-CArPDsLy.js} +1 -1
- package/ui-dist/assets/{chunk-5VM5RSS4-BA4X4gzU.js → chunk-5VM5RSS4-ByyAjdOI.js} +1 -1
- package/ui-dist/assets/{chunk-EX3LRPZG-BD7K-mPD.js → chunk-EX3LRPZG-B6Z9cEKa.js} +1 -1
- package/ui-dist/assets/{chunk-JWPE2WC7-DXv6iHxr.js → chunk-JWPE2WC7-L5jYy4CI.js} +1 -1
- package/ui-dist/assets/{chunk-MOJQB5TN-dQ6gk-_u.js → chunk-MOJQB5TN-BBTZZsOQ.js} +1 -1
- package/ui-dist/assets/{chunk-RYQCIY6F-xEk6bTb7.js → chunk-RYQCIY6F-DyV9CaPT.js} +1 -1
- package/ui-dist/assets/{chunk-V7JOEXUC-zlvz5Y5q.js → chunk-V7JOEXUC-CNztgBrF.js} +1 -1
- package/ui-dist/assets/{chunk-VR4S4FIN--zCSWk5F.js → chunk-VR4S4FIN-Bv62Xmto.js} +1 -1
- package/ui-dist/assets/{chunk-XXDRQBXY-CZ2QWPRe.js → chunk-XXDRQBXY-B5AzJDaz.js} +1 -1
- package/ui-dist/assets/classDiagram-OUVF2IWQ-BssC8UiF.js +1 -0
- package/ui-dist/assets/classDiagram-v2-EOCWNBFH-BssC8UiF.js +1 -0
- package/ui-dist/assets/{cose-bilkent-JH36ORCC-DrRNgFzR.js → cose-bilkent-JH36ORCC-DrzyjyO_.js} +1 -1
- package/ui-dist/assets/{cynefin-VYW2F7L2-T9PONuTh.js → cynefin-VYW2F7L2-ChoChVKW.js} +1 -1
- package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-Dkop4mS3.js → cynefinDiagram-TSTJHNR4-Fv4ecj_f.js} +1 -1
- package/ui-dist/assets/{dagre-VKFMJZFB-Chc8Imxx.js → dagre-VKFMJZFB-Zd466k0O.js} +1 -1
- package/ui-dist/assets/{diagram-FQU43EPY-n4pgnJYP.js → diagram-FQU43EPY-BgL3Uu7z.js} +1 -1
- package/ui-dist/assets/{diagram-G47NLZAW-DnjvVsj5.js → diagram-G47NLZAW-Bcbg2TZF.js} +1 -1
- package/ui-dist/assets/{diagram-NH7WQ7WH-kHZ9astC.js → diagram-NH7WQ7WH-YS4gl6dx.js} +1 -1
- package/ui-dist/assets/{diagram-OA4YK3LP-BAMly3yv.js → diagram-OA4YK3LP-C-A8A6Xi.js} +1 -1
- package/ui-dist/assets/{diagram-WEI45ONY-BTFQhBrp.js → diagram-WEI45ONY-mqkaxwes.js} +1 -1
- package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-c2T4DORg.js → ebnfDiagram-CCIWWBDH-CvH1cI4K.js} +1 -1
- package/ui-dist/assets/{erDiagram-Q63AITRT-BXLi1JHT.js → erDiagram-Q63AITRT-Duhw1MUB.js} +1 -1
- package/ui-dist/assets/{flowDiagram-23GEKE2U-CHzX7naH.js → flowDiagram-23GEKE2U-DUkAECUs.js} +1 -1
- package/ui-dist/assets/{ganttDiagram-NO4QXBWP-BqUKeoDF.js → ganttDiagram-NO4QXBWP-B3cINaqn.js} +1 -1
- package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-DM4K-DoE.js → gitGraphDiagram-IHSO6WYX-BMEHvYy5.js} +1 -1
- package/ui-dist/assets/{graph-BDejhZe_.js → graph-B1-dUGRC.js} +1 -1
- package/ui-dist/assets/{index-BJ-hTdyx.js → index-BFmYzGL9.js} +1 -1
- package/ui-dist/assets/{index-DSAl1a-M.js → index-BGAEBJze.js} +1 -1
- package/ui-dist/assets/{index-CH8tOJZ9.js → index-BMfPEMCH.js} +1 -1
- package/ui-dist/assets/{index-BuCCxdIU.js → index-BglV2AAc.js} +1 -1
- package/ui-dist/assets/{index-CV0k_zvK.js → index-Blix7pGb.js} +1 -1
- package/ui-dist/assets/{index-XnCv8P5f.js → index-CBrXXiQ_.js} +1 -1
- package/ui-dist/assets/{index-CIbvFYbV.js → index-CSMCUMYP.js} +1 -1
- package/ui-dist/assets/index-CUW-AUBO.css +32 -0
- package/ui-dist/assets/{index-Bni14R_J.js → index-CaveK_TY.js} +1 -1
- package/ui-dist/assets/{index-WbyFmgaS.js → index-DHcvu8nk.js} +1 -1
- package/ui-dist/assets/{index-DAM3EGbv.js → index-DSz4Tjbh.js} +1 -1
- package/ui-dist/assets/{index-DVDu5Nwd.js → index-DgNZ8Oto.js} +1 -1
- package/ui-dist/assets/{index-DZBZIqJI.js → index-Drmy6GaQ.js} +1 -1
- package/ui-dist/assets/{index-B4zWD5-i.js → index-DvrowL98.js} +1 -1
- package/ui-dist/assets/{index-BN90mN5Z.js → index-DxHCQu-4.js} +1 -1
- package/ui-dist/assets/{index-DHzaZSPM.js → index-IJhIHwGp.js} +546 -546
- package/ui-dist/assets/{infoDiagram-FWYZ7A6U-PcFBrZTu.js → infoDiagram-FWYZ7A6U-BJ7smuOx.js} +1 -1
- package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-BF2qBweH.js → ishikawaDiagram-FXEZZL3T-Bn7jPYRy.js} +1 -1
- package/ui-dist/assets/{journeyDiagram-5HDEW3XC-D3TqAoEB.js → journeyDiagram-5HDEW3XC-D5NYECBN.js} +1 -1
- package/ui-dist/assets/{kanban-definition-HUTT4EX6-CUiHrckR.js → kanban-definition-HUTT4EX6-CcrYvDYt.js} +1 -1
- package/ui-dist/assets/{layout-DMKAzTyC.js → layout-32M7EWAw.js} +1 -1
- package/ui-dist/assets/{linear-D8C0WdTA.js → linear-DT9kJUfH.js} +1 -1
- package/ui-dist/assets/{map-DUCHGq8e.js → map-C2crE4Bv.js} +1 -1
- package/ui-dist/assets/{mermaid.core-C-eoqr77.js → mermaid.core-B9y8ebhy.js} +4 -4
- package/ui-dist/assets/{mindmap-definition-LN4V7U3C-B54nV_Hu.js → mindmap-definition-LN4V7U3C-CaFsY4LT.js} +1 -1
- package/ui-dist/assets/{pegDiagram-2B236MQR-CcIDNzTo.js → pegDiagram-2B236MQR-BMTtqHzV.js} +1 -1
- package/ui-dist/assets/{pieDiagram-ENE6RG2P-BU4swrSE.js → pieDiagram-ENE6RG2P-DRl32AMI.js} +1 -1
- package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-CnniDL18.js → quadrantDiagram-ABIIQ3AL-pXeC4qE5.js} +1 -1
- package/ui-dist/assets/{railroadDiagram-RFXS5EU6-CiBY7wpV.js → railroadDiagram-RFXS5EU6-tGzWZNPI.js} +1 -1
- package/ui-dist/assets/{requirementDiagram-TGXJPOKE-C7c_tTqS.js → requirementDiagram-TGXJPOKE-DEqEN1a0.js} +1 -1
- package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-DJuTS99d.js → sankeyDiagram-HTMAVEWB-DjVDCNO4.js} +1 -1
- package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-BxhtY70W.js → sequenceDiagram-DBY2YBRQ-CRKMhKhd.js} +1 -1
- package/ui-dist/assets/{sizeCapture-X5ZJPWSS-0A0Kww03.js → sizeCapture-X5ZJPWSS-CWl0gYed.js} +1 -1
- package/ui-dist/assets/{stateDiagram-2N3HPSRC-DPw4bytW.js → stateDiagram-2N3HPSRC-BH6FVEBD.js} +1 -1
- package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-DFWkP3y3.js +1 -0
- package/ui-dist/assets/{swimlanes-5IMT3BWC-D6MMDWrs.js → swimlanes-5IMT3BWC-BZdn1cQ5.js} +2 -2
- package/ui-dist/assets/swimlanesDiagram-G3AALYLV-BwA91U1K.js +8 -0
- package/ui-dist/assets/{timeline-definition-FHXFAJF6-gNKDUxQx.js → timeline-definition-FHXFAJF6-CMofE9DN.js} +1 -1
- package/ui-dist/assets/{vennDiagram-L72KCM5P-BpZCM-ST.js → vennDiagram-L72KCM5P-CszussGd.js} +1 -1
- package/ui-dist/assets/{wardleyDiagram-EHGQE667-CvR0dYqn.js → wardleyDiagram-EHGQE667-C1Op8a5q.js} +1 -1
- package/ui-dist/assets/{xterm-CEksOm0P.js → xterm-BM41mI-I.js} +1 -1
- package/ui-dist/assets/{xychartDiagram-FW5EYKEG-C6_4sxeI.js → xychartDiagram-FW5EYKEG-zi-IHNrX.js} +1 -1
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/channel-Czz1OwXA.js +0 -1
- package/ui-dist/assets/classDiagram-OUVF2IWQ-C_ooAkuA.js +0 -1
- package/ui-dist/assets/classDiagram-v2-EOCWNBFH-C_ooAkuA.js +0 -1
- package/ui-dist/assets/index-BsB7Sc7m.css +0 -32
- package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-ANI7JZ72.js +0 -1
- package/ui-dist/assets/swimlanesDiagram-G3AALYLV-CxjGCcz5.js +0 -8
package/dist/home-paths.js
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "node:fs/promises";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { resolveStoredOrDerivedAgentWorkspaceKey } from "./agent-workspace-key.js";
|
|
8
|
+
import { syncDirectory, syncFileHandle } from "./file-system-durability.js";
|
|
8
9
|
const DEFAULT_INSTANCE_ID = "default";
|
|
9
10
|
const INSTANCE_ID_RE = /^[a-zA-Z0-9_-]+$/;
|
|
10
11
|
const FRIENDLY_PATH_SEGMENT_RE = /[^a-zA-Z0-9._-]+/g;
|
|
@@ -13,8 +14,23 @@ const EXECUTABLE_MODE_BITS = 0o111;
|
|
|
13
14
|
const MIGRATION_BACKUP_DIR_NAME = ".rudder-migration-backups";
|
|
14
15
|
const WORKSPACE_PERMISSION_ERROR_CODES = new Set(["EACCES", "EPERM", "EROFS"]);
|
|
15
16
|
const ORGANIZATION_WORKSPACE_MAP_FILE = ".rudder-organizations.json";
|
|
17
|
+
const ORGANIZATION_WORKSPACE_MAP_LOCK = ".rudder-organizations.lock";
|
|
18
|
+
const ORGANIZATION_WORKSPACE_MAP_LOCK_OWNER_PREFIX = ".rudder-lock-owner-";
|
|
19
|
+
const ORGANIZATION_WORKSPACE_MAP_LOCK_ACQUIRE_PREFIX = ".rudder-organizations.lock.acquire-";
|
|
20
|
+
const ORGANIZATION_WORKSPACE_MAP_LOCK_RECOVERY_PREFIX = ".rudder-organizations.lock.recovered-";
|
|
21
|
+
const ORGANIZATION_WORKSPACE_MAP_LOCK_KIND = "rudder-organization-workspace-map-lock";
|
|
22
|
+
const ORGANIZATION_WORKSPACE_MAP_RECLAIM_PREFIX = ".rudder-lock-reclaim-";
|
|
23
|
+
const ORGANIZATION_WORKSPACE_IDENTITY_FILE = ".rudder-workspace.json";
|
|
24
|
+
const ORGANIZATION_WORKSPACE_MIGRATION_FILE = ".rudder-workspace-migrations.json";
|
|
25
|
+
const ORGANIZATION_WORKSPACE_MAP_LOCK_TIMEOUT_MS = 10_000;
|
|
26
|
+
const ORGANIZATION_WORKSPACE_MAP_STALE_LOCK_MS = 60_000;
|
|
27
|
+
const ORGANIZATION_WORKSPACE_MAP_LOCK_TARGET_EXISTS_CODES = new Set(["EEXIST", "ENOTEMPTY", "EISDIR"]);
|
|
16
28
|
const RESERVED_ORGANIZATION_WORKSPACE_NAMES = new Set([
|
|
17
29
|
ORGANIZATION_WORKSPACE_MAP_FILE,
|
|
30
|
+
ORGANIZATION_WORKSPACE_MAP_LOCK,
|
|
31
|
+
MIGRATION_BACKUP_DIR_NAME,
|
|
32
|
+
"organizations",
|
|
33
|
+
"projects",
|
|
18
34
|
".rudder",
|
|
19
35
|
"backups",
|
|
20
36
|
"data",
|
|
@@ -83,7 +99,9 @@ function resolveAgentWorkspacePathSegment(agent) {
|
|
|
83
99
|
export function resolveOrganizationWorkspaceRoot(orgId) {
|
|
84
100
|
const normalizedOrgId = resolveOrganizationStorageKey(orgId);
|
|
85
101
|
if (usesFriendlyOrganizationWorkspaceHome()) {
|
|
86
|
-
|
|
102
|
+
const mappedFolderName = readOrganizationWorkspaceFolderName(orgId);
|
|
103
|
+
const folderName = mappedFolderName ?? sanitizeOrganizationWorkspaceFolderName(normalizedOrgId, normalizedOrgId);
|
|
104
|
+
return path.resolve(resolveOrganizationWorkspaceHomeDir(), folderName);
|
|
87
105
|
}
|
|
88
106
|
return path.resolve(resolveOrganizationWorkspaceHomeDir(), normalizedOrgId, "workspaces");
|
|
89
107
|
}
|
|
@@ -155,11 +173,14 @@ export function resolveManagedOrganizationCodebaseDir(input) {
|
|
|
155
173
|
return path.resolve(resolveOrganizationWorkspaceRoot(input.orgId), "codebase", sanitizeFriendlyPathSegment(input.repoName, "_default"));
|
|
156
174
|
}
|
|
157
175
|
export async function ensureOrganizationWorkspaceLayout(org) {
|
|
176
|
+
return await updateOrganizationWorkspaceMap(() => ensureOrganizationWorkspaceLayoutUnlocked(org));
|
|
177
|
+
}
|
|
178
|
+
async function ensureOrganizationWorkspaceLayoutUnlocked(org) {
|
|
158
179
|
const orgId = typeof org === "string" ? org : org.id;
|
|
159
|
-
await
|
|
180
|
+
await migrateOrganizationStorageRootUnlocked(orgId);
|
|
160
181
|
let mappingState = null;
|
|
161
182
|
if (typeof org !== "string") {
|
|
162
|
-
mappingState = await
|
|
183
|
+
mappingState = await ensureOrganizationWorkspaceMappingUnlocked(org);
|
|
163
184
|
}
|
|
164
185
|
else if (usesFriendlyOrganizationWorkspaceHome()) {
|
|
165
186
|
const map = await readOrganizationWorkspaceMapFile();
|
|
@@ -168,20 +189,24 @@ export async function ensureOrganizationWorkspaceLayout(org) {
|
|
|
168
189
|
mappingState = { record: existing, created: false };
|
|
169
190
|
}
|
|
170
191
|
}
|
|
171
|
-
|
|
192
|
+
const root = resolveOrganizationWorkspaceRoot(orgId);
|
|
193
|
+
await assertOrganizationWorkspaceRootIsOwned(root);
|
|
194
|
+
await migrateOrganizationWorkspaceRootUnlocked(orgId, {
|
|
172
195
|
failIfMappedFolderMissing: Boolean(mappingState && !mappingState.created),
|
|
173
196
|
});
|
|
174
|
-
|
|
197
|
+
await assertOrganizationWorkspaceRootIsOwned(root);
|
|
175
198
|
const agentsDir = resolveOrganizationAgentsDir(orgId);
|
|
176
199
|
const skillsDir = resolveOrganizationSkillsDir(orgId);
|
|
177
200
|
const projectsDir = resolveOrganizationProjectsDir(orgId);
|
|
178
201
|
try {
|
|
202
|
+
await fs.mkdir(root, { recursive: true });
|
|
203
|
+
await assertOrganizationWorkspaceRootIsOwned(root);
|
|
179
204
|
await Promise.all([
|
|
180
|
-
fs.mkdir(root, { recursive: true }),
|
|
181
205
|
fs.mkdir(agentsDir, { recursive: true }),
|
|
182
206
|
fs.mkdir(skillsDir, { recursive: true }),
|
|
183
207
|
fs.mkdir(projectsDir, { recursive: true }),
|
|
184
208
|
]);
|
|
209
|
+
await ensureOrganizationWorkspaceIdentity(root, orgId);
|
|
185
210
|
}
|
|
186
211
|
catch (error) {
|
|
187
212
|
if (isPermissionError(error)) {
|
|
@@ -196,52 +221,330 @@ export async function ensureOrganizationWorkspaceLayout(org) {
|
|
|
196
221
|
}
|
|
197
222
|
return { root, agentsDir, skillsDir, projectsDir };
|
|
198
223
|
}
|
|
199
|
-
async function
|
|
224
|
+
async function ensureOrganizationWorkspaceMappingUnlocked(org) {
|
|
200
225
|
if (!usesFriendlyOrganizationWorkspaceHome())
|
|
201
226
|
return null;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
existing.updatedAt = now;
|
|
214
|
-
await writeOrganizationWorkspaceMapFile(mapPath, map);
|
|
215
|
-
return { record: existing, created: false };
|
|
216
|
-
}
|
|
217
|
-
const folderName = await allocateOrganizationWorkspaceFolderName({
|
|
218
|
-
homeDir,
|
|
219
|
-
map,
|
|
220
|
-
orgId,
|
|
221
|
-
orgName: org.name,
|
|
222
|
-
orgUrlKey: org.urlKey,
|
|
223
|
-
allowExistingBaseDirectory: !mapState.exists,
|
|
224
|
-
});
|
|
225
|
-
const record = {
|
|
226
|
-
instanceId: resolveRudderInstanceId(),
|
|
227
|
-
orgId,
|
|
228
|
-
folderName,
|
|
229
|
-
orgName: org.name ?? null,
|
|
230
|
-
createdAt: now,
|
|
231
|
-
updatedAt: now,
|
|
232
|
-
};
|
|
233
|
-
map.organizations.push(record);
|
|
227
|
+
const orgId = validatePathSegment(org.id, "org id");
|
|
228
|
+
const homeDir = resolveOrganizationWorkspaceHomeDir();
|
|
229
|
+
const mapPath = resolveOrganizationWorkspaceMapPath();
|
|
230
|
+
await fs.mkdir(homeDir, { recursive: true });
|
|
231
|
+
const now = new Date().toISOString();
|
|
232
|
+
const mapState = await readOrganizationWorkspaceMapFileState();
|
|
233
|
+
const map = mapState.map;
|
|
234
|
+
const existing = findOrganizationWorkspaceMapRecord(map, orgId);
|
|
235
|
+
if (existing) {
|
|
236
|
+
existing.orgName = org.name ?? existing.orgName ?? null;
|
|
237
|
+
existing.updatedAt = now;
|
|
234
238
|
await writeOrganizationWorkspaceMapFile(mapPath, map);
|
|
235
|
-
return { record, created:
|
|
239
|
+
return { record: existing, created: false };
|
|
240
|
+
}
|
|
241
|
+
const folderName = await allocateOrganizationWorkspaceFolderName({
|
|
242
|
+
homeDir,
|
|
243
|
+
map,
|
|
244
|
+
orgId,
|
|
245
|
+
orgName: org.name,
|
|
246
|
+
orgUrlKey: org.urlKey,
|
|
236
247
|
});
|
|
248
|
+
const folderPath = path.resolve(homeDir, folderName);
|
|
249
|
+
// Materialize the identity before publishing the map record so a crash can
|
|
250
|
+
// reclaim a directory that was created just before the map write.
|
|
251
|
+
await fs.mkdir(folderPath, { recursive: true });
|
|
252
|
+
await ensureOrganizationWorkspaceIdentity(folderPath, orgId);
|
|
253
|
+
const record = {
|
|
254
|
+
instanceId: resolveRudderInstanceId(),
|
|
255
|
+
orgId,
|
|
256
|
+
folderName,
|
|
257
|
+
orgName: org.name ?? null,
|
|
258
|
+
createdAt: now,
|
|
259
|
+
updatedAt: now,
|
|
260
|
+
};
|
|
261
|
+
map.organizations.push(record);
|
|
262
|
+
await writeOrganizationWorkspaceMapFile(mapPath, map);
|
|
263
|
+
return { record, created: true };
|
|
237
264
|
}
|
|
238
265
|
async function updateOrganizationWorkspaceMap(fn) {
|
|
239
266
|
const previous = organizationWorkspaceMapUpdateQueue.catch(() => { });
|
|
240
|
-
const current = previous.then(fn, fn);
|
|
267
|
+
const current = previous.then(() => withOrganizationWorkspaceMapLock(fn), () => withOrganizationWorkspaceMapLock(fn));
|
|
241
268
|
organizationWorkspaceMapUpdateQueue = current.catch(() => { });
|
|
242
269
|
return await current;
|
|
243
270
|
}
|
|
271
|
+
async function withOrganizationWorkspaceMapLock(fn) {
|
|
272
|
+
const lockPath = path.resolve(resolveOrganizationWorkspaceHomeDir(), ORGANIZATION_WORKSPACE_MAP_LOCK);
|
|
273
|
+
const lockParentPath = path.dirname(lockPath);
|
|
274
|
+
const startedAt = Date.now();
|
|
275
|
+
const token = randomUUID();
|
|
276
|
+
const ownerFileName = `${ORGANIZATION_WORKSPACE_MAP_LOCK_OWNER_PREFIX}${token}.json`;
|
|
277
|
+
const acquisitionPath = path.join(lockParentPath, `${ORGANIZATION_WORKSPACE_MAP_LOCK_ACQUIRE_PREFIX}${token}`);
|
|
278
|
+
const acquisitionOwnerPath = path.join(acquisitionPath, ownerFileName);
|
|
279
|
+
let acquisitionCreated = false;
|
|
280
|
+
let lockAcquired = false;
|
|
281
|
+
await fs.mkdir(lockParentPath, { recursive: true });
|
|
282
|
+
try {
|
|
283
|
+
// Build a complete owner directory before the final rename. This keeps an
|
|
284
|
+
// acquired lock from ever being observable without its owner record.
|
|
285
|
+
await fs.mkdir(acquisitionPath, { mode: 0o700 });
|
|
286
|
+
acquisitionCreated = true;
|
|
287
|
+
const owner = {
|
|
288
|
+
kind: ORGANIZATION_WORKSPACE_MAP_LOCK_KIND,
|
|
289
|
+
version: 1,
|
|
290
|
+
token,
|
|
291
|
+
pid: process.pid,
|
|
292
|
+
hostname: os.hostname(),
|
|
293
|
+
createdAt: new Date().toISOString(),
|
|
294
|
+
};
|
|
295
|
+
const handle = await fs.open(acquisitionOwnerPath, "wx", 0o600);
|
|
296
|
+
try {
|
|
297
|
+
await handle.writeFile(`${JSON.stringify(owner, null, 2)}\n`, "utf8");
|
|
298
|
+
await syncFileHandle(handle);
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
await handle.close();
|
|
302
|
+
}
|
|
303
|
+
await syncDirectory(acquisitionPath);
|
|
304
|
+
while (true) {
|
|
305
|
+
if (Date.now() - startedAt >= ORGANIZATION_WORKSPACE_MAP_LOCK_TIMEOUT_MS) {
|
|
306
|
+
throw Object.assign(new Error(`Timed out waiting for the organization workspace mapping lock at ${lockPath}.`), { code: "RUDDER_WORKSPACE_MAP_LOCK_TIMEOUT" });
|
|
307
|
+
}
|
|
308
|
+
const existingLock = await lstatIfExists(lockPath);
|
|
309
|
+
if (existingLock) {
|
|
310
|
+
const existingOwner = await readOrganizationWorkspaceMapLockOwner(lockPath);
|
|
311
|
+
if (!existingOwner) {
|
|
312
|
+
const entries = await fs.readdir(lockPath).catch(() => []);
|
|
313
|
+
if (entries.length === 0
|
|
314
|
+
&& await quarantineStaleEmptyOrganizationWorkspaceMapLock(lockPath)) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (entries.length === 0 && Date.now() - startedAt < ORGANIZATION_WORKSPACE_MAP_LOCK_TIMEOUT_MS) {
|
|
318
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
throw organizationWorkspaceMapLockCollision(lockPath);
|
|
322
|
+
}
|
|
323
|
+
const ownerAgeMs = Date.now() - Date.parse(existingOwner.createdAt);
|
|
324
|
+
if (existingOwner.hostname === os.hostname()
|
|
325
|
+
&& ownerAgeMs > ORGANIZATION_WORKSPACE_MAP_STALE_LOCK_MS
|
|
326
|
+
&& !isProcessAlive(existingOwner.pid)) {
|
|
327
|
+
await reclaimStaleOrganizationWorkspaceMapLock(lockPath, existingOwner);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
await fs.rename(acquisitionPath, lockPath);
|
|
335
|
+
lockAcquired = true;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
if (!ORGANIZATION_WORKSPACE_MAP_LOCK_TARGET_EXISTS_CODES.has(errorCode(error) ?? ""))
|
|
340
|
+
throw error;
|
|
341
|
+
// Another process appeared between the existence probe and rename;
|
|
342
|
+
// inspect it through the non-destructive path on the next iteration.
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return await fn();
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
if (lockAcquired) {
|
|
349
|
+
await releaseOrganizationWorkspaceMapLock(lockPath, token);
|
|
350
|
+
}
|
|
351
|
+
else if (acquisitionCreated) {
|
|
352
|
+
await fs.rm(acquisitionPath, { recursive: true, force: true });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function organizationWorkspaceMapLockCollision(lockPath) {
|
|
357
|
+
return Object.assign(new Error(`Refusing to replace the unrecognized organization workspace mapping lock path at ${lockPath}. `
|
|
358
|
+
+ "Rename or restore that directory before starting Rudder."), { code: "RUDDER_WORKSPACE_MAP_LOCK_COLLISION" });
|
|
359
|
+
}
|
|
360
|
+
async function quarantineStaleEmptyOrganizationWorkspaceMapLock(lockPath) {
|
|
361
|
+
const lockStat = await lstatIfExists(lockPath);
|
|
362
|
+
if (!lockStat
|
|
363
|
+
|| !lockStat.isDirectory()
|
|
364
|
+
|| lockStat.isSymbolicLink()
|
|
365
|
+
|| (lockStat.mode & 0o777) !== 0o700
|
|
366
|
+
|| Date.now() - lockStat.mtimeMs <= ORGANIZATION_WORKSPACE_MAP_STALE_LOCK_MS)
|
|
367
|
+
return false;
|
|
368
|
+
const entries = await fs.readdir(lockPath).catch((error) => {
|
|
369
|
+
if (errorCode(error) === "ENOENT")
|
|
370
|
+
return [];
|
|
371
|
+
throw error;
|
|
372
|
+
});
|
|
373
|
+
if (entries.length > 0)
|
|
374
|
+
return false;
|
|
375
|
+
const quarantinePath = path.join(path.dirname(lockPath), `${ORGANIZATION_WORKSPACE_MAP_LOCK_RECOVERY_PREFIX}${randomUUID()}`);
|
|
376
|
+
try {
|
|
377
|
+
await fs.rename(lockPath, quarantinePath);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
if (errorCode(error) === "ENOENT")
|
|
381
|
+
return true;
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
await syncDirectory(path.dirname(lockPath));
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
async function reclaimStaleOrganizationWorkspaceMapLock(lockPath, observedOwner) {
|
|
388
|
+
const claim = {
|
|
389
|
+
version: 1,
|
|
390
|
+
token: randomUUID(),
|
|
391
|
+
ownerToken: observedOwner.token,
|
|
392
|
+
pid: process.pid,
|
|
393
|
+
hostname: os.hostname(),
|
|
394
|
+
createdAt: new Date().toISOString(),
|
|
395
|
+
};
|
|
396
|
+
const reclaimPath = path.join(lockPath, `${ORGANIZATION_WORKSPACE_MAP_RECLAIM_PREFIX}${claim.token}.json`);
|
|
397
|
+
let handle;
|
|
398
|
+
try {
|
|
399
|
+
handle = await fs.open(reclaimPath, "wx", 0o600);
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
if (errorCode(error) === "ENOENT")
|
|
403
|
+
return;
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
await handle.writeFile(`${JSON.stringify(claim)}\n`, "utf8");
|
|
408
|
+
await syncFileHandle(handle);
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
await handle.close();
|
|
412
|
+
}
|
|
413
|
+
// Let concurrently-starting reclaimers publish before electing the oldest live claim.
|
|
414
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
415
|
+
const claims = await readOrganizationWorkspaceMapReclaimClaims(lockPath);
|
|
416
|
+
await Promise.all(claims.map(async (candidate) => {
|
|
417
|
+
if (candidate.hostname === os.hostname()
|
|
418
|
+
&& Date.now() - Date.parse(candidate.createdAt) > ORGANIZATION_WORKSPACE_MAP_STALE_LOCK_MS
|
|
419
|
+
&& !isProcessAlive(candidate.pid)) {
|
|
420
|
+
await fs.rm(candidate.path, { force: true });
|
|
421
|
+
}
|
|
422
|
+
}));
|
|
423
|
+
const liveClaims = (await readOrganizationWorkspaceMapReclaimClaims(lockPath))
|
|
424
|
+
.filter((candidate) => candidate.ownerToken === observedOwner.token)
|
|
425
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.token.localeCompare(right.token));
|
|
426
|
+
if (liveClaims[0]?.token !== claim.token) {
|
|
427
|
+
await fs.rm(reclaimPath, { force: true });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const currentOwner = await readOrganizationWorkspaceMapLockOwner(lockPath);
|
|
431
|
+
const stillStale = currentOwner?.token === observedOwner.token
|
|
432
|
+
&& currentOwner.hostname === os.hostname()
|
|
433
|
+
&& Date.now() - Date.parse(currentOwner.createdAt) > ORGANIZATION_WORKSPACE_MAP_STALE_LOCK_MS
|
|
434
|
+
&& !isProcessAlive(currentOwner.pid);
|
|
435
|
+
if (!stillStale) {
|
|
436
|
+
await fs.rm(reclaimPath, { force: true });
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const tombstonePath = `${lockPath}.reclaimed-${observedOwner.token}-${randomUUID()}`;
|
|
440
|
+
try {
|
|
441
|
+
await fs.rename(lockPath, tombstonePath);
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
if (errorCode(error) !== "ENOENT")
|
|
445
|
+
throw error;
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
// Only the positively identified directory captured by the atomic rename is removed.
|
|
449
|
+
await fs.rm(tombstonePath, { recursive: true, force: true });
|
|
450
|
+
}
|
|
451
|
+
async function readOrganizationWorkspaceMapReclaimClaims(lockPath) {
|
|
452
|
+
try {
|
|
453
|
+
const entries = await fs.readdir(lockPath, { withFileTypes: true });
|
|
454
|
+
const claims = await Promise.all(entries.flatMap((entry) => entry.isFile()
|
|
455
|
+
&& entry.name.startsWith(ORGANIZATION_WORKSPACE_MAP_RECLAIM_PREFIX)
|
|
456
|
+
&& entry.name.endsWith(".json")
|
|
457
|
+
? [readOrganizationWorkspaceMapReclaimClaim(path.join(lockPath, entry.name), entry.name)]
|
|
458
|
+
: []));
|
|
459
|
+
return claims.flatMap((claim) => claim ? [claim] : []);
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
if (errorCode(error) === "ENOENT")
|
|
463
|
+
return [];
|
|
464
|
+
throw error;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async function readOrganizationWorkspaceMapReclaimClaim(claimPath, fileName) {
|
|
468
|
+
try {
|
|
469
|
+
const claim = JSON.parse(await fs.readFile(claimPath, "utf8"));
|
|
470
|
+
if (claim.version !== 1
|
|
471
|
+
|| typeof claim.token !== "string"
|
|
472
|
+
|| fileName !== `${ORGANIZATION_WORKSPACE_MAP_RECLAIM_PREFIX}${claim.token}.json`
|
|
473
|
+
|| typeof claim.ownerToken !== "string"
|
|
474
|
+
|| !Number.isSafeInteger(claim.pid)
|
|
475
|
+
|| (claim.pid ?? 0) <= 0
|
|
476
|
+
|| typeof claim.hostname !== "string"
|
|
477
|
+
|| typeof claim.createdAt !== "string"
|
|
478
|
+
|| !Number.isFinite(Date.parse(claim.createdAt)))
|
|
479
|
+
return null;
|
|
480
|
+
return { ...claim, path: claimPath };
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function releaseOrganizationWorkspaceMapLock(lockPath, token) {
|
|
487
|
+
const currentOwner = await readOrganizationWorkspaceMapLockOwner(lockPath);
|
|
488
|
+
if (currentOwner?.token !== token)
|
|
489
|
+
return;
|
|
490
|
+
const tombstonePath = `${lockPath}.released-${token}-${randomUUID()}`;
|
|
491
|
+
try {
|
|
492
|
+
await fs.rename(lockPath, tombstonePath);
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
if (errorCode(error) !== "ENOENT")
|
|
496
|
+
throw error;
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
await fs.rm(tombstonePath, { recursive: true, force: true });
|
|
500
|
+
}
|
|
501
|
+
async function readOrganizationWorkspaceMapLockOwner(lockPath) {
|
|
502
|
+
try {
|
|
503
|
+
const entries = await fs.readdir(lockPath, { withFileTypes: true });
|
|
504
|
+
const ownerEntries = entries.filter((entry) => entry.isFile()
|
|
505
|
+
&& entry.name.startsWith(ORGANIZATION_WORKSPACE_MAP_LOCK_OWNER_PREFIX)
|
|
506
|
+
&& entry.name.endsWith(".json"));
|
|
507
|
+
if (ownerEntries.length !== 1)
|
|
508
|
+
return null;
|
|
509
|
+
if (entries.some((entry) => entry.name !== ownerEntries[0].name
|
|
510
|
+
&& !(entry.isFile()
|
|
511
|
+
&& entry.name.startsWith(ORGANIZATION_WORKSPACE_MAP_RECLAIM_PREFIX)
|
|
512
|
+
&& entry.name.endsWith(".json"))))
|
|
513
|
+
return null;
|
|
514
|
+
const fileName = ownerEntries[0].name;
|
|
515
|
+
if (!fileName.startsWith(ORGANIZATION_WORKSPACE_MAP_LOCK_OWNER_PREFIX) || !fileName.endsWith(".json")) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
const owner = JSON.parse(await fs.readFile(path.join(lockPath, fileName), "utf8"));
|
|
519
|
+
if (owner.kind !== ORGANIZATION_WORKSPACE_MAP_LOCK_KIND
|
|
520
|
+
|| owner.version !== 1
|
|
521
|
+
|| typeof owner.token !== "string"
|
|
522
|
+
|| fileName !== `${ORGANIZATION_WORKSPACE_MAP_LOCK_OWNER_PREFIX}${owner.token}.json`
|
|
523
|
+
|| !Number.isSafeInteger(owner.pid)
|
|
524
|
+
|| (owner.pid ?? 0) <= 0
|
|
525
|
+
|| typeof owner.hostname !== "string"
|
|
526
|
+
|| typeof owner.createdAt !== "string"
|
|
527
|
+
|| !Number.isFinite(Date.parse(owner.createdAt)))
|
|
528
|
+
return null;
|
|
529
|
+
return owner;
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function isProcessAlive(pid) {
|
|
536
|
+
try {
|
|
537
|
+
process.kill(pid, 0);
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
return errorCode(error) !== "ESRCH";
|
|
542
|
+
}
|
|
543
|
+
}
|
|
244
544
|
export async function migrateOrganizationStorageRoot(orgId) {
|
|
545
|
+
return await updateOrganizationWorkspaceMap(() => migrateOrganizationStorageRootUnlocked(orgId));
|
|
546
|
+
}
|
|
547
|
+
async function migrateOrganizationStorageRootUnlocked(orgId) {
|
|
245
548
|
const canonicalRootPath = resolveOrganizationRoot(orgId);
|
|
246
549
|
const legacyRootPath = resolveLegacyOrganizationRoot(orgId);
|
|
247
550
|
if (canonicalRootPath === legacyRootPath) {
|
|
@@ -316,6 +619,9 @@ export async function migrateOrganizationStorageRoot(orgId) {
|
|
|
316
619
|
};
|
|
317
620
|
}
|
|
318
621
|
export async function migrateOrganizationWorkspaceRoot(orgId, options) {
|
|
622
|
+
return await updateOrganizationWorkspaceMap(() => migrateOrganizationWorkspaceRootUnlocked(orgId, options));
|
|
623
|
+
}
|
|
624
|
+
async function migrateOrganizationWorkspaceRootUnlocked(orgId, options) {
|
|
319
625
|
const canonicalRootPath = resolveOrganizationWorkspaceRoot(orgId);
|
|
320
626
|
const legacyRootPath = resolveLegacyOrganizationWorkspaceRoot(orgId);
|
|
321
627
|
const previousDocumentsRootPath = resolvePreviousDocumentsOrganizationWorkspaceRoot(orgId);
|
|
@@ -337,24 +643,53 @@ export async function migrateOrganizationWorkspaceRoot(orgId, options) {
|
|
|
337
643
|
for (const candidateLegacyRootPath of legacyRootPaths) {
|
|
338
644
|
firstLegacyRootPath = firstLegacyRootPath || candidateLegacyRootPath;
|
|
339
645
|
const legacyExists = await directoryExists(candidateLegacyRootPath);
|
|
340
|
-
if (!legacyExists)
|
|
646
|
+
if (!legacyExists) {
|
|
647
|
+
if (await directoryExists(canonicalRootPath)
|
|
648
|
+
&& await workspaceIdentityMatches(canonicalRootPath, orgId)
|
|
649
|
+
&& await workspaceMigrationIncludesAlias(canonicalRootPath, candidateLegacyRootPath)) {
|
|
650
|
+
await fs.mkdir(path.dirname(candidateLegacyRootPath), { recursive: true });
|
|
651
|
+
await createDirectoryCompatibilityAlias(candidateLegacyRootPath, canonicalRootPath);
|
|
652
|
+
migrated = true;
|
|
653
|
+
migratedFromRootPath = candidateLegacyRootPath;
|
|
654
|
+
}
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (await pathsReferenceSameDirectory(candidateLegacyRootPath, canonicalRootPath))
|
|
341
658
|
continue;
|
|
342
659
|
const canonicalExists = await directoryExists(canonicalRootPath);
|
|
343
660
|
try {
|
|
344
661
|
if (canonicalExists) {
|
|
345
|
-
await
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
662
|
+
await assertWorkspaceRootMoveCanBeAtomic(candidateLegacyRootPath, canonicalRootPath);
|
|
663
|
+
await ensureOrganizationWorkspaceIdentity(canonicalRootPath, orgId);
|
|
664
|
+
await ensureOrganizationWorkspaceIdentity(candidateLegacyRootPath, orgId);
|
|
665
|
+
const compatibilityAliases = new Set([
|
|
666
|
+
...(await readWorkspaceMigrationAliases(canonicalRootPath)),
|
|
667
|
+
...(await readWorkspaceMigrationAliases(candidateLegacyRootPath)),
|
|
668
|
+
path.resolve(candidateLegacyRootPath),
|
|
669
|
+
]);
|
|
670
|
+
// Preserve the legacy root inode while merging. A Run may already have
|
|
671
|
+
// a cwd anywhere below it; renaming this root to the canonical path
|
|
672
|
+
// keeps that cwd attached to the live workspace after the rekey.
|
|
673
|
+
await writeWorkspaceMigrationState(candidateLegacyRootPath, [...compatibilityAliases]);
|
|
674
|
+
await assertCanMergeDirectoryContents(canonicalRootPath, candidateLegacyRootPath);
|
|
675
|
+
const retainedDuplicates = await mergeDirectoryContents(canonicalRootPath, candidateLegacyRootPath);
|
|
676
|
+
if (retainedDuplicates) {
|
|
677
|
+
await archiveRetainedMigrationSource(canonicalRootPath, path.dirname(candidateLegacyRootPath));
|
|
678
|
+
}
|
|
349
679
|
else
|
|
350
|
-
await fs.rmdir(
|
|
680
|
+
await fs.rmdir(canonicalRootPath);
|
|
681
|
+
await moveWorkspacePathWithCompatibilityAlias(candidateLegacyRootPath, canonicalRootPath);
|
|
351
682
|
migrated = true;
|
|
352
683
|
migratedFromRootPath = candidateLegacyRootPath;
|
|
353
684
|
mergedIntoExistingTarget = true;
|
|
354
685
|
continue;
|
|
355
686
|
}
|
|
356
687
|
await fs.mkdir(path.dirname(canonicalRootPath), { recursive: true });
|
|
357
|
-
await
|
|
688
|
+
await assertWorkspaceRootMoveCanBeAtomic(candidateLegacyRootPath, canonicalRootPath);
|
|
689
|
+
// The identity moves with the directory and makes a post-rename crash repairable.
|
|
690
|
+
await ensureOrganizationWorkspaceIdentity(candidateLegacyRootPath, orgId);
|
|
691
|
+
await recordWorkspaceCompatibilityAlias(candidateLegacyRootPath, candidateLegacyRootPath);
|
|
692
|
+
await moveWorkspacePathWithCompatibilityAlias(candidateLegacyRootPath, canonicalRootPath);
|
|
358
693
|
migrated = true;
|
|
359
694
|
migratedFromRootPath = candidateLegacyRootPath;
|
|
360
695
|
}
|
|
@@ -490,33 +825,49 @@ function sanitizeFriendlyPathSegment(value, fallback = "_default") {
|
|
|
490
825
|
return sanitized || fallback;
|
|
491
826
|
}
|
|
492
827
|
function sanitizeOrganizationWorkspaceFolderName(value, fallback) {
|
|
828
|
+
const safeFallback = sanitizeFriendlyPathSegment(fallback, "_default")
|
|
829
|
+
.replace(/[.]+$/g, "")
|
|
830
|
+
.toLowerCase();
|
|
831
|
+
const normalizedFallback = RESERVED_ORGANIZATION_WORKSPACE_NAMES.has(safeFallback)
|
|
832
|
+
? `organization-${safeFallback}`
|
|
833
|
+
: safeFallback;
|
|
493
834
|
const sanitized = sanitizeFriendlyPathSegment(value, fallback)
|
|
494
835
|
.replace(/[.]+$/g, "")
|
|
495
836
|
.toLowerCase();
|
|
496
837
|
const normalized = sanitized && !RESERVED_ORGANIZATION_WORKSPACE_NAMES.has(sanitized)
|
|
497
838
|
? sanitized
|
|
498
|
-
:
|
|
499
|
-
return normalized ||
|
|
839
|
+
: normalizedFallback;
|
|
840
|
+
return normalized || normalizedFallback;
|
|
841
|
+
}
|
|
842
|
+
function isOrganizationWorkspaceMapInternalDirectoryName(name) {
|
|
843
|
+
return name === ORGANIZATION_WORKSPACE_MAP_LOCK
|
|
844
|
+
|| name.startsWith(`${ORGANIZATION_WORKSPACE_MAP_LOCK}.`)
|
|
845
|
+
|| name.startsWith(ORGANIZATION_WORKSPACE_MAP_LOCK_ACQUIRE_PREFIX)
|
|
846
|
+
|| name.startsWith(ORGANIZATION_WORKSPACE_MAP_LOCK_RECOVERY_PREFIX);
|
|
847
|
+
}
|
|
848
|
+
function isOrganizationWorkspaceMapRecord(value) {
|
|
849
|
+
return typeof value === "object"
|
|
850
|
+
&& value !== null
|
|
851
|
+
&& typeof value.instanceId === "string"
|
|
852
|
+
&& typeof value.orgId === "string"
|
|
853
|
+
&& typeof value.folderName === "string"
|
|
854
|
+
&& typeof value.createdAt === "string"
|
|
855
|
+
&& typeof value.updatedAt === "string";
|
|
500
856
|
}
|
|
501
857
|
async function readOrganizationWorkspaceMapFileState() {
|
|
502
858
|
const mapPath = resolveOrganizationWorkspaceMapPath();
|
|
503
859
|
try {
|
|
504
860
|
const raw = await fs.readFile(mapPath, "utf8");
|
|
505
861
|
const parsed = JSON.parse(raw);
|
|
506
|
-
if (parsed.version !== 1 || !Array.isArray(parsed.organizations)
|
|
862
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.organizations)
|
|
863
|
+
|| !parsed.organizations.every(isOrganizationWorkspaceMapRecord)) {
|
|
507
864
|
throw new Error(`Invalid organization workspace mapping file at ${mapPath}. Restore the mapping file or restore organizations from workspace backups before starting Rudder.`);
|
|
508
865
|
}
|
|
509
866
|
return {
|
|
510
867
|
exists: true,
|
|
511
868
|
map: {
|
|
512
869
|
version: 1,
|
|
513
|
-
organizations: parsed.organizations
|
|
514
|
-
&& entry !== null
|
|
515
|
-
&& typeof entry.instanceId === "string"
|
|
516
|
-
&& typeof entry.orgId === "string"
|
|
517
|
-
&& typeof entry.folderName === "string"
|
|
518
|
-
&& typeof entry.createdAt === "string"
|
|
519
|
-
&& typeof entry.updatedAt === "string"),
|
|
870
|
+
organizations: parsed.organizations,
|
|
520
871
|
},
|
|
521
872
|
};
|
|
522
873
|
}
|
|
@@ -547,47 +898,215 @@ function findOrganizationWorkspaceMapRecord(map, orgId) {
|
|
|
547
898
|
function readOrganizationWorkspaceFolderName(orgId) {
|
|
548
899
|
if (!usesFriendlyOrganizationWorkspaceHome())
|
|
549
900
|
return null;
|
|
901
|
+
const mapPath = resolveOrganizationWorkspaceMapPath();
|
|
902
|
+
let raw;
|
|
550
903
|
try {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
904
|
+
raw = fsSync.readFileSync(mapPath, "utf8");
|
|
905
|
+
}
|
|
906
|
+
catch (error) {
|
|
907
|
+
if (errorCode(error) === "ENOENT")
|
|
554
908
|
return null;
|
|
555
|
-
|
|
556
|
-
&& entry !== null
|
|
557
|
-
&& entry.instanceId === resolveRudderInstanceId()
|
|
558
|
-
&& entry.orgId === orgId
|
|
559
|
-
&& typeof entry.folderName === "string");
|
|
560
|
-
return record?.folderName ?? null;
|
|
909
|
+
throw new Error(`Rudder could not read the organization workspace mapping at ${mapPath}.`, { cause: error });
|
|
561
910
|
}
|
|
562
|
-
|
|
911
|
+
let parsed;
|
|
912
|
+
try {
|
|
913
|
+
parsed = JSON.parse(raw);
|
|
914
|
+
}
|
|
915
|
+
catch (error) {
|
|
916
|
+
throw new Error(`Invalid organization workspace mapping file at ${mapPath}.`, { cause: error });
|
|
917
|
+
}
|
|
918
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.organizations)
|
|
919
|
+
|| !parsed.organizations.every(isOrganizationWorkspaceMapRecord)) {
|
|
920
|
+
throw new Error(`Invalid organization workspace mapping file at ${mapPath}.`);
|
|
921
|
+
}
|
|
922
|
+
const record = parsed.organizations.find((entry) => entry.instanceId === resolveRudderInstanceId() && entry.orgId === orgId);
|
|
923
|
+
if (!record)
|
|
563
924
|
return null;
|
|
925
|
+
try {
|
|
926
|
+
if (typeof record.folderName !== "string") {
|
|
927
|
+
throw new Error("Organization workspace folder mapping must contain a folder name.");
|
|
928
|
+
}
|
|
929
|
+
const folderName = validatePathSegment(record.folderName, "organization workspace folder");
|
|
930
|
+
if (folderName !== record.folderName) {
|
|
931
|
+
throw new Error("Organization workspace folder mapping must not contain leading or trailing whitespace.");
|
|
932
|
+
}
|
|
933
|
+
if (RESERVED_ORGANIZATION_WORKSPACE_NAMES.has(folderName.toLowerCase())) {
|
|
934
|
+
throw new Error(`Organization workspace folder mapping uses reserved folder '${folderName}'.`);
|
|
935
|
+
}
|
|
936
|
+
return folderName;
|
|
937
|
+
}
|
|
938
|
+
catch (error) {
|
|
939
|
+
const detail = error instanceof Error && error.message ? ` ${error.message}` : "";
|
|
940
|
+
throw new Error(`Invalid organization workspace folder mapping for '${orgId}'.${detail}`, { cause: error });
|
|
564
941
|
}
|
|
565
942
|
}
|
|
566
943
|
async function writeOrganizationWorkspaceMapFile(mapPath, map) {
|
|
567
944
|
const tempPath = `${mapPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
568
945
|
await fs.mkdir(path.dirname(mapPath), { recursive: true });
|
|
569
|
-
await fs.
|
|
946
|
+
const handle = await fs.open(tempPath, "wx", 0o600);
|
|
947
|
+
try {
|
|
948
|
+
await handle.writeFile(`${JSON.stringify(map, null, 2)}\n`, "utf8");
|
|
949
|
+
await syncFileHandle(handle);
|
|
950
|
+
}
|
|
951
|
+
finally {
|
|
952
|
+
await handle.close();
|
|
953
|
+
}
|
|
570
954
|
await fs.rename(tempPath, mapPath);
|
|
955
|
+
await syncDirectory(path.dirname(mapPath));
|
|
956
|
+
}
|
|
957
|
+
async function ensureOrganizationWorkspaceIdentity(root, orgId) {
|
|
958
|
+
const identityPath = path.join(root, ORGANIZATION_WORKSPACE_IDENTITY_FILE);
|
|
959
|
+
const expectedOrgId = validatePathSegment(orgId, "org id");
|
|
960
|
+
try {
|
|
961
|
+
const existing = JSON.parse(await fs.readFile(identityPath, "utf8"));
|
|
962
|
+
if (existing.orgId !== expectedOrgId) {
|
|
963
|
+
throw new Error(`Organization workspace identity mismatch at ${identityPath}.`);
|
|
964
|
+
}
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
catch (error) {
|
|
968
|
+
if (errorCode(error) === "ENOENT") {
|
|
969
|
+
// Continue below and publish a fresh identity atomically.
|
|
970
|
+
}
|
|
971
|
+
else if (error instanceof SyntaxError) {
|
|
972
|
+
const corruptPath = `${identityPath}.corrupt-${randomUUID()}`;
|
|
973
|
+
await fs.rename(identityPath, corruptPath);
|
|
974
|
+
await syncDirectory(root);
|
|
975
|
+
}
|
|
976
|
+
else {
|
|
977
|
+
throw error;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const tempPath = `${identityPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
981
|
+
try {
|
|
982
|
+
const handle = await fs.open(tempPath, "wx", 0o600);
|
|
983
|
+
try {
|
|
984
|
+
await handle.writeFile(`${JSON.stringify({ version: 1, orgId: expectedOrgId }, null, 2)}\n`, "utf8");
|
|
985
|
+
await syncFileHandle(handle);
|
|
986
|
+
}
|
|
987
|
+
finally {
|
|
988
|
+
await handle.close();
|
|
989
|
+
}
|
|
990
|
+
await fs.rename(tempPath, identityPath);
|
|
991
|
+
await syncDirectory(root);
|
|
992
|
+
}
|
|
993
|
+
catch (error) {
|
|
994
|
+
if (errorCode(error) !== "EEXIST" || !(await workspaceIdentityMatches(root, expectedOrgId))) {
|
|
995
|
+
throw error;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
finally {
|
|
999
|
+
await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
async function workspaceIdentityMatches(root, orgId) {
|
|
1003
|
+
try {
|
|
1004
|
+
const identity = JSON.parse(await fs.readFile(path.join(root, ORGANIZATION_WORKSPACE_IDENTITY_FILE), "utf8"));
|
|
1005
|
+
return identity.orgId === validatePathSegment(orgId, "org id");
|
|
1006
|
+
}
|
|
1007
|
+
catch {
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
async function assertFriendlyWorkspaceRemovalOwnership(orgId, workspaceRootPath, map) {
|
|
1012
|
+
if (!usesFriendlyOrganizationWorkspaceHome())
|
|
1013
|
+
return;
|
|
1014
|
+
const record = findOrganizationWorkspaceMapRecord(map, orgId);
|
|
1015
|
+
if (record) {
|
|
1016
|
+
const normalizedFolderName = record.folderName.toLowerCase();
|
|
1017
|
+
const duplicate = map.organizations.find((entry) => entry.instanceId === resolveRudderInstanceId()
|
|
1018
|
+
&& entry.orgId !== orgId
|
|
1019
|
+
&& entry.folderName.toLowerCase() === normalizedFolderName);
|
|
1020
|
+
if (duplicate) {
|
|
1021
|
+
throw new Error([
|
|
1022
|
+
`Refusing to remove organization workspace '${workspaceRootPath}'.`,
|
|
1023
|
+
`The folder is also mapped to organization '${duplicate.orgId}'.`,
|
|
1024
|
+
].join(" "));
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
const existing = await pathExists(workspaceRootPath);
|
|
1028
|
+
if (!existing)
|
|
1029
|
+
return;
|
|
1030
|
+
if (existing.isSymbolicLink() || !existing.isDirectory()) {
|
|
1031
|
+
throw new Error(`Refusing to remove organization workspace '${workspaceRootPath}' because it is not a regular directory.`);
|
|
1032
|
+
}
|
|
1033
|
+
if (!await workspaceIdentityMatches(workspaceRootPath, orgId)) {
|
|
1034
|
+
throw new Error(`Refusing to remove organization workspace '${workspaceRootPath}' because its identity does not match '${orgId}'.`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
async function recordWorkspaceCompatibilityAlias(root, aliasPath) {
|
|
1038
|
+
const aliases = await readWorkspaceMigrationAliases(root);
|
|
1039
|
+
const normalizedAliasPath = path.resolve(aliasPath);
|
|
1040
|
+
if (aliases.includes(normalizedAliasPath))
|
|
1041
|
+
return;
|
|
1042
|
+
aliases.push(normalizedAliasPath);
|
|
1043
|
+
await writeWorkspaceMigrationState(root, aliases);
|
|
1044
|
+
}
|
|
1045
|
+
async function readWorkspaceMigrationAliases(root) {
|
|
1046
|
+
const migrationPath = path.join(root, ORGANIZATION_WORKSPACE_MIGRATION_FILE);
|
|
1047
|
+
try {
|
|
1048
|
+
const parsed = JSON.parse(await fs.readFile(migrationPath, "utf8"));
|
|
1049
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.compatibilityAliases)) {
|
|
1050
|
+
throw new Error(`Invalid organization workspace migration state at ${migrationPath}.`);
|
|
1051
|
+
}
|
|
1052
|
+
return parsed.compatibilityAliases.filter((entry) => typeof entry === "string");
|
|
1053
|
+
}
|
|
1054
|
+
catch (error) {
|
|
1055
|
+
if (errorCode(error) === "ENOENT")
|
|
1056
|
+
return [];
|
|
1057
|
+
throw error;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
async function writeWorkspaceMigrationState(root, aliases) {
|
|
1061
|
+
const migrationPath = path.join(root, ORGANIZATION_WORKSPACE_MIGRATION_FILE);
|
|
1062
|
+
const state = {
|
|
1063
|
+
version: 1,
|
|
1064
|
+
compatibilityAliases: [...new Set(aliases.map((alias) => path.resolve(alias)))].sort(),
|
|
1065
|
+
};
|
|
1066
|
+
const tempPath = `${migrationPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1067
|
+
const handle = await fs.open(tempPath, "wx", 0o600);
|
|
1068
|
+
try {
|
|
1069
|
+
await handle.writeFile(`${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
1070
|
+
await syncFileHandle(handle);
|
|
1071
|
+
}
|
|
1072
|
+
finally {
|
|
1073
|
+
await handle.close();
|
|
1074
|
+
}
|
|
1075
|
+
await fs.rename(tempPath, migrationPath);
|
|
1076
|
+
await syncDirectory(root);
|
|
1077
|
+
}
|
|
1078
|
+
async function workspaceMigrationIncludesAlias(root, aliasPath) {
|
|
1079
|
+
try {
|
|
1080
|
+
const state = JSON.parse(await fs.readFile(path.join(root, ORGANIZATION_WORKSPACE_MIGRATION_FILE), "utf8"));
|
|
1081
|
+
return state.version === 1
|
|
1082
|
+
&& Array.isArray(state.compatibilityAliases)
|
|
1083
|
+
&& state.compatibilityAliases.includes(path.resolve(aliasPath));
|
|
1084
|
+
}
|
|
1085
|
+
catch {
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
571
1088
|
}
|
|
572
1089
|
async function allocateOrganizationWorkspaceFolderName(input) {
|
|
573
1090
|
const fallback = resolveOrganizationStorageKey(input.orgId);
|
|
574
1091
|
const base = sanitizeOrganizationWorkspaceFolderName(input.orgName ?? input.orgUrlKey, fallback);
|
|
575
1092
|
const usedFolders = new Set(input.map.organizations
|
|
576
1093
|
.filter((entry) => entry.instanceId === resolveRudderInstanceId() && entry.orgId !== input.orgId)
|
|
577
|
-
.map((entry) => entry.folderName));
|
|
1094
|
+
.map((entry) => entry.folderName.toLowerCase()));
|
|
578
1095
|
for (let attempt = 1; attempt < 10000; attempt += 1) {
|
|
579
1096
|
const folderName = attempt === 1 ? base : `${base}-${attempt}`;
|
|
580
|
-
if (usedFolders.has(folderName))
|
|
1097
|
+
if (usedFolders.has(folderName.toLowerCase()))
|
|
581
1098
|
continue;
|
|
582
1099
|
const existing = await pathExists(path.resolve(input.homeDir, folderName));
|
|
583
1100
|
if (existing && !existing.isDirectory())
|
|
584
1101
|
continue;
|
|
585
|
-
if (existing
|
|
1102
|
+
if (existing
|
|
1103
|
+
&& folderName === base
|
|
1104
|
+
&& await workspaceIdentityMatches(path.resolve(input.homeDir, folderName), input.orgId))
|
|
586
1105
|
return folderName;
|
|
587
1106
|
if (existing) {
|
|
588
1107
|
const owned = input.map.organizations.some((entry) => entry.instanceId === resolveRudderInstanceId()
|
|
589
1108
|
&& entry.orgId === input.orgId
|
|
590
|
-
&& entry.folderName === folderName);
|
|
1109
|
+
&& entry.folderName.toLowerCase() === folderName.toLowerCase());
|
|
591
1110
|
if (!owned)
|
|
592
1111
|
continue;
|
|
593
1112
|
}
|
|
@@ -596,22 +1115,38 @@ async function allocateOrganizationWorkspaceFolderName(input) {
|
|
|
596
1115
|
throw new Error("Unable to allocate organization workspace folder name");
|
|
597
1116
|
}
|
|
598
1117
|
export async function removeOrganizationStorage(orgId) {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
1118
|
+
return await updateOrganizationWorkspaceMap(async () => {
|
|
1119
|
+
const normalizedOrgId = validatePathSegment(orgId, "org id");
|
|
1120
|
+
const organizationRootPath = resolveOrganizationRoot(normalizedOrgId);
|
|
1121
|
+
const legacyOrganizationRootPath = resolveLegacyOrganizationRoot(normalizedOrgId);
|
|
1122
|
+
const workspaceRootPath = resolveOrganizationWorkspaceRoot(normalizedOrgId);
|
|
1123
|
+
const previousDocumentsWorkspaceRootPath = resolvePreviousDocumentsOrganizationWorkspaceRoot(normalizedOrgId);
|
|
1124
|
+
const legacyProjectsRootPath = path.resolve(resolveRudderInstanceRoot(), "projects", normalizedOrgId);
|
|
1125
|
+
const mapState = await readOrganizationWorkspaceMapFileState();
|
|
1126
|
+
await assertFriendlyWorkspaceRemovalOwnership(normalizedOrgId, workspaceRootPath, mapState.map);
|
|
1127
|
+
const pathsToRemove = new Set([
|
|
1128
|
+
organizationRootPath,
|
|
1129
|
+
workspaceRootPath,
|
|
1130
|
+
legacyOrganizationRootPath,
|
|
1131
|
+
previousDocumentsWorkspaceRootPath,
|
|
1132
|
+
legacyProjectsRootPath,
|
|
1133
|
+
]);
|
|
1134
|
+
await Promise.all([...pathsToRemove].map((targetPath) => fs.rm(targetPath, { recursive: true, force: true })));
|
|
1135
|
+
const nextOrganizations = mapState.map.organizations.filter((entry) => !(entry.instanceId === resolveRudderInstanceId() && entry.orgId === normalizedOrgId));
|
|
1136
|
+
if (nextOrganizations.length !== mapState.map.organizations.length) {
|
|
1137
|
+
await writeOrganizationWorkspaceMapFile(resolveOrganizationWorkspaceMapPath(), {
|
|
1138
|
+
...mapState.map,
|
|
1139
|
+
organizations: nextOrganizations,
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
return {
|
|
1143
|
+
organizationRootPath,
|
|
1144
|
+
legacyOrganizationRootPath,
|
|
1145
|
+
workspaceRootPath,
|
|
1146
|
+
previousDocumentsWorkspaceRootPath,
|
|
1147
|
+
legacyProjectsRootPath,
|
|
1148
|
+
};
|
|
1149
|
+
});
|
|
615
1150
|
}
|
|
616
1151
|
async function listDirectoryNames(rootPath) {
|
|
617
1152
|
try {
|
|
@@ -679,6 +1214,17 @@ function formatOrganizationWorkspacePermissionMessage(input) {
|
|
|
679
1214
|
"Grant Rudder permission to access Documents, choose a writable folder with RUDDER_ORGANIZATION_WORKSPACE_HOME, or on Windows try running Rudder as administrator if the folder policy requires it.",
|
|
680
1215
|
].join(" ");
|
|
681
1216
|
}
|
|
1217
|
+
async function assertOrganizationWorkspaceRootIsOwned(rootPath) {
|
|
1218
|
+
const stat = await lstatIfExists(rootPath);
|
|
1219
|
+
if (!stat)
|
|
1220
|
+
return;
|
|
1221
|
+
if (stat.isSymbolicLink()) {
|
|
1222
|
+
throw new Error("The organization workspace root cannot be a symbolic link.");
|
|
1223
|
+
}
|
|
1224
|
+
if (!stat.isDirectory()) {
|
|
1225
|
+
throw new Error("The organization workspace root must be a directory.");
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
682
1228
|
async function lstatIfExists(targetPath) {
|
|
683
1229
|
try {
|
|
684
1230
|
return await fs.lstat(targetPath);
|
|
@@ -696,16 +1242,64 @@ async function movePath(sourcePath, targetPath) {
|
|
|
696
1242
|
return;
|
|
697
1243
|
}
|
|
698
1244
|
catch (error) {
|
|
699
|
-
if (errorCode(error)
|
|
700
|
-
throw
|
|
1245
|
+
if (errorCode(error) === "EXDEV") {
|
|
1246
|
+
throw organizationWorkspaceCrossDeviceMigrationError(sourcePath, targetPath);
|
|
1247
|
+
}
|
|
1248
|
+
throw error;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async function pathsReferenceSameDirectory(first, second) {
|
|
1252
|
+
try {
|
|
1253
|
+
const [firstRealPath, secondRealPath] = await Promise.all([fs.realpath(first), fs.realpath(second)]);
|
|
1254
|
+
return firstRealPath === secondRealPath;
|
|
1255
|
+
}
|
|
1256
|
+
catch {
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
async function createDirectoryCompatibilityAlias(aliasPath, targetPath) {
|
|
1261
|
+
await fs.symlink(targetPath, aliasPath, process.platform === "win32" ? "junction" : "dir");
|
|
1262
|
+
await syncDirectory(path.dirname(aliasPath));
|
|
1263
|
+
}
|
|
1264
|
+
function organizationWorkspaceCrossDeviceMigrationError(sourcePath, targetPath) {
|
|
1265
|
+
return Object.assign(new Error(`Cannot atomically migrate organization workspace across filesystems from ${sourcePath} to ${targetPath}.`), { code: "RUDDER_WORKSPACE_CROSS_DEVICE_MIGRATION" });
|
|
1266
|
+
}
|
|
1267
|
+
async function assertWorkspaceRootMoveCanBeAtomic(sourcePath, targetPath) {
|
|
1268
|
+
await assertPathMoveCanBeAtomic(sourcePath, targetPath);
|
|
1269
|
+
}
|
|
1270
|
+
async function assertPathMoveCanBeAtomic(sourcePath, targetPath) {
|
|
1271
|
+
const sourceParentPath = path.dirname(sourcePath);
|
|
1272
|
+
const targetParentPath = path.dirname(targetPath);
|
|
1273
|
+
const [sourceParentStat, targetParentStat] = await Promise.all([
|
|
1274
|
+
fs.stat(sourceParentPath),
|
|
1275
|
+
fs.stat(targetParentPath),
|
|
1276
|
+
]);
|
|
1277
|
+
const sourceVolumeRoot = path.parse(sourceParentPath).root;
|
|
1278
|
+
const targetVolumeRoot = path.parse(targetParentPath).root;
|
|
1279
|
+
const sameVolumeRoot = process.platform === "win32"
|
|
1280
|
+
? sourceVolumeRoot.toLowerCase() === targetVolumeRoot.toLowerCase()
|
|
1281
|
+
: sourceVolumeRoot === targetVolumeRoot;
|
|
1282
|
+
if (sourceParentStat.dev !== targetParentStat.dev || !sameVolumeRoot) {
|
|
1283
|
+
throw organizationWorkspaceCrossDeviceMigrationError(sourcePath, targetPath);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
async function moveWorkspacePathWithCompatibilityAlias(sourcePath, targetPath) {
|
|
1287
|
+
try {
|
|
1288
|
+
await fs.rename(sourcePath, targetPath);
|
|
1289
|
+
}
|
|
1290
|
+
catch (error) {
|
|
1291
|
+
if (errorCode(error) === "EXDEV") {
|
|
1292
|
+
throw organizationWorkspaceCrossDeviceMigrationError(sourcePath, targetPath);
|
|
1293
|
+
}
|
|
1294
|
+
throw error;
|
|
1295
|
+
}
|
|
1296
|
+
try {
|
|
1297
|
+
await createDirectoryCompatibilityAlias(sourcePath, targetPath);
|
|
1298
|
+
}
|
|
1299
|
+
catch (error) {
|
|
1300
|
+
await fs.rename(targetPath, sourcePath).catch(() => { });
|
|
1301
|
+
throw error;
|
|
701
1302
|
}
|
|
702
|
-
await fs.cp(sourcePath, targetPath, {
|
|
703
|
-
recursive: true,
|
|
704
|
-
errorOnExist: true,
|
|
705
|
-
force: false,
|
|
706
|
-
preserveTimestamps: true,
|
|
707
|
-
});
|
|
708
|
-
await fs.rm(sourcePath, { recursive: true, force: false });
|
|
709
1303
|
}
|
|
710
1304
|
async function regularFilesHaveIdenticalContents(sourcePath, targetPath) {
|
|
711
1305
|
const sourceFile = await fs.open(sourcePath, "r");
|
|
@@ -745,8 +1339,8 @@ async function regularFilesHaveIdenticalContents(sourcePath, targetPath) {
|
|
|
745
1339
|
await sourceFile.close();
|
|
746
1340
|
}
|
|
747
1341
|
}
|
|
748
|
-
async function archiveRetainedMigrationSource(sourceRoot) {
|
|
749
|
-
const backupHome = path.join(
|
|
1342
|
+
async function archiveRetainedMigrationSource(sourceRoot, backupParentPath = path.dirname(sourceRoot)) {
|
|
1343
|
+
const backupHome = path.join(backupParentPath, MIGRATION_BACKUP_DIR_NAME);
|
|
750
1344
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
751
1345
|
const backupPath = path.join(backupHome, `${path.basename(sourceRoot)}-${timestamp}-${randomUUID()}`);
|
|
752
1346
|
await fs.mkdir(backupHome, { recursive: true });
|
|
@@ -758,10 +1352,13 @@ async function mergeDirectoryContents(sourceRoot, targetRoot) {
|
|
|
758
1352
|
const entries = await fs.readdir(sourceRoot, { withFileTypes: true });
|
|
759
1353
|
let retainedDuplicates = false;
|
|
760
1354
|
for (const entry of entries) {
|
|
1355
|
+
if (entry.name === ORGANIZATION_WORKSPACE_MIGRATION_FILE)
|
|
1356
|
+
continue;
|
|
761
1357
|
const sourcePath = path.join(sourceRoot, entry.name);
|
|
762
1358
|
const targetPath = path.join(targetRoot, entry.name);
|
|
763
1359
|
const targetStat = await lstatIfExists(targetPath);
|
|
764
1360
|
if (!targetStat) {
|
|
1361
|
+
await assertPathMoveCanBeAtomic(sourcePath, targetPath);
|
|
765
1362
|
await movePath(sourcePath, targetPath);
|
|
766
1363
|
continue;
|
|
767
1364
|
}
|
|
@@ -787,11 +1384,15 @@ async function mergeDirectoryContents(sourceRoot, targetRoot) {
|
|
|
787
1384
|
async function assertCanMergeDirectoryContents(sourceRoot, targetRoot) {
|
|
788
1385
|
const entries = await fs.readdir(sourceRoot, { withFileTypes: true });
|
|
789
1386
|
for (const entry of entries) {
|
|
1387
|
+
if (entry.name === ORGANIZATION_WORKSPACE_MIGRATION_FILE)
|
|
1388
|
+
continue;
|
|
790
1389
|
const sourcePath = path.join(sourceRoot, entry.name);
|
|
791
1390
|
const targetPath = path.join(targetRoot, entry.name);
|
|
792
1391
|
const targetStat = await lstatIfExists(targetPath);
|
|
793
|
-
if (!targetStat)
|
|
1392
|
+
if (!targetStat) {
|
|
1393
|
+
await assertPathMoveCanBeAtomic(sourcePath, targetPath);
|
|
794
1394
|
continue;
|
|
1395
|
+
}
|
|
795
1396
|
if (entry.isDirectory() && targetStat.isDirectory()) {
|
|
796
1397
|
await assertCanMergeDirectoryContents(sourcePath, targetPath);
|
|
797
1398
|
continue;
|
|
@@ -814,7 +1415,9 @@ export async function pruneOrphanedOrganizationStorage(liveOrgIds) {
|
|
|
814
1415
|
const organizationDirNames = await listDirectoryNames(organizationRoot);
|
|
815
1416
|
const legacyProjectDirNames = await listDirectoryNames(legacyProjectsRoot);
|
|
816
1417
|
const legacyProjectsRootExists = await directoryExists(legacyProjectsRoot);
|
|
817
|
-
const removedOrganizationDirNames = organizationDirNames.filter((dirName) => dirName !== MIGRATION_BACKUP_DIR_NAME
|
|
1418
|
+
const removedOrganizationDirNames = organizationDirNames.filter((dirName) => dirName !== MIGRATION_BACKUP_DIR_NAME
|
|
1419
|
+
&& !isOrganizationWorkspaceMapInternalDirectoryName(dirName)
|
|
1420
|
+
&& !liveOrgIdSet.has(dirName));
|
|
818
1421
|
const removedLegacyProjectDirNames = legacyProjectDirNames;
|
|
819
1422
|
await Promise.all([
|
|
820
1423
|
...removedOrganizationDirNames.map((dirName) => fs.rm(path.resolve(organizationRoot, dirName), { recursive: true, force: true })),
|