Files
registry/ts/providers/storage.provider.ts

267 lines
7.1 KiB
TypeScript
Raw Normal View History

/**
* 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<plugins.smartregistry.IBeforePutResult> {
// 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<void> {
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<void> {
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<plugins.smartregistry.IBeforeDeleteResult> {
return { allowed: true };
}
/**
* Called after a package is deleted
*/
public async afterDelete(
context: plugins.smartregistry.IStorageHookContext,
): Promise<void> {
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<string> {
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<Uint8Array | null> {
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<boolean> {
try {
const bucket = await this.config.bucket.getBucketByName(this.config.bucketName);
await bucket.fastRemove({ path });
return true;
} catch {
return false;
}
}
}