contentful-cli-export
Version:
Contentful CLI Export Environment
416 lines (367 loc) • 15.7 kB
JavaScript
#! /usr/bin/env node
const PLACEHOLDER_MANAGEMENT_TOKEN = 'placeholder-management-token'
const PLACEHOLDER_SPACE_ID = 'placeholder-space-id'
const DEFAULT_ALLOWED_LIMIT = 100
const DEFAULT_EXPORT_DIR = 'export/'
;(async function main() {
try {
const localWorkingDir = process.cwd()
const scriptDirectory = await getDirNamePath()
const envValues = await getEnvValues(localWorkingDir, scriptDirectory)
const cmsManagementToken =
envValues?.CMS_MANAGEMENT_TOKEN ?? PLACEHOLDER_MANAGEMENT_TOKEN
const cmsSpaceId = envValues?.CMS_SPACE_ID ?? PLACEHOLDER_SPACE_ID
const cmsMaxEntries =
parseInt(envValues?.CMS_MAX_ALLOWED_LIMIT, 10) ?? DEFAULT_ALLOWED_LIMIT
const cmsExportDir = envValues?.CMS_EXPORT_DIR ?? DEFAULT_EXPORT_DIR
const initialSettings = await parseArguments(
localWorkingDir,
cmsExportDir,
cmsManagementToken,
cmsSpaceId,
cmsMaxEntries
)
const options = await extractOptions(initialSettings)
await performExport(options, initialSettings)
process.exit(0)
} catch (error) {
console.error('@@/ERROR:', error)
process.exit(1)
}
})()
/**
* Reads environment values from .env files.
*
* @param {string} localWorkingDir - The directory path where the library is located.
* @param {string} scriptDirectory - The directory path where the script is running.
* @return {Promise<object>} The environment values.
* @property {string} CMS_MANAGEMENT_TOKEN - The CMA token for Contentful.
* @property {string} CMS_SPACE_ID - The Space ID.
* @property {string|number} CMS_MAX_ALLOWED_LIMIT - The maximum number of entries per query.
* @property {string} CMS_EXPORT_DIR - The default export dir from the working directory.
*
*/
async function getEnvValues(localWorkingDir, scriptDirectory) {
const { existsSync } = await import('node:fs')
const { config } = await import('dotenv')
const envDataFromPath = path =>
existsSync(path) ? config({ path }).parsed : {}
const paths = [
`${scriptDirectory}/../../.env`,
`${scriptDirectory}/../../.env.local`,
`${localWorkingDir}/.env`,
`${localWorkingDir}/.env.local`
]
const envValues = paths.map(envDataFromPath)
return Object.assign({}, ...envValues)
}
/**
* Parses command line arguments and sets default values.
*
* @param {string} rootFolder - The directory path where the .env files are located.
* @param {string} cmsExportDir - The CMS Default Export Directory.
* @param {string} cmsManagementToken - The CMS Management Token.
* @param {string} cmsSpaceId - The CMS Space ID.
* @param {number} [cmsMaxEntries=100] - The CMS Max Entries to fetch at each iteration.
* @returns {Promise<object>} The initial settings.
* @property {string} spaceId - The CMS Space ID.
* @property {string} environmentId - The CMS Environment ID.
* @property {string} managementToken - The CMS Management Token.
* @property {number} maxEntries - The maximum entries to be fetched in each iteration.
* @property {string} rootDestinationFolder - The root destination folder for exports.
* @property {string} defaultExportName - The default name for the export.
* @property {boolean} includeDrafts - Boolean indicating whether to include drafts.
* @property {boolean} includeAssets - Boolean indicating whether to include assets.
* @property {boolean} isVerbose - Boolean indicating verbose mode.
* @property {boolean} shouldCompressFolder - Boolean indicating whether to compress folder.
*
* @throws {Error} If '--environment-id' or '--from' are not provided or if '--management-token' or '--mt' are duplicated.
*/
async function parseArguments(
rootFolder,
cmsExportDir,
cmsManagementToken,
cmsSpaceId,
cmsMaxEntries = DEFAULT_ALLOWED_LIMIT
) {
const minimist = (await import('minimist')).default
const dateFormat = (await import('dateformat')).default
const parsedArgs = minimist(process.argv.slice(2))
await checkArgs(parsedArgs)
const {
'space-id': spaceId = cmsSpaceId,
'management-token': managementToken = parsedArgs['mt'] ??
cmsManagementToken,
'max-allowed-limit': maxEntries = cmsMaxEntries
} = parsedArgs
const rootDestinationFolder = await getDestinationFolder(
rootFolder,
cmsExportDir,
parsedArgs
)
const environmentId = parsedArgs.from || parsedArgs['environment-id']
if (!environmentId) {
console.error('@@/ERROR: An environment-id should be specified')
process.exit(1)
}
const now = new Date()
const currentDate = dateFormat(now, 'yyyy-mm-dd-HH-MM-ss')
const defaultExportName = currentDate + '-' + spaceId + '-' + environmentId
return {
managementToken,
spaceId,
environmentId,
maxEntries,
rootDestinationFolder,
defaultExportName,
includeDrafts: !parsedArgs.hasOwnProperty('only-published'),
includeAssets: parsedArgs.hasOwnProperty('download-assets'),
isVerbose: parsedArgs.hasOwnProperty('verbose'),
shouldCompressFolder: parsedArgs.hasOwnProperty('compress')
}
}
/**
* This function checks the arguments passed in the command line.
*
* @param {Object} parsedArgs - The object that contains the parsed command line arguments.
* @property {string} parsedArgs.from - The FROM environment
* @property {string} parsedArgs.environment-id - The FROM environment
* @property {string} parsedArgs.mt - The Contentful Management Token
* @property {string} parsedArgs.management-token - The Contentful Management Token
* @returns {Promise<void>} If it pass through, the arguments are validated.
*
* @throws {Error} If both 'from' and 'environment-id' options are specified or if neither is specified.
* @throws {Error} If both 'management-token' and 'mt' options are specified.
*/
async function checkArgs(parsedArgs) {
if (!(Boolean(parsedArgs.from) ^ Boolean(parsedArgs['environment-id']))) {
console.error(
"@@/ERROR: Only one of the two options '--environment-id' or '--from' should be specified"
)
process.exit(1)
}
if (Boolean(parsedArgs['management-token']) && Boolean(parsedArgs.mt)) {
console.error(
"@@/ERROR: Only one of the two options '--management-token' or '--mt' can be specified"
)
process.exit(1)
}
}
/**
* This function gets the destination folder based on whether a custom folder is provided or not.
*
* @param {string} rootFolder - The directory path where the script is being executed.
* @param {string} cmsExportDir - The CMS Default Export Directory.
* @param {Object} parsedArgs - The object that contains the parsed command line arguments.
*
* @returns {Promise<string>} The path of the evaluated destination folder.
* @property {string} destinationFolder - The destination folder for the export.
*
* @throws {Error} If the destination folder does not exist or is not accessible.
*/
async function getDestinationFolder(rootFolder, cmsExportDir, parsedArgs) {
/** @type {typeof import('node:fs')} */
const fileSystem = await import('node:fs')
const defaultExportDirectory = cmsExportDir.startsWith('/')
? cmsExportDir
: `${rootFolder}/${cmsExportDir}`
let destinationFolder = parsedArgs['export-dir'] || defaultExportDirectory
destinationFolder = destinationFolder.replace(/\/$/, '') + '/'
// Create destination folder if not present
const destinationFolderExists = fileSystem.existsSync(destinationFolder)
if (!parsedArgs['export-dir'] && !destinationFolderExists) {
fileSystem.mkdirSync(destinationFolder)
}
if (!fileSystem.existsSync(destinationFolder) || destinationFolder === '/') {
console.error(
'@@/ERROR: Destination folder does not exist or is not accessible!'
)
process.exit(1)
}
return destinationFolder
}
/**
* Extracts Contentful exporter options from the initial settings.
*
* @param {object} initialSettings - The initial settings obtained from command line arguments and .env files.
* @property {string} initialSettings.spaceId - The CMS Space ID.
* @property {string} initialSettings.environmentId - The CMS Environment ID.
* @property {string} initialSettings.managementToken - The CMS Management Token.
* @property {number} initialSettings.maxEntries - The maximum entries to be fetched in each iteration.
* @property {string} initialSettings.rootDestinationFolder - The root destination folder for exports.
* @property {string} initialSettings.defaultExportName - The default name for the export.
* @property {boolean} initialSettings.includeDrafts - Boolean indicating whether to include drafts.
* @property {boolean} initialSettings.includeAssets - Boolean indicating whether to include assets.
* @property {boolean} initialSettings.isVerbose - Boolean indicating verbose mode.
* @property {boolean} initialSettings.shouldCompressFolder - Boolean indicating whether to compress folder.
* @return {Promise<import("contentful-export/types.js").Options>} The options for performing the export.
*/
async function extractOptions(initialSettings) {
const contentfulManagement = await import('contentful-management')
const lib = await import('contentful-lib-helpers')
/** @type {typeof import('node:fs')} */
const fileSystem = await import('node:fs')
// Set up filename for export file and log
const isCompressed = initialSettings?.shouldCompressFolder
const rootFolder = initialSettings.rootDestinationFolder
const defaultExportName = initialSettings?.defaultExportName
const exportDirname = rootFolder + defaultExportName + '/'
const mainFolder = isCompressed ? rootFolder : exportDirname
let contentFile = defaultExportName + '.json'
let logFilePath = mainFolder + defaultExportName + '.log'
if (
!(await lib.getEnvironment(
contentfulManagement,
initialSettings.managementToken,
initialSettings.spaceId,
initialSettings.environmentId,
0
))
) {
console.error(
"@@/ERROR: Unable to retrieve Destination environment-id '" +
initialSettings?.environmentId +
"' for space-id '" +
initialSettings?.spaceId +
"'!"
)
console.error(
'@@/ERROR: Could also be that the management token or space-id are invalid.'
)
process.exit(1)
}
fileSystem.mkdirSync(exportDirname)
console.log(
'##/INFO: Export of space-id "' +
initialSettings?.spaceId +
'" and environment-id "' +
initialSettings?.environmentId +
'" started...'
)
console.log(
'##/INFO: Using destination: ' +
(isCompressed ? mainFolder + defaultExportName + '.zip' : mainFolder)
)
return {
managementToken: initialSettings?.managementToken,
spaceId: initialSettings?.spaceId,
environmentId: initialSettings?.environmentId,
exportDir: exportDirname,
contentFile: contentFile,
saveFile: true,
includeDrafts: initialSettings?.includeDrafts,
includeArchived: initialSettings?.includeDrafts,
downloadAssets: initialSettings?.includeAssets,
errorLogFile: logFilePath,
useVerboseRenderer: initialSettings?.isVerbose,
maxAllowedLimit: initialSettings?.maxEntries
}
}
/**
* Performs the export based on the provided options.
*
* @param {import("contentful-export/types.js").Options} options - The options for performing the export.
* @param {object} initialSettings - The initial settings obtained from command line arguments and .env files.
* @property {string} initialSettings.spaceId - The CMS Space ID.
* @property {string} initialSettings.environmentId - The CMS Environment ID.
* @property {string} initialSettings.managementToken - The CMS Management Token.
* @property {number} initialSettings.maxEntries - The maximum entries to be fetched in each iteration.
* @property {string} initialSettings.rootDestinationFolder - The root destination folder for exports.
* @property {string} initialSettings.defaultExportName - The default name for the export.
* @property {boolean} initialSettings.includeDrafts - Boolean indicating whether to include drafts.
* @property {boolean} initialSettings.includeAssets - Boolean indicating whether to include assets.
* @property {boolean} initialSettings.isVerbose - Boolean indicating verbose mode.
* @property {boolean} initialSettings.shouldCompressFolder - Boolean indicating whether to compress folder.
*
* @throws {Error} If there is an error during the ZIP file compress
*/
async function performExport(options, initialSettings) {
const contentfulExport = (await import('contentful-export')).default
const admZip = (await import('adm-zip')).default
/** @type {typeof import('node:fs')} */
const fileSystem = await import('node:fs')
await contentfulExport(options)
const rootExportFolder = initialSettings?.rootDestinationFolder
const defaultExportName = initialSettings?.defaultExportName
const destinationFolder = await buildFilePath(
rootExportFolder,
defaultExportName + '/'
)
const contentFile = await buildFilePath(
destinationFolder,
defaultExportName,
'json'
)
let logFile = await buildFilePath(destinationFolder, defaultExportName, 'log')
let zipFile = await buildFilePath(rootExportFolder, defaultExportName, 'zip')
if (initialSettings?.shouldCompressFolder) {
console.log('##/INFO: Assets exported. Creating the ZIP File')
if (fileSystem.existsSync(destinationFolder)) {
const zip = new admZip()
logFile = await buildFilePath(rootExportFolder, defaultExportName, 'log')
zip.addLocalFolder(destinationFolder, '')
zip.writeZip(zipFile)
await deleteFolderAfterZip(destinationFolder)
} else {
throw new Error('Error happened during ZIP file compression')
}
}
console.log('##/INFO: Export completed')
console.log('##/INFO: File Saved at:')
console.log(
'##/INFO: ' +
(initialSettings?.shouldCompressFolder ? zipFile : contentFile)
)
console.log('##/INFO: Log file (if present) at:')
console.log('##/INFO: ' + logFile)
}
/**
* Gets the current directory's path.
*
* @return {Promise<string>} The path of the current directory.
*/
async function getDirNamePath() {
const { fileURLToPath } = await import('node:url')
const { dirname } = await import('node:path')
const __filename = fileURLToPath(import.meta.url)
return dirname(__filename)
}
/**
* Constructs a file path based on the provided parameters.
*
* @param {string} rootFolder - The root folder for the file path.
* @param {string} [fileName=''] - The name of the file or subdirectory. If only `fileName` is provided, it is treated as a subdirectory.
* @param {string} [ext=''] - The file extension. If only `ext` is provided, `fileName` is treated as the extension.
*
* @returns {Promise<string>} - The constructed file path.
*
* @example
* buildFilePath('/rootFolder', 'subdirectory');
* // Returns: '/rootFolder/subdirectory'
*
* @example
* buildFilePath('/rootFolder', 'file', 'json');
* // Returns: '/rootFolder/file.json'
*
* @example
* buildFilePath('/rootFolder', '', 'zip');
* // Returns: '/rootFolder.zip'
*/
async function buildFilePath(rootFolder, fileName = '', ext = '') {
let filePath = rootFolder
filePath += fileName ? `${fileName}` : ''
filePath += ext ? `.${ext}` : ''
return filePath
}
/**
* Deletes the temporary destination folder after the ZIP file has been created.
*
* @param {string} destinationFolder - The folder to delete.
* @return {Promise<void>}
*/
async function deleteFolderAfterZip(destinationFolder) {
/** @type {typeof import('node:fs')} */
const fileSystem = await import('node:fs')
console.log('##/INFO: Deleting Temporary Destination Folder.... ')
fileSystem.rmSync(destinationFolder, { recursive: true })
}