// src/services/storage/localFileStorage.ts
export class LocalFileStorage {
private dbName = 'pcb-viewer-files';
private dbVersion = 1;
private db: IDBDatabase | null = null;
async initialize(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
// Create object stores
if (!db.objectStoreNames.contains('files')) {
const fileStore = db.createObjectStore('files', { keyPath: 'id' });
fileStore.createIndex('name', 'name', { unique: false });
fileStore.createIndex('uploadedAt', 'uploadedAt', { unique: false });
}
if (!db.objectStoreNames.contains('metadata')) {
db.createObjectStore('metadata', { keyPath: 'fileId' });
}
if (!db.objectStoreNames.contains('cache')) {
const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });
cacheStore.createIndex('fileId', 'fileId', { unique: false });
cacheStore.createIndex('timestamp', 'timestamp', { unique: false });
}
};
});
}
async storeFile(file: StoredFile): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['files'], 'readwrite');
const store = transaction.objectStore('files');
const request = store.put(file);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
async getFile(fileId: string): Promise<StoredFile | null> {
if (!this.db) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['files'], 'readonly');
const store = transaction.objectStore('files');
const request = store.get(fileId);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result || null);
});
}
async getAllFiles(): Promise<StoredFile[]> {
if (!this.db) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['files'], 'readonly');
const store = transaction.objectStore('files');
const request = store.getAll();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
async deleteFile(fileId: string): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['files', 'metadata', 'cache'], 'readwrite');
// Delete from all stores
transaction.objectStore('files').delete(fileId);
transaction.objectStore('metadata').delete(fileId);
// Delete related cache entries
const cacheStore = transaction.objectStore('cache');
const index = cacheStore.index('fileId');
const cacheRequest = index.openCursor(IDBKeyRange.only(fileId));
cacheRequest.onsuccess = (event) => {
const cursor = (event.target as IDBRequest).result;
if (cursor) {
cursor.delete();
cursor.continue();
}
};
transaction.onerror = () => reject(transaction.error);
transaction.oncomplete = () => resolve();
});
}
async storeMetadata(fileId: string, metadata: FileMetadata): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['metadata'], 'readwrite');
const store = transaction.objectStore('metadata');
const request = store.put({ fileId, ...metadata });
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
async getMetadata(fileId: string): Promise<FileMetadata | null> {
if (!this.db) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['metadata'], 'readonly');
const store = transaction.objectStore('metadata');
const request = store.get(fileId);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const result = request.result;
if (result) {
// Remove fileId from the result
const { fileId, ...metadata } = result;
resolve(metadata);
} else {
resolve(null);
}
};
});
}
async cacheData(key: string, data: any, fileId?: string): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['cache'], 'readwrite');
const store = transaction.objectStore('cache');
const request = store.put({
key,
data,
fileId,
timestamp: Date.now()
});
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
async getCachedData(key: string): Promise<any | null> {
if (!this.db) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['cache'], 'readonly');
const store = transaction.objectStore('cache');
const request = store.get(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const result = request.result;
resolve(result ? result.data : null);
};
});
}
async clearOldCache(maxAge: number = 24 * 60 * 60 * 1000): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
const cutoffTime = Date.now() - maxAge;
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['cache'], 'readwrite');
const store = transaction.objectStore('cache');
const index = store.index('timestamp');
const request = index.openCursor(IDBKeyRange.upperBound(cutoffTime));
request.onerror = () => reject(request.error);
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest).result;
if (cursor) {
cursor.delete();
cursor.continue();
} else {
resolve();
}
};
});
}
async getStorageUsage(): Promise<StorageUsage> {
if (!this.db) throw new Error('Database not initialized');
const [files, metadata, cache] = await Promise.all([
this.getAllFiles(),
this.getAllMetadata(),
this.getAllCacheEntries()
]);
const filesSize = files.reduce((total, file) => total + file.size, 0);
const metadataSize = JSON.stringify(metadata).length;
const cacheSize = cache.reduce((total, entry) => total + JSON.stringify(entry.data).length, 0);
return {
totalSize: filesSize + metadataSize + cacheSize,
filesSize,
metadataSize,
cacheSize,
fileCount: files.length,
cacheEntries: cache.length
};
}
private async getAllMetadata(): Promise<any[]> {
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['metadata'], 'readonly');
const store = transaction.objectStore('metadata');
const request = store.getAll();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
private async getAllCacheEntries(): Promise<any[]> {
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['cache'], 'readonly');
const store = transaction.objectStore('cache');
const request = store.getAll();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
}
Phase 7: File Management & Upload System
Parent Issue: #440
Duration: 2-3 days
Goal: Implement comprehensive file management system with drag-and-drop upload, progress tracking, and local file handling.
Overview
This phase focuses on creating a robust file management system that allows users to upload ODB++ design files, track upload progress, manage local files, and handle various file formats. The system will provide an intuitive interface for file operations and ensure reliable file handling.
Detailed Development Steps
7.1 File Upload Infrastructure
Implement Drag & Drop Upload Component
Create Upload Hook with Progress Tracking
7.2 File Management Interface
7.3 File Validation & Processing
7.4 Local File Storage & Caching
7.5 File Download & Export
Acceptance Criteria
Testing Checklist
Output/Deliverables
Dependencies
Next Phase Dependencies
Phase 8 (User Settings) depends on this phase providing:
Estimated Time: 2-3 days
Priority: High (Core functionality)
Complexity: Medium