Skip to content

Commit 82e2cec

Browse files
committed
fix(filesystem): improve type safety and provide methods to access current disk and driver.
1 parent c0ccddb commit 82e2cec

3 files changed

Lines changed: 198 additions & 66 deletions

File tree

packages/filesystem/src/Storage.ts

Lines changed: 126 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { CustomDiskDriverRegistry, DriverConfig, FileLike, FilesystemConfig, KnownDisks } from './types'
12
import { DriveDirectory, DriveFile, DriveManager } from 'flydrive'
2-
import { DriverConfig, FileLike } from './types'
33
import { DriverContract, ObjectMetaData, ObjectVisibility, SignedURLOptions, WriteOptions } from 'flydrive/types'
44
import { rmSync, symlinkSync } from 'node:fs'
55

@@ -10,10 +10,13 @@ import { Readable } from 'node:stream'
1010
import { config } from '@arkstack/common'
1111
import path from 'node:path'
1212

13-
export class Storage implements DriverContract {
13+
export class Storage<
14+
D extends keyof KnownDisks | keyof CustomDiskDriverRegistry = keyof KnownDisks | keyof CustomDiskDriverRegistry
15+
> implements DriverContract {
1416
driver: DriveManager<any>
1517
services: Record<string, () => DriverContract> = {}
16-
diskName: string
18+
diskName: D
19+
driverName: FilesystemConfig['disks'][D]['driver']
1720

1821
constructor() {
1922
const disks = Object.entries(config('filesystem.disks', {})) as Array<[string, DriverConfig]>
@@ -28,6 +31,7 @@ export class Storage implements DriverContract {
2831
}
2932

3033
this.diskName = config('filesystem.default')
34+
this.driverName = config(`filesystem.disks.${this.diskName}.driver`) as never
3135
this.driver = new DriveManager({
3236
default: config('filesystem.default'),
3337
services: this.services
@@ -40,18 +44,21 @@ export class Storage implements DriverContract {
4044
* @param diskName The name of the disk to use. If not provided, the default disk will be used.
4145
* @returns A Storage instance
4246
*/
43-
static disk<K extends string> (diskName?: K): Storage {
47+
static disk<K extends keyof KnownDisks | keyof CustomDiskDriverRegistry> (
48+
diskName?: K
49+
): Storage<K> {
4450
const storage = new Storage()
4551

4652
if (diskName) {
4753
storage.diskName = diskName
54+
storage.driverName = config(`filesystem.disks.${diskName}.driver`)
4855
storage.driver = new DriveManager({
4956
default: diskName,
5057
services: storage.services
5158
})
5259
}
5360

54-
return storage
61+
return storage as never
5562
}
5663

5764
/**
@@ -111,81 +118,156 @@ export class Storage implements DriverContract {
111118
await drive.put(path.join(filePath, name), file.buffer)
112119

113120
const url = await drive.getUrl(path.join(filePath, name))
114-
const pth = this.diskName === 'local' ? path.join(filePath, name) : url
121+
const pth = this.driverName === 'local' ? path.join(filePath, name) : url
115122

116123
return [url, pth]
117124
}
118125

119126
/**
120127
* Return a boolean indicating if the file exists
128+
*
129+
* @param key
130+
* @returns
121131
*/
122132
exists (key: string): Promise<boolean> {
123133
return this.driver.use().exists(key)
124134
}
135+
125136
/**
126137
* Return contents of a object for the given key as a UTF-8 string.
127138
* Should throw "E_CANNOT_READ_FILE" error when the file
128139
* does not exists.
140+
*
141+
* @param key
142+
* @returns
129143
*/
130144
get (key: string): Promise<string> {
131145
return this.driver.use().get(key)
132146
}
147+
148+
/**
149+
* Get the name of the disk currently in use.
150+
*
151+
* @returns
152+
*/
153+
getDiskName (): D {
154+
return this.diskName
155+
}
156+
157+
/**
158+
* Get the name of the driver currently in use.
159+
*
160+
* @returns
161+
*/
162+
getDriverName () {
163+
return this.driverName
164+
}
165+
166+
/**
167+
* Get the driver currently in use.
168+
*
169+
* @returns
170+
*/
171+
getDriver (): DriveManager<any> {
172+
return this.driver
173+
}
174+
133175
/**
134176
* Return contents of a object for the given key as a Readable stream.
135177
* Should throw "E_CANNOT_READ_FILE" error when the file
136178
* does not exists.
179+
*
180+
* @param key
181+
* @returns
137182
*/
138183
getStream (key: string): Promise<Readable> {
139184
return this.driver.use().getStream(key)
140185
}
186+
141187
/**
142188
* Return contents of an object for the given key as an Uint8Array.
143189
* Should throw "E_CANNOT_READ_FILE" error when the file
144190
* does not exists.
191+
*
192+
* @param key
193+
* @returns
145194
*/
146195
getBytes (key: string): Promise<Uint8Array> {
147196
return this.driver.use().getBytes(key)
148197
}
198+
149199
/**
150200
* Return metadata of an object for the given key.
201+
*
202+
* @param key
203+
* @returns
151204
*/
152205
getMetaData (key: string): Promise<ObjectMetaData> {
153206
return this.driver.use().getMetaData(key)
154207
}
208+
155209
/**
156210
* Return the visibility of the file
211+
*
212+
* @param key
213+
* @returns
157214
*/
158215
getVisibility (key: string): Promise<ObjectVisibility> {
159216
return this.driver.use().getVisibility(key)
160217
}
218+
161219
/**
162220
* Return the public URL to access the file
221+
*
222+
* @param key
223+
* @returns
163224
*/
164225
getUrl (key: string): Promise<string> {
165226
return this.driver.use().getUrl(key)
166227
}
228+
167229
/**
168230
* Return the signed/temporary URL to access the file
231+
*
232+
* @param key
233+
* @param options
234+
* @returns
169235
*/
170236
getSignedUrl (key: string, options?: SignedURLOptions): Promise<string> {
171237
return this.driver.use().getSignedUrl(key, options)
172238
}
239+
173240
/**
174241
* Return the signed/temporary URL that can be used to directly upload
175242
* the file contents to the storage.
243+
*
244+
* @param key
245+
* @param options
246+
* @returns
176247
*/
177248
getSignedUploadUrl (key: string, options?: SignedURLOptions): Promise<string> {
178249
return this.driver.use().getSignedUploadUrl(key, options)
179250
}
251+
180252
/**
181253
* Update the visibility of the file
254+
*
255+
* @param key
256+
* @param visibility
257+
* @returns
182258
*/
183259
setVisibility (key: string, visibility: ObjectVisibility): Promise<void> {
184260
return this.driver.use().setVisibility(key, visibility)
185261
}
262+
186263
/**
187264
* Write object to the destination with the provided
188265
* contents.
266+
*
267+
* @param key
268+
* @param contents
269+
* @param options
270+
* @returns
189271
*/
190272
put (key: string, contents: string | Uint8Array | FileLike, options?: WriteOptions): Promise<void> {
191273
if (!(contents instanceof Uint8Array) && typeof contents !== 'string') {
@@ -194,46 +276,78 @@ export class Storage implements DriverContract {
194276

195277
return this.driver.use().put(key, contents, options)
196278
}
279+
197280
/**
198281
* Write object to the destination with the provided
199282
* contents as a readable stream
283+
*
284+
* @param key
285+
* @param contents
286+
* @param options
287+
* @returns
200288
*/
201289
putStream (key: string, contents: Readable, options?: WriteOptions): Promise<void> {
202290
return this.driver.use().putStream(key, contents, options)
203291
}
292+
204293
/**
205294
* Copy the file from within the disk root location. Both
206295
* the "source" and "destination" will be the key names
207296
* and not absolute paths.
297+
*
298+
* @param source
299+
* @param destination
300+
* @param options
301+
* @returns
208302
*/
209303
copy (source: string, destination: string, options?: WriteOptions): Promise<void> {
210304
return this.driver.use().copy(source, destination, options)
211305
}
306+
212307
/**
213308
* Move the file from within the disk root location. Both
214309
* the "source" and "destination" will be the key names
215310
* and not absolute paths.
311+
*
312+
* @param source
313+
* @param destination
314+
* @param options
315+
* @returns
216316
*/
217317
move (source: string, destination: string, options?: WriteOptions): Promise<void> {
218318
return this.driver.use().move(source, destination, options)
219319
}
320+
220321
/**
221322
* Delete the file for the given key. Should not throw
222323
* error when file does not exist in first place
324+
*
325+
* @param key
326+
* @returns
223327
*/
224328
delete (key: string): Promise<void> {
225329
return this.driver.use().delete(key)
226330
}
331+
227332
/**
228333
* Delete the files and directories matching the provided prefix.
334+
*
335+
* @param prefix
336+
* @returns
229337
*/
230338
deleteAll (prefix: string): Promise<void> {
231339
return this.driver.use().deleteAll(prefix)
232340
}
341+
233342
/**
234343
* The list all method must return an array of objects with
235344
* the ability to paginate results (if supported).
345+
*
346+
* @param prefix
347+
* @param options
348+
* @returns
236349
*/
350+
237351
listAll (prefix: string, options?: {
238352
recursive?: boolean;
239353
paginationToken?: string;
@@ -243,15 +357,21 @@ export class Storage implements DriverContract {
243357
}> {
244358
return this.driver.use().listAll(prefix, options)
245359
}
360+
246361
/**
247362
* Switch bucket at runtime if supported.
363+
*
364+
* @param bucket
365+
* @returns
248366
*/
249367
bucket (bucket: string): DriverContract {
250368
return (this.driver.use() as any).bucket(bucket)
251369
}
252370

253371
/**
254372
* Create symbolic links for all configured links in the application configuration.
373+
*
374+
* @param param0
255375
*/
256376
static link ({ force = false }: { force?: boolean } = {}): void {
257377
for (const link in config('filesystem.links')) {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { FtpDriver } from '../src/FtpDriver'
4+
5+
const ftpDriver = new FtpDriver(process.env.FTP_CONNECTION || 'sftp://a.b.c.d:22')
6+
const testPath = process.env.FTP_TEST_PATH || '/'
7+
8+
describe('Filesystem FTP Driver', () => {
9+
it('should be configurable with an object', async () => {
10+
const config = {
11+
host: 'ftp.example.com',
12+
username: 'username',
13+
password: 'password',
14+
port: 22,
15+
verbose: false,
16+
}
17+
const ftpDriver = new FtpDriver(config)
18+
19+
expect(ftpDriver.getConfig()).toEqual(config)
20+
})
21+
22+
it('should be configurable with a string', async () => {
23+
const config = 'ftp://username:password@ftp.example.com'
24+
const ftpDriver = new FtpDriver(config)
25+
26+
expect(ftpDriver.getConfig()).toEqual({
27+
host: 'ftp.example.com',
28+
username: 'username',
29+
password: 'password',
30+
port: 22,
31+
verbose: false,
32+
})
33+
})
34+
35+
it.skip('should list files in a directory', async () => {
36+
console.log(testPath, await ftpDriver.listAll(testPath))
37+
})
38+
39+
it.skip('should exist', async () => {
40+
expect(await ftpDriver.exists(testPath + '/test.txt')).toBe(true)
41+
})
42+
43+
it.skip('should get stream from FTP server', async () => {
44+
const stream = await ftpDriver.getStream(testPath + '/test.txt')
45+
const chunks: Uint8Array[] = []
46+
for await (const chunk of stream) {
47+
chunks.push(chunk)
48+
}
49+
const content = Buffer.concat(chunks).toString('utf-8')
50+
expect(content).toBe('Hello World\n')
51+
})
52+
53+
it.skip('should get file from FTP server', async () => {
54+
const content = await ftpDriver.get(testPath + '/test.txt')
55+
expect(content).toBe('Hello World\n')
56+
})
57+
58+
it.skip('should get bytes from FTP server', async () => {
59+
const content = await ftpDriver.getBytes(testPath + '/test.txt')
60+
expect(Buffer.from(content).toString('utf-8')).toBe('Hello World\n')
61+
})
62+
}, 10000 /* increase timeout for FTP operations */)

0 commit comments

Comments
 (0)