|
| 1 | +import { resolve as pResolve } from 'node:path'; |
| 2 | +import { Readable } from 'node:stream'; |
| 3 | +import { mkdir, writeFile } from 'node:fs/promises'; |
| 4 | +import { configHandler, log } from '@contentstack/cli-utilities'; |
| 5 | + |
| 6 | +import type { AssetManagementAPIConfig, LinkedWorkspace } from '../types/asset-management-api'; |
| 7 | +import type { ExportContext } from '../types/export-types'; |
| 8 | +import { AssetManagementExportAdapter } from './base'; |
| 9 | +import { getAssetItems, writeStreamToFile } from '../utils/export-helpers'; |
| 10 | +import { PROCESS_NAMES, PROCESS_STATUS } from '../constants/index'; |
| 11 | + |
| 12 | +export default class ExportAssets extends AssetManagementExportAdapter { |
| 13 | + constructor(apiConfig: AssetManagementAPIConfig, exportContext: ExportContext) { |
| 14 | + super(apiConfig, exportContext); |
| 15 | + } |
| 16 | + |
| 17 | + async start(workspace: LinkedWorkspace, spaceDir: string): Promise<void> { |
| 18 | + await this.init(); |
| 19 | + const assetsDir = pResolve(spaceDir, 'assets'); |
| 20 | + await mkdir(assetsDir, { recursive: true }); |
| 21 | + log.debug(`Fetching folders and assets for space ${workspace.space_uid}`, this.exportContext.context); |
| 22 | + |
| 23 | + const [folders, assetsData] = await Promise.all([ |
| 24 | + this.getWorkspaceFolders(workspace.space_uid), |
| 25 | + this.getWorkspaceAssets(workspace.space_uid), |
| 26 | + ]); |
| 27 | + |
| 28 | + await writeFile(pResolve(assetsDir, 'folders.json'), JSON.stringify(folders, null, 2)); |
| 29 | + this.tick(true, `folders: ${workspace.space_uid}`, null); |
| 30 | + log.debug(`Wrote folders.json for space ${workspace.space_uid}`, this.exportContext.context); |
| 31 | + |
| 32 | + const assetItems = getAssetItems(assetsData); |
| 33 | + log.debug( |
| 34 | + assetItems.length === 0 |
| 35 | + ? `No assets for space ${workspace.space_uid}, wrote empty assets.json` |
| 36 | + : `Writing ${assetItems.length} assets metadata for space ${workspace.space_uid}`, |
| 37 | + this.exportContext.context, |
| 38 | + ); |
| 39 | + await this.writeItemsToChunkedJson( |
| 40 | + assetsDir, |
| 41 | + 'assets.json', |
| 42 | + 'assets', |
| 43 | + ['uid', 'url', 'filename', 'file_name', 'parent_uid'], |
| 44 | + assetItems, |
| 45 | + ); |
| 46 | + this.tick(true, `assets: ${workspace.space_uid} (${assetItems.length})`, null); |
| 47 | + |
| 48 | + await this.downloadWorkspaceAssets(assetsData, assetsDir, workspace.space_uid); |
| 49 | + } |
| 50 | + |
| 51 | + private async downloadWorkspaceAssets( |
| 52 | + assetsData: unknown, |
| 53 | + assetsDir: string, |
| 54 | + spaceUid: string, |
| 55 | + ): Promise<void> { |
| 56 | + const items = getAssetItems(assetsData); |
| 57 | + if (items.length === 0) { |
| 58 | + log.debug('No assets to download', this.exportContext.context); |
| 59 | + return; |
| 60 | + } |
| 61 | + |
| 62 | + this.updateStatus(PROCESS_STATUS[PROCESS_NAMES.AM_DOWNLOADS].DOWNLOADING); |
| 63 | + log.debug(`Downloading ${items.length} asset file(s) for space ${spaceUid}...`, this.exportContext.context); |
| 64 | + const filesDir = pResolve(assetsDir, 'files'); |
| 65 | + await mkdir(filesDir, { recursive: true }); |
| 66 | + |
| 67 | + const securedAssets = this.exportContext.securedAssets ?? false; |
| 68 | + const authtoken = securedAssets ? configHandler.get('authtoken') : null; |
| 69 | + let lastError: string | null = null; |
| 70 | + let allSuccess = true; |
| 71 | + |
| 72 | + for (const asset of items) { |
| 73 | + const uid = asset.uid ?? asset._uid; |
| 74 | + const url = asset.url; |
| 75 | + const filename = asset.filename ?? asset.file_name ?? 'asset'; |
| 76 | + if (!url || !uid) continue; |
| 77 | + try { |
| 78 | + const separator = url.includes('?') ? '&' : '?'; |
| 79 | + const downloadUrl = securedAssets && authtoken ? `${url}${separator}authtoken=${authtoken}` : url; |
| 80 | + const response = await fetch(downloadUrl); |
| 81 | + if (!response.ok) throw new Error(`HTTP ${response.status}`); |
| 82 | + const body = response.body; |
| 83 | + if (!body) throw new Error('No response body'); |
| 84 | + const nodeStream = Readable.fromWeb(body as Parameters<typeof Readable.fromWeb>[0]); |
| 85 | + const assetFolderPath = pResolve(filesDir, uid); |
| 86 | + await mkdir(assetFolderPath, { recursive: true }); |
| 87 | + const filePath = pResolve(assetFolderPath, filename); |
| 88 | + await writeStreamToFile(nodeStream, filePath); |
| 89 | + log.debug(`Downloaded asset ${uid}`, this.exportContext.context); |
| 90 | + } catch (e) { |
| 91 | + allSuccess = false; |
| 92 | + lastError = (e as Error)?.message ?? PROCESS_STATUS[PROCESS_NAMES.AM_DOWNLOADS].FAILED; |
| 93 | + log.debug(`Failed to download asset ${uid}: ${e}`, this.exportContext.context); |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + this.tick(allSuccess, `downloads: ${spaceUid}`, lastError); |
| 98 | + log.debug('Asset downloads completed', this.exportContext.context); |
| 99 | + } |
| 100 | +} |
0 commit comments