import type { Cloudly } from '../classes.cloudly.js'; import * as plugins from '../plugins.js'; import * as paths from '../paths.js'; import { Image } from './classes.image.js'; export class ImageManager { cloudlyRef: Cloudly; public typedrouter = new plugins.typedrequest.TypedRouter(); public smartbucketInstance!: plugins.smartbucket.SmartBucket; public imageDir!: plugins.smartbucket.Directory; public dockerImageStore!: plugins.docker.DockerImageStore; get db() { return this.cloudlyRef.mongodbConnector.smartdataDb; } public CImage = plugins.smartdata.setDefaultManagerForDoc(this, Image); constructor(cloudlyRefArg: Cloudly) { this.cloudlyRef = cloudlyRefArg; this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'createImage', async (reqArg, toolsArg) => { await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqArg); const image = await this.CImage.create({ name: reqArg.name, description: reqArg.description, versions: [], }); return { image: await image.createSavableObject(), }; }, ), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler('getImage', async (reqArg, toolsArg) => { await toolsArg!.passGuards([this.cloudlyRef.authManager.adminOrClusterIdentityGuard], reqArg); const image = await this.CImage.getInstance({ id: reqArg.imageId, }); return { image: await image.createSavableObject(), }; }), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'deleteImage', async (reqArg, toolsArg) => { await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqArg); const image = await this.CImage.getInstance({ id: reqArg.imageId, }); await image.delete(); return {}; }, ), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getAllImages', async (requestArg, toolsArg) => { await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], requestArg); const images = await this.CImage.getInstances({}); return { images: await Promise.all( images.map((image) => { return image.createSavableObject(); }), ), }; }, ), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'pushImageVersion', async (reqArg, toolsArg) => { await plugins.smartguard.passGuardsOrReject(reqArg, [ this.cloudlyRef.authManager.validIdentityGuard, ]); const refImage = await this.CImage.getInstance({ id: reqArg.imageId, }); if (!refImage) { throw new plugins.typedrequest.TypedResponseError('Image not found'); } const imageVersion = reqArg.versionString; if (!imageVersion) { throw new plugins.typedrequest.TypedResponseError('versionString is required'); } console.log( `got request to push image version ${imageVersion} for image ${refImage.data.name}`, ); const storagePath = await refImage.getStoragePath(imageVersion); refImage.data.versions = [ ...refImage.data.versions.filter((version) => version.versionString !== imageVersion), { versionString: imageVersion, source: 'upload', storagePath, size: 0, createdAt: Date.now(), }, ]; await refImage.save(); const imagePushStream = reqArg.imageStream; (async () => { const smartWebDuplex = new plugins.smartstream.webstream.WebDuplexStream< Uint8Array, Uint8Array >({ writeFunction: async (chunkArg, toolsArg) => { console.log(chunkArg); return chunkArg; }, }); imagePushStream.writeToWebstream(smartWebDuplex.writable); await this.dockerImageStore.storeImage( storagePath, plugins.smartstream.SmartDuplex.fromWebReadableStream(smartWebDuplex.readable), ); })().catch((error) => { console.error(`failed to store image ${refImage.id}:${imageVersion}`, error); }); return { allowed: true, }; }, ), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'pullImageVersion', async (reqArg) => { const image = await this.CImage.getInstance({ id: reqArg.imageId, }); const imageVersion = image.data.versions.find( (version) => version.versionString === reqArg.versionString, ); if (!imageVersion) { throw new plugins.typedrequest.TypedResponseError('Image version not found'); } const readable = await this.imageDir.fastGetStream( { path: imageVersion.storagePath || await image.getStoragePath(reqArg.versionString), }, 'webstream', ); const imageVirtualStream = new plugins.typedrequest.VirtualStream(); (async () => { await imageVirtualStream.readFromWebstream(readable); })().catch((error) => { console.error(`failed to stream image ${image.id}:${reqArg.versionString}`, error); }); return { imageStream: imageVirtualStream, }; }, ), ); } public async start() { // lets setup s3 const s3Descriptor = await this.cloudlyRef.config.appData.waitForAndGetKey('s3Descriptor') as plugins.tsclass.storage.IS3Descriptor; console.log(this.cloudlyRef.config.data.s3Descriptor); this.smartbucketInstance = new plugins.smartbucket.SmartBucket( this.cloudlyRef.config.data.s3Descriptor!, ); const bucketName = s3Descriptor.bucketName!; const bucket = await this.smartbucketInstance.bucketExists(bucketName) ? await this.smartbucketInstance.getBucketByName(bucketName) : await this.smartbucketInstance.createBucket(bucketName); await bucket.fastPut({ path: 'images/00init', contents: 'init', overwrite: true }); this.imageDir = await bucket.getDirectoryFromPath({ path: '/images', }); // lets setup dockerstore await plugins.fsPromises.mkdir(paths.dockerImageStoreDir, { recursive: true }); this.dockerImageStore = new plugins.docker.DockerImageStore({ localDirPath: paths.dockerImageStoreDir, bucketDir: this.imageDir, }); } }