From 7a4e3a201bddbba7b8124173489e42ba2887d80f Mon Sep 17 00:00:00 2001 From: denniswo Date: Mon, 10 Aug 2026 13:59:39 +0200 Subject: [PATCH 1/4] SP-1173: merge the CUI cover sheet into artifacts that are already archives writeToFileWithGivenName serializes a payload into a fresh archive, which would nest a zip inside a zip for commands whose artifact is already one. writeZipToFileWithGivenName takes the archive bytes instead and adds the cover sheet alongside the existing entries, keeping the same three outcomes: original name, "Unclassified - " prefix, or "CUI - .zip". Includes-AI-Code: true Co-authored-by: Cursor --- src/core/utils/cui-file-service.ts | 28 +++++++++++ tests/core/utils/cui-file-service.spec.ts | 58 +++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/src/core/utils/cui-file-service.ts b/src/core/utils/cui-file-service.ts index 9d5c58b2..fbeaaaa9 100644 --- a/src/core/utils/cui-file-service.ts +++ b/src/core/utils/cui-file-service.ts @@ -36,16 +36,44 @@ export class CuiFileService { return this.writeClassifiedArchive(filename, data, cover); } + public async writeZipToFileWithGivenName(zipData: Buffer, filename: string): Promise { + const cover = await this.cuiApi.getCuiPdfCover(); + + if (cover === null) { + fileService.writeBufferToFileWithGivenName(zipData, filename); + return filename; + } + + if (!this.isClassified(cover)) { + const unclassifiedName = this.prefixFileName(filename, CuiFileService.UNCLASSIFIED_PREFIX); + fileService.writeBufferToFileWithGivenName(zipData, unclassifiedName); + return unclassifiedName; + } + + const zip = new AdmZip(zipData); + this.addCoverPage(zip, cover); + + return this.writeArchive(zip, filename); + } + private writeClassifiedArchive(filename: string, data: string, cover: CuiPdfCoverResponse): string { const zip = new AdmZip(); zip.addFile(path.basename(filename), Buffer.from(data, "utf-8"), "", FileConstants.DEFAULT_FILE_PERMISSIONS); + this.addCoverPage(zip, cover); + + return this.writeArchive(zip, filename); + } + + private addCoverPage(zip: AdmZip, cover: CuiPdfCoverResponse): void { zip.addFile( CuiFileService.COVER_SHEET_FILE_NAME, this.decodeCoverPage(cover), "", FileConstants.DEFAULT_FILE_PERMISSIONS ); + } + private writeArchive(zip: AdmZip, filename: string): string { const archiveName = this.buildClassifiedArchiveName(filename); fileService.writeBufferToFileWithGivenName(zip.toBuffer(), archiveName); diff --git a/tests/core/utils/cui-file-service.spec.ts b/tests/core/utils/cui-file-service.spec.ts index 0b91b57d..f47401bb 100644 --- a/tests/core/utils/cui-file-service.spec.ts +++ b/tests/core/utils/cui-file-service.spec.ts @@ -106,4 +106,62 @@ describe("CuiFileService", () => { .rejects.toThrow("CUI marking applies but the response contained no cover page."); }); }); + + describe("when the artifact is already an archive", () => { + const buildExportZip = (): Buffer => { + const zip = new AdmZip(); + zip.addFile("manifest.json", Buffer.from(JSON.stringify({ packageKey: "pkg-1" }))); + zip.addFile("nodes/node-1.json", Buffer.from(JSON.stringify({ key: "node-1" }))); + return zip.toBuffer(); + }; + + const entryNames = (filename: string): string[] => + new AdmZip(readFile(filename)).getEntries().map(entry => entry.entryName).sort(); + + it("Should keep the archive untouched when no marking applies", async () => { + mockAxiosGetWithStatus(COVER_URL, 204, ""); + const exportZip = buildExportZip(); + + const filename = await cuiFileService.writeZipToFileWithGivenName(exportZip, "export.zip"); + + expect(filename).toEqual("export.zip"); + expect(readFile(filename).equals(exportZip)).toBe(true); + }); + + it("Should only prefix the archive when the content is unclassified", async () => { + mockAxiosGetWithStatus(COVER_URL, 200, coverResponse([])); + const exportZip = buildExportZip(); + + const filename = await cuiFileService.writeZipToFileWithGivenName(exportZip, "export.zip"); + + expect(filename).toEqual("Unclassified - export.zip"); + expect(readFile(filename).equals(exportZip)).toBe(true); + }); + + it("Should add the cover sheet into the given archive instead of nesting it", async () => { + mockAxiosGetWithStatus(COVER_URL, 200, coverResponse([{ code: "PRVCY", name: "Privacy" }])); + + const filename = await cuiFileService.writeZipToFileWithGivenName(buildExportZip(), "export.zip"); + + expect(filename).toEqual("CUI - export.zip"); + expect(entryNames(filename)).toEqual([ + CuiFileService.COVER_SHEET_FILE_NAME, + "manifest.json", + "nodes/node-1.json", + ]); + + const archive = new AdmZip(readFile(filename)); + expect(archive.getEntry(CuiFileService.COVER_SHEET_FILE_NAME).getData().equals(PDF_BYTES)).toBe(true); + expect(archive.getEntries().some(entry => entry.entryName.endsWith(".zip"))).toBe(false); + }); + + it("Should fail when the marking applies but no cover page was returned", async () => { + mockAxiosGetWithStatus(COVER_URL, 200, { + resolvedCuiMarking: { categories: [{ code: "PRVCY", name: "Privacy" }] }, + }); + + await expect(cuiFileService.writeZipToFileWithGivenName(buildExportZip(), "export.zip")) + .rejects.toThrow("CUI marking applies but the response contained no cover page."); + }); + }); }); From a523167130b58a39424b74cd1f3edcd89e15050a Mon Sep 17 00:00:00 2001 From: denniswo Date: Mon, 10 Aug 2026 14:02:57 +0200 Subject: [PATCH 2/4] SP-1173: mark the archives the export commands write Route the five writers whose artifact is a single zip through writeZipToFileWithGivenName: config package export --zip, config branch export --zip, t2tc package export, export action-flows and the deprecated pull package. writeLocalArtifact, downloadZip and BaseManager.pullFile become async so they can await the marking decision; pullFile drops its Promise wrapper for plain async/await. writeStreamToFile also picks up the resolve(process.cwd(), ...) it was missing, matching every other writer. Includes-AI-Code: true Co-authored-by: Cursor --- .../action-flow/action-flow.service.ts | 6 ++-- .../branch-export-import.command.service.ts | 7 ++--- .../single-package-export.service.ts | 7 +++-- src/commands/studio/manager/space.manager.ts | 3 -- src/commands/t2tc/t2tc-package.service.ts | 9 ++---- src/core/http/http-shared/base.manager.ts | 30 ++++++++----------- 6 files changed, 25 insertions(+), 37 deletions(-) diff --git a/src/commands/action-flows/action-flow/action-flow.service.ts b/src/commands/action-flows/action-flow/action-flow.service.ts index 3da6dba8..32976c46 100644 --- a/src/commands/action-flows/action-flow/action-flow.service.ts +++ b/src/commands/action-flows/action-flow/action-flow.service.ts @@ -8,7 +8,6 @@ import { fileService, FileService } from "../../../core/utils/file-service"; import { CuiFileService } from "../../../core/utils/cui-file-service"; import { logger } from "../../../core/utils/logger"; import { FileConstants } from "../../../core/utils/file.constants"; -import { resolve } from "node:path"; export class ActionFlowService { public static readonly METADATA_FILE_NAME = "metadata.json"; @@ -35,9 +34,8 @@ export class ActionFlowService { } const fileName = "action-flows_export_" + uuidv4() + ".zip"; - const fullFilePath = resolve(process.cwd(), fileName); - zip.writeZip(fullFilePath, () => fs.chmodSync(fullFilePath, FileConstants.DEFAULT_FILE_PERMISSIONS)); - logger.info(FileService.fileDownloadedMessage + fileName); + const writtenFilename = await this.cuiFileService.writeZipToFileWithGivenName(zip.toBuffer(), fileName); + logger.info(FileService.fileDownloadedMessage + writtenFilename); } public async analyzeActionFlows(packageId: string, outputToJsonFile: boolean): Promise { diff --git a/src/commands/configuration-management/branch/branch-export-import.command.service.ts b/src/commands/configuration-management/branch/branch-export-import.command.service.ts index 1f3c765d..768fd43c 100644 --- a/src/commands/configuration-management/branch/branch-export-import.command.service.ts +++ b/src/commands/configuration-management/branch/branch-export-import.command.service.ts @@ -64,7 +64,7 @@ export class BranchExportImportCommandService { const branchPackageKey = BranchUtils.constructBranchKey(packageKey, branchKey); const sourceDir = await this.exportRewrittenPackageDir(branchPackageKey); try { - const message = this.writeLocalArtifact(sourceDir, packageKey, !!options.zip); + const message = await this.writeLocalArtifact(sourceDir, packageKey, !!options.zip); if (jsonResponse) { await this.writeJson({ packageKey: branchPackageKey, branchName: branchKey }); } else { @@ -152,12 +152,11 @@ export class BranchExportImportCommandService { return extractedDir; } - private writeLocalArtifact(sourceDir: string, packageKey: string, zip: boolean): string { + private async writeLocalArtifact(sourceDir: string, packageKey: string, zip: boolean): Promise { if (zip) { const zipPath = fileService.zipDirectoryAsSinglePackage(sourceDir); try { - const fileName = `${packageKey}.zip`; - fileService.writeBufferToFileWithGivenName(fs.readFileSync(zipPath), resolve(process.cwd(), fileName)); + const fileName = await this.cuiFileService.writeZipToFileWithGivenName(fs.readFileSync(zipPath), `${packageKey}.zip`); return FileService.fileDownloadedMessage + fileName; } finally { fs.rmSync(zipPath, { force: true }); diff --git a/src/commands/configuration-management/single-package-export.service.ts b/src/commands/configuration-management/single-package-export.service.ts index 7eb9001e..ad98ff52 100644 --- a/src/commands/configuration-management/single-package-export.service.ts +++ b/src/commands/configuration-management/single-package-export.service.ts @@ -4,16 +4,18 @@ import { fileService, FileService } from "../../core/utils/file-service"; import { logger } from "../../core/utils/logger"; import { GitService } from "../../core/git-profile/git/git.service"; import { SinglePackageExportApi } from "./api/single-package-export-api"; -import { resolve } from "node:path"; +import { CuiFileService } from "../../core/utils/cui-file-service"; export class SinglePackageExportService { private readonly singlePackageExportApi: SinglePackageExportApi; private readonly gitService: GitService; + private readonly cuiFileService: CuiFileService; constructor(context: Context) { this.singlePackageExportApi = new SinglePackageExportApi(context); this.gitService = new GitService(context); + this.cuiFileService = new CuiFileService(context); } public async exportPackage(packageKey: string, zip: boolean, gitBranch: string): Promise { @@ -25,8 +27,7 @@ export class SinglePackageExportService { } if (zip) { - const fileName = `${packageKey}.zip`; - fileService.writeBufferToFileWithGivenName(packageData, resolve(process.cwd(), fileName)); + const fileName = await this.cuiFileService.writeZipToFileWithGivenName(packageData, `${packageKey}.zip`); logger.info(FileService.fileDownloadedMessage + fileName); return; } diff --git a/src/commands/studio/manager/space.manager.ts b/src/commands/studio/manager/space.manager.ts index 67dd5518..2509b044 100644 --- a/src/commands/studio/manager/space.manager.ts +++ b/src/commands/studio/manager/space.manager.ts @@ -4,18 +4,15 @@ import { BaseManager } from "../../../core/http/http-shared/base.manager"; import { ManagerConfig } from "../../../core/http/http-shared/manager-config.interface"; import { SpaceTransport } from "../interfaces/space.interface"; import { logger } from "../../../core/utils/logger"; -import { CuiFileService } from "../../../core/utils/cui-file-service"; export class SpaceManager extends BaseManager { private static BASE_URL = "/package-manager/api/spaces"; private _jsonResponse: boolean; - private readonly cuiFileService: CuiFileService; constructor(context: Context) { super(context); - this.cuiFileService = new CuiFileService(context); } public get jsonResponse(): boolean { diff --git a/src/commands/t2tc/t2tc-package.service.ts b/src/commands/t2tc/t2tc-package.service.ts index 3cb96fb1..426455b1 100644 --- a/src/commands/t2tc/t2tc-package.service.ts +++ b/src/commands/t2tc/t2tc-package.service.ts @@ -21,7 +21,6 @@ import { StudioService } from "./studio.service"; import { GitService } from "../../core/git-profile/git/git.service"; import * as fs from "fs"; import { FileConstants } from "../../core/utils/file.constants"; -import { resolve } from "node:path"; export class T2tcPackageService { @@ -117,7 +116,7 @@ export class T2tcPackageService { logger.info("Successfully exported packages to branch: " + gitBranch); fs.rmSync(extractedDirectory, { recursive: true }); } else { - this.downloadZip(exportedPackagesZip, unzip); + await this.downloadZip(exportedPackagesZip, unzip); } } @@ -243,7 +242,7 @@ export class T2tcPackageService { return null; } - private downloadZip(exportedZip: AdmZip, unzip: boolean): void { + private async downloadZip(exportedZip: AdmZip, unzip: boolean): Promise { if (unzip) { const fileDownloadedMessage = "Successful download. Downloaded directory: "; const targetDirectoryName = `export_${uuidv4()}`; @@ -251,9 +250,7 @@ export class T2tcPackageService { logger.info(fileDownloadedMessage + targetDirectoryName); } else { const fileDownloadedMessage = "File downloaded successfully. New filename: "; - const filename = `export_${uuidv4()}.zip`; - const fullFilePath = resolve(process.cwd(), filename); - exportedZip.writeZip(fullFilePath, () => fs.chmodSync(fullFilePath, FileConstants.DEFAULT_FILE_PERMISSIONS)); + const filename = await this.cuiFileService.writeZipToFileWithGivenName(exportedZip.toBuffer(), `export_${uuidv4()}.zip`); logger.info(fileDownloadedMessage + filename); } } diff --git a/src/core/http/http-shared/base.manager.ts b/src/core/http/http-shared/base.manager.ts index 1e9295c3..b0327b86 100644 --- a/src/core/http/http-shared/base.manager.ts +++ b/src/core/http/http-shared/base.manager.ts @@ -5,13 +5,16 @@ import { ManagerConfig } from "./manager-config.interface"; import { HttpClient } from "../http-client"; import { Context } from "../../command/cli-context"; import { FileConstants } from "../../utils/file.constants"; +import { CuiFileService } from "../../utils/cui-file-service"; export abstract class BaseManager { private httpClient: () => HttpClient; + protected readonly cuiFileService: CuiFileService; protected readonly fileDownloadedMessage = "File downloaded successfully. New filename: "; protected constructor(context: Context) { this.httpClient = () => context.httpClient; + this.cuiFileService = new CuiFileService(context); } public async pull(): Promise { @@ -36,19 +39,14 @@ export abstract class BaseManager { } public async pullFile(): Promise { - return new Promise((resolve, reject) => { - this.httpClient() - .downloadFile(this.getConfig().pullUrl) - .then(data => { - const filename = this.writeStreamToFile(data); - logger.info(this.fileDownloadedMessage + filename); - resolve(); - }) - .catch(err => { - logger.error(new FatalError(err)); - reject(); - }); - }); + try { + const data = await this.httpClient().downloadFile(this.getConfig().pullUrl); + const filename = await this.writeStreamToFile(data); + logger.info(this.fileDownloadedMessage + filename); + } catch (err) { + logger.error(new FatalError(err)); + throw err; + } } public async push(): Promise { @@ -98,10 +96,8 @@ export abstract class BaseManager { return filename; } - protected writeStreamToFile(data: any): string { - const filename = this.getConfig().exportFileName; - fs.writeFileSync(filename, data, { mode: FileConstants.DEFAULT_FILE_PERMISSIONS }); - return filename; + protected async writeStreamToFile(data: Buffer): Promise { + return this.cuiFileService.writeZipToFileWithGivenName(data, this.getConfig().exportFileName); } protected writeToFileWithGivenName(data: any, filename: string): void { From fd8cc9cbd4c0e9bebd000076bc8b6d06ca423cd5 Mon Sep 17 00:00:00 2001 From: denniswo Date: Mon, 10 Aug 2026 14:06:11 +0200 Subject: [PATCH 3/4] SP-1173: cover CUI marking of archive exports One case per archive writer, asserting the logged name is the classified archive and that it holds the original entries plus the cover sheet. Includes-AI-Code: true Co-authored-by: Cursor --- .../commands/cui-marking-zip-commands.spec.ts | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 tests/commands/cui-marking-zip-commands.spec.ts diff --git a/tests/commands/cui-marking-zip-commands.spec.ts b/tests/commands/cui-marking-zip-commands.spec.ts new file mode 100644 index 00000000..bf66ad3f --- /dev/null +++ b/tests/commands/cui-marking-zip-commands.spec.ts @@ -0,0 +1,146 @@ +import { resolve } from "node:path"; +import * as fs from "node:fs"; +import { readFileSync } from "node:fs"; +import * as os from "node:os"; +import { Readable } from "stream"; +import AdmZip = require("adm-zip"); +import { mockAxiosGet, mockAxiosGetWithStatus, mockAxiosPost } from "../utls/http-requests-mock"; +import { testContext } from "../utls/test-context"; +import { loggingTestTransport } from "../jest.setup"; +import { FileService } from "../../src/core/utils/file-service"; +import { CuiFileService } from "../../src/core/utils/cui-file-service"; +import { ConfigUtils } from "../utls/config-utils"; +import { SinglePackageExportService } from "../../src/commands/configuration-management/single-package-export.service"; +import { BranchExportImportCommandService } from "../../src/commands/configuration-management/branch/branch-export-import.command.service"; +import { T2tcCommandService } from "../../src/commands/t2tc/t2tc-command.service"; +import { ActionFlowCommandService } from "../../src/commands/action-flows/action-flow/action-flow-command.service"; +import { PackageCommandService } from "../../src/commands/studio/command-service/package-command.service"; +import { PackageManifestTransport } from "../../src/commands/configuration-management/interfaces/package-export.interfaces"; + +const COVER_URL = "https://myTeam.celonis.cloud/api/team/cui-settings/cui-pdf-cover"; +const PDF_BYTES = Buffer.from("%PDF-1.4 cover sheet"); + +const PACKAGE_KEY = "pkg-1"; +const BRANCH = "feature-a"; +const PACKAGE_ID = "123-456-789"; +const T2TC_DOWNLOAD_MESSAGE = "File downloaded successfully. New filename: "; + +function markAsClassified(): void { + mockAxiosGetWithStatus(COVER_URL, 200, { + resolvedCuiMarking: { categories: [{ code: "PRVCY", name: "Privacy" }] }, + coverPage: { pdfContent: PDF_BYTES.toString("base64"), encoding: "base64" }, + }); +} + +function loggedFileName(prefix: string = FileService.fileDownloadedMessage): string { + const message = loggingTestTransport.logMessages.map(entry => entry.message).find(entry => entry.includes(prefix)); + return message.split(prefix)[1]; +} + +function markedArchive(prefix?: string): AdmZip { + const filename = loggedFileName(prefix); + expect(filename.startsWith(CuiFileService.CLASSIFIED_PREFIX)).toBe(true); + expect(filename.endsWith(".zip")).toBe(true); + + const archive = new AdmZip(readFileSync(resolve(process.cwd(), filename))); + expect(archive.getEntry(CuiFileService.COVER_SHEET_FILE_NAME).getData().equals(PDF_BYTES)).toBe(true); + + return archive; +} + +function entryNames(archive: AdmZip): string[] { + return archive.getEntries().map(entry => entry.entryName).sort(); +} + +function buildPackageZip(): Buffer { + const zip = new AdmZip(); + zip.addFile("package.json", Buffer.from(JSON.stringify({ key: PACKAGE_KEY, name: "My Package" }))); + zip.addFile("nodes/node-1.json", Buffer.from(JSON.stringify({ key: "node-1", type: "VIEW" }))); + return zip.toBuffer(); +} + +function seedPackageDir(packageKey: string): string { + const dir = fs.mkdtempSync(resolve(os.tmpdir(), "cui-zip-test-")); + fs.mkdirSync(resolve(dir, "nodes")); + fs.writeFileSync(resolve(dir, "package.json"), JSON.stringify({ key: packageKey, name: "My Package" })); + fs.writeFileSync(resolve(dir, "nodes", "root.json"), JSON.stringify({ key: "root", type: "FOLDER" })); + return dir; +} + +describe("CUI marking of archive exports", () => { + + beforeEach(() => { + markAsClassified(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("Should mark the archive of config package export --zip", async () => { + mockAxiosGet(`https://myTeam.celonis.cloud/pacman/api/core/staging/packages/${PACKAGE_KEY}/export-file`, buildPackageZip()); + + await new SinglePackageExportService(testContext).exportPackage(PACKAGE_KEY, true, null); + + expect(loggedFileName()).toEqual(`${CuiFileService.CLASSIFIED_PREFIX}${PACKAGE_KEY}.zip`); + expect(entryNames(markedArchive())).toEqual([ + CuiFileService.COVER_SHEET_FILE_NAME, + "nodes/node-1.json", + "package.json", + ]); + }); + + it("Should mark the archive of config branch export --zip", async () => { + const branchPackageKey = `${PACKAGE_KEY}@${BRANCH}`; + mockAxiosGet(`https://myTeam.celonis.cloud/pacman/api/core/staging/packages/${branchPackageKey}/export-file`, buildPackageZip()); + jest.spyOn(FileService.prototype, "extractZipBufferToTempDirectory").mockReturnValue(seedPackageDir(branchPackageKey)); + + await new BranchExportImportCommandService(testContext).exportBranch(PACKAGE_KEY, BRANCH, { zip: true }); + + expect(loggedFileName()).toEqual(`${CuiFileService.CLASSIFIED_PREFIX}${PACKAGE_KEY}.zip`); + + const archive = markedArchive(); + expect(entryNames(archive)).toContain("package.json"); + expect(JSON.parse(archive.getEntry("package.json").getData().toString()).key).toEqual(PACKAGE_KEY); + }); + + it("Should mark the archive of t2tc package export", async () => { + const manifest: PackageManifestTransport[] = [ConfigUtils.buildManifestForKeyAndFlavor("key-1", "TEST")]; + mockAxiosGet( + "https://myTeam.celonis.cloud/package-manager/api/core/packages/export/batch?packageKeys=key-1&withDependencies=false", + ConfigUtils.buildBatchExportZip(manifest, []).toBuffer() + ); + mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/batch/variables-with-assignments", []); + + await new T2tcCommandService(testContext).batchExportPackages(["key-1"], undefined, false, null, false); + + expect(entryNames(markedArchive(T2TC_DOWNLOAD_MESSAGE))).toContain("manifest.json"); + }); + + it("Should mark the archive of export action-flows", async () => { + const actionFlowFileName = "20240711-scenario-1234.json"; + const actionFlows = new AdmZip(); + actionFlows.addFile(actionFlowFileName, Buffer.from(JSON.stringify({ name: "Automation" }))); + mockAxiosGet(`https://myTeam.celonis.cloud/ems-automation/api/root/${PACKAGE_ID}/export/assets`, actionFlows.toBuffer()); + + await new ActionFlowCommandService(testContext).exportActionFlows(PACKAGE_ID, null); + + expect(entryNames(markedArchive())).toEqual([actionFlowFileName, CuiFileService.COVER_SHEET_FILE_NAME].sort()); + }); + + it("Should mark the archive of the deprecated pull package", async () => { + mockAxiosPost( + `https://myTeam.celonis.cloud/package-manager/api/packages/${PACKAGE_KEY}/export?store=false&draft=false`, + Readable.from(buildPackageZip()) + ); + + await new PackageCommandService(testContext).pullPackage(PACKAGE_KEY, false, null, false); + + expect(loggedFileName()).toEqual(`${CuiFileService.CLASSIFIED_PREFIX}package_${PACKAGE_KEY}.zip`); + expect(entryNames(markedArchive())).toEqual([ + CuiFileService.COVER_SHEET_FILE_NAME, + "nodes/node-1.json", + "package.json", + ]); + }); +}); From 2282d1462f2dbcb716a911aad357a17f243bf550 Mon Sep 17 00:00:00 2001 From: denniswo Date: Mon, 10 Aug 2026 15:05:21 +0200 Subject: [PATCH 4/4] SP-1173: fold the two CUI write paths into one decision Both entry points repeated the cover fetch and the unmarked and unclassified branches, differing only in the classified outcome. writeWithCoverHandling owns the decision and takes that outcome as a callback; writePayload picks the fileService method for the payload it is given and returns the name it wrote. Includes-AI-Code: true Co-authored-by: Cursor --- src/core/utils/cui-file-service.ts | 50 +++++++++++++++++------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/core/utils/cui-file-service.ts b/src/core/utils/cui-file-service.ts index fbeaaaa9..cfc739c0 100644 --- a/src/core/utils/cui-file-service.ts +++ b/src/core/utils/cui-file-service.ts @@ -20,40 +20,46 @@ export class CuiFileService { } public async writeToFileWithGivenName(data: string, filename: string): Promise { - const cover = await this.cuiApi.getCuiPdfCover(); - - if (cover === null) { - fileService.writeToFileWithGivenName(data, filename); - return filename; - } + return this.writeWithCoverHandling(data, filename, cover => + this.writeClassifiedArchive(filename, data, cover) + ); + } - if (!this.isClassified(cover)) { - const unclassifiedName = this.prefixFileName(filename, CuiFileService.UNCLASSIFIED_PREFIX); - fileService.writeToFileWithGivenName(data, unclassifiedName); - return unclassifiedName; - } + public async writeZipToFileWithGivenName(zipData: Buffer, filename: string): Promise { + return this.writeWithCoverHandling(zipData, filename, cover => { + const zip = new AdmZip(zipData); + this.addCoverPage(zip, cover); - return this.writeClassifiedArchive(filename, data, cover); + return this.writeArchive(zip, filename); + }); } - public async writeZipToFileWithGivenName(zipData: Buffer, filename: string): Promise { + private async writeWithCoverHandling( + payload: string | Buffer, + filename: string, + onClassified: (cover: CuiPdfCoverResponse) => Promise | string + ): Promise { const cover = await this.cuiApi.getCuiPdfCover(); - if (cover === null) { - fileService.writeBufferToFileWithGivenName(zipData, filename); - return filename; + if (!cover) { + return this.writePayload(payload, filename); } if (!this.isClassified(cover)) { - const unclassifiedName = this.prefixFileName(filename, CuiFileService.UNCLASSIFIED_PREFIX); - fileService.writeBufferToFileWithGivenName(zipData, unclassifiedName); - return unclassifiedName; + return this.writePayload(payload, this.prefixFileName(filename, CuiFileService.UNCLASSIFIED_PREFIX)); } - const zip = new AdmZip(zipData); - this.addCoverPage(zip, cover); + return onClassified(cover); + } - return this.writeArchive(zip, filename); + private writePayload(payload: string | Buffer, filename: string): string { + if (Buffer.isBuffer(payload)) { + fileService.writeBufferToFileWithGivenName(payload, filename); + } else { + fileService.writeToFileWithGivenName(payload, filename); + } + + return filename; } private writeClassifiedArchive(filename: string, data: string, cover: CuiPdfCoverResponse): string {