UNPKG

native-update

Version:

Foundation package for building a comprehensive update system for Capacitor apps. Provides architecture and interfaces but requires backend implementation.

294 lines 11.6 kB
import { PluginManager } from '../core/plugin-manager'; import { UpdateError, ValidationError, ErrorCode } from '../core/errors'; import { SecurityValidator } from '../core/security'; import { Directory } from '@capacitor/filesystem'; /** * Manages atomic bundle installation with rollback capability */ export class UpdateManager { constructor() { this.filesystem = null; this.updateInProgress = false; this.currentState = null; this.pluginManager = PluginManager.getInstance(); this.securityValidator = SecurityValidator.getInstance(); } /** * Initialize update manager */ async initialize() { this.filesystem = this.pluginManager.getConfigManager().get('filesystem'); if (!this.filesystem) { throw new UpdateError(ErrorCode.MISSING_DEPENDENCY, 'Filesystem not configured'); } } /** * Apply bundle update atomically */ async applyUpdate(bundleId, options) { if (this.updateInProgress) { throw new UpdateError(ErrorCode.UPDATE_FAILED, 'Another update is already in progress'); } const logger = this.pluginManager.getLogger(); const bundleManager = this.pluginManager.getBundleManager(); try { this.updateInProgress = true; logger.info('Starting bundle update', { bundleId }); // Get the new bundle const newBundle = await bundleManager.getBundle(bundleId); if (!newBundle) { throw new UpdateError(ErrorCode.FILE_NOT_FOUND, `Bundle ${bundleId} not found`); } // Verify bundle is ready if (newBundle.status !== 'READY' && newBundle.status !== 'ACTIVE') { throw new UpdateError(ErrorCode.BUNDLE_NOT_READY, `Bundle ${bundleId} is not ready for installation`); } // Get current active bundle const currentBundle = await bundleManager.getActiveBundle(); // Initialize update state this.currentState = { currentBundle, newBundle, backupPath: null, startTime: Date.now(), }; // Validate update await this.validateUpdate(currentBundle, newBundle, options); // Create backup of current state if (currentBundle && currentBundle.bundleId !== 'default') { this.currentState.backupPath = await this.createBackup(currentBundle); } // Apply the update await this.performUpdate(newBundle); // Verify the update await this.verifyUpdate(newBundle); // Mark as active await bundleManager.setActiveBundle(bundleId); // Clean up old bundles if configured if (options === null || options === void 0 ? void 0 : options.cleanupOldBundles) { await bundleManager.cleanupOldBundles(options.keepBundleCount || 3); } logger.info('Bundle update completed successfully', { bundleId, version: newBundle.version, duration: Date.now() - this.currentState.startTime, }); // Clear state this.currentState = null; } catch (error) { logger.error('Bundle update failed', error); // Attempt rollback if (this.currentState) { await this.rollback(); } throw error; } finally { this.updateInProgress = false; } } /** * Validate update before applying */ async validateUpdate(currentBundle, newBundle, options) { const logger = this.pluginManager.getLogger(); const versionManager = this.pluginManager.getVersionManager(); // Check if downgrade if (currentBundle && !(options === null || options === void 0 ? void 0 : options.allowDowngrade)) { if (versionManager.shouldBlockDowngrade(currentBundle.version, newBundle.version)) { throw new ValidationError(ErrorCode.VERSION_DOWNGRADE, `Cannot downgrade from ${currentBundle.version} to ${newBundle.version}`); } } // Verify bundle integrity if (!newBundle.verified) { logger.warn('Bundle not verified, verifying now', { bundleId: newBundle.bundleId, }); // Load bundle data const downloadManager = this.pluginManager.getDownloadManager(); const blob = await downloadManager.loadBlob(newBundle.bundleId); if (!blob) { throw new UpdateError(ErrorCode.FILE_NOT_FOUND, 'Bundle data not found'); } // Verify checksum const arrayBuffer = await blob.arrayBuffer(); const isValid = await this.securityValidator.verifyChecksum(arrayBuffer, newBundle.checksum); if (!isValid) { throw new ValidationError(ErrorCode.CHECKSUM_MISMATCH, 'Bundle checksum verification failed'); } // Verify signature if enabled if (newBundle.signature) { const signatureValid = await this.securityValidator.verifySignature(arrayBuffer, newBundle.signature); if (!signatureValid) { throw new ValidationError(ErrorCode.SIGNATURE_INVALID, 'Bundle signature verification failed'); } } // Mark as verified await this.pluginManager .getBundleManager() .markBundleAsVerified(newBundle.bundleId); } logger.debug('Bundle validation passed', { bundleId: newBundle.bundleId }); } /** * Create backup of current bundle */ async createBackup(bundle) { const backupPath = `backups/${bundle.bundleId}_${Date.now()}`; const logger = this.pluginManager.getLogger(); try { // Create backup directory await this.filesystem.mkdir({ path: backupPath, directory: Directory.Data, recursive: true, }); // Copy bundle files await this.filesystem.copy({ from: bundle.path, to: backupPath, directory: Directory.Data, }); logger.info('Backup created', { bundleId: bundle.bundleId, backupPath }); return backupPath; } catch (error) { logger.error('Failed to create backup', error); throw new UpdateError(ErrorCode.UPDATE_FAILED, 'Failed to create backup', undefined, error); } } /** * Perform the actual update */ async performUpdate(bundle) { const logger = this.pluginManager.getLogger(); try { // Extract bundle to target location const targetPath = `active/${bundle.bundleId}`; // Create target directory await this.filesystem.mkdir({ path: targetPath, directory: Directory.Data, recursive: true, }); // Copy bundle files await this.filesystem.copy({ from: bundle.path, to: targetPath, directory: Directory.Data, }); // Update bundle path bundle.path = targetPath; logger.debug('Bundle files installed', { bundleId: bundle.bundleId, targetPath, }); } catch (error) { throw new UpdateError(ErrorCode.UPDATE_FAILED, 'Failed to install bundle files', undefined, error); } } /** * Verify update was successful */ async verifyUpdate(bundle) { try { // Check if main bundle files exist const indexPath = `${bundle.path}/index.html`; await this.filesystem.stat({ path: indexPath, directory: Directory.Data, }); // Additional verification can be added here } catch (error) { throw new UpdateError(ErrorCode.UPDATE_FAILED, 'Bundle verification failed after installation', undefined, error); } } /** * Rollback to previous state */ async rollback() { var _a; if (!this.currentState) { throw new UpdateError(ErrorCode.ROLLBACK_FAILED, 'No update state to rollback'); } const logger = this.pluginManager.getLogger(); logger.warn('Starting rollback', { from: this.currentState.newBundle.bundleId, to: ((_a = this.currentState.currentBundle) === null || _a === void 0 ? void 0 : _a.bundleId) || 'default', }); try { const bundleManager = this.pluginManager.getBundleManager(); // Restore from backup if available if (this.currentState.backupPath && this.currentState.currentBundle) { const restoredPath = `active/${this.currentState.currentBundle.bundleId}`; await this.filesystem.copy({ from: this.currentState.backupPath, to: restoredPath, directory: Directory.Data, }); // Update bundle path this.currentState.currentBundle.path = restoredPath; await bundleManager.saveBundleInfo(this.currentState.currentBundle); } // Restore active bundle if (this.currentState.currentBundle) { await bundleManager.setActiveBundle(this.currentState.currentBundle.bundleId); } else { // No previous bundle, clear active await bundleManager.clearActiveBundle(); } logger.info('Rollback completed successfully'); } catch (error) { logger.error('Rollback failed', error); throw new UpdateError(ErrorCode.ROLLBACK_FAILED, 'Failed to rollback update', undefined, error); } finally { // Clean up backup if (this.currentState.backupPath) { try { await this.filesystem.rmdir({ path: this.currentState.backupPath, directory: Directory.Data, recursive: true, }); } catch (error) { logger.warn('Failed to clean up backup', error); } } } } /** * Get current update progress */ getUpdateProgress() { var _a, _b; return { inProgress: this.updateInProgress, bundleId: (_a = this.currentState) === null || _a === void 0 ? void 0 : _a.newBundle.bundleId, startTime: (_b = this.currentState) === null || _b === void 0 ? void 0 : _b.startTime, }; } /** * Cancel current update (if possible) */ async cancelUpdate() { if (!this.updateInProgress || !this.currentState) { return; } const logger = this.pluginManager.getLogger(); logger.warn('Cancelling update', { bundleId: this.currentState.newBundle.bundleId, }); // Attempt rollback await this.rollback(); this.updateInProgress = false; this.currentState = null; } } //# sourceMappingURL=update-manager.js.map