Files
oldboy/src/lib/project-system/file-manager.ts

317 lines
7.5 KiB
TypeScript

import type { File, CreateFileData, Result } from './types';
import type { FileDatabase } from './db';
import { compressFiles, decompressFiles, filesToShareUrl, filesFromShareUrl } from './compression';
import { loadSystemFiles, isSystemFileId, getSystemFile } from './system-files';
async function wrapResult<T>(fn: () => Promise<T>, errorMsg: string): Promise<Result<T>> {
try {
const data = await fn();
return { success: true, data };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error(errorMsg),
};
}
}
/**
* Generate a unique ID
*/
function generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
type FileChangeListener = () => void;
/**
* File Manager - Main API for managing files
*/
export class FileManager {
private db: FileDatabase;
private changeListeners: Set<FileChangeListener> = new Set();
constructor(db: FileDatabase) {
this.db = db;
}
/**
* Subscribe to file changes
*/
onFilesChanged(listener: FileChangeListener): () => void {
this.changeListeners.add(listener);
return () => this.changeListeners.delete(listener);
}
/**
* Notify all listeners that files have changed
*/
private notifyChange(): void {
this.changeListeners.forEach(listener => listener());
}
/**
* Initialize the file manager (initializes database)
*/
async init(): Promise<void> {
await this.db.init();
}
/**
* Create a new file
*/
async createFile(data: CreateFileData): Promise<Result<File>> {
try {
const file: File = {
id: generateId(),
title: data.title,
content: data.content || '',
};
await this.db.put(file);
this.notifyChange();
return { success: true, data: file };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Failed to create file'),
};
}
}
/**
* Get a file by ID (checks both user and system files)
*/
async getFile(id: string): Promise<Result<File>> {
try {
if (isSystemFileId(id)) {
const systemFile = getSystemFile(id);
if (systemFile) {
return { success: true, data: systemFile };
}
}
const file = await this.db.get(id);
if (!file) {
return {
success: false,
error: new Error(`File not found: ${id}`),
};
}
return { success: true, data: file };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Failed to get file'),
};
}
}
/**
* Get all files (user files + system files)
*/
async getAllFiles(): Promise<Result<File[]>> {
try {
const userFiles = await this.db.getAll();
const systemFiles = await loadSystemFiles();
return {
success: true,
data: [...systemFiles, ...userFiles],
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Failed to get files'),
};
}
}
/**
* Update a file (system files cannot be updated directly)
*/
async updateFile(id: string, updates: Partial<Pick<File, 'title' | 'content'>>): Promise<Result<File>> {
try {
if (isSystemFileId(id)) {
return {
success: false,
error: new Error('Cannot update system files directly. Use "Save As" to create a copy.'),
};
}
const existingFile = await this.db.get(id);
if (!existingFile) {
return {
success: false,
error: new Error(`File not found: ${id}`),
};
}
const updatedFile: File = {
...existingFile,
...(updates.title !== undefined && { title: updates.title }),
...(updates.content !== undefined && { content: updates.content }),
};
await this.db.put(updatedFile);
this.notifyChange();
return { success: true, data: updatedFile };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Failed to update file'),
};
}
}
/**
* Delete a file (system files cannot be deleted)
*/
async deleteFile(id: string): Promise<Result<void>> {
if (isSystemFileId(id)) {
return {
success: false,
error: new Error('Cannot delete system files'),
};
}
const result = await wrapResult(() => this.db.delete(id), 'Failed to delete file');
if (result.success) {
this.notifyChange();
}
return result;
}
/**
* Duplicate a file (works with both user and system files)
*/
async duplicateFile(id: string): Promise<Result<File>> {
try {
const result = await this.getFile(id);
if (!result.success) {
return result;
}
const originalFile = result.data;
const duplicatedFile: File = {
id: generateId(),
title: `${originalFile.title} (copy)`,
content: originalFile.content,
};
await this.db.put(duplicatedFile);
this.notifyChange();
return { success: true, data: duplicatedFile };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Failed to duplicate file'),
};
}
}
/**
* Generate unique untitled filename
*/
async generateUntitledFilename(): Promise<string> {
const result = await this.getAllFiles();
if (!result.success) {
return 'Untitled-1.orc';
}
const files = result.data;
const untitledPattern = /^Untitled-(\d+)\.orc$/;
let maxNum = 0;
files.forEach(file => {
const match = file.title.match(untitledPattern);
if (match) {
const num = parseInt(match[1], 10);
if (num > maxNum) {
maxNum = num;
}
}
});
return `Untitled-${maxNum + 1}.orc`;
}
/**
* Export files to a shareable URL
*/
async exportFilesToUrl(fileIds: string[], baseUrl?: string): Promise<Result<string>> {
try {
const files: File[] = [];
for (const id of fileIds) {
const file = await this.db.get(id);
if (file) {
files.push(file);
}
}
if (files.length === 0) {
return {
success: false,
error: new Error('No files to export'),
};
}
const url = filesToShareUrl(files, baseUrl);
return { success: true, data: url };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Failed to export files'),
};
}
}
/**
* Import files from a URL
*/
async importFilesFromUrl(url: string): Promise<Result<File[]>> {
try {
const files = filesFromShareUrl(url);
// Generate new IDs for imported files
const importedFiles = files.map(file => ({
...file,
id: generateId(),
}));
for (const file of importedFiles) {
await this.db.put(file);
}
this.notifyChange();
return { success: true, data: importedFiles };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Failed to import files'),
};
}
}
/**
* Clear all files (use with caution!)
*/
async clearAllFiles(): Promise<Result<void>> {
const result = await wrapResult(() => this.db.clear(), 'Failed to clear files');
if (result.success) {
this.notifyChange();
}
return result;
}
}