Comprehensive examples for implementing file storage with the Asterisms JS SDK Backend.
This guide provides detailed examples of storage implementation using the Asterisms JS SDK Backend. It covers various storage scenarios, from simple file uploads to complex file management systems with caching and batch operations.
// Basic file upload
import { getAsterismsBackendSDK } from '@asterisms/sdk-backend';
const sdk = getAsterismsBackendSDK(props);
await sdk.boot();
const storage = sdk.storage();
async function uploadUserDocument(file: File, userId: string) {
try {
const result = await storage.upload(file, {
bucketId: 'user-documents',
prefix: `users/${userId}/documents/`
});
console.log('File uploaded successfully:', {
externalId: result.externalId,
fileName: result.fileName,
size: result.size
});
return result;
} catch (error) {
console.error('Failed to upload file:', error);
throw error;
}
}// Download file with proper response handling
async function downloadUserDocument(externalId: string, fileName: string) {
const storage = sdk.storage();
try {
const response = await storage.download(externalId, { fileName });
// For JSON files
if (fileName.endsWith('.json')) {
const content = await response.text();
return JSON.parse(content);
}
// For binary files
const blob = await response.blob();
return blob;
} catch (error) {
console.error('Failed to download file:', error);
throw error;
}
}// Get file information
async function getFileInfo(externalId: string) {
const storage = sdk.storage();
try {
const fileData = await storage.getData(externalId);
return {
fileName: fileData.fileName,
size: fileData.size,
contentType: fileData.contentType,
lastModified: fileData.lastModified,
isPrivate: fileData.private,
bucketId: fileData.bucketId
};
} catch (error) {
console.error('Failed to get file metadata:', error);
throw error;
}
}// src/routes/api/storage/upload/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ request, locals }) => {
try {
const formData = await request.formData();
const file = formData.get('file') as File;
const bucketId = formData.get('bucketId') as string;
const prefix = formData.get('prefix') as string;
const isPrivate = formData.get('private') === 'true';
if (!file) {
return json({ error: 'No file provided' }, { status: 400 });
}
// Validate file
const maxSize = 50 * 1024 * 1024; // 50MB
if (file.size > maxSize) {
return json({ error: 'File too large' }, { status: 413 });
}
const allowedTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'application/pdf',
'text/plain',
'application/json',
'application/zip',
'application/x-zip-compressed'
];
if (!allowedTypes.includes(file.type)) {
return json({ error: 'File type not allowed' }, { status: 400 });
}
const sdk = locals.sdk;
if (!sdk) {
return json({ error: 'SDK not available' }, { status: 503 });
}
const storage = sdk.storage();
const result = await storage.upload(file, {
bucketId: bucketId || 'default',
prefix: prefix || '',
metadata: { private: isPrivate }
});
return json({
success: true,
file: {
externalId: result.externalId,
fileName: result.fileName,
size: result.size,
contentType: result.contentType,
path: result.path
}
});
} catch (error) {
console.error('Storage upload error:', error);
return json({ error: 'Failed to upload file' }, { status: 500 });
}
};// src/routes/api/storage/[externalId]/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ params, url, locals }) => {
try {
const sdk = locals.sdk;
if (!sdk) {
return json({ error: 'SDK not available' }, { status: 503 });
}
const storage = sdk.storage();
const fileName = url.searchParams.get('fileName');
const inline = url.searchParams.get('inline') === 'true';
const response = await storage.download(params.externalId, { fileName });
const headers = new Headers();
headers.set('Content-Type', response.headers.get('content-type') || 'application/octet-stream');
if (inline) {
headers.set('Content-Disposition', 'inline');
} else {
headers.set(
'Content-Disposition',
response.headers.get('content-disposition') || 'attachment'
);
}
return new Response(response.body, { headers });
} catch (error) {
console.error('Storage download error:', error);
return json({ error: 'Failed to download file' }, { status: 500 });
}
};
export const DELETE: RequestHandler = async ({ params, locals }) => {
try {
const sdk = locals.sdk;
if (!sdk) {
return json({ error: 'SDK not available' }, { status: 503 });
}
const storage = sdk.storage();
await storage.delete(params.externalId);
return json({ success: true });
} catch (error) {
console.error('Storage delete error:', error);
return json({ error: 'Failed to delete file' }, { status: 500 });
}
};// src/routes/api/storage/list/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url, locals }) => {
try {
const sdk = locals.sdk;
if (!sdk) {
return json({ error: 'SDK not available' }, { status: 503 });
}
const bucketId = url.searchParams.get('bucketId');
const path = url.searchParams.get('path');
const limit = parseInt(url.searchParams.get('limit') || '50');
const offset = parseInt(url.searchParams.get('offset') || '0');
const storage = sdk.storage();
const files = await storage.list({ bucketId, path });
// Simple pagination
const paginatedFiles = files.slice(offset, offset + limit);
return json({
success: true,
files: paginatedFiles.map((file) => ({
externalId: file.externalId,
fileName: file.fileName,
size: file.size,
contentType: file.contentType,
lastModified: file.lastModified,
private: file.private
})),
pagination: {
total: files.length,
offset,
limit,
hasMore: offset + limit < files.length
}
});
} catch (error) {
console.error('Storage list error:', error);
return json({ error: 'Failed to list files' }, { status: 500 });
}
};// src/routes/api/storage/bulk/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
interface BulkDeleteRequest {
externalIds: string[];
}
export const DELETE: RequestHandler = async ({ request, locals }) => {
try {
const { externalIds } = (await request.json()) as BulkDeleteRequest;
if (!Array.isArray(externalIds) || externalIds.length === 0) {
return json({ error: 'External IDs array is required' }, { status: 400 });
}
const sdk = locals.sdk;
if (!sdk) {
return json({ error: 'SDK not available' }, { status: 503 });
}
const storage = sdk.storage();
// Delete files in batches to avoid overwhelming the system
const batchSize = 50;
const results = [];
for (let i = 0; i < externalIds.length; i += batchSize) {
const batch = externalIds.slice(i, i + batchSize);
const batchPromises = batch.map(async (externalId) => {
try {
await storage.delete(externalId);
return { externalId, success: true };
} catch (error) {
return { externalId, success: false, error: error.message };
}
});
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
}
const successCount = results.filter((r) => r.success).length;
const failureCount = results.filter((r) => !r.success).length;
return json({
success: true,
results: {
total: externalIds.length,
successful: successCount,
failed: failureCount,
details: results
}
});
} catch (error) {
console.error('Bulk delete error:', error);
return json({ error: 'Failed to delete files' }, { status: 500 });
}
};// lib/services/fileManager.ts
import type { AsterismsBackendSDK, StorageFileData } from '@asterisms/sdk-backend';
export class FileManager {
private sdk: AsterismsBackendSDK;
private cache = new Map<string, StorageFileData>();
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes
constructor(sdk: AsterismsBackendSDK) {
this.sdk = sdk;
}
async uploadFile(
file: File,
options: {
bucketId?: string;
prefix?: string;
tags?: string[];
isPrivate?: boolean;
} = {}
): Promise<StorageFileData> {
const storage = this.sdk.storage();
try {
const result = await storage.upload(file, {
bucketId: options.bucketId || 'default',
prefix: options.prefix || '',
metadata: {
tags: options.tags?.join(','),
private: options.isPrivate
}
});
// Cache the result
this.cache.set(result.externalId, result);
return result;
} catch (error) {
console.error('Failed to upload file:', error);
throw error;
}
}
async getFileInfo(externalId: string): Promise<StorageFileData | null> {
// Check cache first
const cached = this.cache.get(externalId);
if (cached) {
return cached;
}
const storage = this.sdk.storage();
try {
const fileData = await storage.getData(externalId);
this.cache.set(externalId, fileData);
return fileData;
} catch (error) {
console.error('Failed to get file info:', error);
return null;
}
}
async downloadFile(externalId: string, fileName?: string): Promise<Blob> {
const storage = this.sdk.storage();
try {
const response = await storage.download(externalId, { fileName });
return await response.blob();
} catch (error) {
console.error('Failed to download file:', error);
throw error;
}
}
async replaceFile(externalId: string, newFile: File): Promise<StorageFileData> {
const storage = this.sdk.storage();
try {
const result = await storage.replace(externalId, newFile);
// Update cache
this.cache.set(externalId, result);
return result;
} catch (error) {
console.error('Failed to replace file:', error);
throw error;
}
}
async deleteFile(externalId: string): Promise<void> {
const storage = this.sdk.storage();
try {
await storage.delete(externalId);
// Remove from cache
this.cache.delete(externalId);
} catch (error) {
console.error('Failed to delete file:', error);
throw error;
}
}
async listFiles(
options: {
bucketId?: string;
path?: string;
tags?: string[];
} = {}
): Promise<StorageFileData[]> {
const storage = this.sdk.storage();
try {
const files = await storage.list({
bucketId: options.bucketId,
path: options.path
});
// Filter by tags if provided
if (options.tags && options.tags.length > 0) {
return files.filter((file) => {
const fileTags = file.metadata?.tags?.split(',') || [];
return options.tags.some((tag) => fileTags.includes(tag));
});
}
return files;
} catch (error) {
console.error('Failed to list files:', error);
throw error;
}
}
clearCache(): void {
this.cache.clear();
}
}// lib/services/imageProcessor.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
export class ImageProcessor {
private sdk: AsterismsBackendSDK;
constructor(sdk: AsterismsBackendSDK) {
this.sdk = sdk;
}
async resizeImage(
file: File,
maxWidth: number,
maxHeight: number,
quality: number = 0.8
): Promise<File> {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
// Calculate new dimensions
let { width, height } = img;
if (width > height) {
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
} else {
if (height > maxHeight) {
width = (width * maxHeight) / height;
height = maxHeight;
}
}
canvas.width = width;
canvas.height = height;
// Draw resized image
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(
(blob) => {
if (blob) {
const resizedFile = new File([blob], file.name, {
type: 'image/jpeg',
lastModified: Date.now()
});
resolve(resizedFile);
} else {
reject(new Error('Failed to resize image'));
}
},
'image/jpeg',
quality
);
};
img.onerror = () => reject(new Error('Failed to load image'));
img.src = URL.createObjectURL(file);
});
}
async uploadWithThumbnail(
file: File,
bucketId: string = 'images'
): Promise<{
original: StorageFileData;
thumbnail: StorageFileData;
}> {
const storage = this.sdk.storage();
try {
// Upload original
const original = await storage.upload(file, {
bucketId,
prefix: 'originals/'
});
// Create and upload thumbnail
const thumbnail = await this.resizeImage(file, 200, 200, 0.7);
const thumbnailResult = await storage.upload(thumbnail, {
bucketId,
prefix: 'thumbnails/',
metadata: { originalId: original.externalId }
});
return {
original,
thumbnail: thumbnailResult
};
} catch (error) {
console.error('Failed to upload with thumbnail:', error);
throw error;
}
}
}// lib/services/documentManager.ts
import type { AsterismsBackendSDK, StorageFileData } from '@asterisms/sdk-backend';
export interface DocumentMetadata {
title: string;
description?: string;
tags: string[];
category: string;
author: string;
createdAt: Date;
version: number;
}
export class DocumentManager {
private sdk: AsterismsBackendSDK;
private readonly DOCUMENTS_BUCKET = 'documents';
constructor(sdk: AsterismsBackendSDK) {
this.sdk = sdk;
}
async uploadDocument(
file: File,
metadata: DocumentMetadata
): Promise<{
file: StorageFileData;
metadata: DocumentMetadata;
}> {
const storage = this.sdk.storage();
try {
// Upload the file
const fileResult = await storage.upload(file, {
bucketId: this.DOCUMENTS_BUCKET,
prefix: `${metadata.category}/`,
metadata: {
...metadata,
createdAt: metadata.createdAt.toISOString()
}
});
// Upload metadata as JSON
const metadataFile = new File(
[JSON.stringify(metadata)],
`${fileResult.externalId}.metadata.json`,
{ type: 'application/json' }
);
await storage.upload(metadataFile, {
bucketId: this.DOCUMENTS_BUCKET,
prefix: 'metadata/',
metadata: { documentId: fileResult.externalId }
});
return { file: fileResult, metadata };
} catch (error) {
console.error('Failed to upload document:', error);
throw error;
}
}
async getDocumentMetadata(externalId: string): Promise<DocumentMetadata | null> {
const storage = this.sdk.storage();
try {
const response = await storage.download(`${externalId}.metadata.json`);
const metadataText = await response.text();
const metadata = JSON.parse(metadataText);
return {
...metadata,
createdAt: new Date(metadata.createdAt)
};
} catch (error) {
console.error('Failed to get document metadata:', error);
return null;
}
}
async searchDocuments(query: {
tags?: string[];
category?: string;
author?: string;
dateRange?: { from: Date; to: Date };
}): Promise<Array<{ file: StorageFileData; metadata: DocumentMetadata }>> {
const storage = this.sdk.storage();
try {
const files = await storage.list({
bucketId: this.DOCUMENTS_BUCKET,
path: query.category ? `${query.category}/` : undefined
});
const results = [];
for (const file of files) {
const metadata = await this.getDocumentMetadata(file.externalId);
if (!metadata) continue;
// Apply filters
if (query.tags && !query.tags.some((tag) => metadata.tags.includes(tag))) {
continue;
}
if (query.author && metadata.author !== query.author) {
continue;
}
if (query.dateRange) {
const createdAt = metadata.createdAt;
if (createdAt < query.dateRange.from || createdAt > query.dateRange.to) {
continue;
}
}
results.push({ file, metadata });
}
return results;
} catch (error) {
console.error('Failed to search documents:', error);
throw error;
}
}
async updateDocumentMetadata(
externalId: string,
updates: Partial<DocumentMetadata>
): Promise<void> {
const storage = this.sdk.storage();
try {
const currentMetadata = await this.getDocumentMetadata(externalId);
if (!currentMetadata) {
throw new Error('Document metadata not found');
}
const updatedMetadata = {
...currentMetadata,
...updates,
version: currentMetadata.version + 1
};
const metadataFile = new File(
[JSON.stringify(updatedMetadata)],
`${externalId}.metadata.json`,
{ type: 'application/json' }
);
await storage.replace(`${externalId}.metadata.json`, metadataFile);
} catch (error) {
console.error('Failed to update document metadata:', error);
throw error;
}
}
}<!-- lib/components/FileUpload.svelte -->
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { StorageFileData } from '@asterisms/sdk-backend';
export let bucketId: string = 'default';
export let prefix: string = '';
export let accept: string = '*/*';
export let multiple: boolean = false;
export let maxSize: number = 10 * 1024 * 1024; // 10MB
export let disabled: boolean = false;
const dispatch = createEventDispatcher<{
upload: { files: StorageFileData[] };
error: { message: string };
progress: { percent: number };
}>();
let dragOver = false;
let uploading = false;
let uploadProgress = 0;
async function handleFiles(files: FileList) {
if (disabled || uploading) return;
const fileArray = Array.from(files);
// Validate files
for (const file of fileArray) {
if (file.size > maxSize) {
dispatch('error', { message: `File ${file.name} is too large` });
return;
}
}
uploading = true;
uploadProgress = 0;
try {
const uploadedFiles: StorageFileData[] = [];
for (let i = 0; i < fileArray.length; i++) {
const file = fileArray[i];
const formData = new FormData();
formData.append('file', file);
formData.append('bucketId', bucketId);
formData.append('prefix', prefix);
const response = await fetch('/api/storage/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Failed to upload ${file.name}`);
}
const result = await response.json();
uploadedFiles.push(result.file);
uploadProgress = Math.round(((i + 1) / fileArray.length) * 100);
dispatch('progress', { percent: uploadProgress });
}
dispatch('upload', { files: uploadedFiles });
} catch (error) {
dispatch('error', { message: error.message });
} finally {
uploading = false;
uploadProgress = 0;
}
}
function handleDragOver(event: DragEvent) {
event.preventDefault();
dragOver = true;
}
function handleDragLeave(event: DragEvent) {
event.preventDefault();
dragOver = false;
}
function handleDrop(event: DragEvent) {
event.preventDefault();
dragOver = false;
const files = event.dataTransfer?.files;
if (files && files.length > 0) {
handleFiles(files);
}
}
</script>
<div class="file-upload">
<div
class="drop-zone"
class:drag-over={dragOver}
class:uploading
class:disabled
on:dragover={handleDragOver}
on:dragleave={handleDragLeave}
on:drop={handleDrop}
>
<input
type="file"
{accept}
{multiple}
on:change={(e) => handleFiles(e.target.files)}
disabled={disabled || uploading}
/>
{#if uploading}
<div class="upload-status">
<div class="progress-bar">
<div class="progress-fill" style="width: {uploadProgress}%"></div>
</div>
<p>Uploading... {uploadProgress}%</p>
</div>
{:else}
<div class="upload-prompt">
<svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7,10 12,15 17,10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
<p>Drop files here or click to browse</p>
<p class="hint">Max size: {Math.round(maxSize / 1024 / 1024)}MB</p>
</div>
{/if}
</div>
</div>
<style>
.file-upload {
width: 100%;
max-width: 600px;
margin: 0 auto;
}
.drop-zone {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.drop-zone:hover:not(.disabled):not(.uploading) {
border-color: #007bff;
background-color: #f8f9fa;
}
.drop-zone.drag-over {
border-color: #007bff;
background-color: #e3f2fd;
}
.drop-zone.uploading {
border-color: #28a745;
background-color: #f8fff9;
}
.drop-zone.disabled {
opacity: 0.5;
cursor: not-allowed;
}
input[type="file"] {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.upload-icon {
width: 48px;
height: 48px;
margin: 0 auto 16px;
color: #666;
}
.upload-prompt p {
margin: 8px 0;
color: #666;
}
.hint {
font-size: 0.9em;
color: #999;
}
.upload-status {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.progress-bar {
width: 100%;
max-width: 300px;
height: 8px;
background-color: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #28a745;
transition: width 0.3s ease;
}
</style><!-- lib/components/FileGallery.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import type { StorageFileData } from '@asterisms/sdk-backend';
export let bucketId: string = 'default';
export let path: string = '';
export let allowDelete: boolean = true;
export let allowDownload: boolean = true;
let files: StorageFileData[] = [];
let loading = true;
let error: string | null = null;
onMount(() => {
loadFiles();
});
async function loadFiles() {
loading = true;
error = null;
try {
const params = new URLSearchParams();
if (bucketId) params.append('bucketId', bucketId);
if (path) params.append('path', path);
const response = await fetch(`/api/storage/list?${params}`);
if (!response.ok) {
throw new Error('Failed to load files');
}
const result = await response.json();
files = result.files;
} catch (err) {
error = err.message;
} finally {
loading = false;
}
}
async function downloadFile(file: StorageFileData) {
try {
const response = await fetch(`/api/storage/${file.externalId}?fileName=${encodeURIComponent(file.fileName)}`);
if (!response.ok) {
throw new Error('Failed to download file');
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (err) {
alert(`Failed to download file: ${err.message}`);
}
}
async function deleteFile(file: StorageFileData) {
if (!confirm(`Are you sure you want to delete ${file.fileName}?`)) {
return;
}
try {
const response = await fetch(`/api/storage/${file.externalId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete file');
}
// Remove from local list
files = files.filter(f => f.externalId !== file.externalId);
} catch (err) {
alert(`Failed to delete file: ${err.message}`);
}
}
function formatFileSize(bytes: number): string {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
function isImage(contentType: string): boolean {
return contentType.startsWith('image/');
}
function getFileIcon(contentType: string): string {
if (contentType.startsWith('image/')) return '🖼️';
if (contentType.startsWith('video/')) return '🎥';
if (contentType.startsWith('audio/')) return '🎵';
if (contentType.includes('pdf')) return '📄';
if (contentType.includes('text')) return '📝';
if (contentType.includes('zip')) return '📦';
return '📁';
}
</script>
<div class="file-gallery">
{#if loading}
<div class="loading">Loading files...</div>
{:else if error}
<div class="error">Error: {error}</div>
{:else if files.length === 0}
<div class="empty">No files found</div>
{:else}
<div class="files-grid">
{#each files as file (file.externalId)}
<div class="file-card">
<div class="file-preview">
{#if isImage(file.contentType)}
<img
src="/api/storage/{file.externalId}?fileName={encodeURIComponent(file.fileName)}&inline=true"
alt={file.fileName}
loading="lazy"
/>
{:else}
<div class="file-icon">
{getFileIcon(file.contentType)}
</div>
{/if}
</div>
<div class="file-info">
<h3 class="file-name" title={file.fileName}>{file.fileName}</h3>
<p class="file-size">{formatFileSize(file.size)}</p>
<p class="file-type">{file.contentType}</p>
<p class="file-date">{new Date(file.lastModified).toLocaleDateString()}</p>
</div>
<div class="file-actions">
{#if allowDownload}
<button
class="btn btn-primary"
on:click={() => downloadFile(file)}
>
Download
</button>
{/if}
{#if allowDelete}
<button
class="btn btn-danger"
on:click={() => deleteFile(file)}
>
Delete
</button>
{/if}
</div>
</div>
{/each}
</div>
{/if}
</div>
<style>
.file-gallery {
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.loading, .error, .empty {
text-align: center;
padding: 40px;
color: #666;
}
.error {
color: #dc3545;
}
.files-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
padding: 20px;
}
.file-card {
border: 1px solid #e9ecef;
border-radius: 8px;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.file-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.file-preview {
height: 150px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
overflow: hidden;
}
.file-preview img {
max-width: 100%;
max-height: 100%;
object-fit: cover;
}
.file-icon {
font-size: 48px;
}
.file-info {
padding: 16px;
}
.file-name {
font-size: 1.1em;
font-weight: 600;
margin: 0 0 8px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-size, .file-type, .file-date {
margin: 4px 0;
font-size: 0.9em;
color: #666;
}
.file-actions {
padding: 16px;
display: flex;
gap: 8px;
border-top: 1px solid #e9ecef;
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
</style>// tests/storage.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { getAsterismsBackendSDK } from '@asterisms/sdk-backend';
import type { StorageService } from '@asterisms/sdk-backend';
describe('Storage Service', () => {
let sdk: AsterismsBackendSDK;
let storage: StorageService;
beforeEach(async () => {
sdk = getAsterismsBackendSDK({
// Test configuration
});
await sdk.boot();
storage = sdk.storage();
});
describe('File Upload', () => {
it('should upload a text file successfully', async () => {
const fileContent = 'Hello, World!';
const file = new File([fileContent], 'test.txt', { type: 'text/plain' });
const result = await storage.upload(file, {
bucketId: 'test-bucket',
prefix: 'test/'
});
expect(result.fileName).toBe('test.txt');
expect(result.contentType).toBe('text/plain');
expect(result.size).toBe(fileContent.length);
expect(result.bucketId).toBe('test-bucket');
});
it('should upload a JSON file successfully', async () => {
const jsonData = { name: 'Test', value: 42 };
const file = new File([JSON.stringify(jsonData)], 'data.json', {
type: 'application/json'
});
const result = await storage.upload(file);
expect(result.fileName).toBe('data.json');
expect(result.contentType).toBe('application/json');
});
});
describe('File Download', () => {
it('should download uploaded file with correct content', async () => {
const originalContent = 'Test file content';
const file = new File([originalContent], 'download-test.txt', {
type: 'text/plain'
});
const uploadResult = await storage.upload(file);
const downloadResponse = await storage.download(uploadResult.externalId);
const downloadedContent = await downloadResponse.text();
expect(downloadedContent).toBe(originalContent);
});
it('should download with custom filename', async () => {
const file = new File(['content'], 'original.txt', { type: 'text/plain' });
const uploadResult = await storage.upload(file);
const downloadResponse = await storage.download(uploadResult.externalId, {
fileName: 'custom-name.txt'
});
const contentDisposition = downloadResponse.headers.get('content-disposition');
expect(contentDisposition).toContain('custom-name.txt');
});
});
describe('File Metadata', () => {
it('should retrieve file metadata correctly', async () => {
const file = new File(['metadata test'], 'metadata.txt', {
type: 'text/plain'
});
const uploadResult = await storage.upload(file);
const metadata = await storage.getData(uploadResult.externalId);
expect(metadata.fileName).toBe('metadata.txt');
expect(metadata.contentType).toBe('text/plain');
expect(metadata.size).toBe(13); // 'metadata test'.length
expect(metadata.externalId).toBe(uploadResult.externalId);
});
});
describe('File Operations', () => {
it('should replace file successfully', async () => {
const originalFile = new File(['original'], 'replace.txt', {
type: 'text/plain'
});
const newFile = new File(['replaced'], 'replace.txt', {
type: 'text/plain'
});
const uploadResult = await storage.upload(originalFile);
const replaceResult = await storage.replace(uploadResult.externalId, newFile);
expect(replaceResult.externalId).toBe(uploadResult.externalId);
expect(replaceResult.size).toBe(8); // 'replaced'.length
const downloadResponse = await storage.download(uploadResult.externalId);
const content = await downloadResponse.text();
expect(content).toBe('replaced');
});
it('should delete file successfully', async () => {
const file = new File(['delete me'], 'delete.txt', { type: 'text/plain' });
const uploadResult = await storage.upload(file);
await storage.delete(uploadResult.externalId);
// Should throw error when trying to get deleted file
await expect(storage.getData(uploadResult.externalId)).rejects.toThrow();
});
});
describe('File Listing', () => {
it('should list files in bucket', async () => {
const bucketId = 'list-test';
const files = [
new File(['content1'], 'file1.txt', { type: 'text/plain' }),
new File(['content2'], 'file2.txt', { type: 'text/plain' })
];
// Upload test files
for (const file of files) {
await storage.upload(file, { bucketId });
}
const listedFiles = await storage.list({ bucketId });
expect(listedFiles.length).toBeGreaterThanOrEqual(2);
const fileNames = listedFiles.map((f) => f.fileName);
expect(fileNames).toContain('file1.txt');
expect(fileNames).toContain('file2.txt');
});
it('should list files with path prefix', async () => {
const bucketId = 'prefix-test';
const prefix = 'documents/';
const file = new File(['content'], 'prefixed.txt', { type: 'text/plain' });
await storage.upload(file, { bucketId, prefix });
const listedFiles = await storage.list({ bucketId, path: prefix });
expect(listedFiles.some((f) => f.fileName === 'prefixed.txt')).toBe(true);
});
});
});// tests/storage-integration.test.ts
import { test, expect } from '@playwright/test';
test.describe('Storage API Integration', () => {
test('should handle file upload flow', async ({ page }) => {
// Navigate to upload page
await page.goto('/upload');
// Create a test file
const fileContent = 'Integration test file content';
const file = await page.evaluateHandle(() => {
const blob = new Blob(['Integration test file content'], { type: 'text/plain' });
return new File([blob], 'integration-test.txt', { type: 'text/plain' });
});
// Upload file
await page.setInputFiles('input[type="file"]', {
name: 'integration-test.txt',
mimeType: 'text/plain',
buffer: Buffer.from(fileContent)
});
// Wait for upload completion
await expect(page.locator('.upload-success')).toBeVisible();
// Verify file appears in listing
await page.goto('/files');
await expect(page.locator('text=integration-test.txt')).toBeVisible();
});
test('should handle file download', async ({ page }) => {
// First upload a file
await page.goto('/upload');
await page.setInputFiles('input[type="file"]', {
name: 'download-test.txt',
mimeType: 'text/plain',
buffer: Buffer.from('Download test content')
});
await expect(page.locator('.upload-success')).toBeVisible();
// Navigate to files page
await page.goto('/files');
// Start download
const downloadPromise = page.waitForDownload();
await page.locator('text=Download').first().click();
const download = await downloadPromise;
// Verify download
expect(download.suggestedFilename()).toBe('download-test.txt');
});
test('should handle file deletion', async ({ page }) => {
// Upload a file first
await page.goto('/upload');
await page.setInputFiles('input[type="file"]', {
name: 'delete-test.txt',
mimeType: 'text/plain',
buffer: Buffer.from('Delete test content')
});
await expect(page.locator('.upload-success')).toBeVisible();
// Go to files page
await page.goto('/files');
await expect(page.locator('text=delete-test.txt')).toBeVisible();
// Delete file
page.on('dialog', (dialog) => dialog.accept());
await page.locator('text=Delete').first().click();
// Verify file is removed
await expect(page.locator('text=delete-test.txt')).not.toBeVisible();
});
});// lib/security/secureUpload.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
// File validation types (inline instead of separate import)
export interface FileValidationRule {
maxSize?: number;
allowedTypes?: string[];
allowedExtensions?: string[];
requireExtension?: boolean;
sanitizeFileName?: boolean;
}
export class FileValidator {
private rules: FileValidationRule;
constructor(rules: FileValidationRule = {}) {
this.rules = {
maxSize: 10 * 1024 * 1024, // 10MB default
allowedTypes: ['*/*'],
allowedExtensions: [],
requireExtension: false,
sanitizeFileName: true,
...rules
};
}
validate(file: File): { valid: boolean; errors: string[] } {
const errors: string[] = [];
// Check file size
if (this.rules.maxSize && file.size > this.rules.maxSize) {
errors.push(`File size ${file.size} exceeds maximum allowed size ${this.rules.maxSize}`);
}
// Check MIME type
if (this.rules.allowedTypes && !this.rules.allowedTypes.includes('*/*')) {
const isAllowed = this.rules.allowedTypes.some((type) => {
if (type.endsWith('/*')) {
return file.type.startsWith(type.slice(0, -1));
}
return file.type === type;
});
if (!isAllowed) {
errors.push(`File type ${file.type} is not allowed`);
}
}
// Check file extension
const extension = this.getFileExtension(file.name);
if (this.rules.requireExtension && !extension) {
errors.push('File must have an extension');
}
if (this.rules.allowedExtensions && this.rules.allowedExtensions.length > 0) {
if (!extension || !this.rules.allowedExtensions.includes(extension)) {
errors.push(`File extension ${extension} is not allowed`);
}
}
// Check filename
if (this.rules.sanitizeFileName && !this.isValidFileName(file.name)) {
errors.push('File name contains invalid characters');
}
return {
valid: errors.length === 0,
errors
};
}
private getFileExtension(fileName: string): string {
const parts = fileName.split('.');
return parts.length > 1 ? parts.pop()?.toLowerCase() || '' : '';
}
private isValidFileName(fileName: string): boolean {
// Allow alphanumeric, spaces, hyphens, underscores, and dots
return /^[a-zA-Z0-9\s\-_.]+$/.test(fileName);
}
sanitizeFileName(fileName: string): string {
return fileName
.replace(/[^a-zA-Z0-9\s\-_.]/g, '')
.replace(/\s+/g, '-')
.toLowerCase();
}
}
export class SecureUploadHandler {
private sdk: AsterismsBackendSDK;
private validator: FileValidator;
constructor(sdk: AsterismsBackendSDK, validationRules?: FileValidationRule) {
this.sdk = sdk;
this.validator = new FileValidator(validationRules);
}
async secureUpload(
file: File,
options: {
bucketId: string;
prefix?: string;
userId?: string;
sanitizeFileName?: boolean;
}
): Promise<StorageFileData> {
// Validate file
const validation = this.validator.validate(file);
if (!validation.valid) {
throw new Error(`File validation failed: ${validation.errors.join(', ')}`);
}
// Sanitize filename if requested
let sanitizedFile = file;
if (options.sanitizeFileName) {
const sanitizedName = this.validator.sanitizeFileName(file.name);
sanitizedFile = new File([file], sanitizedName, { type: file.type });
}
// Add user-specific prefix for isolation
const prefix = options.userId
? `${options.prefix || ''}users/${options.userId}/`
: options.prefix || '';
const storage = this.sdk.storage();
try {
const result = await storage.upload(sanitizedFile, {
bucketId: options.bucketId,
prefix,
metadata: {
uploadedBy: options.userId,
uploadedAt: new Date().toISOString(),
originalName: file.name
}
});
return result;
} catch (error) {
console.error('Secure upload failed:', error);
throw new Error('Failed to upload file securely');
}
}
}// lib/optimization/batchOperations.ts
import type { AsterismsBackendSDK, StorageFileData } from '@asterisms/sdk-backend';
export class BatchStorageOperations {
private sdk: AsterismsBackendSDK;
constructor(sdk: AsterismsBackendSDK) {
this.sdk = sdk;
}
async batchUpload(
files: File[],
options: {
bucketId: string;
prefix?: string;
batchSize?: number;
onProgress?: (completed: number, total: number) => void;
}
): Promise<{ successful: StorageFileData[]; failed: Array<{ file: File; error: string }> }> {
const storage = this.sdk.storage();
const batchSize = options.batchSize || 10;
const successful: StorageFileData[] = [];
const failed: Array<{ file: File; error: string }> = [];
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
const batchPromises = batch.map(async (file) => {
try {
const result = await storage.upload(file, {
bucketId: options.bucketId,
prefix: options.prefix
});
return { success: true, result, file };
} catch (error) {
return { success: false, error: error.message, file };
}
});
const batchResults = await Promise.all(batchPromises);
for (const result of batchResults) {
if (result.success) {
successful.push(result.result);
} else {
failed.push({ file: result.file, error: result.error });
}
}
if (options.onProgress) {
options.onProgress(i + batch.length, files.length);
}
}
return { successful, failed };
}
async batchDelete(
externalIds: string[],
options: {
batchSize?: number;
onProgress?: (completed: number, total: number) => void;
} = {}
): Promise<{ successful: string[]; failed: Array<{ externalId: string; error: string }> }> {
const storage = this.sdk.storage();
const batchSize = options.batchSize || 20;
const successful: string[] = [];
const failed: Array<{ externalId: string; error: string }> = [];
for (let i = 0; i < externalIds.length; i += batchSize) {
const batch = externalIds.slice(i, i + batchSize);
const batchPromises = batch.map(async (externalId) => {
try {
await storage.delete(externalId);
return { success: true, externalId };
} catch (error) {
return { success: false, error: error.message, externalId };
}
});
const batchResults = await Promise.all(batchPromises);
for (const result of batchResults) {
if (result.success) {
successful.push(result.externalId);
} else {
failed.push({ externalId: result.externalId, error: result.error });
}
}
if (options.onProgress) {
options.onProgress(i + batch.length, externalIds.length);
}
}
return { successful, failed };
}
}This comprehensive storage examples file provides:
The examples follow the same comprehensive structure as the notification examples and provide practical, production-ready code that developers can use as a foundation for their storage implementations.