@directus/storage-driver-azure 9.21.2
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/index.js +64 -0
- package/dist/index.test.js +373 -0
- package/package.json +45 -0
- package/readme.md +3 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { BlobServiceClient, StorageSharedKeyCredential } from '@azure/storage-blob';
|
|
2
|
+
import { normalizePath } from '@directus/utils';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export class DriverAzure {
|
|
5
|
+
containerClient;
|
|
6
|
+
signedCredentials;
|
|
7
|
+
root;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.signedCredentials = new StorageSharedKeyCredential(config.accountName, config.accountKey);
|
|
10
|
+
const client = new BlobServiceClient(config.endpoint ?? `https://${config.accountName}.blob.core.windows.net`, this.signedCredentials);
|
|
11
|
+
this.containerClient = client.getContainerClient(config.containerName);
|
|
12
|
+
this.root = config.root ? normalizePath(config.root, { removeLeading: true }) : '';
|
|
13
|
+
}
|
|
14
|
+
fullPath(filepath) {
|
|
15
|
+
return normalizePath(join(this.root, filepath));
|
|
16
|
+
}
|
|
17
|
+
async read(filepath, range) {
|
|
18
|
+
const { readableStreamBody } = await this.containerClient
|
|
19
|
+
.getBlobClient(this.fullPath(filepath))
|
|
20
|
+
.download(range?.start, range?.end ? range.end - (range.start || 0) : undefined);
|
|
21
|
+
if (!readableStreamBody) {
|
|
22
|
+
throw new Error(`No stream returned for file "${filepath}"`);
|
|
23
|
+
}
|
|
24
|
+
return readableStreamBody;
|
|
25
|
+
}
|
|
26
|
+
async write(filepath, content, type = 'application/octet-stream') {
|
|
27
|
+
const blockBlobClient = this.containerClient.getBlockBlobClient(this.fullPath(filepath));
|
|
28
|
+
await blockBlobClient.uploadStream(content, undefined, undefined, {
|
|
29
|
+
blobHTTPHeaders: { blobContentType: type },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async delete(filepath) {
|
|
33
|
+
await this.containerClient.getBlockBlobClient(this.fullPath(filepath)).deleteIfExists();
|
|
34
|
+
}
|
|
35
|
+
async stat(filepath) {
|
|
36
|
+
const props = await this.containerClient.getBlobClient(this.fullPath(filepath)).getProperties();
|
|
37
|
+
return {
|
|
38
|
+
size: props.contentLength,
|
|
39
|
+
modified: props.lastModified,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async exists(filepath) {
|
|
43
|
+
return await this.containerClient.getBlockBlobClient(this.fullPath(filepath)).exists();
|
|
44
|
+
}
|
|
45
|
+
async move(src, dest) {
|
|
46
|
+
await this.copy(src, dest);
|
|
47
|
+
await this.containerClient.getBlockBlobClient(this.fullPath(src)).deleteIfExists();
|
|
48
|
+
}
|
|
49
|
+
async copy(src, dest) {
|
|
50
|
+
const source = this.containerClient.getBlockBlobClient(this.fullPath(src));
|
|
51
|
+
const target = this.containerClient.getBlockBlobClient(this.fullPath(dest));
|
|
52
|
+
const poller = await target.beginCopyFromURL(source.url);
|
|
53
|
+
await poller.pollUntilDone();
|
|
54
|
+
}
|
|
55
|
+
async *list(prefix = '') {
|
|
56
|
+
const blobs = this.containerClient.listBlobsFlat({
|
|
57
|
+
prefix: this.fullPath(prefix),
|
|
58
|
+
});
|
|
59
|
+
for await (const blob of blobs) {
|
|
60
|
+
yield blob.name.substring(this.root.length);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export default DriverAzure;
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { BlobServiceClient, StorageSharedKeyCredential } from '@azure/storage-blob';
|
|
2
|
+
import { normalizePath } from '@directus/utils';
|
|
3
|
+
import { isReadableStream } from '@directus/utils/node';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { PassThrough } from 'node:stream';
|
|
6
|
+
import { afterEach, describe, expect, test, vi, beforeEach } from 'vitest';
|
|
7
|
+
import { DriverAzure } from './index.js';
|
|
8
|
+
import { randAlphaNumeric, randDirectoryPath, randDomainName, randFilePath, randGitBranch as randContainer, randNumber, randPastDate, randWord, randText, randFileType, randUrl, } from '@ngneat/falso';
|
|
9
|
+
vi.mock('@directus/utils/node');
|
|
10
|
+
vi.mock('@directus/utils');
|
|
11
|
+
vi.mock('@azure/storage-blob');
|
|
12
|
+
vi.mock('node:path');
|
|
13
|
+
let sample;
|
|
14
|
+
let driver;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
sample = {
|
|
17
|
+
config: {
|
|
18
|
+
containerName: randContainer(),
|
|
19
|
+
accountName: randWord(),
|
|
20
|
+
accountKey: randAlphaNumeric({ length: 40 }).join(''),
|
|
21
|
+
root: randDirectoryPath(),
|
|
22
|
+
endpoint: `https://${randDomainName()}`,
|
|
23
|
+
},
|
|
24
|
+
path: {
|
|
25
|
+
input: randFilePath(),
|
|
26
|
+
inputFull: randFilePath(),
|
|
27
|
+
src: randFilePath(),
|
|
28
|
+
srcFull: randFilePath(),
|
|
29
|
+
dest: randFilePath(),
|
|
30
|
+
destFull: randFilePath(),
|
|
31
|
+
},
|
|
32
|
+
range: {
|
|
33
|
+
start: randNumber(),
|
|
34
|
+
end: randNumber(),
|
|
35
|
+
},
|
|
36
|
+
stream: new PassThrough(),
|
|
37
|
+
text: randText(),
|
|
38
|
+
file: {
|
|
39
|
+
type: randFileType(),
|
|
40
|
+
size: randNumber(),
|
|
41
|
+
modified: randPastDate(),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
driver = new DriverAzure({
|
|
45
|
+
containerName: sample.config.containerName,
|
|
46
|
+
accountKey: sample.config.accountKey,
|
|
47
|
+
accountName: sample.config.accountName,
|
|
48
|
+
});
|
|
49
|
+
driver['fullPath'] = vi.fn().mockImplementation((input) => {
|
|
50
|
+
if (input === sample.path.src)
|
|
51
|
+
return sample.path.srcFull;
|
|
52
|
+
if (input === sample.path.dest)
|
|
53
|
+
return sample.path.destFull;
|
|
54
|
+
if (input === sample.path.input)
|
|
55
|
+
return sample.path.inputFull;
|
|
56
|
+
return '';
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
vi.resetAllMocks();
|
|
61
|
+
});
|
|
62
|
+
describe('#constructor', () => {
|
|
63
|
+
test('Creates signed credentials', () => {
|
|
64
|
+
expect(StorageSharedKeyCredential).toHaveBeenCalledWith(sample.config.accountName, sample.config.accountKey);
|
|
65
|
+
expect(driver['signedCredentials']).toBeInstanceOf(StorageSharedKeyCredential);
|
|
66
|
+
});
|
|
67
|
+
test('Creates blob service client and sets containerClient', () => {
|
|
68
|
+
const mockSignedCredentials = {};
|
|
69
|
+
vi.mocked(StorageSharedKeyCredential).mockReturnValueOnce(mockSignedCredentials);
|
|
70
|
+
const mockContainerClient = {};
|
|
71
|
+
const mockBlobServiceClient = {
|
|
72
|
+
getContainerClient: vi.fn().mockReturnValue(mockContainerClient),
|
|
73
|
+
};
|
|
74
|
+
vi.mocked(BlobServiceClient).mockReturnValue(mockBlobServiceClient);
|
|
75
|
+
const driver = new DriverAzure({
|
|
76
|
+
containerName: sample.config.containerName,
|
|
77
|
+
accountName: sample.config.accountName,
|
|
78
|
+
accountKey: sample.config.accountKey,
|
|
79
|
+
});
|
|
80
|
+
expect(BlobServiceClient).toHaveBeenCalledWith(`https://${sample.config.accountName}.blob.core.windows.net`, mockSignedCredentials);
|
|
81
|
+
expect(mockBlobServiceClient.getContainerClient).toHaveBeenCalledWith(sample.config.containerName);
|
|
82
|
+
expect(driver['containerClient']).toBe(mockContainerClient);
|
|
83
|
+
});
|
|
84
|
+
test('Allows overriding endpoint with optional setting', () => {
|
|
85
|
+
test('Creates blob service client and sets containerClient', () => {
|
|
86
|
+
const mockSignedCredentials = {};
|
|
87
|
+
vi.mocked(StorageSharedKeyCredential).mockReturnValueOnce(mockSignedCredentials);
|
|
88
|
+
const mockContainerClient = {};
|
|
89
|
+
const mockBlobServiceClient = {
|
|
90
|
+
getContainerClient: vi.fn().mockReturnValue(mockContainerClient),
|
|
91
|
+
};
|
|
92
|
+
vi.mocked(BlobServiceClient).mockReturnValue(mockBlobServiceClient);
|
|
93
|
+
const driver = new DriverAzure({
|
|
94
|
+
containerName: sample.config.containerName,
|
|
95
|
+
accountName: sample.config.accountName,
|
|
96
|
+
accountKey: sample.config.accountKey,
|
|
97
|
+
endpoint: sample.config.endpoint,
|
|
98
|
+
});
|
|
99
|
+
expect(BlobServiceClient).toHaveBeenCalledWith(sample.config.endpoint, mockSignedCredentials);
|
|
100
|
+
expect(mockBlobServiceClient.getContainerClient).toHaveBeenCalledWith(sample.config.containerName);
|
|
101
|
+
expect(driver['containerClient']).toBe(mockContainerClient);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
test('Defaults root path to empty string', () => {
|
|
105
|
+
expect(driver['root']).toBe('');
|
|
106
|
+
});
|
|
107
|
+
test('Normalizes config path when root is given', () => {
|
|
108
|
+
vi.mocked(normalizePath).mockReturnValue(sample.path.inputFull);
|
|
109
|
+
new DriverAzure({
|
|
110
|
+
containerName: sample.config.containerName,
|
|
111
|
+
accountName: sample.config.accountName,
|
|
112
|
+
accountKey: sample.config.accountKey,
|
|
113
|
+
root: sample.path.input,
|
|
114
|
+
});
|
|
115
|
+
expect(normalizePath).toHaveBeenCalledWith(sample.path.input, { removeLeading: true });
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('#fullPath', () => {
|
|
119
|
+
test('Returns normalized joined path', () => {
|
|
120
|
+
vi.mocked(join).mockReturnValue(sample.path.inputFull);
|
|
121
|
+
vi.mocked(normalizePath).mockReturnValue(sample.path.inputFull);
|
|
122
|
+
const driver = new DriverAzure({
|
|
123
|
+
containerName: sample.config.containerName,
|
|
124
|
+
accountName: sample.config.accountName,
|
|
125
|
+
accountKey: sample.config.accountKey,
|
|
126
|
+
});
|
|
127
|
+
driver['root'] = sample.config.root;
|
|
128
|
+
const result = driver['fullPath'](sample.path.input);
|
|
129
|
+
expect(join).toHaveBeenCalledWith(sample.config.root, sample.path.input);
|
|
130
|
+
expect(normalizePath).toHaveBeenCalledWith(sample.path.inputFull);
|
|
131
|
+
expect(result).toBe(sample.path.inputFull);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe('#read', () => {
|
|
135
|
+
let mockDownload;
|
|
136
|
+
beforeEach(async () => {
|
|
137
|
+
mockDownload = vi.fn().mockResolvedValue({ readableStreamBody: sample.stream });
|
|
138
|
+
const mockBlobClient = vi.fn().mockReturnValue({
|
|
139
|
+
download: mockDownload,
|
|
140
|
+
});
|
|
141
|
+
driver['containerClient'] = {
|
|
142
|
+
getBlobClient: mockBlobClient,
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
test('Uses blobClient at full path', async () => {
|
|
146
|
+
await driver.read(sample.path.input);
|
|
147
|
+
expect(driver['fullPath']).toHaveBeenCalledWith(sample.path.input);
|
|
148
|
+
expect(driver['containerClient'].getBlobClient).toHaveBeenCalledWith(sample.path.inputFull);
|
|
149
|
+
});
|
|
150
|
+
test('Calls download with undefined undefined when no range is passed', async () => {
|
|
151
|
+
await driver.read(sample.path.input);
|
|
152
|
+
expect(mockDownload).toHaveBeenCalledWith(undefined, undefined);
|
|
153
|
+
});
|
|
154
|
+
test('Calls download with offset if start range is provided', async () => {
|
|
155
|
+
await driver.read(sample.path.input, { start: sample.range.start });
|
|
156
|
+
expect(mockDownload).toHaveBeenCalledWith(sample.range.start, undefined);
|
|
157
|
+
});
|
|
158
|
+
test('Calls download with count if end range is provided', async () => {
|
|
159
|
+
await driver.read(sample.path.input, { end: sample.range.end });
|
|
160
|
+
expect(mockDownload).toHaveBeenCalledWith(undefined, sample.range.end);
|
|
161
|
+
});
|
|
162
|
+
test('Calls download with offset and count if start and end ranges are provided', async () => {
|
|
163
|
+
await driver.read(sample.path.input, sample.range);
|
|
164
|
+
expect(mockDownload).toHaveBeenCalledWith(sample.range.start, sample.range.end - sample.range.start);
|
|
165
|
+
});
|
|
166
|
+
test('Throws error when no readable stream is returned', async () => {
|
|
167
|
+
mockDownload.mockResolvedValue({ readableStreamBody: undefined });
|
|
168
|
+
try {
|
|
169
|
+
await driver.read(sample.path.input);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
expect(err).toBeInstanceOf(Error);
|
|
173
|
+
expect(err.message).toBe(`No stream returned for file "${sample.path.input}"`);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
describe('#write', () => {
|
|
178
|
+
let mockUploadStream;
|
|
179
|
+
let mockBlockBlobClient;
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
mockUploadStream = vi.fn();
|
|
182
|
+
mockBlockBlobClient = vi.fn().mockReturnValue({
|
|
183
|
+
uploadStream: mockUploadStream,
|
|
184
|
+
});
|
|
185
|
+
driver['containerClient'] = {
|
|
186
|
+
getBlockBlobClient: mockBlockBlobClient,
|
|
187
|
+
};
|
|
188
|
+
vi.mocked(isReadableStream).mockReturnValue(true);
|
|
189
|
+
});
|
|
190
|
+
test('Gets BlockBlobClient for file path', async () => {
|
|
191
|
+
await driver.write(sample.path.input, sample.stream);
|
|
192
|
+
expect(mockBlockBlobClient).toHaveBeenCalledWith(sample.path.inputFull);
|
|
193
|
+
});
|
|
194
|
+
test('Uploads stream through uploadStream', async () => {
|
|
195
|
+
await driver.write(sample.path.input, sample.stream);
|
|
196
|
+
expect(mockUploadStream).toHaveBeenCalledWith(sample.stream, undefined, undefined, {
|
|
197
|
+
blobHTTPHeaders: { blobContentType: 'application/octet-stream' },
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
test('Allows optional mime type to be set', async () => {
|
|
201
|
+
await driver.write(sample.path.input, sample.stream, sample.file.type);
|
|
202
|
+
expect(mockUploadStream).toHaveBeenCalledWith(sample.stream, undefined, undefined, {
|
|
203
|
+
blobHTTPHeaders: { blobContentType: sample.file.type },
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('#delete', () => {
|
|
208
|
+
let mockDeleteIfExists;
|
|
209
|
+
beforeEach(() => {
|
|
210
|
+
mockDeleteIfExists = vi.fn().mockResolvedValue(true);
|
|
211
|
+
const mockBlockBlobClient = vi.fn().mockReturnValue({
|
|
212
|
+
deleteIfExists: mockDeleteIfExists,
|
|
213
|
+
});
|
|
214
|
+
driver['containerClient'] = {
|
|
215
|
+
getBlockBlobClient: mockBlockBlobClient,
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
test('Uses blobClient at full path', async () => {
|
|
219
|
+
await driver.delete(sample.path.input);
|
|
220
|
+
expect(driver['fullPath']).toHaveBeenCalledWith(sample.path.input);
|
|
221
|
+
expect(driver['containerClient'].getBlockBlobClient).toHaveBeenCalledWith(sample.path.inputFull);
|
|
222
|
+
});
|
|
223
|
+
test('Returns delete result', async () => {
|
|
224
|
+
await driver.delete(sample.path.input);
|
|
225
|
+
expect(mockDeleteIfExists).toHaveBeenCalled();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
describe('#stat', () => {
|
|
229
|
+
beforeEach(() => {
|
|
230
|
+
const mockGetProperties = vi.fn().mockReturnValue({
|
|
231
|
+
contentLength: sample.file.size,
|
|
232
|
+
lastModified: sample.file.modified,
|
|
233
|
+
});
|
|
234
|
+
const mockBlobClient = vi.fn().mockReturnValue({
|
|
235
|
+
getProperties: mockGetProperties,
|
|
236
|
+
});
|
|
237
|
+
driver['containerClient'] = {
|
|
238
|
+
getBlobClient: mockBlobClient,
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
test('Uses blobClient at full path', async () => {
|
|
242
|
+
await driver.stat(sample.path.input);
|
|
243
|
+
expect(driver['fullPath']).toHaveBeenCalledWith(sample.path.input);
|
|
244
|
+
expect(driver['containerClient'].getBlobClient).toHaveBeenCalledWith(sample.path.inputFull);
|
|
245
|
+
});
|
|
246
|
+
test('Returns contentLength/lastModified as size/modified from getProperties', async () => {
|
|
247
|
+
const result = await driver.stat(sample.path.input);
|
|
248
|
+
expect(result).toStrictEqual({
|
|
249
|
+
size: sample.file.size,
|
|
250
|
+
modified: sample.file.modified,
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
describe('#exists', () => {
|
|
255
|
+
let mockExists;
|
|
256
|
+
beforeEach(() => {
|
|
257
|
+
mockExists = vi.fn().mockResolvedValue(true);
|
|
258
|
+
const mockBlockBlobClient = vi.fn().mockReturnValue({
|
|
259
|
+
exists: mockExists,
|
|
260
|
+
});
|
|
261
|
+
driver['containerClient'] = {
|
|
262
|
+
getBlockBlobClient: mockBlockBlobClient,
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
test('Uses blobClient at full path', async () => {
|
|
266
|
+
await driver.exists(sample.path.input);
|
|
267
|
+
expect(driver['fullPath']).toHaveBeenCalledWith(sample.path.input);
|
|
268
|
+
expect(driver['containerClient'].getBlockBlobClient).toHaveBeenCalledWith(sample.path.inputFull);
|
|
269
|
+
});
|
|
270
|
+
test('Returns exists result', async () => {
|
|
271
|
+
const result = await driver.exists(sample.path.input);
|
|
272
|
+
expect(mockExists).toHaveBeenCalled();
|
|
273
|
+
expect(result).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
describe('#move', () => {
|
|
277
|
+
let mockDeleteIfExists;
|
|
278
|
+
let mockBlockBlobClient;
|
|
279
|
+
beforeEach(() => {
|
|
280
|
+
mockDeleteIfExists = vi.fn();
|
|
281
|
+
mockBlockBlobClient = vi.fn().mockReturnValue({
|
|
282
|
+
deleteIfExists: mockDeleteIfExists,
|
|
283
|
+
});
|
|
284
|
+
driver['containerClient'] = {
|
|
285
|
+
getBlockBlobClient: mockBlockBlobClient,
|
|
286
|
+
};
|
|
287
|
+
driver.copy = vi.fn();
|
|
288
|
+
});
|
|
289
|
+
test('Calls #copy with src and dest', async () => {
|
|
290
|
+
await driver.move(sample.path.src, sample.path.dest);
|
|
291
|
+
expect(driver.copy).toHaveBeenCalledWith(sample.path.src, sample.path.dest);
|
|
292
|
+
});
|
|
293
|
+
test('Deletes src file after copy is completed', async () => {
|
|
294
|
+
await driver.move(sample.path.src, sample.path.dest);
|
|
295
|
+
expect(driver['fullPath']).toHaveBeenCalledWith(sample.path.src);
|
|
296
|
+
expect(mockBlockBlobClient).toHaveBeenCalledWith(sample.path.srcFull);
|
|
297
|
+
expect(mockDeleteIfExists).toHaveBeenCalledOnce();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
describe('#copy', () => {
|
|
301
|
+
let mockPollUntilDone;
|
|
302
|
+
let mockBeginCopyFromUrl;
|
|
303
|
+
let mockBlockBlobClient;
|
|
304
|
+
let mockUrl;
|
|
305
|
+
beforeEach(() => {
|
|
306
|
+
mockPollUntilDone = vi.fn();
|
|
307
|
+
mockBeginCopyFromUrl = vi.fn().mockResolvedValue({
|
|
308
|
+
pollUntilDone: mockPollUntilDone,
|
|
309
|
+
});
|
|
310
|
+
mockUrl = randUrl();
|
|
311
|
+
mockBlockBlobClient = vi
|
|
312
|
+
.fn()
|
|
313
|
+
.mockReturnValueOnce({
|
|
314
|
+
url: mockUrl,
|
|
315
|
+
})
|
|
316
|
+
.mockReturnValueOnce({
|
|
317
|
+
beginCopyFromURL: mockBeginCopyFromUrl,
|
|
318
|
+
});
|
|
319
|
+
driver['containerClient'] = {
|
|
320
|
+
getBlockBlobClient: mockBlockBlobClient,
|
|
321
|
+
};
|
|
322
|
+
});
|
|
323
|
+
test('Gets BlockBlobClient for src and dest', async () => {
|
|
324
|
+
await driver.copy(sample.path.src, sample.path.dest);
|
|
325
|
+
expect(driver['fullPath']).toHaveBeenCalledTimes(2);
|
|
326
|
+
expect(driver['fullPath']).toHaveBeenCalledWith(sample.path.src);
|
|
327
|
+
expect(driver['fullPath']).toHaveBeenCalledWith(sample.path.dest);
|
|
328
|
+
expect(mockBlockBlobClient).toHaveBeenCalledTimes(2);
|
|
329
|
+
expect(mockBlockBlobClient).toHaveBeenCalledWith(sample.path.srcFull);
|
|
330
|
+
expect(mockBlockBlobClient).toHaveBeenCalledWith(sample.path.destFull);
|
|
331
|
+
});
|
|
332
|
+
test('Calls beginCopyFromUrl with source url', async () => {
|
|
333
|
+
await driver.copy(sample.path.src, sample.path.dest);
|
|
334
|
+
expect(mockBeginCopyFromUrl).toHaveBeenCalledOnce();
|
|
335
|
+
expect(mockBeginCopyFromUrl).toHaveBeenCalledWith(mockUrl);
|
|
336
|
+
});
|
|
337
|
+
test('Waits for the polling to be done', async () => {
|
|
338
|
+
await driver.copy(sample.path.src, sample.path.dest);
|
|
339
|
+
expect(mockPollUntilDone).toHaveBeenCalledOnce();
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
describe('#list', () => {
|
|
343
|
+
let mockListBlobsFlat;
|
|
344
|
+
beforeEach(() => {
|
|
345
|
+
mockListBlobsFlat = vi.fn().mockReturnValue([]);
|
|
346
|
+
driver['containerClient'] = {
|
|
347
|
+
listBlobsFlat: mockListBlobsFlat,
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
test('Uses listBlobsFlat at default empty path', async () => {
|
|
351
|
+
await driver.list().next();
|
|
352
|
+
expect(driver['fullPath']).toHaveBeenCalledWith('');
|
|
353
|
+
expect(mockListBlobsFlat).toHaveBeenCalledWith({
|
|
354
|
+
prefix: '',
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
test('Allows for optional prefix', async () => {
|
|
358
|
+
await driver.list(sample.path.input).next();
|
|
359
|
+
expect(driver['fullPath']).toHaveBeenCalledWith(sample.path.input);
|
|
360
|
+
expect(mockListBlobsFlat).toHaveBeenCalledWith({
|
|
361
|
+
prefix: sample.path.inputFull,
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
test('Returns blob.name for each returned blob', async () => {
|
|
365
|
+
const mockFile = randFilePath();
|
|
366
|
+
mockListBlobsFlat.mockReturnValue([{ name: mockFile }]);
|
|
367
|
+
const output = [];
|
|
368
|
+
for await (const filepath of driver.list()) {
|
|
369
|
+
output.push(filepath);
|
|
370
|
+
}
|
|
371
|
+
expect(output).toStrictEqual([mockFile]);
|
|
372
|
+
});
|
|
373
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@directus/storage-driver-azure",
|
|
3
|
+
"version": "9.21.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Azure file storage abstraction for `@directus/storage`",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/directus/directus.git",
|
|
9
|
+
"directory": "packages/storage-driver-azure"
|
|
10
|
+
},
|
|
11
|
+
"funding": "https://github.com/directus/directus?sponsor=1",
|
|
12
|
+
"license": "GPL-3.0",
|
|
13
|
+
"author": "Rijk van Zanten <rijkvanzanten@me.com>",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./dist/index.js",
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"!**/*.d.ts?(.map)"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@azure/storage-blob": "12.12.0",
|
|
28
|
+
"@directus/storage": "9.21.2",
|
|
29
|
+
"@directus/utils": "9.21.2"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@directus/tsconfig": "0.0.6",
|
|
33
|
+
"@ngneat/falso": "6.3.0",
|
|
34
|
+
"@vitest/coverage-c8": "0.25.3",
|
|
35
|
+
"typescript": "4.9.3",
|
|
36
|
+
"vitest": "0.25.3"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc --build",
|
|
40
|
+
"dev": "tsc --watch",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest",
|
|
43
|
+
"test:coverage": "vitest run --coverage"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/readme.md
ADDED