Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fix-no-download-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@salesforce/b2c-cli': patch
'@salesforce/b2c-tooling-sdk': patch
---

Fix `--no-download` flag on `job export` to actually skip downloading the archive from the instance
50 changes: 28 additions & 22 deletions packages/b2c-cli/src/commands/job/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import {Flags} from '@oclif/core';
import {JobCommand} from '@salesforce/b2c-tooling-sdk/cli';
import {
siteArchiveExport,
siteArchiveExportToPath,
JobExecutionError,
type SiteArchiveExportResult,
type ExportDataUnitsConfiguration,
type WaitForJobOptions,
} from '@salesforce/b2c-tooling-sdk/operations/jobs';
import {t, withDocs} from '../../i18n/index.js';

Expand Down Expand Up @@ -100,10 +102,11 @@
};

protected operations = {
siteArchiveExport,
siteArchiveExportToPath,
};

async run(): Promise<SiteArchiveExportResult & {localPath?: string}> {
async run(): Promise<SiteArchiveExportResult & {localPath?: string; archiveKept?: boolean}> {

Check warning on line 109 in packages/b2c-cli/src/commands/job/export.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

Async method 'run' has a complexity of 21. Maximum allowed is 20

Check warning on line 109 in packages/b2c-cli/src/commands/job/export.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Async method 'run' has a complexity of 21. Maximum allowed is 20
this.requireOAuthCredentials();
this.requireWebDavCredentials();

Expand Down Expand Up @@ -167,8 +170,7 @@
return {
execution: {execution_status: 'finished', exit_status: {code: 'skipped'}},
archiveFilename: '',
archiveKept: false,
} as unknown as SiteArchiveExportResult & {localPath?: string};
} as unknown as SiteArchiveExportResult & {localPath?: string; archiveKept?: boolean};
}

