@concavejs/blobstore-node-fs 0.0.1-alpha.4
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/dist/fs-storage.d.ts +25 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +117 -0
- package/package.json +29 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem-based Storage implementation for Node.js
|
|
3
|
+
*
|
|
4
|
+
* Implements the platform-agnostic BlobStore interface using Node.js fs APIs
|
|
5
|
+
*/
|
|
6
|
+
import type { BlobStore, StorageMetadata, StorageOptions } from "@concavejs/core/abstractions";
|
|
7
|
+
export declare class FsBlobStore implements BlobStore {
|
|
8
|
+
private storageDir;
|
|
9
|
+
private metadataDir;
|
|
10
|
+
constructor(baseDir?: string);
|
|
11
|
+
private ensureDirectories;
|
|
12
|
+
private pathExists;
|
|
13
|
+
private generateStorageId;
|
|
14
|
+
private getObjectPath;
|
|
15
|
+
private getMetadataPath;
|
|
16
|
+
private calculateSha256;
|
|
17
|
+
store(blob: Blob | ArrayBuffer, options?: StorageOptions): Promise<StorageMetadata>;
|
|
18
|
+
get(storageId: string): Promise<Blob | null>;
|
|
19
|
+
delete(storageId: string): Promise<void>;
|
|
20
|
+
getUrl(storageId: string): Promise<string | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Get metadata for a stored object
|
|
23
|
+
*/
|
|
24
|
+
getMetadata(storageId: string): Promise<StorageMetadata | null>;
|
|
25
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FsBlobStore } from "./fs-storage";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// src/fs-storage.ts
|
|
2
|
+
import { mkdir, writeFile, readFile, unlink, access } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { constants as fsConstants } from "node:fs";
|
|
5
|
+
|
|
6
|
+
class FsBlobStore {
|
|
7
|
+
storageDir;
|
|
8
|
+
metadataDir;
|
|
9
|
+
constructor(baseDir = "./.concave/local/storage") {
|
|
10
|
+
this.storageDir = join(baseDir, "objects");
|
|
11
|
+
this.metadataDir = join(baseDir, "metadata");
|
|
12
|
+
}
|
|
13
|
+
async ensureDirectories() {
|
|
14
|
+
await mkdir(this.storageDir, { recursive: true });
|
|
15
|
+
await mkdir(this.metadataDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
async pathExists(path) {
|
|
18
|
+
try {
|
|
19
|
+
await access(path, fsConstants.F_OK);
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
generateStorageId() {
|
|
26
|
+
return crypto.randomUUID();
|
|
27
|
+
}
|
|
28
|
+
getObjectPath(storageId) {
|
|
29
|
+
const shard = storageId.substring(0, 2);
|
|
30
|
+
return join(this.storageDir, shard, storageId);
|
|
31
|
+
}
|
|
32
|
+
getMetadataPath(storageId) {
|
|
33
|
+
const shard = storageId.substring(0, 2);
|
|
34
|
+
return join(this.metadataDir, shard, `${storageId}.json`);
|
|
35
|
+
}
|
|
36
|
+
async calculateSha256(data) {
|
|
37
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
38
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
39
|
+
return Buffer.from(hashArray).toString("base64");
|
|
40
|
+
}
|
|
41
|
+
async store(blob, options) {
|
|
42
|
+
await this.ensureDirectories();
|
|
43
|
+
const storageId = options?.storageId ?? this.generateStorageId();
|
|
44
|
+
const objectPath = this.getObjectPath(storageId);
|
|
45
|
+
const metadataPath = this.getMetadataPath(storageId);
|
|
46
|
+
await mkdir(join(this.storageDir, storageId.substring(0, 2)), { recursive: true });
|
|
47
|
+
await mkdir(join(this.metadataDir, storageId.substring(0, 2)), { recursive: true });
|
|
48
|
+
const arrayBuffer = blob instanceof ArrayBuffer ? blob : await blob.arrayBuffer();
|
|
49
|
+
const sha256 = await this.calculateSha256(arrayBuffer);
|
|
50
|
+
await writeFile(objectPath, new Uint8Array(arrayBuffer));
|
|
51
|
+
const uploadedAt = Date.now();
|
|
52
|
+
const metadata = {
|
|
53
|
+
_id: storageId,
|
|
54
|
+
sha256,
|
|
55
|
+
size: arrayBuffer.byteLength,
|
|
56
|
+
contentType: options?.contentType || (blob instanceof Blob ? blob.type : undefined) || "application/octet-stream",
|
|
57
|
+
uploadedAt
|
|
58
|
+
};
|
|
59
|
+
await writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
60
|
+
return metadata;
|
|
61
|
+
}
|
|
62
|
+
async get(storageId) {
|
|
63
|
+
const objectPath = this.getObjectPath(storageId);
|
|
64
|
+
try {
|
|
65
|
+
const fileExists = await this.pathExists(objectPath);
|
|
66
|
+
if (!fileExists) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const data = await readFile(objectPath);
|
|
70
|
+
const metadata = await this.getMetadata(storageId);
|
|
71
|
+
const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
72
|
+
return new Blob([arrayBuffer], { type: metadata?.contentType || "application/octet-stream" });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error("Error reading object from filesystem:", error);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async delete(storageId) {
|
|
79
|
+
const objectPath = this.getObjectPath(storageId);
|
|
80
|
+
const metadataPath = this.getMetadataPath(storageId);
|
|
81
|
+
try {
|
|
82
|
+
await Promise.all([
|
|
83
|
+
unlink(objectPath).catch(() => {}),
|
|
84
|
+
unlink(metadataPath).catch(() => {})
|
|
85
|
+
]);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error("Error deleting object from filesystem:", error);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async getUrl(storageId) {
|
|
92
|
+
const objectPath = this.getObjectPath(storageId);
|
|
93
|
+
const fileExists = await this.pathExists(objectPath);
|
|
94
|
+
if (!fileExists) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const host = process.env.CONVEX_CLOUD_URL || "http://localhost:8799";
|
|
98
|
+
return `${host}/api/storage/${storageId}`;
|
|
99
|
+
}
|
|
100
|
+
async getMetadata(storageId) {
|
|
101
|
+
const metadataPath = this.getMetadataPath(storageId);
|
|
102
|
+
try {
|
|
103
|
+
const fileExists = await this.pathExists(metadataPath);
|
|
104
|
+
if (!fileExists) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const data = await readFile(metadataPath, "utf-8");
|
|
108
|
+
return JSON.parse(data);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error("Error reading metadata from filesystem:", error);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
FsBlobStore
|
|
117
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@concavejs/blobstore-node-fs",
|
|
3
|
+
"version": "0.0.1-alpha.4",
|
|
4
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "rm -rf dist && bun build ./src/index.ts --outfile dist/index.js --target node --format esm && tsc -p tsconfig.json --emitDeclarationOnly --declaration --declarationMap false --outDir dist",
|
|
21
|
+
"test": "vitest --run --passWithNoTests"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@concavejs/core": "0.0.1-alpha.4"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^24.9.1"
|
|
28
|
+
}
|
|
29
|
+
}
|