@milaboratories/pl-drivers 1.2.35 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clients/download.d.ts +6 -2
- package/dist/clients/download.d.ts.map +1 -1
- package/dist/clients/helpers.d.ts +2 -2
- package/dist/clients/helpers.d.ts.map +1 -1
- package/dist/drivers/helpers/ls_list_entry.d.ts +8 -13
- package/dist/drivers/helpers/ls_list_entry.d.ts.map +1 -1
- package/dist/drivers/helpers/ls_storage_entry.d.ts +6 -8
- package/dist/drivers/helpers/ls_storage_entry.d.ts.map +1 -1
- package/dist/drivers/ls.d.ts +40 -12
- package/dist/drivers/ls.d.ts.map +1 -1
- package/dist/drivers/types.d.ts +80 -0
- package/dist/drivers/types.d.ts.map +1 -0
- package/dist/drivers/upload.d.ts +18 -26
- package/dist/drivers/upload.d.ts.map +1 -1
- package/dist/helpers/validate.d.ts +2 -0
- package/dist/helpers/validate.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +820 -792
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/clients/download.test.ts +17 -15
- package/src/clients/download.ts +34 -21
- package/src/clients/helpers.ts +13 -43
- package/src/drivers/download_blob.test.ts +4 -4
- package/src/drivers/helpers/ls_list_entry.test.ts +5 -7
- package/src/drivers/helpers/ls_list_entry.ts +31 -36
- package/src/drivers/helpers/ls_storage_entry.ts +18 -62
- package/src/drivers/logs.test.ts +3 -3
- package/src/drivers/ls.test.ts +112 -30
- package/src/drivers/ls.ts +233 -83
- package/src/drivers/types.ts +41 -0
- package/src/drivers/upload.test.ts +43 -82
- package/src/drivers/upload.ts +59 -111
- package/src/helpers/validate.ts +6 -0
- package/src/index.ts +3 -0
package/src/drivers/ls.test.ts
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
HmacSha256Signer
|
|
4
|
-
} from '@milaboratories/ts-helpers';
|
|
5
|
-
import { LsDriver } from './ls';
|
|
1
|
+
import { ConsoleLoggerAdapter, HmacSha256Signer } from '@milaboratories/ts-helpers';
|
|
2
|
+
import { DefaultVirtualLocalStorages, LsDriver } from './ls';
|
|
6
3
|
import { createLsFilesClient } from '../clients/helpers';
|
|
7
4
|
import { TestHelpers } from '@milaboratories/pl-client';
|
|
8
5
|
import * as os from 'node:os';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import { test, expect } from '@jest/globals';
|
|
8
|
+
import { isImportFileHandleIndex, isImportFileHandleUpload } from '@milaboratories/pl-model-common';
|
|
9
|
+
|
|
10
|
+
const assetsPath = path.resolve('../../../assets');
|
|
9
11
|
|
|
10
12
|
test('should ok when get all storages from ls driver', async () => {
|
|
11
13
|
const signer = new HmacSha256Signer('abc');
|
|
12
14
|
const logger = new ConsoleLoggerAdapter();
|
|
13
15
|
await TestHelpers.withTempRoot(async (client) => {
|
|
14
16
|
const lsClient = createLsFilesClient(client, logger);
|
|
15
|
-
const driver =
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
const driver = await LsDriver.init(
|
|
18
|
+
logger,
|
|
19
|
+
client,
|
|
20
|
+
signer,
|
|
21
|
+
DefaultVirtualLocalStorages(),
|
|
22
|
+
[],
|
|
23
|
+
() => {
|
|
24
|
+
throw Error();
|
|
25
|
+
}
|
|
26
|
+
);
|
|
18
27
|
|
|
19
28
|
const got = await driver.getStorageList();
|
|
20
29
|
|
|
@@ -22,9 +31,7 @@ test('should ok when get all storages from ls driver', async () => {
|
|
|
22
31
|
expect(got.find((se) => se.name == 'library')?.handle).toContain('library');
|
|
23
32
|
expect(got.find((se) => se.name == 'library')?.initialFullPath).toEqual('');
|
|
24
33
|
expect(got.find((se) => se.name == 'local')?.handle).toContain('/');
|
|
25
|
-
expect(got.find((se) => se.name == 'local')?.initialFullPath).toEqual(
|
|
26
|
-
os.homedir()
|
|
27
|
-
);
|
|
34
|
+
expect(got.find((se) => se.name == 'local')?.initialFullPath).toEqual(os.homedir());
|
|
28
35
|
|
|
29
36
|
console.log('got all storage entries: ', got);
|
|
30
37
|
});
|
|
@@ -34,10 +41,16 @@ test('should ok when list files from remote storage in ls driver', async () => {
|
|
|
34
41
|
const signer = new HmacSha256Signer('abc');
|
|
35
42
|
const logger = new ConsoleLoggerAdapter();
|
|
36
43
|
await TestHelpers.withTempRoot(async (client) => {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
const driver = await LsDriver.init(
|
|
45
|
+
logger,
|
|
46
|
+
client,
|
|
47
|
+
signer,
|
|
48
|
+
DefaultVirtualLocalStorages(),
|
|
49
|
+
[],
|
|
50
|
+
() => {
|
|
51
|
+
throw Error();
|
|
52
|
+
}
|
|
53
|
+
);
|
|
41
54
|
|
|
42
55
|
const storages = await driver.getStorageList();
|
|
43
56
|
const library = storages.find((se) => se.name == 'library')!.handle;
|
|
@@ -45,25 +58,19 @@ test('should ok when list files from remote storage in ls driver', async () => {
|
|
|
45
58
|
const topLevelDir = await driver.listFiles(library, '');
|
|
46
59
|
expect(topLevelDir.entries.length).toBeGreaterThan(1);
|
|
47
60
|
|
|
48
|
-
const testDir = topLevelDir.entries.find((d) =>
|
|
49
|
-
d.name.includes('ls_dir_structure')
|
|
50
|
-
);
|
|
61
|
+
const testDir = topLevelDir.entries.find((d) => d.name.includes('ls_dir_structure'));
|
|
51
62
|
expect(testDir).toBeDefined();
|
|
52
63
|
expect(testDir!.type).toEqual('dir');
|
|
53
64
|
expect(testDir!.fullPath).toEqual('/ls_dir_structure_test');
|
|
54
65
|
expect(testDir!.name).toEqual('ls_dir_structure_test');
|
|
55
66
|
|
|
56
67
|
const secondDirs = await driver.listFiles(library, testDir!.fullPath);
|
|
57
|
-
expect(secondDirs.parent).toEqual('/ls_dir_structure_test/');
|
|
58
68
|
expect(secondDirs.entries).toHaveLength(2);
|
|
59
69
|
expect(secondDirs.entries[0].type).toEqual('dir');
|
|
60
|
-
expect(secondDirs.entries[0].fullPath).toEqual(
|
|
61
|
-
'/ls_dir_structure_test/abc'
|
|
62
|
-
);
|
|
70
|
+
expect(secondDirs.entries[0].fullPath).toEqual('/ls_dir_structure_test/abc');
|
|
63
71
|
expect(secondDirs.entries[0].name).toEqual('abc');
|
|
64
72
|
|
|
65
73
|
const f = await driver.listFiles(library, secondDirs.entries[0].fullPath);
|
|
66
|
-
expect(f.parent).toEqual('/ls_dir_structure_test/abc/');
|
|
67
74
|
expect(f.entries).toHaveLength(1);
|
|
68
75
|
expect(f.entries[0].type).toEqual('file');
|
|
69
76
|
expect(f.entries[0].fullPath).toEqual('/ls_dir_structure_test/abc/42.txt');
|
|
@@ -76,15 +83,90 @@ test('should ok when list files from local storage in ls driver', async () => {
|
|
|
76
83
|
const signer = new HmacSha256Signer('abc');
|
|
77
84
|
const logger = new ConsoleLoggerAdapter();
|
|
78
85
|
await TestHelpers.withTempRoot(async (client) => {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
const driver = await LsDriver.init(
|
|
87
|
+
logger,
|
|
88
|
+
client,
|
|
89
|
+
signer,
|
|
90
|
+
DefaultVirtualLocalStorages(),
|
|
91
|
+
[],
|
|
92
|
+
() => {
|
|
93
|
+
throw Error();
|
|
94
|
+
}
|
|
95
|
+
);
|
|
83
96
|
|
|
84
97
|
const storages = await driver.getStorageList();
|
|
85
|
-
const local = storages.find((se) => se.name == 'local')
|
|
98
|
+
const local = storages.find((se) => se.name == 'local')!;
|
|
86
99
|
|
|
87
|
-
const
|
|
88
|
-
expect(
|
|
100
|
+
const defaultDir = await driver.listFiles(local.handle, local.initialFullPath);
|
|
101
|
+
expect(defaultDir.entries.length).toBeGreaterThan(1);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('should ok when list files from local storage in ls driver and correctly apply local projections', async () => {
|
|
106
|
+
const signer = new HmacSha256Signer('abc');
|
|
107
|
+
const logger = new ConsoleLoggerAdapter();
|
|
108
|
+
await TestHelpers.withTempRoot(async (client) => {
|
|
109
|
+
let dialogRet = '';
|
|
110
|
+
const driver = await LsDriver.init(
|
|
111
|
+
logger,
|
|
112
|
+
client,
|
|
113
|
+
signer,
|
|
114
|
+
DefaultVirtualLocalStorages(),
|
|
115
|
+
[{ storageId: 'test_storage', localPath: path.join(assetsPath, 'ls_dir_structure_test') }],
|
|
116
|
+
async () => [dialogRet]
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const storages = await driver.getStorageList();
|
|
120
|
+
const local = storages.find((se) => se.name == 'local')!;
|
|
121
|
+
|
|
122
|
+
{
|
|
123
|
+
dialogRet = path.join(assetsPath, 'ls_dir_structure_test', 'abc', '42.txt');
|
|
124
|
+
const result = await driver.showOpenSingleFileDialog();
|
|
125
|
+
expect(result.file).toBeDefined();
|
|
126
|
+
|
|
127
|
+
expect(isImportFileHandleIndex(result.file!)).toStrictEqual(true);
|
|
128
|
+
const size = await driver.getLocalFileSize(result.file!);
|
|
129
|
+
expect(size).toStrictEqual(3);
|
|
130
|
+
const content = await driver.getLocalFileContent(result.file!);
|
|
131
|
+
expect(Buffer.from(content).toString()).toStrictEqual('42\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
{
|
|
135
|
+
dialogRet = path.join(assetsPath, 'answer_to_the_ultimate_question.txt');
|
|
136
|
+
const result = await driver.showOpenSingleFileDialog();
|
|
137
|
+
expect(result.file).toBeDefined();
|
|
138
|
+
|
|
139
|
+
expect(isImportFileHandleUpload(result.file!)).toStrictEqual(true);
|
|
140
|
+
const size = await driver.getLocalFileSize(result.file!);
|
|
141
|
+
expect(size).toStrictEqual(3);
|
|
142
|
+
const content = await driver.getLocalFileContent(result.file!);
|
|
143
|
+
expect(Buffer.from(content).toString()).toStrictEqual('42\n');
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('should ok when get file using local dialog, and read its content', async () => {
|
|
149
|
+
const signer = new HmacSha256Signer('abc');
|
|
150
|
+
const logger = new ConsoleLoggerAdapter();
|
|
151
|
+
await TestHelpers.withTempRoot(async (client) => {
|
|
152
|
+
const driver = await LsDriver.init(
|
|
153
|
+
logger,
|
|
154
|
+
client,
|
|
155
|
+
signer,
|
|
156
|
+
DefaultVirtualLocalStorages(),
|
|
157
|
+
[],
|
|
158
|
+
async () => [path.join(assetsPath, 'answer_to_the_ultimate_question.txt')]
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const result = await driver.showOpenSingleFileDialog();
|
|
162
|
+
expect(result.file).toBeDefined();
|
|
163
|
+
|
|
164
|
+
const size = await driver.getLocalFileSize(result.file!);
|
|
165
|
+
expect(size).toStrictEqual(3);
|
|
166
|
+
const content = await driver.getLocalFileContent(result.file!);
|
|
167
|
+
expect(Buffer.from(content).toString()).toStrictEqual('42\n');
|
|
168
|
+
|
|
169
|
+
const multiResult = await driver.showOpenMultipleFilesDialog();
|
|
170
|
+
expect(multiResult.files![0]).toStrictEqual(result.file);
|
|
89
171
|
});
|
|
90
172
|
});
|
package/src/drivers/ls.ts
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isNotNullResourceId,
|
|
3
|
-
PlClient,
|
|
4
|
-
ResourceData,
|
|
5
|
-
ResourceId
|
|
6
|
-
} from '@milaboratories/pl-client';
|
|
1
|
+
import { isNotNullResourceId, PlClient, ResourceData, ResourceId } from '@milaboratories/pl-client';
|
|
7
2
|
import { MiLogger, Signer } from '@milaboratories/ts-helpers';
|
|
8
3
|
import * as sdk from '@milaboratories/pl-model-common';
|
|
4
|
+
import {
|
|
5
|
+
isImportFileHandleIndex,
|
|
6
|
+
LocalImportFileHandle,
|
|
7
|
+
LsEntry,
|
|
8
|
+
OpenDialogOps,
|
|
9
|
+
OpenMultipleFilesResponse,
|
|
10
|
+
OpenSingleFileResponse,
|
|
11
|
+
TableRange
|
|
12
|
+
} from '@milaboratories/pl-model-common';
|
|
9
13
|
import { ClientLs } from '../clients/ls_api';
|
|
10
14
|
import * as path from 'node:path';
|
|
11
|
-
import * as
|
|
15
|
+
import * as fsp from 'node:fs/promises';
|
|
12
16
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
createIndexImportHandle,
|
|
18
|
+
createUploadImportHandle,
|
|
19
|
+
parseIndexHandle,
|
|
20
|
+
parseUploadHandle
|
|
17
21
|
} from './helpers/ls_list_entry';
|
|
18
|
-
import {
|
|
22
|
+
import { createLocalStorageHandle, createRemoteStorageHandle, parseStorageHandle } from './helpers/ls_storage_entry';
|
|
23
|
+
import { LocalStorageProjection } from './types';
|
|
24
|
+
import * as os from 'node:os';
|
|
25
|
+
import { createLsFilesClient } from '../clients/helpers';
|
|
26
|
+
import { validateAbsolute } from '../helpers/validate';
|
|
19
27
|
|
|
20
28
|
/**
|
|
21
29
|
* Extends public and safe SDK's driver API with methods used internally in the middle
|
|
@@ -23,113 +31,255 @@ import { fromStorageHandle, toStorageEntry } from './helpers/ls_storage_entry';
|
|
|
23
31
|
*/
|
|
24
32
|
export interface InternalLsDriver extends sdk.LsDriver {
|
|
25
33
|
/**
|
|
26
|
-
* Given local path, generates well
|
|
34
|
+
* Given local path, generates well-structured and signed upload handle.
|
|
27
35
|
* To be used in tests and in implementation of the native file selection UI API.
|
|
28
36
|
* */
|
|
29
|
-
getLocalFileHandle(localPath: string): Promise<sdk.
|
|
37
|
+
getLocalFileHandle(localPath: string): Promise<sdk.LocalImportFileHandle>;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
|
-
export
|
|
33
|
-
|
|
40
|
+
export type OpenFileDialogCallback = (
|
|
41
|
+
multipleFiles: boolean,
|
|
42
|
+
ops?: OpenDialogOps
|
|
43
|
+
) => Promise<undefined | string[]>;
|
|
44
|
+
|
|
45
|
+
/** Allows to add parts of local FS as virtual storages, presenting homogeneous API to UI */
|
|
46
|
+
export type VirtualLocalStorageSpec = {
|
|
47
|
+
/** Virtual storage ID, must not intersect with other storage ids */
|
|
48
|
+
readonly name: string;
|
|
49
|
+
|
|
50
|
+
/** Local path to "chroot" the API in */
|
|
51
|
+
readonly root: string;
|
|
52
|
+
|
|
53
|
+
/** Used as hint to UI controls to, set as initial path during browsing */
|
|
54
|
+
readonly initialPath: string;
|
|
55
|
+
};
|
|
34
56
|
|
|
35
|
-
|
|
57
|
+
export function DefaultVirtualLocalStorages(): VirtualLocalStorageSpec[] {
|
|
58
|
+
const home = os.homedir();
|
|
59
|
+
return path.sep === '/'
|
|
60
|
+
? [{ name: 'local', root: '/', initialPath: home }]
|
|
61
|
+
: [
|
|
62
|
+
{
|
|
63
|
+
name: 'local',
|
|
64
|
+
root: path.parse(home).root, // disk where home directory resides
|
|
65
|
+
initialPath: home
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class LsDriver implements InternalLsDriver {
|
|
71
|
+
private constructor(
|
|
36
72
|
private readonly logger: MiLogger,
|
|
37
|
-
private readonly
|
|
38
|
-
|
|
73
|
+
private readonly lsClient: ClientLs,
|
|
74
|
+
/** Pl storage id, to resource id. The resource id can be used to make LS GRPC calls to. */
|
|
75
|
+
private readonly storageIdToResourceId: Record<string, ResourceId>,
|
|
39
76
|
private readonly signer: Signer,
|
|
40
|
-
|
|
77
|
+
/** Virtual storages by name */
|
|
78
|
+
private readonly virtualStoragesMap: Map<string, VirtualLocalStorageSpec>,
|
|
79
|
+
/** Local projections by storageId */
|
|
80
|
+
private readonly localProjectionsMap: Map<string, LocalStorageProjection>,
|
|
81
|
+
private readonly openFileDialogCallback: OpenFileDialogCallback
|
|
41
82
|
) {}
|
|
42
83
|
|
|
84
|
+
public async getLocalFileContent(
|
|
85
|
+
file: LocalImportFileHandle,
|
|
86
|
+
range?: TableRange
|
|
87
|
+
): Promise<Uint8Array> {
|
|
88
|
+
const localPath = await this.tryResolveLocalFileHandle(file);
|
|
89
|
+
if (range) throw new Error('Range request not yet supported.');
|
|
90
|
+
return await fsp.readFile(localPath);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public async getLocalFileSize(file: LocalImportFileHandle): Promise<number> {
|
|
94
|
+
const localPath = await this.tryResolveLocalFileHandle(file);
|
|
95
|
+
const stat = await fsp.stat(localPath);
|
|
96
|
+
return stat.size;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public async showOpenMultipleFilesDialog(
|
|
100
|
+
ops?: OpenDialogOps
|
|
101
|
+
): Promise<OpenMultipleFilesResponse> {
|
|
102
|
+
const result = await this.openFileDialogCallback(true, ops);
|
|
103
|
+
if (result === undefined) return {};
|
|
104
|
+
return {
|
|
105
|
+
files: await Promise.all(result.map((localPath) => this.getLocalFileHandle(localPath)))
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public async showOpenSingleFileDialog(ops?: OpenDialogOps): Promise<OpenSingleFileResponse> {
|
|
110
|
+
const result = await this.openFileDialogCallback(false, ops);
|
|
111
|
+
if (result === undefined) return {};
|
|
112
|
+
return {
|
|
113
|
+
file: await this.getLocalFileHandle(result[0])
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolves local handle to local file path.
|
|
119
|
+
*
|
|
120
|
+
* @param handle handle to be resolved
|
|
121
|
+
* @private
|
|
122
|
+
*/
|
|
123
|
+
private async tryResolveLocalFileHandle(handle: LocalImportFileHandle): Promise<string> {
|
|
124
|
+
if (isImportFileHandleIndex(handle)) {
|
|
125
|
+
const handleData = parseIndexHandle(handle);
|
|
126
|
+
const localProjection = this.localProjectionsMap.get(handleData.storageId);
|
|
127
|
+
if (!localProjection)
|
|
128
|
+
throw new Error(`Storage ${handleData.storageId} is not mounted locally.`);
|
|
129
|
+
return path.join(localProjection.localPath, handleData.path);
|
|
130
|
+
} else {
|
|
131
|
+
const handleData = parseUploadHandle(handle);
|
|
132
|
+
// checking it is a valid local handle from out machine
|
|
133
|
+
this.signer.verify(
|
|
134
|
+
handleData.localPath,
|
|
135
|
+
handleData.pathSignature,
|
|
136
|
+
'Failed to validate local file handle signature.'
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const localPath = handleData.localPath;
|
|
140
|
+
|
|
141
|
+
const stat = await fsp.stat(localPath, { bigint: true });
|
|
142
|
+
if (String(stat.mtimeMs / 1000n) !== handleData.modificationTime)
|
|
143
|
+
throw new Error('File has changed since the handle was created.');
|
|
144
|
+
|
|
145
|
+
return localPath;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
43
149
|
public async getLocalFileHandle(
|
|
44
150
|
localPath: string
|
|
45
|
-
): Promise<sdk.
|
|
46
|
-
|
|
47
|
-
|
|
151
|
+
): Promise<sdk.ImportFileHandle & LocalImportFileHandle> {
|
|
152
|
+
validateAbsolute(localPath);
|
|
153
|
+
|
|
154
|
+
// Checking if local path is directly reachable by pl, because it is in one of the
|
|
155
|
+
// locally mounted storages
|
|
156
|
+
for (const lp of this.localProjectionsMap.values()) {
|
|
157
|
+
// note: empty lp.localPath will match any address
|
|
158
|
+
if (localPath.startsWith(lp.localPath)) {
|
|
159
|
+
// Just in case:
|
|
160
|
+
// > path.relative("/a/b", "/a/b/c");
|
|
161
|
+
// 'c'
|
|
162
|
+
const pathWithinStorage =
|
|
163
|
+
lp.localPath === '' ? localPath : path.relative(lp.localPath, localPath);
|
|
164
|
+
return createIndexImportHandle(
|
|
165
|
+
lp.storageId,
|
|
166
|
+
pathWithinStorage
|
|
167
|
+
) as sdk.ImportFileHandleIndex & LocalImportFileHandle;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// we get here if none of the local projections matched the path
|
|
172
|
+
|
|
173
|
+
const stat = await fsp.stat(localPath, { bigint: true });
|
|
174
|
+
return createUploadImportHandle(
|
|
48
175
|
localPath,
|
|
49
176
|
this.signer,
|
|
50
177
|
stat.size,
|
|
51
178
|
stat.mtimeMs / 1000n // integer division
|
|
52
|
-
);
|
|
179
|
+
) as sdk.ImportFileHandleUpload & LocalImportFileHandle;
|
|
53
180
|
}
|
|
54
181
|
|
|
55
182
|
public async getStorageList(): Promise<sdk.StorageEntry[]> {
|
|
56
|
-
return
|
|
57
|
-
this.
|
|
58
|
-
|
|
59
|
-
|
|
183
|
+
return [
|
|
184
|
+
...[...this.virtualStoragesMap.values()].map((vs) => ({
|
|
185
|
+
name: vs.name,
|
|
186
|
+
handle: createLocalStorageHandle(vs.name, vs.root),
|
|
187
|
+
initialFullPath: vs.initialPath
|
|
188
|
+
})),
|
|
189
|
+
...Object.entries(this.storageIdToResourceId!).map(([storageId, resourceId]) => ({
|
|
190
|
+
name: storageId,
|
|
191
|
+
handle: createRemoteStorageHandle(storageId, resourceId),
|
|
192
|
+
initialFullPath: '' // we don't have any additional information from where to start browsing remote storages
|
|
193
|
+
}))
|
|
194
|
+
] as sdk.StorageEntry[];
|
|
60
195
|
}
|
|
61
196
|
|
|
62
197
|
public async listFiles(
|
|
63
198
|
storageHandle: sdk.StorageHandle,
|
|
64
|
-
|
|
199
|
+
fullPath: string
|
|
65
200
|
): Promise<sdk.ListFilesResult> {
|
|
66
|
-
const
|
|
201
|
+
const storageData = parseStorageHandle(storageHandle);
|
|
67
202
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
203
|
+
if (storageData.isRemote) {
|
|
204
|
+
const response = await this.lsClient.list(storageData, fullPath);
|
|
205
|
+
return {
|
|
206
|
+
entries: response.items.map((e) => ({
|
|
207
|
+
type: e.isDir ? 'dir' : 'file',
|
|
208
|
+
name: e.name,
|
|
209
|
+
fullPath: e.fullName,
|
|
210
|
+
handle: createIndexImportHandle(storageData.name, e.fullName)
|
|
211
|
+
}))
|
|
212
|
+
};
|
|
72
213
|
} else {
|
|
73
|
-
|
|
74
|
-
}
|
|
214
|
+
if (path.sep === '/' && fullPath === '') fullPath = '/';
|
|
75
215
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
remote: storage.remote
|
|
81
|
-
});
|
|
82
|
-
}
|
|
216
|
+
const lsRoot =
|
|
217
|
+
storageData.rootPath === ''
|
|
218
|
+
? validateAbsolute(fullPath)
|
|
219
|
+
: path.join(storageData.rootPath, fullPath);
|
|
83
220
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
221
|
+
const entries: LsEntry[] = [];
|
|
222
|
+
for await (const dirent of await fsp.opendir(lsRoot)) {
|
|
223
|
+
if (!dirent.isFile() && !dirent.isDirectory()) continue;
|
|
87
224
|
|
|
88
|
-
|
|
225
|
+
// We cannot use no dirent.fullPath no dirent.parentPath,
|
|
226
|
+
// since the former is deprecated
|
|
227
|
+
// and the later works differently on different versions.
|
|
228
|
+
const absolutePath = path.join(lsRoot, dirent.name);
|
|
229
|
+
|
|
230
|
+
entries.push({
|
|
231
|
+
type: dirent.isFile() ? 'file' : 'dir',
|
|
232
|
+
name: dirent.name,
|
|
233
|
+
fullPath: absolutePath,
|
|
234
|
+
handle: await this.getLocalFileHandle(absolutePath)
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { entries };
|
|
239
|
+
}
|
|
89
240
|
}
|
|
90
241
|
|
|
91
|
-
|
|
242
|
+
public static async init(
|
|
92
243
|
logger: MiLogger,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const files = await fs.opendir(fullPath);
|
|
103
|
-
const direntsWithStats: any[] = [];
|
|
104
|
-
for await (const dirent of files) {
|
|
105
|
-
// We cannot use no dirent.path no dirent.parentPath,
|
|
106
|
-
// since the former is deprecated
|
|
107
|
-
// and the later works differently on different versions.
|
|
108
|
-
const fullName = path.join(fullPath, dirent.name);
|
|
109
|
-
|
|
110
|
-
direntsWithStats.push({
|
|
111
|
-
directory: fullPath,
|
|
112
|
-
fullName,
|
|
113
|
-
dirent,
|
|
114
|
-
stat: await fs.stat(fullName)
|
|
115
|
-
});
|
|
116
|
-
}
|
|
244
|
+
client: PlClient,
|
|
245
|
+
signer: Signer,
|
|
246
|
+
virtualStorages: VirtualLocalStorageSpec[],
|
|
247
|
+
/** Pl storages available locally */
|
|
248
|
+
localProjections: LocalStorageProjection[],
|
|
249
|
+
openFileDialogCallback: OpenFileDialogCallback
|
|
250
|
+
): Promise<LsDriver> {
|
|
251
|
+
const lsClient = createLsFilesClient(client, logger);
|
|
117
252
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
253
|
+
// validating inputs
|
|
254
|
+
for (const vp of virtualStorages) validateAbsolute(vp.root);
|
|
255
|
+
for (const lp of localProjections) if (lp.localPath !== '') validateAbsolute(lp.localPath);
|
|
256
|
+
|
|
257
|
+
// creating indexed maps for quick access
|
|
258
|
+
const virtualStoragesMap = new Map(virtualStorages.map((s) => [s.name, s]));
|
|
259
|
+
const localProjectionsMap = new Map(localProjections.map((s) => [s.storageId, s]));
|
|
125
260
|
|
|
126
|
-
|
|
261
|
+
// validating there is no intersection
|
|
262
|
+
if (
|
|
263
|
+
new Set([...virtualStoragesMap.keys(), ...localProjectionsMap.keys()]).size !==
|
|
264
|
+
virtualStoragesMap.size + localProjectionsMap.size
|
|
265
|
+
)
|
|
266
|
+
throw new Error(
|
|
267
|
+
'Intersection between local projection storage ids and virtual storages names detected.'
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
return new LsDriver(
|
|
271
|
+
logger,
|
|
272
|
+
lsClient,
|
|
273
|
+
await doGetAvailableStorageIds(client),
|
|
274
|
+
signer,
|
|
275
|
+
virtualStoragesMap,
|
|
276
|
+
localProjectionsMap,
|
|
277
|
+
openFileDialogCallback
|
|
278
|
+
);
|
|
127
279
|
}
|
|
128
280
|
}
|
|
129
281
|
|
|
130
|
-
async function doGetAvailableStorageIds(
|
|
131
|
-
client: PlClient
|
|
132
|
-
): Promise<Record<string, ResourceId>> {
|
|
282
|
+
async function doGetAvailableStorageIds(client: PlClient): Promise<Record<string, ResourceId>> {
|
|
133
283
|
return client.withReadTx('GetAvailableStorageIds', async (tx) => {
|
|
134
284
|
const lsProviderId = await tx.getResourceByName('LSProvider');
|
|
135
285
|
const provider = await tx.getResourceData(lsProviderId, true);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const ImportFileHandleUploadData = z.object({
|
|
4
|
+
/** Local file path, to take data for upload */
|
|
5
|
+
localPath: z.string(),
|
|
6
|
+
/** Path signature, to check this data was generated by us */
|
|
7
|
+
pathSignature: z.string(),
|
|
8
|
+
/** File size in bytes */
|
|
9
|
+
sizeBytes: z.string(),
|
|
10
|
+
/** Modification time unix timestamp in seconds */
|
|
11
|
+
modificationTime: z.string()
|
|
12
|
+
});
|
|
13
|
+
export type ImportFileHandleUploadData = z.infer<typeof ImportFileHandleUploadData>;
|
|
14
|
+
|
|
15
|
+
export const ImportFileHandleIndexData = z.object({
|
|
16
|
+
/** Pl storage id */
|
|
17
|
+
storageId: z.string(),
|
|
18
|
+
/** Path inside storage */
|
|
19
|
+
path: z.string()
|
|
20
|
+
});
|
|
21
|
+
export type ImportFileHandleIndexData = z.infer<typeof ImportFileHandleIndexData>;
|
|
22
|
+
|
|
23
|
+
export const ImportFileHandleData = z.union([
|
|
24
|
+
ImportFileHandleUploadData,
|
|
25
|
+
ImportFileHandleIndexData
|
|
26
|
+
]);
|
|
27
|
+
export type ImportFileHandleData = z.infer<typeof ImportFileHandleData>;
|
|
28
|
+
|
|
29
|
+
/** Defines which storages from pl are available via local paths */
|
|
30
|
+
export type LocalStorageProjection = {
|
|
31
|
+
/** Pl storage id */
|
|
32
|
+
readonly storageId: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Local path, the storage is mounted at.
|
|
36
|
+
*
|
|
37
|
+
* Empty string means that this storage accepts absolute paths, and operates inside the same OS.
|
|
38
|
+
* This matches the behaviour how pl interprets FS storage config.
|
|
39
|
+
* */
|
|
40
|
+
readonly localPath: string;
|
|
41
|
+
};
|