@push.rocks/smartdb 5.0.5 → 5.2.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/.smartconfig.json +26 -5
- package/dist_rust/rustdb_linux_amd64 +0 -0
- package/dist_rust/rustdb_linux_amd64.tsrust-build.json +14 -0
- package/dist_rust/rustdb_linux_arm64 +0 -0
- package/dist_rust/rustdb_linux_arm64.tsrust-build.json +14 -0
- package/dist_rust/rustdb_macos_amd64 +0 -0
- package/dist_rust/rustdb_macos_amd64.tsrust-build.json +14 -0
- package/dist_rust/rustdb_macos_arm64 +0 -0
- package/dist_rust/rustdb_macos_arm64.tsrust-build.json +14 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +2 -1
- package/dist_ts/index.js +2 -1
- package/dist_ts/ts_local/classes.localsmartdb.d.ts +10 -1
- package/dist_ts/ts_local/classes.localsmartdb.js +133 -2
- package/dist_ts/ts_local/index.d.ts +2 -0
- package/dist_ts/ts_local/index.js +2 -1
- package/dist_ts/ts_smartdb/index.d.ts +2 -0
- package/dist_ts/ts_smartdb/index.js +2 -1
- package/dist_ts/ts_smartdb/plugins.d.ts +3 -1
- package/dist_ts/ts_smartdb/plugins.js +4 -2
- package/dist_ts/ts_smartdb/rust-db-bridge.d.ts +3 -0
- package/dist_ts/ts_smartdb/rust-db-bridge.js +8 -1
- package/dist_ts/ts_smartdb/server/SmartdbServer.js +4 -1
- package/dist_ts/ts_smartdb/storage-root-relocation.d.ts +27 -0
- package/dist_ts/ts_smartdb/storage-root-relocation.js +211 -0
- package/package.json +9 -4
- package/readme.hints.md +11 -1
- package/readme.md +74 -2
- package/readme.plan.md +1 -1
- package/scripts/release-artifacts.ts +626 -0
- package/third-party-notices.md +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +7 -0
- package/ts/ts_local/classes.localsmartdb.ts +151 -1
- package/ts/ts_local/index.ts +9 -0
- package/ts/ts_smartdb/index.ts +9 -0
- package/ts/ts_smartdb/plugins.ts +3 -1
- package/ts/ts_smartdb/rust-db-bridge.ts +26 -0
- package/ts/ts_smartdb/server/SmartdbServer.ts +3 -0
- package/ts/ts_smartdb/storage-root-relocation.ts +277 -0
|
@@ -4,20 +4,32 @@ import * as net from 'net';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import * as os from 'os';
|
|
6
6
|
import {
|
|
7
|
-
RustDbBridge,
|
|
8
7
|
SmartdbServer,
|
|
9
8
|
type ILocalSmartDbOfflinePhysicalNamespaceInspection,
|
|
10
9
|
type ILocalSmartDbOfflinePhysicalNamespaceInspectionResult,
|
|
10
|
+
type ILocalSmartDbStoppedStorageRootRelocationInput,
|
|
11
|
+
type ILocalSmartDbStoppedStorageRootRelocationReceipt,
|
|
11
12
|
type ILocalSmartDbOfflineStringValueInspection,
|
|
12
13
|
type ISmartDbManagementOperationOptions,
|
|
13
14
|
type TLocalSmartDbOfflineStringValueInspectionResult,
|
|
14
15
|
} from '../ts_smartdb/index.js';
|
|
16
|
+
import {
|
|
17
|
+
RustDbBridge,
|
|
18
|
+
rustDbBridgeStorageRootRelocation,
|
|
19
|
+
} from '../ts_smartdb/rust-db-bridge.js';
|
|
15
20
|
import { StorageMigrator } from '../ts_migration/index.js';
|
|
16
21
|
import {
|
|
17
22
|
normalizeLocalSmartDbOfflinePhysicalNamespaceInspectionInput,
|
|
18
23
|
normalizeLocalSmartDbOfflineInspectionInput,
|
|
19
24
|
validateSmartDbManagementOperationOptions,
|
|
20
25
|
} from '../ts_smartdb/offline-inspection.js';
|
|
26
|
+
import {
|
|
27
|
+
assertNoLocalSmartDbStorageRootRelocationWorkspace,
|
|
28
|
+
isLocalSmartDbStorageRootRelocationErrorCode,
|
|
29
|
+
LocalSmartDbStorageRootRelocationError,
|
|
30
|
+
normalizeLocalSmartDbStoppedStorageRootRelocationInput,
|
|
31
|
+
normalizeLocalSmartDbStoppedStorageRootRelocationReceipt,
|
|
32
|
+
} from '../ts_smartdb/storage-root-relocation.js';
|
|
21
33
|
|
|
22
34
|
/**
|
|
23
35
|
* Connection information returned by LocalSmartDb.start()
|
|
@@ -65,6 +77,8 @@ export interface ILocalSmartDbOptions {
|
|
|
65
77
|
* ```
|
|
66
78
|
*/
|
|
67
79
|
export class LocalSmartDb {
|
|
80
|
+
private static storageRootRelocationGate: Promise<void> = Promise.resolve();
|
|
81
|
+
private static storageRootRelocationBridge: RustDbBridge | undefined;
|
|
68
82
|
private options: ILocalSmartDbOptions;
|
|
69
83
|
private server: SmartdbServer | null = null;
|
|
70
84
|
private generatedSocketPath: string | null = null;
|
|
@@ -77,6 +91,141 @@ export class LocalSmartDb {
|
|
|
77
91
|
return new RustDbBridge();
|
|
78
92
|
}
|
|
79
93
|
|
|
94
|
+
private static createStorageRootRelocationBridge(): RustDbBridge {
|
|
95
|
+
return new RustDbBridge();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private static async withStorageRootRelocationGate<TResult>(
|
|
99
|
+
operationArg: () => Promise<TResult>,
|
|
100
|
+
): Promise<TResult> {
|
|
101
|
+
const previous = LocalSmartDb.storageRootRelocationGate;
|
|
102
|
+
let release!: () => void;
|
|
103
|
+
LocalSmartDb.storageRootRelocationGate = new Promise<void>((resolveArg) => {
|
|
104
|
+
release = resolveArg;
|
|
105
|
+
});
|
|
106
|
+
await previous;
|
|
107
|
+
try {
|
|
108
|
+
return await operationArg();
|
|
109
|
+
} finally {
|
|
110
|
+
release();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private static remainingManagementTimeout(
|
|
115
|
+
deadlineArg: number | undefined,
|
|
116
|
+
): number | undefined {
|
|
117
|
+
if (deadlineArg === undefined) return undefined;
|
|
118
|
+
const remaining = deadlineArg - Date.now();
|
|
119
|
+
if (remaining <= 0) {
|
|
120
|
+
throw new LocalSmartDbStorageRootRelocationError('IO');
|
|
121
|
+
}
|
|
122
|
+
return remaining;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Relocate one stopped, plain file-storage root without starting its engine.
|
|
127
|
+
*/
|
|
128
|
+
public static async relocateStoppedStorageRoot(
|
|
129
|
+
inputArg: ILocalSmartDbStoppedStorageRootRelocationInput,
|
|
130
|
+
optionsArg?: ISmartDbManagementOperationOptions,
|
|
131
|
+
): Promise<ILocalSmartDbStoppedStorageRootRelocationReceipt> {
|
|
132
|
+
let input: ILocalSmartDbStoppedStorageRootRelocationInput;
|
|
133
|
+
try {
|
|
134
|
+
input = normalizeLocalSmartDbStoppedStorageRootRelocationInput(inputArg);
|
|
135
|
+
validateSmartDbManagementOperationOptions(optionsArg);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error instanceof LocalSmartDbStorageRootRelocationError) throw error;
|
|
138
|
+
throw new LocalSmartDbStorageRootRelocationError('INVALID_REQUEST');
|
|
139
|
+
}
|
|
140
|
+
if (optionsArg?.signal?.aborted) {
|
|
141
|
+
throw new LocalSmartDbStorageRootRelocationError('IO');
|
|
142
|
+
}
|
|
143
|
+
if (process.platform !== 'linux' && process.platform !== 'darwin') {
|
|
144
|
+
throw new LocalSmartDbStorageRootRelocationError('UNSUPPORTED');
|
|
145
|
+
}
|
|
146
|
+
const deadline = optionsArg?.timeoutMs === undefined
|
|
147
|
+
? undefined
|
|
148
|
+
: Date.now() + optionsArg.timeoutMs;
|
|
149
|
+
|
|
150
|
+
return LocalSmartDb.withStorageRootRelocationGate(async () => {
|
|
151
|
+
const retainedBridge = LocalSmartDb.storageRootRelocationBridge;
|
|
152
|
+
if (retainedBridge) {
|
|
153
|
+
try {
|
|
154
|
+
await retainedBridge.terminate({ gracePeriodMs: 0 });
|
|
155
|
+
} catch {
|
|
156
|
+
throw new LocalSmartDbStorageRootRelocationError('RECOVERY_REQUIRED');
|
|
157
|
+
}
|
|
158
|
+
if (!retainedBridge.ownershipReleased) {
|
|
159
|
+
throw new LocalSmartDbStorageRootRelocationError('RECOVERY_REQUIRED');
|
|
160
|
+
}
|
|
161
|
+
if (LocalSmartDb.storageRootRelocationBridge === retainedBridge) {
|
|
162
|
+
LocalSmartDb.storageRootRelocationBridge = undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let bridge: RustDbBridge | undefined;
|
|
167
|
+
let receipt: ILocalSmartDbStoppedStorageRootRelocationReceipt | undefined;
|
|
168
|
+
let operationError: LocalSmartDbStorageRootRelocationError | undefined;
|
|
169
|
+
try {
|
|
170
|
+
bridge = LocalSmartDb.createStorageRootRelocationBridge();
|
|
171
|
+
LocalSmartDb.storageRootRelocationBridge = bridge;
|
|
172
|
+
const spawnTimeout = LocalSmartDb.remainingManagementTimeout(deadline);
|
|
173
|
+
const spawned = await bridge.spawn({
|
|
174
|
+
...(optionsArg?.signal ? { signal: optionsArg.signal } : {}),
|
|
175
|
+
...(spawnTimeout === undefined ? {} : { timeoutMs: spawnTimeout }),
|
|
176
|
+
});
|
|
177
|
+
if (!spawned) {
|
|
178
|
+
throw new LocalSmartDbStorageRootRelocationError('IO');
|
|
179
|
+
}
|
|
180
|
+
const commandTimeout = LocalSmartDb.remainingManagementTimeout(deadline);
|
|
181
|
+
const result = await bridge[rustDbBridgeStorageRootRelocation](input, {
|
|
182
|
+
...(optionsArg?.signal ? { signal: optionsArg.signal } : {}),
|
|
183
|
+
...(commandTimeout === undefined ? {} : { timeoutMs: commandTimeout }),
|
|
184
|
+
});
|
|
185
|
+
receipt = normalizeLocalSmartDbStoppedStorageRootRelocationReceipt(result, input);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error instanceof LocalSmartDbStorageRootRelocationError) {
|
|
188
|
+
operationError = error;
|
|
189
|
+
} else {
|
|
190
|
+
const requestWasDispatched = error instanceof Error
|
|
191
|
+
&& 'requestId' in error
|
|
192
|
+
&& typeof error.requestId === 'string';
|
|
193
|
+
if (
|
|
194
|
+
requestWasDispatched
|
|
195
|
+
&& error instanceof Error
|
|
196
|
+
&& 'responseErrorCode' in error
|
|
197
|
+
&& isLocalSmartDbStorageRootRelocationErrorCode(error.responseErrorCode)
|
|
198
|
+
) {
|
|
199
|
+
operationError = new LocalSmartDbStorageRootRelocationError(error.responseErrorCode);
|
|
200
|
+
} else {
|
|
201
|
+
operationError = new LocalSmartDbStorageRootRelocationError(
|
|
202
|
+
requestWasDispatched ? 'RECOVERY_REQUIRED' : 'IO',
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (bridge) {
|
|
209
|
+
try {
|
|
210
|
+
await bridge.terminate({ gracePeriodMs: 0 });
|
|
211
|
+
if (!bridge.ownershipReleased) {
|
|
212
|
+
throw new Error('termination was not confirmed');
|
|
213
|
+
}
|
|
214
|
+
if (LocalSmartDb.storageRootRelocationBridge === bridge) {
|
|
215
|
+
LocalSmartDb.storageRootRelocationBridge = undefined;
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
operationError = new LocalSmartDbStorageRootRelocationError('RECOVERY_REQUIRED');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (operationError) throw operationError;
|
|
222
|
+
if (!receipt) {
|
|
223
|
+
throw new LocalSmartDbStorageRootRelocationError('RECOVERY_REQUIRED');
|
|
224
|
+
}
|
|
225
|
+
return receipt;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
80
229
|
private static async runOfflineInspection<
|
|
81
230
|
TInput extends { folderPath: string },
|
|
82
231
|
TResult,
|
|
@@ -261,6 +410,7 @@ export class LocalSmartDb {
|
|
|
261
410
|
await LocalSmartDb.cleanStaleSockets();
|
|
262
411
|
|
|
263
412
|
// Run storage migration before starting the Rust engine
|
|
413
|
+
await assertNoLocalSmartDbStorageRootRelocationWorkspace(this.options.folderPath);
|
|
264
414
|
const migrator = new StorageMigrator(this.options.folderPath);
|
|
265
415
|
await migrator.run();
|
|
266
416
|
|
package/ts/ts_local/index.ts
CHANGED
|
@@ -8,3 +8,12 @@ export type {
|
|
|
8
8
|
ILocalSmartDbOfflineStringValueInspection,
|
|
9
9
|
TLocalSmartDbOfflineStringValueInspectionResult,
|
|
10
10
|
} from '../ts_smartdb/service-types.js';
|
|
11
|
+
export type {
|
|
12
|
+
ILocalSmartDbStoppedStorageRootRelocationInput,
|
|
13
|
+
ILocalSmartDbStoppedStorageRootRelocationReceipt,
|
|
14
|
+
TLocalSmartDbStorageRootRelocationErrorCode,
|
|
15
|
+
} from '../ts_smartdb/storage-root-relocation.js';
|
|
16
|
+
export {
|
|
17
|
+
LocalSmartDbStorageRootRelocationError,
|
|
18
|
+
localSmartDbStorageRootRelocationErrorCodes,
|
|
19
|
+
} from '../ts_smartdb/storage-root-relocation.js';
|
package/ts/ts_smartdb/index.ts
CHANGED
|
@@ -81,5 +81,14 @@ export {
|
|
|
81
81
|
normalizeSmartDbResourceFenceError,
|
|
82
82
|
smartDbResourceFenceErrorCodes,
|
|
83
83
|
} from './service-types.js';
|
|
84
|
+
export type {
|
|
85
|
+
ILocalSmartDbStoppedStorageRootRelocationInput,
|
|
86
|
+
ILocalSmartDbStoppedStorageRootRelocationReceipt,
|
|
87
|
+
TLocalSmartDbStorageRootRelocationErrorCode,
|
|
88
|
+
} from './storage-root-relocation.js';
|
|
89
|
+
export {
|
|
90
|
+
LocalSmartDbStorageRootRelocationError,
|
|
91
|
+
localSmartDbStorageRootRelocationErrorCodes,
|
|
92
|
+
} from './storage-root-relocation.js';
|
|
84
93
|
export type { TSmartDbEffectivePayloadInputV1 } from './resource-fencing.js';
|
|
85
94
|
export { createSmartDbEffectivePayloadSha256V1 } from './resource-fencing.js';
|
package/ts/ts_smartdb/plugins.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// node native scope
|
|
2
2
|
import * as buffer from 'node:buffer';
|
|
3
3
|
import * as crypto from 'node:crypto';
|
|
4
|
+
import * as fs from 'node:fs/promises';
|
|
5
|
+
import * as path from 'node:path';
|
|
4
6
|
|
|
5
|
-
export { buffer, crypto };
|
|
7
|
+
export { buffer, crypto, fs, path };
|
|
6
8
|
|
|
7
9
|
// @push.rocks scope
|
|
8
10
|
import * as smartrust from '@push.rocks/smartrust';
|
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
normalizeLocalSmartDbOfflineInspectionInput,
|
|
12
12
|
validateSmartDbManagementOperationOptions,
|
|
13
13
|
} from './offline-inspection.js';
|
|
14
|
+
import {
|
|
15
|
+
normalizeLocalSmartDbStoppedStorageRootRelocationInput,
|
|
16
|
+
type ILocalSmartDbStoppedStorageRootRelocationInput,
|
|
17
|
+
type ILocalSmartDbStoppedStorageRootRelocationReceipt,
|
|
18
|
+
} from './storage-root-relocation.js';
|
|
14
19
|
import type {
|
|
15
20
|
ISmartDbHealth,
|
|
16
21
|
ISmartDbDatabaseTenantInput,
|
|
@@ -180,6 +185,10 @@ type TOfflinePhysicalNamespaceInspectionCommandParams = {
|
|
|
180
185
|
} & ILocalSmartDbOfflinePhysicalNamespaceInspection['limits'];
|
|
181
186
|
|
|
182
187
|
type TSmartDbCommands = {
|
|
188
|
+
relocateStoppedStorageRoot: {
|
|
189
|
+
params: ILocalSmartDbStoppedStorageRootRelocationInput;
|
|
190
|
+
result: ILocalSmartDbStoppedStorageRootRelocationReceipt;
|
|
191
|
+
};
|
|
183
192
|
inspectOfflinePhysicalNamespaces: {
|
|
184
193
|
params: TOfflinePhysicalNamespaceInspectionCommandParams;
|
|
185
194
|
result: ILocalSmartDbOfflinePhysicalNamespaceInspectionResult;
|
|
@@ -276,6 +285,10 @@ type TSmartDbCommands = {
|
|
|
276
285
|
};
|
|
277
286
|
};
|
|
278
287
|
|
|
288
|
+
export const rustDbBridgeStorageRootRelocation = Symbol(
|
|
289
|
+
'smartdb.internal.storage-root-relocation',
|
|
290
|
+
);
|
|
291
|
+
|
|
279
292
|
export type TAuthMetadataPermissionTransitionStatus =
|
|
280
293
|
| 'not-present'
|
|
281
294
|
| 'already-target'
|
|
@@ -557,6 +570,19 @@ export class RustDbBridge extends EventEmitter {
|
|
|
557
570
|
|
|
558
571
|
// --- Convenience methods for each management command ---
|
|
559
572
|
|
|
573
|
+
public async [rustDbBridgeStorageRootRelocation](
|
|
574
|
+
inputArg: ILocalSmartDbStoppedStorageRootRelocationInput,
|
|
575
|
+
optionsArg?: ISmartDbManagementOperationOptions,
|
|
576
|
+
): Promise<ILocalSmartDbStoppedStorageRootRelocationReceipt> {
|
|
577
|
+
const input = normalizeLocalSmartDbStoppedStorageRootRelocationInput(inputArg);
|
|
578
|
+
validateSmartDbManagementOperationOptions(optionsArg);
|
|
579
|
+
return await this.bridge.sendCommand(
|
|
580
|
+
'relocateStoppedStorageRoot',
|
|
581
|
+
input,
|
|
582
|
+
optionsArg,
|
|
583
|
+
) as ILocalSmartDbStoppedStorageRootRelocationReceipt;
|
|
584
|
+
}
|
|
585
|
+
|
|
560
586
|
public async inspectOfflinePhysicalNamespaces(
|
|
561
587
|
inputArg: ILocalSmartDbOfflinePhysicalNamespaceInspection,
|
|
562
588
|
optionsArg?: ISmartDbManagementOperationOptions,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
StorageMigrator,
|
|
9
9
|
} from '../../ts_migration/index.js';
|
|
10
10
|
import { validateSmartDbManagementOperationOptions } from '../offline-inspection.js';
|
|
11
|
+
import { assertNoLocalSmartDbStorageRootRelocationWorkspace } from '../storage-root-relocation.js';
|
|
11
12
|
import type {
|
|
12
13
|
IOpLogEntry,
|
|
13
14
|
IOpLogResult,
|
|
@@ -226,6 +227,8 @@ export class SmartdbServer {
|
|
|
226
227
|
checkpoint();
|
|
227
228
|
// Run storage migration for file-based storage before starting Rust engine
|
|
228
229
|
if (this.options.storage === 'file' && this.options.storagePath) {
|
|
230
|
+
await assertNoLocalSmartDbStorageRootRelocationWorkspace(this.options.storagePath);
|
|
231
|
+
checkpoint();
|
|
229
232
|
const migrator = new StorageMigrator(this.options.storagePath);
|
|
230
233
|
await migrator.run(startupAbortController.signal);
|
|
231
234
|
checkpoint();
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
export const localSmartDbStorageRootRelocationErrorCodes = [
|
|
4
|
+
'INVALID_REQUEST',
|
|
5
|
+
'UNSUPPORTED',
|
|
6
|
+
'BUSY',
|
|
7
|
+
'NOT_FOUND',
|
|
8
|
+
'DESTINATION_CONFLICT',
|
|
9
|
+
'IDENTITY_MISMATCH',
|
|
10
|
+
'STATE_CORRUPT',
|
|
11
|
+
'IO',
|
|
12
|
+
'RECOVERY_REQUIRED',
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export type TLocalSmartDbStorageRootRelocationErrorCode =
|
|
16
|
+
(typeof localSmartDbStorageRootRelocationErrorCodes)[number];
|
|
17
|
+
|
|
18
|
+
export interface ILocalSmartDbStoppedStorageRootRelocationInput {
|
|
19
|
+
sourceFolderPath: string;
|
|
20
|
+
destinationFolderPath: string;
|
|
21
|
+
relocationId: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ILocalSmartDbStoppedStorageRootRelocationReceipt {
|
|
25
|
+
format: 'smartdb.storage-root-relocation.receipt.v1';
|
|
26
|
+
version: 1;
|
|
27
|
+
relocationId: string;
|
|
28
|
+
sourceFolderPath: string;
|
|
29
|
+
destinationFolderPath: string;
|
|
30
|
+
providerRootId: string;
|
|
31
|
+
storageRootDevice: string;
|
|
32
|
+
storageRootInode: string;
|
|
33
|
+
receiptSha256: string;
|
|
34
|
+
sourceReceiptRetained: true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class LocalSmartDbStorageRootRelocationError extends Error {
|
|
38
|
+
public readonly code: TLocalSmartDbStorageRootRelocationErrorCode;
|
|
39
|
+
|
|
40
|
+
public constructor(codeArg: TLocalSmartDbStorageRootRelocationErrorCode) {
|
|
41
|
+
super(`SmartDB stopped storage-root relocation failed with code ${codeArg}`);
|
|
42
|
+
this.name = 'LocalSmartDbStorageRootRelocationError';
|
|
43
|
+
this.code = codeArg;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const relocationInputKeys = [
|
|
48
|
+
'sourceFolderPath',
|
|
49
|
+
'destinationFolderPath',
|
|
50
|
+
'relocationId',
|
|
51
|
+
] as const;
|
|
52
|
+
const relocationReceiptKeys = [
|
|
53
|
+
'format',
|
|
54
|
+
'version',
|
|
55
|
+
'relocationId',
|
|
56
|
+
'sourceFolderPath',
|
|
57
|
+
'destinationFolderPath',
|
|
58
|
+
'providerRootId',
|
|
59
|
+
'storageRootDevice',
|
|
60
|
+
'storageRootInode',
|
|
61
|
+
'receiptSha256',
|
|
62
|
+
'sourceReceiptRetained',
|
|
63
|
+
] as const;
|
|
64
|
+
const maximumFolderPathBytes = 4095;
|
|
65
|
+
const maximumRelocationIdBytes = 256;
|
|
66
|
+
const maximumU64Decimal = '18446744073709551615';
|
|
67
|
+
const receiptDigestDomain = 'smartdb.storage-root-relocation.receipt.v1\0';
|
|
68
|
+
|
|
69
|
+
const relocationError = (
|
|
70
|
+
codeArg: TLocalSmartDbStorageRootRelocationErrorCode,
|
|
71
|
+
): LocalSmartDbStorageRootRelocationError =>
|
|
72
|
+
new LocalSmartDbStorageRootRelocationError(codeArg);
|
|
73
|
+
|
|
74
|
+
const isPlainExactDataObject = (
|
|
75
|
+
valueArg: unknown,
|
|
76
|
+
keysArg: readonly string[],
|
|
77
|
+
): valueArg is Record<string, unknown> => {
|
|
78
|
+
if (
|
|
79
|
+
typeof valueArg !== 'object'
|
|
80
|
+
|| valueArg === null
|
|
81
|
+
|| Array.isArray(valueArg)
|
|
82
|
+
|| Object.getPrototypeOf(valueArg) !== Object.prototype
|
|
83
|
+
) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const ownKeys = Reflect.ownKeys(valueArg);
|
|
87
|
+
if (
|
|
88
|
+
ownKeys.length !== keysArg.length
|
|
89
|
+
|| ownKeys.some((keyArg) => typeof keyArg !== 'string' || !keysArg.includes(keyArg))
|
|
90
|
+
) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const descriptors = Object.getOwnPropertyDescriptors(valueArg);
|
|
94
|
+
return keysArg.every((keyArg) => {
|
|
95
|
+
const descriptor = descriptors[keyArg];
|
|
96
|
+
return descriptor !== undefined
|
|
97
|
+
&& descriptor.enumerable
|
|
98
|
+
&& Object.prototype.hasOwnProperty.call(descriptor, 'value');
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const isValidUnicodeScalarString = (valueArg: string): boolean => {
|
|
103
|
+
for (let index = 0; index < valueArg.length; index++) {
|
|
104
|
+
const code = valueArg.charCodeAt(index);
|
|
105
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
106
|
+
const next = valueArg.charCodeAt(index + 1);
|
|
107
|
+
if (next < 0xdc00 || next > 0xdfff) return false;
|
|
108
|
+
index++;
|
|
109
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const isNestedPath = (parentArg: string, childArg: string): boolean => {
|
|
117
|
+
const relative = plugins.path.relative(parentArg, childArg);
|
|
118
|
+
return relative !== ''
|
|
119
|
+
&& relative !== '..'
|
|
120
|
+
&& !relative.startsWith(`..${plugins.path.sep}`)
|
|
121
|
+
&& !plugins.path.isAbsolute(relative);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const normalizeFolderPath = (valueArg: unknown): string => {
|
|
125
|
+
if (
|
|
126
|
+
typeof valueArg !== 'string'
|
|
127
|
+
|| !plugins.path.isAbsolute(valueArg)
|
|
128
|
+
|| !isValidUnicodeScalarString(valueArg)
|
|
129
|
+
|| [...valueArg].some(characterArg => /\p{Cc}/u.test(characterArg))
|
|
130
|
+
) {
|
|
131
|
+
throw relocationError('INVALID_REQUEST');
|
|
132
|
+
}
|
|
133
|
+
const initialBytes = plugins.buffer.Buffer.byteLength(valueArg, 'utf8');
|
|
134
|
+
if (initialBytes < 1 || initialBytes > maximumFolderPathBytes) {
|
|
135
|
+
throw relocationError('INVALID_REQUEST');
|
|
136
|
+
}
|
|
137
|
+
const normalized = plugins.path.resolve(valueArg);
|
|
138
|
+
const normalizedBytes = plugins.buffer.Buffer.byteLength(normalized, 'utf8');
|
|
139
|
+
if (
|
|
140
|
+
normalizedBytes < 1
|
|
141
|
+
|| normalizedBytes > maximumFolderPathBytes
|
|
142
|
+
|| plugins.path.parse(normalized).root === normalized
|
|
143
|
+
|| !isValidUnicodeScalarString(normalized)
|
|
144
|
+
|| [...normalized].some(characterArg => /\p{Cc}/u.test(characterArg))
|
|
145
|
+
) {
|
|
146
|
+
throw relocationError('INVALID_REQUEST');
|
|
147
|
+
}
|
|
148
|
+
return normalized;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const normalizeLocalSmartDbStoppedStorageRootRelocationInput = (
|
|
152
|
+
inputArg: ILocalSmartDbStoppedStorageRootRelocationInput,
|
|
153
|
+
): ILocalSmartDbStoppedStorageRootRelocationInput => {
|
|
154
|
+
if (!isPlainExactDataObject(inputArg, relocationInputKeys)) {
|
|
155
|
+
throw relocationError('INVALID_REQUEST');
|
|
156
|
+
}
|
|
157
|
+
const sourceFolderPath = normalizeFolderPath(inputArg.sourceFolderPath);
|
|
158
|
+
const destinationFolderPath = normalizeFolderPath(inputArg.destinationFolderPath);
|
|
159
|
+
const sourceFolderPathBytes = plugins.buffer.Buffer.byteLength(sourceFolderPath, 'utf8');
|
|
160
|
+
const destinationFolderPathBytes = plugins.buffer.Buffer.byteLength(destinationFolderPath, 'utf8');
|
|
161
|
+
if (
|
|
162
|
+
sourceFolderPath === destinationFolderPath
|
|
163
|
+
|| isNestedPath(sourceFolderPath, destinationFolderPath)
|
|
164
|
+
|| isNestedPath(destinationFolderPath, sourceFolderPath)
|
|
165
|
+
|| destinationFolderPathBytes > sourceFolderPathBytes
|
|
166
|
+
) {
|
|
167
|
+
throw relocationError('INVALID_REQUEST');
|
|
168
|
+
}
|
|
169
|
+
if (
|
|
170
|
+
typeof inputArg.relocationId !== 'string'
|
|
171
|
+
|| plugins.buffer.Buffer.byteLength(inputArg.relocationId, 'utf8') > maximumRelocationIdBytes
|
|
172
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9._:@-]*$/.test(inputArg.relocationId)
|
|
173
|
+
) {
|
|
174
|
+
throw relocationError('INVALID_REQUEST');
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
sourceFolderPath,
|
|
178
|
+
destinationFolderPath,
|
|
179
|
+
relocationId: inputArg.relocationId,
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const receiptDigest = (
|
|
184
|
+
receiptArg: Omit<ILocalSmartDbStoppedStorageRootRelocationReceipt, 'receiptSha256'>,
|
|
185
|
+
): string => plugins.crypto
|
|
186
|
+
.createHash('sha256')
|
|
187
|
+
.update(receiptDigestDomain, 'utf8')
|
|
188
|
+
.update(JSON.stringify(receiptArg), 'utf8')
|
|
189
|
+
.digest('hex');
|
|
190
|
+
|
|
191
|
+
const isCanonicalU64Decimal = (valueArg: unknown): valueArg is string =>
|
|
192
|
+
typeof valueArg === 'string'
|
|
193
|
+
&& /^(?:0|[1-9][0-9]{0,19})$/.test(valueArg)
|
|
194
|
+
&& (valueArg.length < 20 || valueArg <= maximumU64Decimal);
|
|
195
|
+
|
|
196
|
+
const isLowercaseSha256 = (valueArg: unknown): valueArg is string =>
|
|
197
|
+
typeof valueArg === 'string' && /^[0-9a-f]{64}$/.test(valueArg);
|
|
198
|
+
|
|
199
|
+
export const normalizeLocalSmartDbStoppedStorageRootRelocationReceipt = (
|
|
200
|
+
valueArg: unknown,
|
|
201
|
+
inputArg: ILocalSmartDbStoppedStorageRootRelocationInput,
|
|
202
|
+
): ILocalSmartDbStoppedStorageRootRelocationReceipt => {
|
|
203
|
+
if (!isPlainExactDataObject(valueArg, relocationReceiptKeys)) {
|
|
204
|
+
throw relocationError('RECOVERY_REQUIRED');
|
|
205
|
+
}
|
|
206
|
+
const receipt = valueArg as unknown as ILocalSmartDbStoppedStorageRootRelocationReceipt;
|
|
207
|
+
if (
|
|
208
|
+
receipt.format !== 'smartdb.storage-root-relocation.receipt.v1'
|
|
209
|
+
|| receipt.version !== 1
|
|
210
|
+
|| receipt.relocationId !== inputArg.relocationId
|
|
211
|
+
|| receipt.sourceFolderPath !== inputArg.sourceFolderPath
|
|
212
|
+
|| receipt.destinationFolderPath !== inputArg.destinationFolderPath
|
|
213
|
+
|| !isLowercaseSha256(receipt.providerRootId)
|
|
214
|
+
|| !isCanonicalU64Decimal(receipt.storageRootDevice)
|
|
215
|
+
|| !isCanonicalU64Decimal(receipt.storageRootInode)
|
|
216
|
+
|| !isLowercaseSha256(receipt.receiptSha256)
|
|
217
|
+
|| receipt.sourceReceiptRetained !== true
|
|
218
|
+
) {
|
|
219
|
+
throw relocationError('RECOVERY_REQUIRED');
|
|
220
|
+
}
|
|
221
|
+
const withoutDigest = {
|
|
222
|
+
format: receipt.format,
|
|
223
|
+
version: receipt.version,
|
|
224
|
+
relocationId: receipt.relocationId,
|
|
225
|
+
sourceFolderPath: receipt.sourceFolderPath,
|
|
226
|
+
destinationFolderPath: receipt.destinationFolderPath,
|
|
227
|
+
providerRootId: receipt.providerRootId,
|
|
228
|
+
storageRootDevice: receipt.storageRootDevice,
|
|
229
|
+
storageRootInode: receipt.storageRootInode,
|
|
230
|
+
sourceReceiptRetained: receipt.sourceReceiptRetained,
|
|
231
|
+
} satisfies Omit<ILocalSmartDbStoppedStorageRootRelocationReceipt, 'receiptSha256'>;
|
|
232
|
+
if (receiptDigest(withoutDigest) !== receipt.receiptSha256) {
|
|
233
|
+
throw relocationError('RECOVERY_REQUIRED');
|
|
234
|
+
}
|
|
235
|
+
return { ...withoutDigest, receiptSha256: receipt.receiptSha256 };
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export const assertNoLocalSmartDbStorageRootRelocationWorkspace = async (
|
|
239
|
+
storagePathArg: string,
|
|
240
|
+
): Promise<void> => {
|
|
241
|
+
const storagePath = plugins.path.resolve(storagePathArg);
|
|
242
|
+
const internalPath = plugins.path.join(storagePath, '.__rustdb_internal');
|
|
243
|
+
const workspacePath = plugins.path.join(internalPath, 'storage-root-relocation');
|
|
244
|
+
try {
|
|
245
|
+
const storage = await plugins.fs.stat(storagePath);
|
|
246
|
+
if (!storage.isDirectory()) {
|
|
247
|
+
throw new Error('not a directory');
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
|
|
251
|
+
throw new Error('SmartDB storage-root relocation workspace inspection is ambiguous');
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const internal = await plugins.fs.lstat(internalPath);
|
|
255
|
+
if (internal.isSymbolicLink() || !internal.isDirectory()) {
|
|
256
|
+
throw new Error('not a safe directory');
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
|
|
260
|
+
throw new Error('SmartDB storage-root relocation workspace inspection is ambiguous');
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
await plugins.fs.lstat(workspacePath);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
|
|
266
|
+
throw new Error('SmartDB storage-root relocation workspace inspection is ambiguous');
|
|
267
|
+
}
|
|
268
|
+
throw new Error('SmartDB storage-root relocation recovery must complete before startup');
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export const isLocalSmartDbStorageRootRelocationErrorCode = (
|
|
272
|
+
valueArg: unknown,
|
|
273
|
+
): valueArg is TLocalSmartDbStorageRootRelocationErrorCode =>
|
|
274
|
+
typeof valueArg === 'string'
|
|
275
|
+
&& localSmartDbStorageRootRelocationErrorCodes.includes(
|
|
276
|
+
valueArg as TLocalSmartDbStorageRootRelocationErrorCode,
|
|
277
|
+
);
|