feat(blob-client): scaffold @bytelyst/blob-client — browser/RN-safe blob storage via SAS URLs (12 tests)
This commit is contained in:
parent
6984b37a24
commit
1373b6dd8b
@ -57,6 +57,13 @@ export default [
|
||||
beforeAll: 'readonly',
|
||||
afterAll: 'readonly',
|
||||
crypto: 'readonly',
|
||||
Blob: 'readonly',
|
||||
File: 'readonly',
|
||||
FormData: 'readonly',
|
||||
URLSearchParams: 'readonly',
|
||||
URL: 'readonly',
|
||||
BodyInit: 'readonly',
|
||||
ReadableStream: 'readonly',
|
||||
localStorage: 'readonly',
|
||||
sessionStorage: 'readonly',
|
||||
ReactNode: 'readonly',
|
||||
|
||||
21
packages/blob-client/package.json
Normal file
21
packages/blob-client/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@bytelyst/blob-client",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"description": "Browser/React Native-safe blob storage client — SAS URL upload/download via platform-service",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "vitest run"
|
||||
}
|
||||
}
|
||||
276
packages/blob-client/src/index.test.ts
Normal file
276
packages/blob-client/src/index.test.ts
Normal file
@ -0,0 +1,276 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createBlobClient, BlobApiError, BlobUploadError } from './index.js';
|
||||
|
||||
const mockFetch = vi.fn();
|
||||
globalThis.fetch = mockFetch;
|
||||
|
||||
function jsonResponse(data: unknown, status = 200): Response {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: () => Promise.resolve(data),
|
||||
headers: new Headers(),
|
||||
} as unknown as Response;
|
||||
}
|
||||
|
||||
function blobClient(overrides?: Partial<Parameters<typeof createBlobClient>[0]>) {
|
||||
return createBlobClient({
|
||||
baseUrl: 'http://localhost:4003/api',
|
||||
productId: 'testapp',
|
||||
getAccessToken: () => 'test-token',
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockFetch.mockReset();
|
||||
});
|
||||
|
||||
describe('createBlobClient', () => {
|
||||
describe('getSasUrl', () => {
|
||||
it('requests a SAS URL from platform-service', async () => {
|
||||
const sasResponse = {
|
||||
sasUrl: 'https://storage.blob.core.windows.net/attachments/blob?sig=abc',
|
||||
container: 'attachments',
|
||||
blobName: 'testapp/user1/photo.jpg',
|
||||
permissions: 'r',
|
||||
expiresInMinutes: 60,
|
||||
expiresAt: '2026-03-02T00:00:00.000Z',
|
||||
};
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse(sasResponse));
|
||||
|
||||
const client = blobClient();
|
||||
const result = await client.getSasUrl('attachments', 'testapp/user1/photo.jpg');
|
||||
|
||||
expect(result).toEqual(sasResponse);
|
||||
expect(mockFetch).toHaveBeenCalledOnce();
|
||||
|
||||
const [url, init] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe('http://localhost:4003/api/blob/sas');
|
||||
expect(init.method).toBe('POST');
|
||||
expect(JSON.parse(init.body)).toEqual({
|
||||
container: 'attachments',
|
||||
blobName: 'testapp/user1/photo.jpg',
|
||||
permissions: 'r',
|
||||
expiresInMinutes: 60,
|
||||
});
|
||||
});
|
||||
|
||||
it('sends auth and product headers', async () => {
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse({ sasUrl: 'https://x' }));
|
||||
|
||||
const client = blobClient();
|
||||
await client.getSasUrl('attachments', 'blob.jpg');
|
||||
|
||||
const headers = mockFetch.mock.calls[0][1].headers;
|
||||
expect(headers['Authorization']).toBe('Bearer test-token');
|
||||
expect(headers['x-product-id']).toBe('testapp');
|
||||
expect(headers['x-request-id']).toBeDefined();
|
||||
});
|
||||
|
||||
it('omits auth header when no token', async () => {
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse({ sasUrl: 'https://x' }));
|
||||
|
||||
const client = blobClient({ getAccessToken: () => null });
|
||||
await client.getSasUrl('attachments', 'blob.jpg');
|
||||
|
||||
const headers = mockFetch.mock.calls[0][1].headers;
|
||||
expect(headers['Authorization']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws BlobApiError on non-ok response', async () => {
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse({ message: 'Unauthorized' }, 401));
|
||||
|
||||
const client = blobClient();
|
||||
await expect(client.getSasUrl('releases', 'secret.bin')).rejects.toThrow(BlobApiError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upload', () => {
|
||||
it('requests SAS then uploads directly to Azure', async () => {
|
||||
const sasResponse = {
|
||||
sasUrl: 'https://storage.blob.core.windows.net/attachments/testapp/user1/photo.jpg?sig=abc',
|
||||
container: 'attachments',
|
||||
blobName: 'testapp/user1/photo.jpg',
|
||||
permissions: 'w',
|
||||
expiresInMinutes: 30,
|
||||
expiresAt: '2026-03-02T00:00:00.000Z',
|
||||
};
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(jsonResponse(sasResponse)) // SAS request
|
||||
.mockResolvedValueOnce({ ok: true, status: 201 } as Response); // Azure upload
|
||||
|
||||
const client = blobClient();
|
||||
const result = await client.upload('attachments', 'file-data', {
|
||||
contentType: 'image/jpeg',
|
||||
blobName: 'testapp/user1/photo.jpg',
|
||||
});
|
||||
|
||||
expect(result.container).toBe('attachments');
|
||||
expect(result.blobName).toBe('testapp/user1/photo.jpg');
|
||||
expect(result.sasUrl).toBe(
|
||||
'https://storage.blob.core.windows.net/attachments/testapp/user1/photo.jpg'
|
||||
);
|
||||
|
||||
// Verify Azure upload call
|
||||
const [uploadUrl, uploadInit] = mockFetch.mock.calls[1];
|
||||
expect(uploadUrl).toBe(sasResponse.sasUrl);
|
||||
expect(uploadInit.method).toBe('PUT');
|
||||
expect(uploadInit.headers['x-ms-blob-type']).toBe('BlockBlob');
|
||||
expect(uploadInit.headers['Content-Type']).toBe('image/jpeg');
|
||||
expect(uploadInit.body).toBe('file-data');
|
||||
});
|
||||
|
||||
it('auto-generates blobName when not provided', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
sasUrl: 'https://storage.blob.core.windows.net/attachments/testapp/123-abc?sig=x',
|
||||
container: 'attachments',
|
||||
blobName: 'testapp/123-abc',
|
||||
permissions: 'w',
|
||||
expiresInMinutes: 30,
|
||||
expiresAt: '2026-03-02T00:00:00.000Z',
|
||||
})
|
||||
)
|
||||
.mockResolvedValueOnce({ ok: true, status: 201 } as Response);
|
||||
|
||||
const client = blobClient();
|
||||
const result = await client.upload('attachments', 'data', {
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
|
||||
// Verify the SAS request included a generated blobName starting with productId
|
||||
const sasBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||
expect(sasBody.blobName).toMatch(/^testapp\//);
|
||||
expect(result.container).toBe('attachments');
|
||||
});
|
||||
|
||||
it('throws BlobUploadError when Azure returns non-ok', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
sasUrl: 'https://storage.blob.core.windows.net/attachments/blob?sig=abc',
|
||||
container: 'attachments',
|
||||
blobName: 'blob',
|
||||
permissions: 'w',
|
||||
expiresInMinutes: 30,
|
||||
expiresAt: '2026-03-02T00:00:00.000Z',
|
||||
})
|
||||
)
|
||||
.mockResolvedValueOnce({ ok: false, status: 403 } as Response);
|
||||
|
||||
const client = blobClient();
|
||||
await expect(
|
||||
client.upload('attachments', 'data', { contentType: 'text/plain', blobName: 'blob' })
|
||||
).rejects.toThrow(BlobUploadError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('download', () => {
|
||||
it('requests read SAS then fetches the blob', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
sasUrl: 'https://storage.blob.core.windows.net/attachments/blob?sig=read',
|
||||
container: 'attachments',
|
||||
blobName: 'blob',
|
||||
permissions: 'r',
|
||||
expiresInMinutes: 15,
|
||||
expiresAt: '2026-03-02T00:00:00.000Z',
|
||||
})
|
||||
)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
blob: () => Promise.resolve(new Blob()),
|
||||
} as unknown as Response);
|
||||
|
||||
const client = blobClient();
|
||||
const res = await client.download('attachments', 'blob');
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Second call should be to the SAS URL
|
||||
expect(mockFetch.mock.calls[1][0]).toBe(
|
||||
'https://storage.blob.core.windows.net/attachments/blob?sig=read'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws BlobApiError on download failure', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
sasUrl: 'https://storage.blob.core.windows.net/attachments/blob?sig=read',
|
||||
container: 'attachments',
|
||||
blobName: 'blob',
|
||||
permissions: 'r',
|
||||
expiresInMinutes: 15,
|
||||
expiresAt: '2026-03-02T00:00:00.000Z',
|
||||
})
|
||||
)
|
||||
.mockResolvedValueOnce({ ok: false, status: 404 } as Response);
|
||||
|
||||
const client = blobClient();
|
||||
await expect(client.download('attachments', 'blob')).rejects.toThrow(BlobApiError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
it('lists blobs with prefix and limit', async () => {
|
||||
const listResponse = {
|
||||
blobs: [{ name: 'testapp/user1/photo.jpg', container: 'attachments', size: 1024 }],
|
||||
count: 1,
|
||||
container: 'attachments',
|
||||
prefix: 'testapp/user1/',
|
||||
};
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse(listResponse));
|
||||
|
||||
const client = blobClient();
|
||||
const result = await client.list('attachments', { prefix: 'testapp/user1/', limit: 10 });
|
||||
|
||||
expect(result).toEqual(listResponse);
|
||||
const url = mockFetch.mock.calls[0][0] as string;
|
||||
expect(url).toContain('container=attachments');
|
||||
expect(url).toContain('prefix=testapp%2Fuser1%2F');
|
||||
expect(url).toContain('limit=10');
|
||||
});
|
||||
|
||||
it('works without options', async () => {
|
||||
mockFetch.mockResolvedValueOnce(
|
||||
jsonResponse({ blobs: [], count: 0, container: 'audio', prefix: null })
|
||||
);
|
||||
|
||||
const client = blobClient();
|
||||
await client.list('audio');
|
||||
|
||||
const url = mockFetch.mock.calls[0][0] as string;
|
||||
expect(url).toContain('container=audio');
|
||||
expect(url).not.toContain('prefix=');
|
||||
expect(url).not.toContain('limit=');
|
||||
});
|
||||
});
|
||||
|
||||
describe('info', () => {
|
||||
it('fetches blob metadata', async () => {
|
||||
const infoResponse = {
|
||||
name: 'testapp/user1/photo.jpg',
|
||||
container: 'attachments',
|
||||
contentType: 'image/jpeg',
|
||||
size: 2048,
|
||||
lastModified: '2026-03-01T00:00:00.000Z',
|
||||
url: 'https://storage.blob.core.windows.net/attachments/testapp/user1/photo.jpg',
|
||||
metadata: {},
|
||||
};
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse(infoResponse));
|
||||
|
||||
const client = blobClient();
|
||||
const result = await client.info('attachments', 'testapp/user1/photo.jpg');
|
||||
|
||||
expect(result).toEqual(infoResponse);
|
||||
const url = mockFetch.mock.calls[0][0] as string;
|
||||
expect(url).toContain('/blob/info/attachments/testapp%2Fuser1%2Fphoto.jpg');
|
||||
});
|
||||
});
|
||||
});
|
||||
289
packages/blob-client/src/index.ts
Normal file
289
packages/blob-client/src/index.ts
Normal file
@ -0,0 +1,289 @@
|
||||
/**
|
||||
* Browser/React Native-safe blob storage client.
|
||||
*
|
||||
* Wraps the platform-service blob endpoints to provide:
|
||||
* - SAS URL generation for direct upload/download
|
||||
* - Direct blob upload via SAS URL (PUT with raw body)
|
||||
* - Direct blob download via SAS URL
|
||||
* - Blob listing and metadata
|
||||
*
|
||||
* Requires a fetch-compatible environment (browser, React Native, Node 18+).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { createBlobClient } from '@bytelyst/blob-client';
|
||||
*
|
||||
* const blob = createBlobClient({
|
||||
* baseUrl: 'http://localhost:4003/api',
|
||||
* productId: 'nomgap',
|
||||
* getAccessToken: () => authClient.getAccessToken(),
|
||||
* });
|
||||
*
|
||||
* // Upload a file
|
||||
* const { url } = await blob.upload('attachments', file, {
|
||||
* contentType: 'image/jpeg',
|
||||
* blobName: 'nomgap/user123/photos/meal.jpg',
|
||||
* });
|
||||
*
|
||||
* // Download a file
|
||||
* const data = await blob.download('attachments', 'nomgap/user123/photos/meal.jpg');
|
||||
*
|
||||
* // List blobs
|
||||
* const { blobs } = await blob.list('attachments', { prefix: 'nomgap/user123/' });
|
||||
* ```
|
||||
*/
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────
|
||||
|
||||
export interface BlobClientConfig {
|
||||
/** Platform-service base URL (e.g. "http://localhost:4003/api"). */
|
||||
baseUrl: string;
|
||||
|
||||
/** Product identifier sent as x-product-id header. */
|
||||
productId: string;
|
||||
|
||||
/** Function that returns the current access token, or null. */
|
||||
getAccessToken: () => string | null;
|
||||
|
||||
/** Request timeout in milliseconds for API calls. Default: 15000. */
|
||||
timeoutMs?: number;
|
||||
|
||||
/** Upload timeout in milliseconds for direct blob uploads. Default: 120000. */
|
||||
uploadTimeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface SasUrlResponse {
|
||||
sasUrl: string;
|
||||
container: string;
|
||||
blobName: string;
|
||||
permissions: string;
|
||||
expiresInMinutes: number;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export interface BlobInfo {
|
||||
name: string;
|
||||
container: string;
|
||||
contentType?: string;
|
||||
size: number;
|
||||
lastModified?: string;
|
||||
url: string;
|
||||
metadata: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ListBlobsResponse {
|
||||
blobs: BlobInfo[];
|
||||
count: number;
|
||||
container: string;
|
||||
prefix: string | null;
|
||||
}
|
||||
|
||||
export interface UploadOptions {
|
||||
/** Content-Type of the blob (e.g. "image/jpeg", "application/pdf"). */
|
||||
contentType: string;
|
||||
|
||||
/** Full blob path. If omitted, auto-generated as `<productId>/<userId>/<timestamp>-<random>`. */
|
||||
blobName?: string;
|
||||
|
||||
/** SAS token expiry in minutes. Default: 30. */
|
||||
expiresInMinutes?: number;
|
||||
}
|
||||
|
||||
export interface UploadResult {
|
||||
sasUrl: string;
|
||||
container: string;
|
||||
blobName: string;
|
||||
}
|
||||
|
||||
export interface BlobClient {
|
||||
/** Get a SAS URL for direct upload or download. */
|
||||
getSasUrl(
|
||||
container: string,
|
||||
blobName: string,
|
||||
permissions?: 'r' | 'w' | 'rw' | 'rwc',
|
||||
expiresInMinutes?: number
|
||||
): Promise<SasUrlResponse>;
|
||||
|
||||
/** Upload a blob via SAS URL (requests SAS, then PUTs directly to Azure). */
|
||||
upload(
|
||||
container: string,
|
||||
data: Blob | ArrayBuffer | Uint8Array | string,
|
||||
options: UploadOptions
|
||||
): Promise<UploadResult>;
|
||||
|
||||
/** Download a blob via SAS URL. Returns the Response for streaming. */
|
||||
download(container: string, blobName: string): Promise<Response>;
|
||||
|
||||
/** List blobs in a container. */
|
||||
list(
|
||||
container: string,
|
||||
options?: { prefix?: string; limit?: number }
|
||||
): Promise<ListBlobsResponse>;
|
||||
|
||||
/** Get blob metadata/info. */
|
||||
info(container: string, blobName: string): Promise<BlobInfo>;
|
||||
}
|
||||
|
||||
// ── Errors ───────────────────────────────────────────────────
|
||||
|
||||
export class BlobApiError extends Error {
|
||||
constructor(
|
||||
public readonly status: number,
|
||||
public readonly body: unknown,
|
||||
message?: string
|
||||
) {
|
||||
super(message ?? `Blob API error ${status}`);
|
||||
this.name = 'BlobApiError';
|
||||
}
|
||||
}
|
||||
|
||||
export class BlobUploadError extends Error {
|
||||
constructor(
|
||||
public readonly status: number,
|
||||
message?: string
|
||||
) {
|
||||
super(message ?? `Blob upload failed with status ${status}`);
|
||||
this.name = 'BlobUploadError';
|
||||
}
|
||||
}
|
||||
|
||||
// ── UUID helper ──────────────────────────────────────────────
|
||||
|
||||
function uuid(): string {
|
||||
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
||||
return globalThis.crypto.randomUUID();
|
||||
}
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Factory ──────────────────────────────────────────────────
|
||||
|
||||
export function createBlobClient(config: BlobClientConfig): BlobClient {
|
||||
const {
|
||||
baseUrl,
|
||||
productId,
|
||||
getAccessToken,
|
||||
timeoutMs = 15_000,
|
||||
uploadTimeoutMs = 120_000,
|
||||
} = config;
|
||||
|
||||
function authHeaders(): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-product-id': productId,
|
||||
'x-request-id': uuid(),
|
||||
};
|
||||
const token = getAccessToken();
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function apiRequest<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const url = `${baseUrl}${path}`;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const res = await globalThis.fetch(url, {
|
||||
method,
|
||||
headers: authHeaders(),
|
||||
body: body != null ? JSON.stringify(body) : undefined,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const json = await res.json().catch(() => ({}));
|
||||
|
||||
if (!res.ok) {
|
||||
throw new BlobApiError(
|
||||
res.status,
|
||||
json,
|
||||
(json as Record<string, string>).message ?? `HTTP ${res.status}`
|
||||
);
|
||||
}
|
||||
|
||||
return json as T;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function getSasUrl(
|
||||
container: string,
|
||||
blobName: string,
|
||||
permissions: 'r' | 'w' | 'rw' | 'rwc' = 'r',
|
||||
expiresInMinutes = 60
|
||||
): Promise<SasUrlResponse> {
|
||||
return apiRequest<SasUrlResponse>('POST', '/blob/sas', {
|
||||
container,
|
||||
blobName,
|
||||
permissions,
|
||||
expiresInMinutes,
|
||||
});
|
||||
}
|
||||
|
||||
async function upload(
|
||||
container: string,
|
||||
data: Blob | ArrayBuffer | Uint8Array | string,
|
||||
options: UploadOptions
|
||||
): Promise<UploadResult> {
|
||||
const blobName = options.blobName ?? `${productId}/${Date.now()}-${uuid().slice(0, 8)}`;
|
||||
|
||||
const sas = await getSasUrl(container, blobName, 'w', options.expiresInMinutes ?? 30);
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), uploadTimeoutMs);
|
||||
|
||||
try {
|
||||
const res = await globalThis.fetch(sas.sasUrl, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'x-ms-blob-type': 'BlockBlob',
|
||||
'Content-Type': options.contentType,
|
||||
},
|
||||
body: data as BodyInit,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new BlobUploadError(res.status, `Upload to Azure failed: HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
return { sasUrl: sas.sasUrl.split('?')[0], container, blobName };
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function download(container: string, blobName: string): Promise<Response> {
|
||||
const sas = await getSasUrl(container, blobName, 'r', 15);
|
||||
|
||||
const res = await globalThis.fetch(sas.sasUrl);
|
||||
if (!res.ok) {
|
||||
throw new BlobApiError(res.status, null, `Download failed: HTTP ${res.status}`);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async function list(
|
||||
container: string,
|
||||
options?: { prefix?: string; limit?: number }
|
||||
): Promise<ListBlobsResponse> {
|
||||
const params = new URLSearchParams({ container });
|
||||
if (options?.prefix) params.set('prefix', options.prefix);
|
||||
if (options?.limit) params.set('limit', String(options.limit));
|
||||
|
||||
return apiRequest<ListBlobsResponse>('GET', `/blob/list?${params.toString()}`);
|
||||
}
|
||||
|
||||
async function info(container: string, blobName: string): Promise<BlobInfo> {
|
||||
return apiRequest<BlobInfo>(
|
||||
'GET',
|
||||
`/blob/info/${encodeURIComponent(container)}/${encodeURIComponent(blobName)}`
|
||||
);
|
||||
}
|
||||
|
||||
return { getSasUrl, upload, download, list, info };
|
||||
}
|
||||
10
packages/blob-client/tsconfig.json
Normal file
10
packages/blob-client/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"lib": ["ES2022", "DOM"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user