@mantiq/filesystem 0.0.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/README.md +19 -0
- package/package.json +83 -0
- package/src/FilesystemManager.ts +177 -0
- package/src/FilesystemServiceProvider.ts +30 -0
- package/src/contracts/FilesystemConfig.ts +60 -0
- package/src/contracts/FilesystemDriver.ts +45 -0
- package/src/drivers/AzureBlobDriver.ts +377 -0
- package/src/drivers/FTPDriver.ts +299 -0
- package/src/drivers/GCSDriver.ts +311 -0
- package/src/drivers/LocalDriver.ts +281 -0
- package/src/drivers/NullDriver.ts +35 -0
- package/src/drivers/S3Driver.ts +449 -0
- package/src/drivers/SFTPDriver.ts +306 -0
- package/src/errors/FileExistsError.ts +7 -0
- package/src/errors/FileNotFoundError.ts +7 -0
- package/src/errors/FilesystemError.ts +7 -0
- package/src/helpers/mime.ts +61 -0
- package/src/helpers/storage.ts +13 -0
- package/src/index.ts +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @mantiq/filesystem
|
|
2
|
+
|
|
3
|
+
Filesystem abstraction for MantiqJS — Local, S3, GCS, Azure Blob, FTP, and SFTP drivers.
|
|
4
|
+
|
|
5
|
+
Part of [MantiqJS](https://github.com/abdullahkhan/mantiq) — a batteries-included TypeScript web framework for Bun.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add @mantiq/filesystem
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Documentation
|
|
14
|
+
|
|
15
|
+
See the [MantiqJS repository](https://github.com/abdullahkhan/mantiq) for full documentation.
|
|
16
|
+
|
|
17
|
+
## License
|
|
18
|
+
|
|
19
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mantiq/filesystem",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Filesystem abstraction — local, S3, GCS, R2, Azure, FTP, SFTP drivers with uniform API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Abdullah Khan",
|
|
8
|
+
"homepage": "https://github.com/abdullahkhan/mantiq/tree/main/packages/filesystem",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/abdullahkhan/mantiq.git",
|
|
12
|
+
"directory": "packages/filesystem"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/abdullahkhan/mantiq/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mantiq",
|
|
19
|
+
"mantiqjs",
|
|
20
|
+
"bun",
|
|
21
|
+
"typescript",
|
|
22
|
+
"framework",
|
|
23
|
+
"filesystem"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"bun": ">=1.1.0"
|
|
27
|
+
},
|
|
28
|
+
"main": "./src/index.ts",
|
|
29
|
+
"types": "./src/index.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"bun": "./src/index.ts",
|
|
33
|
+
"default": "./src/index.ts"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"src/",
|
|
38
|
+
"package.json",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target bun",
|
|
44
|
+
"test": "bun test",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"clean": "rm -rf dist"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@mantiq/core": "workspace:*"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"bun-types": "latest",
|
|
53
|
+
"typescript": "^5.7.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@aws-sdk/client-s3": "^3.0.0",
|
|
57
|
+
"@aws-sdk/s3-request-presigner": "^3.0.0",
|
|
58
|
+
"@google-cloud/storage": "^7.0.0",
|
|
59
|
+
"@azure/storage-blob": "^12.0.0",
|
|
60
|
+
"basic-ftp": "^5.0.0",
|
|
61
|
+
"ssh2-sftp-client": "^11.0.0"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"@aws-sdk/client-s3": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"@aws-sdk/s3-request-presigner": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"@google-cloud/storage": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@azure/storage-blob": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"basic-ftp": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"ssh2-sftp-client": {
|
|
80
|
+
"optional": true
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { DriverManager } from '@mantiq/core'
|
|
2
|
+
import type { FilesystemDriver, PutOptions } from './contracts/FilesystemDriver.ts'
|
|
3
|
+
import type { FilesystemConfig, DiskConfig } from './contracts/FilesystemConfig.ts'
|
|
4
|
+
import { LocalDriver } from './drivers/LocalDriver.ts'
|
|
5
|
+
import { NullDriver } from './drivers/NullDriver.ts'
|
|
6
|
+
import { S3Driver } from './drivers/S3Driver.ts'
|
|
7
|
+
import { GCSDriver } from './drivers/GCSDriver.ts'
|
|
8
|
+
import { AzureBlobDriver } from './drivers/AzureBlobDriver.ts'
|
|
9
|
+
import { FTPDriver } from './drivers/FTPDriver.ts'
|
|
10
|
+
import { SFTPDriver } from './drivers/SFTPDriver.ts'
|
|
11
|
+
|
|
12
|
+
export class FilesystemManager implements DriverManager<FilesystemDriver>, FilesystemDriver {
|
|
13
|
+
private readonly config: FilesystemConfig
|
|
14
|
+
private readonly disks = new Map<string, FilesystemDriver>()
|
|
15
|
+
private readonly customCreators = new Map<string, (config: DiskConfig) => FilesystemDriver>()
|
|
16
|
+
|
|
17
|
+
constructor(config?: Partial<FilesystemConfig>) {
|
|
18
|
+
this.config = {
|
|
19
|
+
default: config?.default ?? 'local',
|
|
20
|
+
disks: config?.disks ?? {},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── DriverManager ─────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
driver(name?: string): FilesystemDriver {
|
|
27
|
+
const diskName = name ?? this.getDefaultDriver()
|
|
28
|
+
|
|
29
|
+
if (!this.disks.has(diskName)) {
|
|
30
|
+
this.disks.set(diskName, this.createDriver(diskName))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return this.disks.get(diskName)!
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
disk(name?: string): FilesystemDriver {
|
|
37
|
+
return this.driver(name)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
extend(name: string, factory: (config: DiskConfig) => FilesystemDriver): void {
|
|
41
|
+
this.customCreators.set(name, factory)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getDefaultDriver(): string {
|
|
45
|
+
return this.config.default
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
forgetDisk(name: string): void {
|
|
49
|
+
this.disks.delete(name)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
forgetDisks(): void {
|
|
53
|
+
this.disks.clear()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── FilesystemDriver (delegates to default disk) ──────────────────────────
|
|
57
|
+
|
|
58
|
+
exists(path: string) { return this.driver().exists(path) }
|
|
59
|
+
get(path: string) { return this.driver().get(path) }
|
|
60
|
+
getBytes(path: string) { return this.driver().getBytes(path) }
|
|
61
|
+
stream(path: string) { return this.driver().stream(path) }
|
|
62
|
+
|
|
63
|
+
put(path: string, contents: string | Uint8Array, options?: PutOptions) { return this.driver().put(path, contents, options) }
|
|
64
|
+
putStream(path: string, stream: ReadableStream, options?: PutOptions) { return this.driver().putStream(path, stream, options) }
|
|
65
|
+
append(path: string, contents: string) { return this.driver().append(path, contents) }
|
|
66
|
+
prepend(path: string, contents: string) { return this.driver().prepend(path, contents) }
|
|
67
|
+
|
|
68
|
+
delete(path: string | string[]) { return this.driver().delete(path) }
|
|
69
|
+
copy(from: string, to: string) { return this.driver().copy(from, to) }
|
|
70
|
+
move(from: string, to: string) { return this.driver().move(from, to) }
|
|
71
|
+
|
|
72
|
+
size(path: string) { return this.driver().size(path) }
|
|
73
|
+
lastModified(path: string) { return this.driver().lastModified(path) }
|
|
74
|
+
mimeType(path: string) { return this.driver().mimeType(path) }
|
|
75
|
+
path(filePath: string) { return this.driver().path(filePath) }
|
|
76
|
+
|
|
77
|
+
url(path: string) { return this.driver().url(path) }
|
|
78
|
+
temporaryUrl(path: string, expiration: number, options?: Record<string, any>) { return this.driver().temporaryUrl(path, expiration, options) }
|
|
79
|
+
|
|
80
|
+
files(directory?: string) { return this.driver().files(directory) }
|
|
81
|
+
allFiles(directory?: string) { return this.driver().allFiles(directory) }
|
|
82
|
+
directories(directory?: string) { return this.driver().directories(directory) }
|
|
83
|
+
allDirectories(directory?: string) { return this.driver().allDirectories(directory) }
|
|
84
|
+
makeDirectory(path: string) { return this.driver().makeDirectory(path) }
|
|
85
|
+
deleteDirectory(directory: string) { return this.driver().deleteDirectory(directory) }
|
|
86
|
+
|
|
87
|
+
setVisibility(path: string, visibility: 'public' | 'private') { return this.driver().setVisibility(path, visibility) }
|
|
88
|
+
getVisibility(path: string) { return this.driver().getVisibility(path) }
|
|
89
|
+
|
|
90
|
+
// ── Internal ──────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
private createDriver(name: string): FilesystemDriver {
|
|
93
|
+
const diskConfig = this.config.disks[name]
|
|
94
|
+
const driverName = diskConfig?.driver ?? name
|
|
95
|
+
|
|
96
|
+
const custom = this.customCreators.get(driverName)
|
|
97
|
+
if (custom) return custom(diskConfig ?? { driver: driverName })
|
|
98
|
+
|
|
99
|
+
if (!diskConfig) {
|
|
100
|
+
throw new Error(`Disk "${name}" is not configured. Define it in config/filesystem.ts or use extend().`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
switch (driverName) {
|
|
104
|
+
case 'local':
|
|
105
|
+
return new LocalDriver(
|
|
106
|
+
diskConfig.root ?? '/tmp/mantiq-storage',
|
|
107
|
+
diskConfig.url as string | undefined,
|
|
108
|
+
(diskConfig.visibility as 'public' | 'private') ?? 'public',
|
|
109
|
+
)
|
|
110
|
+
case 'null':
|
|
111
|
+
return new NullDriver()
|
|
112
|
+
case 's3':
|
|
113
|
+
case 'r2':
|
|
114
|
+
case 'spaces':
|
|
115
|
+
case 'minio':
|
|
116
|
+
return new S3Driver({
|
|
117
|
+
bucket: diskConfig.bucket as string,
|
|
118
|
+
region: diskConfig.region as string | undefined,
|
|
119
|
+
key: diskConfig.key as string | undefined,
|
|
120
|
+
secret: diskConfig.secret as string | undefined,
|
|
121
|
+
token: diskConfig.token as string | undefined,
|
|
122
|
+
endpoint: diskConfig.endpoint as string | undefined,
|
|
123
|
+
forcePathStyle: diskConfig.forcePathStyle as boolean | undefined,
|
|
124
|
+
root: diskConfig.root,
|
|
125
|
+
url: diskConfig.url,
|
|
126
|
+
visibility: diskConfig.visibility,
|
|
127
|
+
})
|
|
128
|
+
case 'gcs':
|
|
129
|
+
return new GCSDriver({
|
|
130
|
+
bucket: diskConfig.bucket as string,
|
|
131
|
+
projectId: diskConfig.projectId as string | undefined,
|
|
132
|
+
keyFilename: diskConfig.keyFilename as string | undefined,
|
|
133
|
+
credentials: diskConfig.credentials as Record<string, any> | undefined,
|
|
134
|
+
root: diskConfig.root,
|
|
135
|
+
url: diskConfig.url,
|
|
136
|
+
visibility: diskConfig.visibility,
|
|
137
|
+
})
|
|
138
|
+
case 'azure':
|
|
139
|
+
return new AzureBlobDriver({
|
|
140
|
+
container: diskConfig.container as string,
|
|
141
|
+
connectionString: diskConfig.connectionString as string | undefined,
|
|
142
|
+
accountName: diskConfig.accountName as string | undefined,
|
|
143
|
+
accountKey: diskConfig.accountKey as string | undefined,
|
|
144
|
+
sasToken: diskConfig.sasToken as string | undefined,
|
|
145
|
+
root: diskConfig.root,
|
|
146
|
+
url: diskConfig.url,
|
|
147
|
+
visibility: diskConfig.visibility,
|
|
148
|
+
})
|
|
149
|
+
case 'ftp':
|
|
150
|
+
return new FTPDriver({
|
|
151
|
+
host: diskConfig.host as string,
|
|
152
|
+
port: diskConfig.port as number | undefined,
|
|
153
|
+
username: diskConfig.username as string | undefined,
|
|
154
|
+
password: diskConfig.password as string | undefined,
|
|
155
|
+
secure: diskConfig.secure as boolean | 'implicit' | undefined,
|
|
156
|
+
timeout: diskConfig.timeout as number | undefined,
|
|
157
|
+
root: diskConfig.root,
|
|
158
|
+
url: diskConfig.url,
|
|
159
|
+
visibility: diskConfig.visibility,
|
|
160
|
+
})
|
|
161
|
+
case 'sftp':
|
|
162
|
+
return new SFTPDriver({
|
|
163
|
+
host: diskConfig.host as string,
|
|
164
|
+
port: diskConfig.port as number | undefined,
|
|
165
|
+
username: diskConfig.username as string | undefined,
|
|
166
|
+
password: diskConfig.password as string | undefined,
|
|
167
|
+
privateKey: diskConfig.privateKey as string | undefined,
|
|
168
|
+
passphrase: diskConfig.passphrase as string | undefined,
|
|
169
|
+
root: diskConfig.root,
|
|
170
|
+
url: diskConfig.url,
|
|
171
|
+
visibility: diskConfig.visibility,
|
|
172
|
+
})
|
|
173
|
+
default:
|
|
174
|
+
throw new Error(`Unsupported filesystem driver: "${driverName}". Use extend() to register custom drivers.`)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ServiceProvider, ConfigRepository, Application } from '@mantiq/core'
|
|
2
|
+
import { FilesystemManager } from './FilesystemManager.ts'
|
|
3
|
+
import { FILESYSTEM_MANAGER } from './helpers/storage.ts'
|
|
4
|
+
import type { FilesystemConfig } from './contracts/FilesystemConfig.ts'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CONFIG: FilesystemConfig = {
|
|
7
|
+
default: 'local',
|
|
8
|
+
disks: {
|
|
9
|
+
local: { driver: 'local', root: 'storage/app' },
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class FilesystemServiceProvider extends ServiceProvider {
|
|
14
|
+
override register(): void {
|
|
15
|
+
this.app.singleton(FilesystemManager, (c) => {
|
|
16
|
+
const config = c.make(ConfigRepository).get<FilesystemConfig>('filesystem', DEFAULT_CONFIG)
|
|
17
|
+
const app = c.make(Application)
|
|
18
|
+
|
|
19
|
+
// Normalize relative root paths to absolute using the app base path
|
|
20
|
+
for (const disk of Object.values(config.disks)) {
|
|
21
|
+
if (disk.root && !disk.root.startsWith('/')) {
|
|
22
|
+
disk.root = app.basePath_(disk.root)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return new FilesystemManager(config)
|
|
27
|
+
})
|
|
28
|
+
this.app.alias(FilesystemManager, FILESYSTEM_MANAGER)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface DiskConfig {
|
|
2
|
+
driver: string
|
|
3
|
+
root?: string
|
|
4
|
+
url?: string
|
|
5
|
+
visibility?: 'public' | 'private'
|
|
6
|
+
[key: string]: unknown
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface S3DiskConfig extends DiskConfig {
|
|
10
|
+
driver: 's3'
|
|
11
|
+
bucket: string
|
|
12
|
+
region?: string
|
|
13
|
+
key?: string
|
|
14
|
+
secret?: string
|
|
15
|
+
token?: string
|
|
16
|
+
endpoint?: string
|
|
17
|
+
forcePathStyle?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface GCSDiskConfig extends DiskConfig {
|
|
21
|
+
driver: 'gcs'
|
|
22
|
+
bucket: string
|
|
23
|
+
projectId?: string
|
|
24
|
+
keyFilename?: string
|
|
25
|
+
credentials?: Record<string, any>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AzureDiskConfig extends DiskConfig {
|
|
29
|
+
driver: 'azure'
|
|
30
|
+
container: string
|
|
31
|
+
connectionString?: string
|
|
32
|
+
accountName?: string
|
|
33
|
+
accountKey?: string
|
|
34
|
+
sasToken?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface FTPDiskConfig extends DiskConfig {
|
|
38
|
+
driver: 'ftp'
|
|
39
|
+
host: string
|
|
40
|
+
port?: number
|
|
41
|
+
username?: string
|
|
42
|
+
password?: string
|
|
43
|
+
secure?: boolean | 'implicit'
|
|
44
|
+
timeout?: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SFTPDiskConfig extends DiskConfig {
|
|
48
|
+
driver: 'sftp'
|
|
49
|
+
host: string
|
|
50
|
+
port?: number
|
|
51
|
+
username?: string
|
|
52
|
+
password?: string
|
|
53
|
+
privateKey?: string
|
|
54
|
+
passphrase?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface FilesystemConfig {
|
|
58
|
+
default: string
|
|
59
|
+
disks: Record<string, DiskConfig>
|
|
60
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface PutOptions {
|
|
2
|
+
visibility?: 'public' | 'private'
|
|
3
|
+
mimeType?: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface FilesystemDriver {
|
|
7
|
+
// ── Reads ───────────────────────────────────────────────────────────────────
|
|
8
|
+
exists(path: string): Promise<boolean>
|
|
9
|
+
get(path: string): Promise<string | null>
|
|
10
|
+
getBytes(path: string): Promise<Uint8Array | null>
|
|
11
|
+
stream(path: string): Promise<ReadableStream | null>
|
|
12
|
+
|
|
13
|
+
// ── Writes ──────────────────────────────────────────────────────────────────
|
|
14
|
+
put(path: string, contents: string | Uint8Array, options?: PutOptions): Promise<void>
|
|
15
|
+
putStream(path: string, stream: ReadableStream, options?: PutOptions): Promise<void>
|
|
16
|
+
append(path: string, contents: string): Promise<void>
|
|
17
|
+
prepend(path: string, contents: string): Promise<void>
|
|
18
|
+
|
|
19
|
+
// ── Operations ──────────────────────────────────────────────────────────────
|
|
20
|
+
delete(path: string | string[]): Promise<boolean>
|
|
21
|
+
copy(from: string, to: string): Promise<void>
|
|
22
|
+
move(from: string, to: string): Promise<void>
|
|
23
|
+
|
|
24
|
+
// ── Metadata ────────────────────────────────────────────────────────────────
|
|
25
|
+
size(path: string): Promise<number>
|
|
26
|
+
lastModified(path: string): Promise<number>
|
|
27
|
+
mimeType(path: string): Promise<string | null>
|
|
28
|
+
path(filePath: string): string
|
|
29
|
+
|
|
30
|
+
// ── URLs ────────────────────────────────────────────────────────────────────
|
|
31
|
+
url(path: string): string
|
|
32
|
+
temporaryUrl(path: string, expiration: number, options?: Record<string, any>): Promise<string>
|
|
33
|
+
|
|
34
|
+
// ── Directories ─────────────────────────────────────────────────────────────
|
|
35
|
+
files(directory?: string): Promise<string[]>
|
|
36
|
+
allFiles(directory?: string): Promise<string[]>
|
|
37
|
+
directories(directory?: string): Promise<string[]>
|
|
38
|
+
allDirectories(directory?: string): Promise<string[]>
|
|
39
|
+
makeDirectory(path: string): Promise<void>
|
|
40
|
+
deleteDirectory(directory: string): Promise<boolean>
|
|
41
|
+
|
|
42
|
+
// ── Visibility ──────────────────────────────────────────────────────────────
|
|
43
|
+
setVisibility(path: string, visibility: 'public' | 'private'): Promise<void>
|
|
44
|
+
getVisibility(path: string): Promise<string>
|
|
45
|
+
}
|