wip-backup-manager
Version:
This utility enables backup & restore of Kentico Kontent projects
180 lines (147 loc) • 7.1 kB
text/typescript
import { AssetContracts } from '@kentico/kontent-management';
import * as fs from 'fs';
import { get } from 'https';
import JSZip = require('jszip');
import { IExportAllResult } from '../export';
import { IBinaryFile, IImportSource } from '../import';
import { IZipServiceConfig } from './zip.models';
export class ZipService {
private readonly zipExtension: string = '.zip';
private readonly contentTypesName: string = 'contentTypes.json';
private readonly contentItemsName: string = 'contentItems.json';
private readonly taxonomiesName: string = 'taxonomies.json';
private readonly assetsName: string = 'assets.json';
private readonly languageVariantsName: string = 'languageVariants.json';
private readonly contentTypeSnippetsName: string = 'contentTypesSnippets.json';
private readonly metadataName: string = 'metadata.json';
private readonly languages: string = 'languages.json';
private readonly filesName: string = 'files';
private readonly assetFoldersName: string = 'assetFolders.json';
private readonly validationName: string = 'validation.json';
private readonly filenameWithExtension: string;
constructor(private config: IZipServiceConfig) {
this.filenameWithExtension = config.filename + this.zipExtension;
}
public async extractZipAsync(): Promise<IImportSource> {
const filePath = `./${this.filenameWithExtension}`;
if (this.config.enableLog) {
console.log(`Reading file '${filePath}'`);
}
const file = await fs.promises.readFile(filePath);
if (this.config.enableLog) {
console.log(`Unzipping file`);
}
const unzippedFile = await JSZip.loadAsync(file);
if (this.config.enableLog) {
console.log(`Parsing zip contents`);
}
const assets = await this.readAndParseJsonFile(unzippedFile, this.assetsName);
const result = {
importData: {
assets,
contentTypes: await this.readAndParseJsonFile(unzippedFile, this.contentTypesName),
languageVariants: await this.readAndParseJsonFile(unzippedFile, this.languageVariantsName),
languages: await this.readAndParseJsonFile(unzippedFile, this.languages),
contentItems: await this.readAndParseJsonFile(unzippedFile, this.contentItemsName),
contentTypeSnippets: await this.readAndParseJsonFile(unzippedFile, this.contentTypeSnippetsName),
taxonomies: await this.readAndParseJsonFile(unzippedFile, this.taxonomiesName),
},
assetFolders: await this.readAndParseJsonFile(unzippedFile, this.assetFoldersName),
binaryFiles: await this.extractBinaryFilesAsync(unzippedFile, assets),
validation: await this.readAndParseJsonFile(unzippedFile, this.validationName),
metadata: await this.readAndParseJsonFile(unzippedFile, this.metadataName),
};
if (this.config.enableLog) {
console.log(`Pasing zip completed`);
}
return result;
}
public async createZipAsync(exportData: IExportAllResult): Promise<void> {
const zip = new JSZip();
if (this.config.enableLog) {
console.log(`Parsing json`);
}
zip.file(this.contentTypesName, JSON.stringify(exportData.data.contentTypes));
zip.file(this.validationName, JSON.stringify(exportData.validation));
zip.file(this.contentItemsName, JSON.stringify(exportData.data.contentItems));
zip.file(this.taxonomiesName, JSON.stringify(exportData.data.taxonomies));
zip.file(this.assetsName, JSON.stringify(exportData.data.assets));
zip.file(this.languageVariantsName, JSON.stringify(exportData.data.languageVariants));
zip.file(this.metadataName, JSON.stringify(exportData.metadata));
zip.file(this.languages, JSON.stringify(exportData.data.languages));
zip.file(this.contentTypeSnippetsName, JSON.stringify(exportData.data.contentTypeSnippets));
zip.file(this.assetFoldersName, JSON.stringify(exportData.data.assetFolders));
const assetsFolder = zip.folder(this.filesName);
if (this.config.enableLog) {
console.log(`Adding assets to zip`);
}
for (const asset of exportData.data.assets) {
const assetIdShortFolder = assetsFolder.folder(asset.id.substr(0, 3));
const assetIdFolder = assetIdShortFolder.folder(asset.id);
const assetFilename = asset.file_name;
assetIdFolder.file(assetFilename, this.getBinaryDataFromUrl(asset.url, this.config.enableLog), {
binary: true
});
}
const filePath = './' + this.filenameWithExtension;
if (this.config.enableLog) {
console.log(`Generating zip file '${filePath}'`);
}
const content = await zip.generateAsync({ type: 'nodebuffer' });
console.log(`Writing file '${filePath}'`);
await fs.promises.writeFile(filePath, content);
console.log(`File saved`);
}
private async extractBinaryFilesAsync(
zip: JSZip,
assets: AssetContracts.IAssetModelContract[]
): Promise<IBinaryFile[]> {
const binaryFiles: IBinaryFile[] = [];
const files = zip.files;
for (const asset of assets) {
const assetFile = files[this.getFullAssetPath(asset.id, asset.file_name)];
const binaryData = await assetFile.async('nodebuffer');
binaryFiles.push({
asset,
binaryData
});
}
return binaryFiles;
}
/**
* Gets path to asset within zip folder. Uses tree format using asset ids such as:
* "files/3b4/3b42f36c-2e67-4605-a8d3-fee2498e5224/image.jpg"
*/
private getFullAssetPath(assetId: string, filename: string): string {
return `${this.filesName}/${assetId.substr(0, 3)}/${assetId}/${filename}`;
}
private async readAndParseJsonFile(fileContents: any, filename: string): Promise<any> {
const files = fileContents.files;
const file = files[filename];
if (!file) {
throw Error(`Invalid file '${filename}'`);
}
const text = await file.async('text');
return JSON.parse(text);
}
private getBinaryDataFromUrl(url: string, enableLog: boolean): Promise<any> {
return new Promise((resolve, reject) => {
get(url, res => {
if (enableLog) {
console.log(`Downloading asset: ${url}`);
}
const data: any[] = [];
res.on('data', chunk => {
data.push(chunk);
})
.on('end', () => {
const buffer = Buffer.concat(data);
resolve(buffer);
})
.on('error', error => {
reject(error);
});
});
});
}
}