Files
docker/ts/classes.imagestore.ts

181 lines
6.2 KiB
TypeScript

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<void> {
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<void>((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<void>((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<plugins.smartstream.stream.Readable> {
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);
}
}