@syncular/server 0.0.1 → 0.0.2-127
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 +25 -0
- package/dist/blobs/adapters/database.d.ts.map +1 -1
- package/dist/blobs/adapters/database.js +25 -3
- package/dist/blobs/adapters/database.js.map +1 -1
- package/dist/blobs/adapters/filesystem.d.ts +31 -0
- package/dist/blobs/adapters/filesystem.d.ts.map +1 -0
- package/dist/blobs/adapters/filesystem.js +140 -0
- package/dist/blobs/adapters/filesystem.js.map +1 -0
- package/dist/blobs/adapters/s3.d.ts +3 -2
- package/dist/blobs/adapters/s3.d.ts.map +1 -1
- package/dist/blobs/adapters/s3.js +49 -0
- package/dist/blobs/adapters/s3.js.map +1 -1
- package/dist/blobs/index.d.ts +1 -0
- package/dist/blobs/index.d.ts.map +1 -1
- package/dist/blobs/index.js +6 -5
- package/dist/blobs/index.js.map +1 -1
- package/dist/clients.d.ts +1 -0
- package/dist/clients.d.ts.map +1 -1
- package/dist/clients.js.map +1 -1
- package/dist/compaction.d.ts +1 -1
- package/dist/compaction.js +1 -1
- package/dist/dialect/base.d.ts +83 -0
- package/dist/dialect/base.d.ts.map +1 -0
- package/dist/dialect/base.js +144 -0
- package/dist/dialect/base.js.map +1 -0
- package/dist/dialect/helpers.d.ts +10 -0
- package/dist/dialect/helpers.d.ts.map +1 -0
- package/dist/dialect/helpers.js +59 -0
- package/dist/dialect/helpers.js.map +1 -0
- package/dist/dialect/index.d.ts +2 -0
- package/dist/dialect/index.d.ts.map +1 -1
- package/dist/dialect/index.js +3 -1
- package/dist/dialect/index.js.map +1 -1
- package/dist/dialect/types.d.ts +38 -46
- package/dist/dialect/types.d.ts.map +1 -1
- package/dist/{shapes → handlers}/create-handler.d.ts +18 -5
- package/dist/handlers/create-handler.d.ts.map +1 -0
- package/dist/{shapes → handlers}/create-handler.js +140 -43
- package/dist/handlers/create-handler.js.map +1 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +4 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/registry.d.ts.map +1 -0
- package/dist/handlers/registry.js.map +1 -0
- package/dist/{shapes → handlers}/types.d.ts +7 -7
- package/dist/{shapes → handlers}/types.d.ts.map +1 -1
- package/dist/{shapes → handlers}/types.js.map +1 -1
- package/dist/helpers/conflict.d.ts +1 -1
- package/dist/helpers/conflict.d.ts.map +1 -1
- package/dist/helpers/emitted-change.d.ts +1 -1
- package/dist/helpers/emitted-change.d.ts.map +1 -1
- package/dist/helpers/index.js +4 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -16
- package/dist/index.js.map +1 -1
- package/dist/notify.d.ts +47 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.js +85 -0
- package/dist/notify.js.map +1 -0
- package/dist/proxy/handler.d.ts +1 -1
- package/dist/proxy/handler.d.ts.map +1 -1
- package/dist/proxy/handler.js +15 -11
- package/dist/proxy/handler.js.map +1 -1
- package/dist/proxy/index.d.ts +2 -2
- package/dist/proxy/index.d.ts.map +1 -1
- package/dist/proxy/index.js +3 -3
- package/dist/proxy/index.js.map +1 -1
- package/dist/proxy/mutation-detector.d.ts +4 -0
- package/dist/proxy/mutation-detector.d.ts.map +1 -1
- package/dist/proxy/mutation-detector.js +209 -24
- package/dist/proxy/mutation-detector.js.map +1 -1
- package/dist/proxy/oplog.d.ts +2 -1
- package/dist/proxy/oplog.d.ts.map +1 -1
- package/dist/proxy/oplog.js +15 -9
- package/dist/proxy/oplog.js.map +1 -1
- package/dist/proxy/registry.d.ts +0 -11
- package/dist/proxy/registry.d.ts.map +1 -1
- package/dist/proxy/registry.js +0 -24
- package/dist/proxy/registry.js.map +1 -1
- package/dist/proxy/types.d.ts +2 -0
- package/dist/proxy/types.d.ts.map +1 -1
- package/dist/pull.d.ts +4 -3
- package/dist/pull.d.ts.map +1 -1
- package/dist/pull.js +565 -314
- package/dist/pull.js.map +1 -1
- package/dist/push.d.ts +15 -3
- package/dist/push.d.ts.map +1 -1
- package/dist/push.js +359 -229
- package/dist/push.js.map +1 -1
- package/dist/realtime/index.js +1 -1
- package/dist/realtime/types.d.ts +2 -0
- package/dist/realtime/types.d.ts.map +1 -1
- package/dist/schema.d.ts +11 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/snapshot-chunks/db-metadata.d.ts +6 -1
- package/dist/snapshot-chunks/db-metadata.d.ts.map +1 -1
- package/dist/snapshot-chunks/db-metadata.js +261 -92
- package/dist/snapshot-chunks/db-metadata.js.map +1 -1
- package/dist/snapshot-chunks/index.d.ts +0 -1
- package/dist/snapshot-chunks/index.d.ts.map +1 -1
- package/dist/snapshot-chunks/index.js +2 -3
- package/dist/snapshot-chunks/index.js.map +1 -1
- package/dist/snapshot-chunks/types.d.ts +20 -5
- package/dist/snapshot-chunks/types.d.ts.map +1 -1
- package/dist/snapshot-chunks.d.ts +12 -8
- package/dist/snapshot-chunks.d.ts.map +1 -1
- package/dist/snapshot-chunks.js +40 -12
- package/dist/snapshot-chunks.js.map +1 -1
- package/dist/subscriptions/index.js +1 -1
- package/dist/subscriptions/resolve.d.ts +6 -6
- package/dist/subscriptions/resolve.d.ts.map +1 -1
- package/dist/subscriptions/resolve.js +53 -14
- package/dist/subscriptions/resolve.js.map +1 -1
- package/package.json +28 -7
- package/src/blobs/adapters/database.test.ts +67 -0
- package/src/blobs/adapters/database.ts +34 -9
- package/src/blobs/adapters/filesystem.test.ts +132 -0
- package/src/blobs/adapters/filesystem.ts +189 -0
- package/src/blobs/adapters/s3.test.ts +522 -0
- package/src/blobs/adapters/s3.ts +55 -2
- package/src/blobs/index.ts +1 -0
- package/src/clients.ts +1 -0
- package/src/compaction.ts +1 -1
- package/src/dialect/base.ts +292 -0
- package/src/dialect/helpers.ts +61 -0
- package/src/dialect/index.ts +2 -0
- package/src/dialect/types.ts +50 -54
- package/src/{shapes → handlers}/create-handler.ts +219 -64
- package/src/{shapes → handlers}/types.ts +10 -7
- package/src/helpers/conflict.ts +1 -1
- package/src/helpers/emitted-change.ts +1 -1
- package/src/index.ts +2 -1
- package/src/notify.test.ts +516 -0
- package/src/notify.ts +131 -0
- package/src/proxy/handler.test.ts +120 -0
- package/src/proxy/handler.ts +18 -10
- package/src/proxy/index.ts +2 -1
- package/src/proxy/mutation-detector.test.ts +71 -0
- package/src/proxy/mutation-detector.ts +227 -29
- package/src/proxy/oplog.ts +19 -10
- package/src/proxy/registry.ts +0 -33
- package/src/proxy/types.ts +2 -0
- package/src/pull.ts +788 -405
- package/src/push.ts +507 -312
- package/src/realtime/types.ts +2 -0
- package/src/schema.ts +11 -1
- package/src/snapshot-chunks/db-metadata.test.ts +169 -0
- package/src/snapshot-chunks/db-metadata.ts +347 -105
- package/src/snapshot-chunks/index.ts +0 -1
- package/src/snapshot-chunks/types.ts +31 -5
- package/src/snapshot-chunks.ts +60 -21
- package/src/subscriptions/resolve.ts +73 -18
- package/dist/shapes/create-handler.d.ts.map +0 -1
- package/dist/shapes/create-handler.js.map +0 -1
- package/dist/shapes/index.d.ts.map +0 -1
- package/dist/shapes/index.js +0 -4
- package/dist/shapes/index.js.map +0 -1
- package/dist/shapes/registry.d.ts.map +0 -1
- package/dist/shapes/registry.js.map +0 -1
- package/dist/snapshot-chunks/adapters/s3.d.ts +0 -63
- package/dist/snapshot-chunks/adapters/s3.d.ts.map +0 -1
- package/dist/snapshot-chunks/adapters/s3.js +0 -50
- package/dist/snapshot-chunks/adapters/s3.js.map +0 -1
- package/src/snapshot-chunks/adapters/s3.ts +0 -68
- /package/dist/{shapes → handlers}/index.d.ts +0 -0
- /package/dist/{shapes → handlers}/registry.d.ts +0 -0
- /package/dist/{shapes → handlers}/registry.js +0 -0
- /package/dist/{shapes → handlers}/types.js +0 -0
- /package/src/{shapes → handlers}/index.ts +0 -0
- /package/src/{shapes → handlers}/registry.ts +0 -0
|
@@ -46,15 +46,16 @@ export interface BlobTokenSigner {
|
|
|
46
46
|
*/
|
|
47
47
|
export function createHmacTokenSigner(secret: string): BlobTokenSigner {
|
|
48
48
|
const encoder = new TextEncoder();
|
|
49
|
+
const keyPromise = crypto.subtle.importKey(
|
|
50
|
+
'raw',
|
|
51
|
+
encoder.encode(secret),
|
|
52
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
53
|
+
false,
|
|
54
|
+
['sign', 'verify']
|
|
55
|
+
);
|
|
49
56
|
|
|
50
57
|
async function hmacSign(data: string): Promise<string> {
|
|
51
|
-
const key = await
|
|
52
|
-
'raw',
|
|
53
|
-
encoder.encode(secret),
|
|
54
|
-
{ name: 'HMAC', hash: 'SHA-256' },
|
|
55
|
-
false,
|
|
56
|
-
['sign']
|
|
57
|
-
);
|
|
58
|
+
const key = await keyPromise;
|
|
58
59
|
const signature = await crypto.subtle.sign(
|
|
59
60
|
'HMAC',
|
|
60
61
|
key,
|
|
@@ -63,6 +64,18 @@ export function createHmacTokenSigner(secret: string): BlobTokenSigner {
|
|
|
63
64
|
return bufferToHex(new Uint8Array(signature));
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
async function hmacVerify(
|
|
68
|
+
data: string,
|
|
69
|
+
signatureHex: string
|
|
70
|
+
): Promise<boolean> {
|
|
71
|
+
const parsedSignature = hexToBuffer(signatureHex);
|
|
72
|
+
if (!parsedSignature) return false;
|
|
73
|
+
const signature = new Uint8Array(parsedSignature.length);
|
|
74
|
+
signature.set(parsedSignature);
|
|
75
|
+
const key = await keyPromise;
|
|
76
|
+
return crypto.subtle.verify('HMAC', key, signature, encoder.encode(data));
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
return {
|
|
67
80
|
async sign(payload, _expiresIn) {
|
|
68
81
|
const data = JSON.stringify(payload);
|
|
@@ -75,8 +88,8 @@ export function createHmacTokenSigner(secret: string): BlobTokenSigner {
|
|
|
75
88
|
const [dataB64, sig] = token.split('.');
|
|
76
89
|
if (!dataB64 || !sig) return null;
|
|
77
90
|
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
91
|
+
const isValidSig = await hmacVerify(dataB64, sig);
|
|
92
|
+
if (!isValidSig) return null;
|
|
80
93
|
|
|
81
94
|
try {
|
|
82
95
|
const data = JSON.parse(atob(dataB64)) as {
|
|
@@ -101,6 +114,18 @@ function bufferToHex(buffer: Uint8Array): string {
|
|
|
101
114
|
.join('');
|
|
102
115
|
}
|
|
103
116
|
|
|
117
|
+
function hexToBuffer(hex: string): Uint8Array | null {
|
|
118
|
+
if (hex.length === 0 || hex.length % 2 !== 0) return null;
|
|
119
|
+
if (!/^[0-9a-f]+$/i.test(hex)) return null;
|
|
120
|
+
|
|
121
|
+
const out = new Uint8Array(hex.length / 2);
|
|
122
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
123
|
+
const pair = hex.slice(i, i + 2);
|
|
124
|
+
out[i / 2] = Number.parseInt(pair, 16);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
104
129
|
export interface DatabaseBlobStorageAdapterOptions<
|
|
105
130
|
DB extends SyncBlobsDb = SyncBlobsDb,
|
|
106
131
|
> {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mkdtemp, readdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import type { BlobStorageAdapter } from '@syncular/core';
|
|
6
|
+
import { createHmacTokenSigner } from './database';
|
|
7
|
+
import { createFilesystemBlobStorageAdapter } from './filesystem';
|
|
8
|
+
|
|
9
|
+
let basePath: string;
|
|
10
|
+
let adapter: BlobStorageAdapter;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
basePath = await mkdtemp(join(tmpdir(), 'syncular-blob-test-'));
|
|
14
|
+
adapter = createFilesystemBlobStorageAdapter({
|
|
15
|
+
basePath,
|
|
16
|
+
baseUrl: 'https://example.com/api/sync',
|
|
17
|
+
tokenSigner: createHmacTokenSigner('test-secret'),
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await rm(basePath, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const testHash =
|
|
26
|
+
'sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890';
|
|
27
|
+
const testData = new TextEncoder().encode('hello world');
|
|
28
|
+
|
|
29
|
+
describe('createFilesystemBlobStorageAdapter', () => {
|
|
30
|
+
test('put + get round-trip', async () => {
|
|
31
|
+
await adapter.put!(testHash, testData);
|
|
32
|
+
const result = await adapter.get!(testHash);
|
|
33
|
+
expect(result).toEqual(testData);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('putStream + getStream round-trip', async () => {
|
|
37
|
+
const inputStream = new ReadableStream<Uint8Array>({
|
|
38
|
+
start(controller) {
|
|
39
|
+
controller.enqueue(testData);
|
|
40
|
+
controller.close();
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await adapter.putStream!(testHash, inputStream);
|
|
45
|
+
|
|
46
|
+
const outputStream = await adapter.getStream!(testHash);
|
|
47
|
+
expect(outputStream).not.toBeNull();
|
|
48
|
+
|
|
49
|
+
const reader = outputStream!.getReader();
|
|
50
|
+
const chunks: Uint8Array[] = [];
|
|
51
|
+
while (true) {
|
|
52
|
+
const { done, value } = await reader.read();
|
|
53
|
+
if (done) break;
|
|
54
|
+
chunks.push(value);
|
|
55
|
+
}
|
|
56
|
+
const result = new Uint8Array(chunks.reduce((acc, c) => acc + c.length, 0));
|
|
57
|
+
let offset = 0;
|
|
58
|
+
for (const chunk of chunks) {
|
|
59
|
+
result.set(chunk, offset);
|
|
60
|
+
offset += chunk.length;
|
|
61
|
+
}
|
|
62
|
+
expect(result).toEqual(testData);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('get returns null for missing blob', async () => {
|
|
66
|
+
const result = await adapter.get!(testHash);
|
|
67
|
+
expect(result).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('getStream returns null for missing blob', async () => {
|
|
71
|
+
const result = await adapter.getStream!(testHash);
|
|
72
|
+
expect(result).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('exists returns true after put', async () => {
|
|
76
|
+
expect(await adapter.exists(testHash)).toBe(false);
|
|
77
|
+
await adapter.put!(testHash, testData);
|
|
78
|
+
expect(await adapter.exists(testHash)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('delete removes the blob', async () => {
|
|
82
|
+
await adapter.put!(testHash, testData);
|
|
83
|
+
expect(await adapter.exists(testHash)).toBe(true);
|
|
84
|
+
await adapter.delete(testHash);
|
|
85
|
+
expect(await adapter.exists(testHash)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('delete is idempotent for missing blob', async () => {
|
|
89
|
+
await adapter.delete(testHash);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('getMetadata returns size', async () => {
|
|
93
|
+
await adapter.put!(testHash, testData);
|
|
94
|
+
const meta = await adapter.getMetadata!(testHash);
|
|
95
|
+
expect(meta).toEqual({ size: testData.length });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('getMetadata returns null for missing blob', async () => {
|
|
99
|
+
const meta = await adapter.getMetadata!(testHash);
|
|
100
|
+
expect(meta).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('creates hash-based subdirectories', async () => {
|
|
104
|
+
await adapter.put!(testHash, testData);
|
|
105
|
+
// hex = abcdef..., so subdirs should be "ab/cd"
|
|
106
|
+
const firstLevel = await readdir(basePath);
|
|
107
|
+
expect(firstLevel).toContain('ab');
|
|
108
|
+
const secondLevel = await readdir(join(basePath, 'ab'));
|
|
109
|
+
expect(secondLevel).toContain('cd');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('signUpload returns a token URL', async () => {
|
|
113
|
+
const result = await adapter.signUpload({
|
|
114
|
+
hash: testHash,
|
|
115
|
+
size: 100,
|
|
116
|
+
mimeType: 'application/octet-stream',
|
|
117
|
+
expiresIn: 60,
|
|
118
|
+
});
|
|
119
|
+
expect(result.url).toContain('/blobs/');
|
|
120
|
+
expect(result.url).toContain('token=');
|
|
121
|
+
expect(result.method).toBe('PUT');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('signDownload returns a token URL', async () => {
|
|
125
|
+
const url = await adapter.signDownload({
|
|
126
|
+
hash: testHash,
|
|
127
|
+
expiresIn: 60,
|
|
128
|
+
});
|
|
129
|
+
expect(url).toContain('/blobs/');
|
|
130
|
+
expect(url).toContain('token=');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem blob storage adapter.
|
|
3
|
+
*
|
|
4
|
+
* Stores blobs as files on disk with 2-level hash-based subdirectories.
|
|
5
|
+
* Uploads/downloads go through the server's blob routes using signed tokens
|
|
6
|
+
* (same pattern as the database adapter).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
mkdir,
|
|
11
|
+
open,
|
|
12
|
+
readFile,
|
|
13
|
+
rename,
|
|
14
|
+
stat,
|
|
15
|
+
unlink,
|
|
16
|
+
writeFile,
|
|
17
|
+
} from 'node:fs/promises';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
|
+
import type {
|
|
20
|
+
BlobSignDownloadOptions,
|
|
21
|
+
BlobSignedUpload,
|
|
22
|
+
BlobSignUploadOptions,
|
|
23
|
+
BlobStorageAdapter,
|
|
24
|
+
} from '@syncular/core';
|
|
25
|
+
import type { BlobTokenSigner } from './database';
|
|
26
|
+
|
|
27
|
+
export interface FilesystemBlobStorageAdapterOptions {
|
|
28
|
+
/** Directory root for blob files */
|
|
29
|
+
basePath: string;
|
|
30
|
+
/** Server base URL for upload/download routes (e.g. "/api/sync") */
|
|
31
|
+
baseUrl: string;
|
|
32
|
+
/** Token signer for authorization */
|
|
33
|
+
tokenSigner: BlobTokenSigner;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve hash to a 2-level subdirectory path:
|
|
38
|
+
* `{basePath}/{hex[0..2]}/{hex[2..4]}/{hex}`
|
|
39
|
+
*/
|
|
40
|
+
function hashToFilePath(basePath: string, hash: string): string {
|
|
41
|
+
const hex = hash.startsWith('sha256:') ? hash.slice(7) : hash;
|
|
42
|
+
return join(basePath, hex.slice(0, 2), hex.slice(2, 4), hex);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function tmpPath(filePath: string): string {
|
|
46
|
+
return `${filePath}.${Date.now()}.tmp`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a filesystem blob storage adapter.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const adapter = createFilesystemBlobStorageAdapter({
|
|
55
|
+
* basePath: '/data/blobs',
|
|
56
|
+
* baseUrl: 'https://api.example.com/api/sync',
|
|
57
|
+
* tokenSigner: createHmacTokenSigner(process.env.BLOB_SECRET!),
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function createFilesystemBlobStorageAdapter(
|
|
62
|
+
options: FilesystemBlobStorageAdapterOptions
|
|
63
|
+
): BlobStorageAdapter {
|
|
64
|
+
const { basePath, tokenSigner } = options;
|
|
65
|
+
const normalizedBaseUrl = options.baseUrl.replace(/\/$/, '');
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
name: 'filesystem',
|
|
69
|
+
|
|
70
|
+
async signUpload(opts: BlobSignUploadOptions): Promise<BlobSignedUpload> {
|
|
71
|
+
const expiresAt = Date.now() + opts.expiresIn * 1000;
|
|
72
|
+
const token = await tokenSigner.sign(
|
|
73
|
+
{ hash: opts.hash, action: 'upload', expiresAt },
|
|
74
|
+
opts.expiresIn
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const url = `${normalizedBaseUrl}/blobs/${encodeURIComponent(opts.hash)}/upload?token=${encodeURIComponent(token)}`;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
url,
|
|
81
|
+
method: 'PUT',
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': opts.mimeType,
|
|
84
|
+
'Content-Length': String(opts.size),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async signDownload(opts: BlobSignDownloadOptions): Promise<string> {
|
|
90
|
+
const expiresAt = Date.now() + opts.expiresIn * 1000;
|
|
91
|
+
const token = await tokenSigner.sign(
|
|
92
|
+
{ hash: opts.hash, action: 'download', expiresAt },
|
|
93
|
+
opts.expiresIn
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return `${normalizedBaseUrl}/blobs/${encodeURIComponent(opts.hash)}/download?token=${encodeURIComponent(token)}`;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async exists(hash: string): Promise<boolean> {
|
|
100
|
+
try {
|
|
101
|
+
await stat(hashToFilePath(basePath, hash));
|
|
102
|
+
return true;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
async delete(hash: string): Promise<void> {
|
|
109
|
+
try {
|
|
110
|
+
await unlink(hashToFilePath(basePath, hash));
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
async getMetadata(
|
|
117
|
+
hash: string
|
|
118
|
+
): Promise<{ size: number; mimeType?: string } | null> {
|
|
119
|
+
try {
|
|
120
|
+
const s = await stat(hashToFilePath(basePath, hash));
|
|
121
|
+
return { size: s.size };
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
async put(hash: string, data: Uint8Array): Promise<void> {
|
|
128
|
+
const filePath = hashToFilePath(basePath, hash);
|
|
129
|
+
const tmp = tmpPath(filePath);
|
|
130
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
131
|
+
await writeFile(tmp, data);
|
|
132
|
+
await rename(tmp, filePath);
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
async putStream(
|
|
136
|
+
hash: string,
|
|
137
|
+
stream: ReadableStream<Uint8Array>
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
const filePath = hashToFilePath(basePath, hash);
|
|
140
|
+
const tmp = tmpPath(filePath);
|
|
141
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
142
|
+
|
|
143
|
+
const fh = await open(tmp, 'w');
|
|
144
|
+
try {
|
|
145
|
+
const reader = stream.getReader();
|
|
146
|
+
while (true) {
|
|
147
|
+
const { done, value } = await reader.read();
|
|
148
|
+
if (done) break;
|
|
149
|
+
await fh.write(value);
|
|
150
|
+
}
|
|
151
|
+
} finally {
|
|
152
|
+
await fh.close();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await rename(tmp, filePath);
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
async get(hash: string): Promise<Uint8Array | null> {
|
|
159
|
+
try {
|
|
160
|
+
const buf = await readFile(hashToFilePath(basePath, hash));
|
|
161
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
async getStream(hash: string): Promise<ReadableStream<Uint8Array> | null> {
|
|
169
|
+
let data: Buffer;
|
|
170
|
+
try {
|
|
171
|
+
data = await readFile(hashToFilePath(basePath, hash));
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
const bytes = new Uint8Array(
|
|
177
|
+
data.buffer,
|
|
178
|
+
data.byteOffset,
|
|
179
|
+
data.byteLength
|
|
180
|
+
);
|
|
181
|
+
return new ReadableStream<Uint8Array>({
|
|
182
|
+
start(controller) {
|
|
183
|
+
controller.enqueue(bytes);
|
|
184
|
+
controller.close();
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|