mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
reorganized storage adapter and added test suites for adapter and fields (#124)
* reorganized storage adapter and added test suites for adapter and fields * added build command in ci pipeline * updated workflow to also run node tests * updated workflow: try with separate tasks * updated workflow: try with separate tasks * updated workflow: added tsx as dev dependency * updated workflow: try with find instead of glob
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import { readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
||||
import { type Static, Type, isFile, parse } from "bknd/utils";
|
||||
import type { FileBody, FileListObject, FileMeta, FileUploadPayload } from "bknd/media";
|
||||
import { StorageAdapter, guessMimeType as guess } from "bknd/media";
|
||||
|
||||
export const localAdapterConfig = Type.Object(
|
||||
{
|
||||
path: Type.String({ default: "./" }),
|
||||
},
|
||||
{ title: "Local", description: "Local file system storage" },
|
||||
);
|
||||
export type LocalAdapterConfig = Static<typeof localAdapterConfig>;
|
||||
|
||||
export class StorageLocalAdapter extends StorageAdapter {
|
||||
private config: LocalAdapterConfig;
|
||||
|
||||
constructor(config: any) {
|
||||
super();
|
||||
this.config = parse(localAdapterConfig, config);
|
||||
}
|
||||
|
||||
getSchema() {
|
||||
return localAdapterConfig;
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return "local";
|
||||
}
|
||||
|
||||
async listObjects(prefix?: string): Promise<FileListObject[]> {
|
||||
const files = await readdir(this.config.path);
|
||||
const fileStats = await Promise.all(
|
||||
files
|
||||
.filter((file) => !prefix || file.startsWith(prefix))
|
||||
.map(async (file) => {
|
||||
const stats = await stat(`${this.config.path}/${file}`);
|
||||
return {
|
||||
key: file,
|
||||
last_modified: stats.mtime,
|
||||
size: stats.size,
|
||||
};
|
||||
}),
|
||||
);
|
||||
return fileStats;
|
||||
}
|
||||
|
||||
private async computeEtag(body: FileBody): Promise<string> {
|
||||
const content = isFile(body) ? body : new Response(body);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", await content.arrayBuffer());
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
|
||||
// Wrap the hex string in quotes for ETag format
|
||||
return `"${hashHex}"`;
|
||||
}
|
||||
|
||||
async putObject(key: string, body: FileBody): Promise<string | FileUploadPayload> {
|
||||
if (body === null) {
|
||||
throw new Error("Body is empty");
|
||||
}
|
||||
|
||||
const filePath = `${this.config.path}/${key}`;
|
||||
const is_file = isFile(body);
|
||||
await writeFile(filePath, is_file ? body.stream() : body);
|
||||
|
||||
return await this.computeEtag(body);
|
||||
}
|
||||
|
||||
async deleteObject(key: string): Promise<void> {
|
||||
try {
|
||||
await unlink(`${this.config.path}/${key}`);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async objectExists(key: string): Promise<boolean> {
|
||||
try {
|
||||
const stats = await stat(`${this.config.path}/${key}`);
|
||||
return stats.isFile();
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getObject(key: string, headers: Headers): Promise<Response> {
|
||||
try {
|
||||
const content = await readFile(`${this.config.path}/${key}`);
|
||||
const mimeType = guess(key);
|
||||
|
||||
return new Response(content, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": mimeType || "application/octet-stream",
|
||||
"Content-Length": content.length.toString(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle file reading errors
|
||||
return new Response("", { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
getObjectUrl(key: string): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async getObjectMeta(key: string): Promise<FileMeta> {
|
||||
const stats = await stat(`${this.config.path}/${key}`);
|
||||
return {
|
||||
type: guess(key) || "application/octet-stream",
|
||||
size: stats.size,
|
||||
};
|
||||
}
|
||||
|
||||
toJSON(secrets?: boolean) {
|
||||
return {
|
||||
type: this.getName(),
|
||||
config: this.config,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user