Skip to content

Instantly share code, notes, and snippets.

@dadamssg
Created September 6, 2024 17:37
Show Gist options
  • Save dadamssg/1ef36059fb44834b163f67141c13ee6d to your computer and use it in GitHub Desktop.
Save dadamssg/1ef36059fb44834b163f67141c13ee6d to your computer and use it in GitHub Desktop.
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