-
-
Save dadamssg/1ef36059fb44834b163f67141c13ee6d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { type LazyContent, LazyFile } from "@mjackson/lazy-file"; | |
import type { FileStorage } from "@mjackson/file-storage"; | |
import Client, { type ConnectOptions } from "ssh2-sftp-client"; | |
import path from "path"; | |
import { Readable } from "stream"; | |
import mime from "mime-types"; | |
import { finished } from "stream/promises"; | |
/** | |
* An SFTP implementation of the `FileStorage` interface. | |
*/ | |
export class SftpFileStorage implements FileStorage { | |
#connectOptions: ConnectOptions; | |
#folder: string; | |
constructor(connectOptions: ConnectOptions, folder: string) { | |
this.#connectOptions = connectOptions; | |
this.#folder = folder; | |
} | |
async has(key: string): Promise<boolean> { | |
const info = await this.exec((client) => { | |
return client.exists(path.resolve(this.#folder, key)); | |
}); | |
return !!info; | |
} | |
async set(key: string, file: File): Promise<void> { | |
await this.exec(async (client) => { | |
const writeStream = client.createWriteStream( | |
path.resolve(this.#folder, key), | |
); | |
// @ts-ignore | |
const contents = Readable.fromWeb(file.stream()); | |
await finished(contents.pipe(writeStream)); | |
}); | |
} | |
async get(key: string): Promise<LazyFile | null> { | |
const stat = await this.exec((client) => { | |
return client.stat(path.resolve(this.#folder, key)); | |
}); | |
const exec = this.exec.bind(this); | |
const folder = this.#folder; | |
const lazyContent: LazyContent = { | |
byteLength: stat.size, | |
stream(start, end) { | |
return new ReadableStream({ | |
async pull(controller) { | |
await exec(async (client) => { | |
const sftpStream = client.createReadStream( | |
path.resolve(folder, key), | |
{ | |
start, | |
end, | |
}, | |
); | |
const reader = Readable.toWeb(sftpStream).getReader(); | |
let done = false; | |
while (!done) { | |
const { done: isDone, value } = await reader.read(); | |
done = isDone; | |
if (done) { | |
controller.close(); | |
} else { | |
controller.enqueue(value); | |
} | |
} | |
}); | |
}, | |
}); | |
}, | |
}; | |
const mimeType = mime.lookup(key) || "application/octet-stream"; | |
return new LazyFile(lazyContent, key, { type: mimeType }); | |
} | |
async remove(key: string): Promise<void> { | |
await this.exec(async (client) => { | |
await client.delete(path.resolve(this.#folder, key)); | |
}); | |
} | |
async exec<T>(cb: (sftp: Client) => T) { | |
const client = new Client(); | |
await client.connect(this.#connectOptions); | |
try { | |
return await cb(client); | |
} finally { | |
await client.end(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment