import * as plugins from './plugins.js'; import * as paths from './paths.js'; import { logger } from './logger.js'; import type { DockerHost } from './classes.host.js'; const smartfileFactory = plugins.smartfile.SmartFileFactory.nodeFs(); export interface IDockerImageStoreConstructorOptions { /** * used for preparing images for longer term storage */ localDirPath: string; /** * a smartbucket dir for longer term storage. */ bucketDir: plugins.smartbucket.Directory; } export class DockerImageStore { public options: IDockerImageStoreConstructorOptions; constructor(optionsArg: IDockerImageStoreConstructorOptions) { this.options = optionsArg; } // Method to store tar stream public async storeImage( imageName: string, tarStream: plugins.smartstream.stream.Readable, ): Promise { logger.log('info', `Storing image ${imageName}...`); const uniqueProcessingId = plugins.smartunique.shortId(); const initialTarDownloadPath = plugins.path.join( this.options.localDirPath, `${uniqueProcessingId}.tar`, ); const extractionDir = plugins.path.join( this.options.localDirPath, uniqueProcessingId, ); // Create a write stream to store the tar file const writeStream = plugins.fs.createWriteStream(initialTarDownloadPath); // lets wait for the write stream to finish await new Promise((resolve, reject) => { tarStream.pipe(writeStream); writeStream.on('finish', () => resolve()); writeStream.on('error', reject); }); logger.log( 'info', `Image ${imageName} stored locally for processing. Extracting...`, ); // lets process the image await plugins.smartarchive.SmartArchive.create() .file(initialTarDownloadPath) .extract(extractionDir); logger.log('info', `Image ${imageName} extracted.`); await plugins.fs.promises.rm(initialTarDownloadPath, { force: true }); logger.log('info', `deleted original tar to save space.`); logger.log('info', `now repackaging for s3...`); const smartfileIndexJson = await smartfileFactory.fromFilePath( plugins.path.join(extractionDir, 'index.json'), ); const smartfileManifestJson = await smartfileFactory.fromFilePath( plugins.path.join(extractionDir, 'manifest.json'), ); const smartfileOciLayoutJson = await smartfileFactory.fromFilePath( plugins.path.join(extractionDir, 'oci-layout'), ); // repositories file is optional in OCI image tars const repositoriesPath = plugins.path.join(extractionDir, 'repositories'); const hasRepositories = plugins.fs.existsSync(repositoriesPath); const smartfileRepositoriesJson = hasRepositories ? await smartfileFactory.fromFilePath(repositoriesPath) : null; const indexJson = JSON.parse(smartfileIndexJson.contents.toString()); const manifestJson = JSON.parse(smartfileManifestJson.contents.toString()); const ociLayoutJson = JSON.parse( smartfileOciLayoutJson.contents.toString(), ); if (indexJson.manifests?.[0]?.annotations) { indexJson.manifests[0].annotations['io.containerd.image.name'] = imageName; } if (manifestJson?.[0]?.RepoTags) { manifestJson[0].RepoTags[0] = imageName; } if (smartfileRepositoriesJson) { const repositoriesJson = JSON.parse( smartfileRepositoriesJson.contents.toString(), ); const repoFirstKey = Object.keys(repositoriesJson)[0]; const repoFirstValue = repositoriesJson[repoFirstKey]; repositoriesJson[imageName] = repoFirstValue; delete repositoriesJson[repoFirstKey]; smartfileRepositoriesJson.contents = Buffer.from( JSON.stringify(repositoriesJson, null, 2), ); } smartfileIndexJson.contents = Buffer.from( JSON.stringify(indexJson, null, 2), ); smartfileManifestJson.contents = Buffer.from( JSON.stringify(manifestJson, null, 2), ); smartfileOciLayoutJson.contents = Buffer.from( JSON.stringify(ociLayoutJson, null, 2), ); const writePromises = [ smartfileIndexJson.write(), smartfileManifestJson.write(), smartfileOciLayoutJson.write(), ]; if (smartfileRepositoriesJson) { writePromises.push(smartfileRepositoriesJson.write()); } await Promise.all(writePromises); logger.log('info', 'repackaging archive for s3...'); const tartools = new plugins.smartarchive.TarTools(); const newTarPack = await tartools.getDirectoryPackStream(extractionDir); const finalTarName = `${uniqueProcessingId}.processed.tar`; const finalTarPath = plugins.path.join( this.options.localDirPath, finalTarName, ); const finalWriteStream = plugins.fs.createWriteStream(finalTarPath); await new Promise((resolve, reject) => { newTarPack.pipe(finalWriteStream); finalWriteStream.on('finish', () => resolve()); finalWriteStream.on('error', reject); }); logger.log('ok', `Repackaged image ${imageName} for s3.`); await plugins.fs.promises.rm(extractionDir, { recursive: true, force: true }); // Remove existing file in bucket if it exists (smartbucket v4 no longer silently overwrites) try { await this.options.bucketDir.fastRemove({ path: `${imageName}.tar` }); } catch (e) { // File may not exist, which is fine } const finalTarReadStream = plugins.fs.createReadStream(finalTarPath); await this.options.bucketDir.fastPutStream({ stream: finalTarReadStream, path: `${imageName}.tar`, }); await plugins.fs.promises.rm(finalTarPath, { force: true }); } public async start() { // Ensure the local directory exists and is empty await plugins.fs.promises.rm(this.options.localDirPath, { recursive: true, force: true }); await plugins.fs.promises.mkdir(this.options.localDirPath, { recursive: true }); } public async stop() {} // Method to retrieve tar stream public async getImage( imageName: string, ): Promise { const imagePath = plugins.path.join( this.options.localDirPath, `${imageName}.tar`, ); if (!plugins.fs.existsSync(imagePath)) { throw new Error(`Image ${imageName} does not exist.`); } return plugins.fs.createReadStream(imagePath); } }