@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
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import type { FilesystemDriver, PutOptions } from '../contracts/FilesystemDriver.ts'
|
|
2
|
+
import { FilesystemError } from '../errors/FilesystemError.ts'
|
|
3
|
+
import { FileNotFoundError } from '../errors/FileNotFoundError.ts'
|
|
4
|
+
import { guessMimeType } from '../helpers/mime.ts'
|
|
5
|
+
|
|
6
|
+
export interface SFTPConfig {
|
|
7
|
+
host: string
|
|
8
|
+
port?: number
|
|
9
|
+
username?: string
|
|
10
|
+
password?: string
|
|
11
|
+
privateKey?: string | Buffer
|
|
12
|
+
passphrase?: string
|
|
13
|
+
root?: string
|
|
14
|
+
url?: string
|
|
15
|
+
visibility?: 'public' | 'private'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* SFTP (SSH File Transfer Protocol) filesystem driver.
|
|
20
|
+
*
|
|
21
|
+
* Requires: bun add ssh2-sftp-client
|
|
22
|
+
*/
|
|
23
|
+
export class SFTPDriver implements FilesystemDriver {
|
|
24
|
+
private _client: any = null
|
|
25
|
+
private _connected = false
|
|
26
|
+
private readonly root: string
|
|
27
|
+
private readonly urlBase: string | undefined
|
|
28
|
+
private readonly _config: SFTPConfig
|
|
29
|
+
|
|
30
|
+
constructor(config: SFTPConfig) {
|
|
31
|
+
this._config = config
|
|
32
|
+
this.root = (config.root ?? '/').replace(/\/+$/, '')
|
|
33
|
+
this.urlBase = config.url
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async client(): Promise<any> {
|
|
37
|
+
if (!this._client) {
|
|
38
|
+
let SftpClient: any
|
|
39
|
+
try {
|
|
40
|
+
const mod = await import('ssh2-sftp-client')
|
|
41
|
+
SftpClient = mod.default ?? mod
|
|
42
|
+
} catch {
|
|
43
|
+
throw new FilesystemError(
|
|
44
|
+
'The ssh2-sftp-client package is required for the SFTP driver. Install it with: bun add ssh2-sftp-client',
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
this._client = new SftpClient()
|
|
48
|
+
this._connected = false
|
|
49
|
+
}
|
|
50
|
+
if (!this._connected) {
|
|
51
|
+
const opts: any = {
|
|
52
|
+
host: this._config.host,
|
|
53
|
+
port: this._config.port ?? 22,
|
|
54
|
+
username: this._config.username,
|
|
55
|
+
}
|
|
56
|
+
if (this._config.password) opts.password = this._config.password
|
|
57
|
+
if (this._config.privateKey) opts.privateKey = this._config.privateKey
|
|
58
|
+
if (this._config.passphrase) opts.passphrase = this._config.passphrase
|
|
59
|
+
await this._client.connect(opts)
|
|
60
|
+
this._connected = true
|
|
61
|
+
}
|
|
62
|
+
return this._client
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private fullPath(path: string): string {
|
|
66
|
+
return `${this.root}/${path.replace(/^\/+/, '')}`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Disconnect from the SFTP server. */
|
|
70
|
+
async disconnect(): Promise<void> {
|
|
71
|
+
if (this._client && this._connected) {
|
|
72
|
+
await this._client.end()
|
|
73
|
+
this._connected = false
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Reads ─────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
async exists(path: string): Promise<boolean> {
|
|
80
|
+
const sftp = await this.client()
|
|
81
|
+
const result = await sftp.exists(this.fullPath(path))
|
|
82
|
+
return result !== false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async get(path: string): Promise<string | null> {
|
|
86
|
+
const sftp = await this.client()
|
|
87
|
+
try {
|
|
88
|
+
const buffer = await sftp.get(this.fullPath(path))
|
|
89
|
+
if (Buffer.isBuffer(buffer)) return buffer.toString('utf-8')
|
|
90
|
+
if (typeof buffer === 'string') return buffer
|
|
91
|
+
return null
|
|
92
|
+
} catch {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async getBytes(path: string): Promise<Uint8Array | null> {
|
|
98
|
+
const sftp = await this.client()
|
|
99
|
+
try {
|
|
100
|
+
const buffer = await sftp.get(this.fullPath(path))
|
|
101
|
+
if (Buffer.isBuffer(buffer)) return new Uint8Array(buffer)
|
|
102
|
+
if (typeof buffer === 'string') return new TextEncoder().encode(buffer)
|
|
103
|
+
return null
|
|
104
|
+
} catch {
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async stream(path: string): Promise<ReadableStream | null> {
|
|
110
|
+
if (!(await this.exists(path))) return null
|
|
111
|
+
const bytes = await this.getBytes(path)
|
|
112
|
+
if (!bytes) return null
|
|
113
|
+
return new ReadableStream({
|
|
114
|
+
start(controller) {
|
|
115
|
+
controller.enqueue(bytes)
|
|
116
|
+
controller.close()
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Writes ────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
async put(path: string, contents: string | Uint8Array, options?: PutOptions): Promise<void> {
|
|
124
|
+
const sftp = await this.client()
|
|
125
|
+
const fp = this.fullPath(path)
|
|
126
|
+
// Ensure parent directory exists
|
|
127
|
+
const dir = fp.split('/').slice(0, -1).join('/')
|
|
128
|
+
if (dir) await sftp.mkdir(dir, true)
|
|
129
|
+
const buffer = typeof contents === 'string' ? Buffer.from(contents, 'utf-8') : Buffer.from(contents)
|
|
130
|
+
await sftp.put(buffer, fp)
|
|
131
|
+
|
|
132
|
+
const visibility = options?.visibility ?? this._config.visibility
|
|
133
|
+
if (visibility) {
|
|
134
|
+
const mode = visibility === 'public' ? 0o644 : 0o600
|
|
135
|
+
await sftp.chmod(fp, mode)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async putStream(path: string, stream: ReadableStream, options?: PutOptions): Promise<void> {
|
|
140
|
+
const body = new Uint8Array(await new Response(stream).arrayBuffer())
|
|
141
|
+
await this.put(path, body, options)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async append(path: string, contents: string): Promise<void> {
|
|
145
|
+
const sftp = await this.client()
|
|
146
|
+
const fp = this.fullPath(path)
|
|
147
|
+
await sftp.append(Buffer.from(contents, 'utf-8'), fp)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async prepend(path: string, contents: string): Promise<void> {
|
|
151
|
+
const existing = (await this.get(path)) ?? ''
|
|
152
|
+
await this.put(path, contents + existing)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Operations ────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
async delete(path: string | string[]): Promise<boolean> {
|
|
158
|
+
const paths = Array.isArray(path) ? path : [path]
|
|
159
|
+
const sftp = await this.client()
|
|
160
|
+
try {
|
|
161
|
+
for (const p of paths) {
|
|
162
|
+
await sftp.delete(this.fullPath(p))
|
|
163
|
+
}
|
|
164
|
+
return true
|
|
165
|
+
} catch {
|
|
166
|
+
return false
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async copy(from: string, to: string): Promise<void> {
|
|
171
|
+
// SFTP doesn't support server-side copy — download and re-upload
|
|
172
|
+
const bytes = await this.getBytes(from)
|
|
173
|
+
if (!bytes) throw new FileNotFoundError(from)
|
|
174
|
+
await this.put(to, bytes)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async move(from: string, to: string): Promise<void> {
|
|
178
|
+
const sftp = await this.client()
|
|
179
|
+
await sftp.rename(this.fullPath(from), this.fullPath(to))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Metadata ──────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
async size(path: string): Promise<number> {
|
|
185
|
+
const sftp = await this.client()
|
|
186
|
+
try {
|
|
187
|
+
const stats = await sftp.stat(this.fullPath(path))
|
|
188
|
+
return stats.size
|
|
189
|
+
} catch {
|
|
190
|
+
throw new FileNotFoundError(path)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async lastModified(path: string): Promise<number> {
|
|
195
|
+
const sftp = await this.client()
|
|
196
|
+
try {
|
|
197
|
+
const stats = await sftp.stat(this.fullPath(path))
|
|
198
|
+
return stats.modifyTime
|
|
199
|
+
} catch {
|
|
200
|
+
throw new FileNotFoundError(path)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async mimeType(path: string): Promise<string | null> {
|
|
205
|
+
return guessMimeType(path) ?? null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
path(filePath: string): string {
|
|
209
|
+
return this.fullPath(filePath)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── URLs ──────────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
url(path: string): string {
|
|
215
|
+
if (!this.urlBase) {
|
|
216
|
+
throw new FilesystemError('URL generation is not supported — no url configured for this SFTP disk.', { path })
|
|
217
|
+
}
|
|
218
|
+
return `${this.urlBase.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async temporaryUrl(_path: string, _expiration: number, _options?: Record<string, any>): Promise<string> {
|
|
222
|
+
throw new FilesystemError('Temporary URLs are not supported by the SFTP driver.')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Directories ───────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
async files(directory: string = ''): Promise<string[]> {
|
|
228
|
+
const sftp = await this.client()
|
|
229
|
+
try {
|
|
230
|
+
const entries = await sftp.list(this.fullPath(directory))
|
|
231
|
+
return entries
|
|
232
|
+
.filter((e: any) => e.type === '-') // regular file
|
|
233
|
+
.map((e: any) => directory ? `${directory}/${e.name}` : e.name)
|
|
234
|
+
} catch {
|
|
235
|
+
return []
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async allFiles(directory: string = ''): Promise<string[]> {
|
|
240
|
+
const results: string[] = []
|
|
241
|
+
const entries = await this.files(directory)
|
|
242
|
+
results.push(...entries)
|
|
243
|
+
|
|
244
|
+
const dirs = await this.directories(directory)
|
|
245
|
+
for (const dir of dirs) {
|
|
246
|
+
results.push(...(await this.allFiles(dir)))
|
|
247
|
+
}
|
|
248
|
+
return results.sort()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async directories(directory: string = ''): Promise<string[]> {
|
|
252
|
+
const sftp = await this.client()
|
|
253
|
+
try {
|
|
254
|
+
const entries = await sftp.list(this.fullPath(directory))
|
|
255
|
+
return entries
|
|
256
|
+
.filter((e: any) => e.type === 'd') // directory
|
|
257
|
+
.map((e: any) => directory ? `${directory}/${e.name}` : e.name)
|
|
258
|
+
} catch {
|
|
259
|
+
return []
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async allDirectories(directory: string = ''): Promise<string[]> {
|
|
264
|
+
const results: string[] = []
|
|
265
|
+
const dirs = await this.directories(directory)
|
|
266
|
+
for (const dir of dirs) {
|
|
267
|
+
results.push(dir)
|
|
268
|
+
results.push(...(await this.allDirectories(dir)))
|
|
269
|
+
}
|
|
270
|
+
return results.sort()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async makeDirectory(path: string): Promise<void> {
|
|
274
|
+
const sftp = await this.client()
|
|
275
|
+
await sftp.mkdir(this.fullPath(path), true)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async deleteDirectory(directory: string): Promise<boolean> {
|
|
279
|
+
const sftp = await this.client()
|
|
280
|
+
try {
|
|
281
|
+
await sftp.rmdir(this.fullPath(directory), true)
|
|
282
|
+
return true
|
|
283
|
+
} catch {
|
|
284
|
+
return false
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Visibility ────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
async setVisibility(path: string, visibility: 'public' | 'private'): Promise<void> {
|
|
291
|
+
const sftp = await this.client()
|
|
292
|
+
const mode = visibility === 'public' ? 0o644 : 0o600
|
|
293
|
+
await sftp.chmod(this.fullPath(path), mode)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async getVisibility(path: string): Promise<string> {
|
|
297
|
+
const sftp = await this.client()
|
|
298
|
+
try {
|
|
299
|
+
const stats = await sftp.stat(this.fullPath(path))
|
|
300
|
+
// Check if others have read permission
|
|
301
|
+
return (stats.permissions & 0o004) ? 'public' : 'private'
|
|
302
|
+
} catch {
|
|
303
|
+
return this._config.visibility ?? 'private'
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const EXT_MAP: Record<string, string> = {
|
|
2
|
+
'.html': 'text/html',
|
|
3
|
+
'.htm': 'text/html',
|
|
4
|
+
'.css': 'text/css',
|
|
5
|
+
'.js': 'application/javascript',
|
|
6
|
+
'.mjs': 'application/javascript',
|
|
7
|
+
'.json': 'application/json',
|
|
8
|
+
'.xml': 'application/xml',
|
|
9
|
+
'.csv': 'text/csv',
|
|
10
|
+
'.txt': 'text/plain',
|
|
11
|
+
'.md': 'text/markdown',
|
|
12
|
+
'.yaml': 'application/x-yaml',
|
|
13
|
+
'.yml': 'application/x-yaml',
|
|
14
|
+
'.toml': 'application/toml',
|
|
15
|
+
'.png': 'image/png',
|
|
16
|
+
'.jpg': 'image/jpeg',
|
|
17
|
+
'.jpeg': 'image/jpeg',
|
|
18
|
+
'.gif': 'image/gif',
|
|
19
|
+
'.webp': 'image/webp',
|
|
20
|
+
'.avif': 'image/avif',
|
|
21
|
+
'.svg': 'image/svg+xml',
|
|
22
|
+
'.ico': 'image/x-icon',
|
|
23
|
+
'.bmp': 'image/bmp',
|
|
24
|
+
'.tiff': 'image/tiff',
|
|
25
|
+
'.tif': 'image/tiff',
|
|
26
|
+
'.pdf': 'application/pdf',
|
|
27
|
+
'.zip': 'application/zip',
|
|
28
|
+
'.gz': 'application/gzip',
|
|
29
|
+
'.tar': 'application/x-tar',
|
|
30
|
+
'.br': 'application/x-brotli',
|
|
31
|
+
'.7z': 'application/x-7z-compressed',
|
|
32
|
+
'.rar': 'application/vnd.rar',
|
|
33
|
+
'.mp3': 'audio/mpeg',
|
|
34
|
+
'.ogg': 'audio/ogg',
|
|
35
|
+
'.wav': 'audio/wav',
|
|
36
|
+
'.flac': 'audio/flac',
|
|
37
|
+
'.aac': 'audio/aac',
|
|
38
|
+
'.mp4': 'video/mp4',
|
|
39
|
+
'.webm': 'video/webm',
|
|
40
|
+
'.mov': 'video/quicktime',
|
|
41
|
+
'.avi': 'video/x-msvideo',
|
|
42
|
+
'.mkv': 'video/x-matroska',
|
|
43
|
+
'.woff': 'font/woff',
|
|
44
|
+
'.woff2': 'font/woff2',
|
|
45
|
+
'.ttf': 'font/ttf',
|
|
46
|
+
'.otf': 'font/otf',
|
|
47
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
48
|
+
'.wasm': 'application/wasm',
|
|
49
|
+
'.doc': 'application/msword',
|
|
50
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
51
|
+
'.xls': 'application/vnd.ms-excel',
|
|
52
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
53
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
54
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function guessMimeType(path: string): string | undefined {
|
|
58
|
+
const dot = path.lastIndexOf('.')
|
|
59
|
+
if (dot === -1) return undefined
|
|
60
|
+
return EXT_MAP[path.slice(dot).toLowerCase()]
|
|
61
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Application } from '@mantiq/core'
|
|
2
|
+
import type { FilesystemManager } from '../FilesystemManager.ts'
|
|
3
|
+
import type { FilesystemDriver } from '../contracts/FilesystemDriver.ts'
|
|
4
|
+
|
|
5
|
+
export const FILESYSTEM_MANAGER = Symbol('FilesystemManager')
|
|
6
|
+
|
|
7
|
+
export function storage(): FilesystemManager
|
|
8
|
+
export function storage(disk: string): FilesystemDriver
|
|
9
|
+
export function storage(disk?: string): FilesystemManager | FilesystemDriver {
|
|
10
|
+
const manager = Application.getInstance().make<FilesystemManager>(FILESYSTEM_MANAGER)
|
|
11
|
+
if (disk === undefined) return manager
|
|
12
|
+
return manager.disk(disk)
|
|
13
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// ── Contracts ─────────────────────────────────────────────────────────────────
|
|
2
|
+
export type { FilesystemDriver, PutOptions } from './contracts/FilesystemDriver.ts'
|
|
3
|
+
export type {
|
|
4
|
+
FilesystemConfig,
|
|
5
|
+
DiskConfig,
|
|
6
|
+
S3DiskConfig,
|
|
7
|
+
GCSDiskConfig,
|
|
8
|
+
AzureDiskConfig,
|
|
9
|
+
FTPDiskConfig,
|
|
10
|
+
SFTPDiskConfig,
|
|
11
|
+
} from './contracts/FilesystemConfig.ts'
|
|
12
|
+
|
|
13
|
+
// ── Core ──────────────────────────────────────────────────────────────────────
|
|
14
|
+
export { FilesystemManager } from './FilesystemManager.ts'
|
|
15
|
+
export { FilesystemServiceProvider } from './FilesystemServiceProvider.ts'
|
|
16
|
+
|
|
17
|
+
// ── Drivers ───────────────────────────────────────────────────────────────────
|
|
18
|
+
export { LocalDriver } from './drivers/LocalDriver.ts'
|
|
19
|
+
export { NullDriver } from './drivers/NullDriver.ts'
|
|
20
|
+
export { S3Driver } from './drivers/S3Driver.ts'
|
|
21
|
+
export type { S3Config } from './drivers/S3Driver.ts'
|
|
22
|
+
export { GCSDriver } from './drivers/GCSDriver.ts'
|
|
23
|
+
export type { GCSConfig } from './drivers/GCSDriver.ts'
|
|
24
|
+
export { AzureBlobDriver } from './drivers/AzureBlobDriver.ts'
|
|
25
|
+
export type { AzureConfig } from './drivers/AzureBlobDriver.ts'
|
|
26
|
+
export { FTPDriver } from './drivers/FTPDriver.ts'
|
|
27
|
+
export type { FTPConfig } from './drivers/FTPDriver.ts'
|
|
28
|
+
export { SFTPDriver } from './drivers/SFTPDriver.ts'
|
|
29
|
+
export type { SFTPConfig } from './drivers/SFTPDriver.ts'
|
|
30
|
+
|
|
31
|
+
// ── Errors ────────────────────────────────────────────────────────────────────
|
|
32
|
+
export { FilesystemError } from './errors/FilesystemError.ts'
|
|
33
|
+
export { FileNotFoundError } from './errors/FileNotFoundError.ts'
|
|
34
|
+
export { FileExistsError } from './errors/FileExistsError.ts'
|
|
35
|
+
|
|
36
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
37
|
+
export { storage, FILESYSTEM_MANAGER } from './helpers/storage.ts'
|
|
38
|
+
export { guessMimeType } from './helpers/mime.ts'
|