this.log(
Expand All @@ -179,25 +181,29 @@

this.log(t('commands.job.export.dataUnits', 'Data units: {{dataUnits}}', {dataUnits: JSON.stringify(dataUnits)}));

const waitOptions: WaitForJobOptions = {
timeout: timeout ? timeout * 1000 : undefined,
onProgress: (exec, elapsed) => {
if (!this.jsonEnabled()) {
const elapsedSec = Math.floor(elapsed / 1000);
this.log(
t('commands.job.export.progress', ' Status: {{status}} ({{elapsed}}s elapsed)', {
status: exec.execution_status,
elapsed: elapsedSec.toString(),
}),
);
}
},
};

try {
const result = await this.operations.siteArchiveExportToPath(this.instance, dataUnits, output, {
keepArchive: keepArchive || noDownload,
extractZip: !zipOnly,
waitOptions: {
timeout: timeout ? timeout * 1000 : undefined,
onProgress: (exec, elapsed) => {
if (!this.jsonEnabled()) {
const elapsedSec = Math.floor(elapsed / 1000);
this.log(
t('commands.job.export.progress', ' Status: {{status}} ({{elapsed}}s elapsed)', {
status: exec.execution_status,
elapsed: elapsedSec.toString(),
}),
);
}
},
},
});
const result: SiteArchiveExportResult & {localPath?: string; archiveKept?: boolean} = noDownload
? await this.operations.siteArchiveExport(this.instance, dataUnits, {waitOptions})
: await this.operations.siteArchiveExportToPath(this.instance, dataUnits, output, {
keepArchive,
extractZip: !zipOnly,
waitOptions,
});

const durationSec = result.execution.duration ? (result.execution.duration / 1000).toFixed(1) : 'N/A';
this.log(
Expand All @@ -215,7 +221,7 @@
);
}

if (result.archiveKept) {
if (noDownload || result.archiveKept) {
this.log(
t('commands.job.export.archiveKept', 'Archive kept at: Impex/src/instance/{{filename}}', {
filename: result.archiveFilename,
Expand Down
16 changes: 9 additions & 7 deletions packages/b2c-cli/test/commands/job/export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,11 @@ describe('job export', () => {
expect(result.execution.exit_status.code).to.equal('skipped');
});

it('passes keepArchive when --no-download is set', async () => {
it('calls siteArchiveExport (not siteArchiveExportToPath) when --no-download is set', async () => {
const command: any = await createCommand({
output: './export',
'global-data': 'meta_data',
'no-download': true,
'zip-only': true,
json: true,
});
stubCommon(command);
Expand All @@ -128,15 +127,18 @@ describe('job export', () => {
const exportStub = sinon.stub().resolves({
execution: {execution_status: 'finished', exit_status: {code: 'OK'}} as any,
archiveFilename: 'a.zip',
archiveKept: true,
});
command.operations = {...command.operations, siteArchiveExportToPath: exportStub};
const exportToPathStub = sinon.stub().rejects(new Error('Should not be called'));
command.operations = {
...command.operations,
siteArchiveExport: exportStub,
siteArchiveExportToPath: exportToPathStub,
};

await command.run();

const options = exportStub.getCall(0).args[3];
expect(options.keepArchive).to.equal(true);
expect(options.extractZip).to.equal(false);
expect(exportStub.calledOnce).to.equal(true);
expect(exportToPathStub.called).to.equal(false);
});

it('shows job log and errors on JobExecutionError when show-log is true', async () => {
Expand Down
7 changes: 2 additions & 5 deletions packages/b2c-tooling-sdk/src/operations/content/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as path from 'node:path';
import JSZip from 'jszip';
import type {B2CInstance} from '../../instance/index.js';
import {getLogger} from '../../logging/logger.js';
import {siteArchiveExport} from '../jobs/site-archive.js';
import {siteArchiveExportToBuffer} from '../jobs/site-archive.js';
import {Library, LibraryNode} from './library.js';
import type {
FetchContentLibraryOptions,
Expand Down Expand Up @@ -68,10 +68,7 @@ export async function fetchContentLibrary(

const dataUnits = isSiteLibrary ? {sites: {[libraryId]: {content: true}}} : {libraries: {[libraryId]: true}};

const result = await siteArchiveExport(instance, dataUnits, {waitOptions});
if (!result.data) {
throw new Error('No archive data returned from export');
}
const result = await siteArchiveExportToBuffer(instance, dataUnits, {waitOptions});

archiveData = result.data;
const zip = await JSZip.loadAsync(result.data);
Expand Down
7 changes: 6 additions & 1 deletion packages/b2c-tooling-sdk/src/operations/jobs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ export type {
} from './run.js';

// Site archive import/export
export {siteArchiveImport, siteArchiveExport, siteArchiveExportToPath} from './site-archive.js';
export {
siteArchiveImport,
siteArchiveExport,
siteArchiveExportToBuffer,
siteArchiveExportToPath,
} from './site-archive.js';

export type {
SiteArchiveImportOptions,
Expand Down
77 changes: 52 additions & 25 deletions packages/b2c-tooling-sdk/src/operations/jobs/site-archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,6 @@ export interface ExportDataUnitsConfiguration {
* Options for site archive export.
*/
export interface SiteArchiveExportOptions {
/** Keep archive on instance after download (default: false) */
keepArchive?: boolean;
/** Wait options for job completion */
waitOptions?: WaitForJobOptions;
}
Expand All @@ -343,10 +341,6 @@ export interface SiteArchiveExportResult {
execution: JobExecution;
/** Archive filename on instance */
archiveFilename: string;
/** Archive content as buffer (if downloaded) */
data?: Buffer;
/** Whether archive was kept on instance */
archiveKept: boolean;
}

/**
Expand Down Expand Up @@ -384,13 +378,12 @@ export async function siteArchiveExport(
options: SiteArchiveExportOptions = {},
): Promise<SiteArchiveExportResult> {
const logger = getLogger();
const {keepArchive = false, waitOptions} = options;
const {waitOptions} = options;

// Generate archive filename
const timestamp = new Date().toISOString().replace(/[:.-]+/g, '');
const archiveDirName = `${timestamp}_export`;
const zipFilename = `${archiveDirName}.zip`;
const webdavPath = `Impex/src/instance/${zipFilename}`;

logger.debug({jobId: EXPORT_JOB_ID, dataUnits}, `Executing ${EXPORT_JOB_ID} job`);

Expand Down Expand Up @@ -450,32 +443,70 @@ export async function siteArchiveExport(
throw error;
}

// Download archive
return {
execution,
archiveFilename: zipFilename,
};
}

/**
* Exports a site archive and downloads it to memory.
*
* Runs the export job on the instance, downloads the archive via WebDAV,
* and returns the data as a Buffer. Optionally keeps the archive on the instance.
*
* @param instance - B2C instance to export from
* @param dataUnits - Data units configuration specifying what to export
* @param options - Export and download options
* @returns Export result with archive data buffer
*
* @example
* ```typescript
* const result = await siteArchiveExportDownload(instance, {
* global_data: { meta_data: true }
* });
* const zip = await JSZip.loadAsync(result.data);
* ```
*/
export async function siteArchiveExportToBuffer(
instance: B2CInstance,
dataUnits: Partial<ExportDataUnitsConfiguration>,
options: SiteArchiveExportOptions & {keepArchive?: boolean} = {},
): Promise<SiteArchiveExportResult & {data: Buffer; archiveKept: boolean}> {
const logger = getLogger();
const {keepArchive = false, ...exportOptions} = options;

const result = await siteArchiveExport(instance, dataUnits, exportOptions);

// Download archive from instance via WebDAV
const webdavPath = `Impex/src/instance/${result.archiveFilename}`;
logger.debug({path: webdavPath}, `Downloading archive: ${webdavPath}`);
const archiveData = await instance.webdav.get(webdavPath);
const data = Buffer.from(await instance.webdav.get(webdavPath));

// Clean up if not keeping
// Clean up from instance if not keeping
if (!keepArchive) {
await instance.webdav.delete(webdavPath);
logger.debug({path: webdavPath}, `Archive deleted: ${webdavPath}`);
}

return {
execution,
archiveFilename: zipFilename,
data: Buffer.from(archiveData),
...result,
data,
archiveKept: keepArchive,
};
}

/**
* Exports a site archive and saves it to a local path.
* Exports a site archive, downloads it, and saves it to a local path.
*
* Runs the export job on the instance, downloads the archive via WebDAV,
* and saves it locally. Optionally keeps the archive on the instance.
*
* @param instance - B2C instance to export from
* @param dataUnits - Data units configuration
* @param outputPath - Local path to save the archive
* @param options - Export options
* @returns Export result
* @param options - Export and download options
* @returns Export result with local path
*
* @example
* ```typescript
Expand All @@ -490,16 +521,12 @@ export async function siteArchiveExportToPath(
instance: B2CInstance,
dataUnits: Partial<ExportDataUnitsConfiguration>,
outputPath: string,
options: SiteArchiveExportOptions & {extractZip?: boolean} = {},
): Promise<SiteArchiveExportResult & {localPath: string}> {
options: SiteArchiveExportOptions & {keepArchive?: boolean; extractZip?: boolean} = {},
): Promise<SiteArchiveExportResult & {localPath: string; archiveKept: boolean}> {
const logger = getLogger();
const {extractZip = true, ...exportOptions} = options;

const result = await siteArchiveExport(instance, dataUnits, exportOptions);
const {extractZip = true, ...downloadOptions} = options;

if (!result.data) {
throw new Error('No archive data returned');
}
const result = await siteArchiveExportToBuffer(instance, dataUnits, downloadOptions);

// Determine output handling
const isZipPath = outputPath.endsWith('.zip');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,9 @@ describe('operations/jobs/site-archive', () => {
expect(content.toString()).to.include('test-export-data');
});

it('should export without downloading when localPath is not provided', async () => {
it('should run export job without downloading the archive', async () => {
let webdavGetRequested = false;

server.use(
http.post(`${OCAPI_BASE}/jobs/sfcc-site-archive-export/executions`, () => {
return HttpResponse.json({
Expand All @@ -379,14 +381,12 @@ describe('operations/jobs/site-archive', () => {
});
}),
http.get(`${WEBDAV_BASE}/Impex/src/instance/*`, () => {
webdavGetRequested = true;
return new HttpResponse(Buffer.from('PK\x03\x04test-data'), {
status: 200,
headers: {'Content-Type': 'application/zip'},
});
}),
http.delete(`${WEBDAV_BASE}/Impex/src/instance/*`, () => {
return new HttpResponse(null, {status: 204});
}),
);

const result = await siteArchiveExport(
Expand All @@ -396,7 +396,8 @@ describe('operations/jobs/site-archive', () => {
);

expect(result.execution.id).to.equal('export-2');
expect(result.data).to.be.instanceOf(Buffer);
expect(webdavGetRequested).to.be.false;
expect(result).to.not.have.property('data');
});

it('should throw JobExecutionError when export fails', async () => {
Expand Down Expand Up @@ -449,15 +450,6 @@ describe('operations/jobs/site-archive', () => {
is_log_file_existing: false,
});
}),
http.get(`${WEBDAV_BASE}/Impex/src/instance/*`, () => {
return new HttpResponse(Buffer.from('PK\x03\x04test-data'), {
status: 200,
headers: {'Content-Type': 'application/zip'},
});
}),
http.delete(`${WEBDAV_BASE}/Impex/src/instance/*`, () => {
return new HttpResponse(null, {status: 204});
}),
);

const result = await siteArchiveExport(
Expand Down
Loading