/** * IStorageHooks implementation for smartregistry * Integrates Stack.Gallery's storage with smartregistry */ import * as plugins from '../plugins.ts'; import type { TRegistryProtocol } from '../interfaces/auth.interfaces.ts'; import { Package } from '../models/package.ts'; import { Organization } from '../models/organization.ts'; import { AuditService } from '../services/audit.service.ts'; export interface IStorageProviderConfig { bucket: plugins.smartbucket.SmartBucket; bucketName: string; basePath: string; } /** * Storage hooks implementation that tracks packages in MongoDB * and stores artifacts in S3 via smartbucket */ export class StackGalleryStorageHooks implements plugins.smartregistry.IStorageHooks { private config: IStorageProviderConfig; constructor(config: IStorageProviderConfig) { this.config = config; } /** * Called before a package is stored */ public async beforePut( context: plugins.smartregistry.IStorageHookContext ): Promise { // Validate organization exists and has quota const orgId = context.actor?.orgId; if (orgId) { const org = await Organization.findById(orgId); if (!org) { return { allowed: false, reason: `Organization not found: ${orgId}` }; } // Check storage quota const newSize = context.metadata?.size || 0; if (!org.hasStorageAvailable(newSize)) { return { allowed: false, reason: 'Organization storage quota exceeded' }; } } return { allowed: true }; } /** * Called after a package is successfully stored */ public async afterPut( context: plugins.smartregistry.IStorageHookContext ): Promise { const protocol = context.protocol as TRegistryProtocol; const packageName = context.metadata?.packageName || context.key; const version = context.metadata?.version || 'unknown'; const orgId = context.actor?.orgId || ''; const packageId = Package.generateId(protocol, orgId, packageName); // Get or create package record let pkg = await Package.findById(packageId); if (!pkg) { pkg = new Package(); pkg.id = packageId; pkg.organizationId = orgId; pkg.protocol = protocol; pkg.name = packageName; pkg.createdById = context.actor?.userId || ''; pkg.createdAt = new Date(); } // Add version pkg.addVersion({ version, publishedAt: new Date(), publishedById: context.actor?.userId || '', size: context.metadata?.size || 0, digest: context.metadata?.digest, downloads: 0, metadata: {}, }); // Set latest tag if not set if (!pkg.distTags['latest']) { pkg.distTags['latest'] = version; } await pkg.save(); // Update organization storage usage if (orgId) { const org = await Organization.findById(orgId); if (org) { await org.updateStorageUsage(context.metadata?.size || 0); } } // Audit log if (context.actor?.userId) { await AuditService.withContext({ actorId: context.actor.userId, actorType: 'user', organizationId: orgId, }).logPackagePublished(packageId, packageName, version, orgId, ''); } } /** * Called after a package is fetched */ public async afterGet( context: plugins.smartregistry.IStorageHookContext ): Promise { const protocol = context.protocol as TRegistryProtocol; const packageName = context.metadata?.packageName || context.key; const version = context.metadata?.version; const orgId = context.actor?.orgId || ''; const packageId = Package.generateId(protocol, orgId, packageName); const pkg = await Package.findById(packageId); if (pkg) { await pkg.incrementDownloads(version); } } /** * Called before a package is deleted */ public async beforeDelete( context: plugins.smartregistry.IStorageHookContext ): Promise { return { allowed: true }; } /** * Called after a package is deleted */ public async afterDelete( context: plugins.smartregistry.IStorageHookContext ): Promise { const protocol = context.protocol as TRegistryProtocol; const packageName = context.metadata?.packageName || context.key; const version = context.metadata?.version; const orgId = context.actor?.orgId || ''; const packageId = Package.generateId(protocol, orgId, packageName); const pkg = await Package.findById(packageId); if (!pkg) return; if (version) { const versionData = pkg.versions[version]; if (versionData) { const sizeReduction = versionData.size; delete pkg.versions[version]; pkg.storageBytes -= sizeReduction; for (const [tag, ver] of Object.entries(pkg.distTags)) { if (ver === version) { delete pkg.distTags[tag]; } } if (Object.keys(pkg.versions).length === 0) { await pkg.delete(); } else { await pkg.save(); } if (orgId) { const org = await Organization.findById(orgId); if (org) { await org.updateStorageUsage(-sizeReduction); } } } } else { const sizeReduction = pkg.storageBytes; await pkg.delete(); if (orgId) { const org = await Organization.findById(orgId); if (org) { await org.updateStorageUsage(-sizeReduction); } } } // Audit log if (context.actor?.userId) { await AuditService.withContext({ actorId: context.actor.userId, actorType: 'user', organizationId: orgId, }).log('PACKAGE_DELETED', 'package', { resourceId: packageId, resourceName: packageName, metadata: { version }, success: true, }); } } /** * Get the S3 path for a package artifact */ public getArtifactPath( protocol: string, organizationName: string, packageName: string, version: string, filename: string ): string { return `${this.config.basePath}/${protocol}/${organizationName}/${packageName}/${version}/${filename}`; } /** * Store artifact in S3 */ public async storeArtifact( path: string, data: Uint8Array, contentType?: string ): Promise { const bucket = await this.config.bucket.getBucketByName(this.config.bucketName); await bucket.fastPut({ path, contents: data as unknown as string, }); return path; } /** * Fetch artifact from S3 */ public async fetchArtifact(path: string): Promise { try { const bucket = await this.config.bucket.getBucketByName(this.config.bucketName); const file = await bucket.fastGet({ path }); if (!file) return null; return new Uint8Array(file); } catch { return null; } } /** * Delete artifact from S3 */ public async deleteArtifact(path: string): Promise { try { const bucket = await this.config.bucket.getBucketByName(this.config.bucketName); await bucket.fastRemove({ path }); return true; } catch { return false; } } }