Skip to content

Commit bc94e30

Browse files
committed
feat: archive and explorer utils
1 parent ced6931 commit bc94e30

7 files changed

Lines changed: 378 additions & 96 deletions

File tree

src/archive.utils.ts

Lines changed: 171 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,148 @@
11
import http, { RefinedResponse, ResponseType } from "k6/http";
22
import { getHeaders } from "./user.utils";
3-
import { check, fail } from "k6";
3+
import { bytes, check, fail } from "k6";
44
//@ts-ignore
55
import { FormData } from "https://jslib.k6.io/formdata/0.0.2/index.js";
6+
import {
7+
AppImportAnalysis,
8+
AppImportResult,
9+
ImportAnalysisResult,
10+
} from "./models";
611

712
const rootUrl = __ENV.ROOT_URL;
813

9-
export function launchExport(apps: string[]): RefinedResponse<ResponseType | undefined> {
14+
export const EXPORT_TIMEOUT = 30 * 60 * 1000; // 30 minutes
15+
16+
export function importArchive(
17+
fileData: bytes,
18+
): RefinedResponse<ResponseType | undefined> {
19+
let headers = getHeaders();
20+
const fd = new FormData();
21+
const contentType = "application/zip";
22+
//@ts-ignore
23+
fd.append("file", http.file(fileData, "archive.zip", contentType));
24+
//@ts-ignore
25+
headers["Content-Type"] = "multipart/form-data; boundary=" + fd.boundary;
26+
return http.post(`${rootUrl}/archive/import/upload`, fd.body(), { headers });
27+
}
28+
29+
export function importArchiveOrFail(fileData: bytes): string {
30+
const res = importArchive(fileData);
31+
if (res.status !== 200) {
32+
fail(`Failed to import archive. Response: ${res.status} - ${res.body}`);
33+
}
34+
return res.json("importId") as string;
35+
}
36+
37+
export function analyzeImport(
38+
importId: string,
39+
): RefinedResponse<ResponseType | undefined> {
40+
return http.get(`${rootUrl}/archive/import/analyze/${importId}`, {
41+
headers: getHeaders(),
42+
});
43+
}
44+
45+
export function analyzeImportOrFail(importId: string): ImportAnalysisResult {
46+
const res = analyzeImport(importId);
47+
if (res.status !== 200) {
48+
fail(
49+
`Failed to analyze import with id ${importId}. Response: ${res.status} - ${res.body}`,
50+
);
51+
}
52+
return res.json() as ImportAnalysisResult;
53+
}
54+
55+
export function launchImport(
56+
importId: string,
57+
details: { apps: Record<string, AppImportAnalysis> },
58+
): RefinedResponse<ResponseType | undefined> {
59+
const payload = JSON.stringify(details);
60+
return http.post(`${rootUrl}/archive/import/${importId}/launch`, payload, {
61+
headers: getHeaders("application/json"),
62+
});
63+
}
64+
65+
export function launchImportOrFail(
66+
importId: string,
67+
details: { apps: Record<string, AppImportAnalysis> },
68+
): Record<string, AppImportResult> {
69+
const res = launchImport(importId, details);
70+
if (res.status !== 200) {
71+
fail(
72+
`Failed to launch import with id ${importId}. Response: ${res.status} - ${res.body}`,
73+
);
74+
}
75+
return res.json() as Record<string, AppImportResult>;
76+
}
77+
78+
export function launchExport(
79+
apps: string[],
80+
): RefinedResponse<ResponseType | undefined> {
1081
const payload = JSON.stringify({ apps });
11-
const res = http.post(
12-
`${rootUrl}/archive/export`,
13-
payload,
14-
{ headers: getHeaders("application/json") },
15-
);
82+
const res = http.post(`${rootUrl}/archive/export`, payload, {
83+
headers: getHeaders("application/json"),
84+
});
1685
return res;
1786
}
1887

1988
export function launchExportOrFail(apps: string[]): string {
2089
const res = launchExport(apps);
2190
const ok = check(res, {
2291
"should have exportId in response": (r) => r.json("exportId") !== undefined,
23-
"should have message in response": (r) => r.json("message") === "export.in.progress",
92+
"should have message in response": (r) =>
93+
r.json("message") === "export.in.progress",
2494
});
2595
if (!ok) {
26-
fail(`Failed to launch export for apps ${apps.join(", ")}. Response: ${res.status} - ${res.body}`);
96+
fail(
97+
`Failed to launch export for apps ${apps.join(", ")}. Response: ${res.status} - ${res.body}`,
98+
);
2799
}
28100
return res.json("exportId") as string;
29101
}
30102

103+
export function verifyExportFiles(
104+
exportId: string,
105+
): RefinedResponse<ResponseType | undefined> {
106+
const res = http.get(`${rootUrl}/archive/export/verify/${exportId}`, {
107+
headers: getHeaders(),
108+
});
109+
return res;
110+
}
31111

32-
export function verifyExportFiles(exportId: string): RefinedResponse<ResponseType | undefined> {
33-
const res = http.get(
34-
`${rootUrl}/archive/export/verify/${exportId}`,
35-
{ headers: getHeaders() },
36-
);
112+
export function downloadExportFile(
113+
exportId: string,
114+
): RefinedResponse<ResponseType | undefined> {
115+
const res = http.get(`${rootUrl}/archive/export/${exportId}`, {
116+
headers: getHeaders(),
117+
responseType: "binary", // Ensure we get binary data
118+
});
37119
return res;
38120
}
39121

40-
export function downloadExportFile(exportId: string): RefinedResponse<ResponseType | undefined> {
41-
const res = http.get(
42-
`${rootUrl}/archive/export/${exportId}`,
43-
{
44-
headers: getHeaders(),
45-
responseType: 'binary', // Ensure we get binary data
46-
},
47-
);
122+
export function duplicateResource(
123+
application: string,
124+
resourceId: string,
125+
): RefinedResponse<ResponseType | undefined> {
126+
const payload = JSON.stringify({ application, resourceId });
127+
const res = http.post(`${rootUrl}/archive/duplicate`, payload, {
128+
headers: getHeaders("application/json"),
129+
});
48130
return res;
49131
}
50132

133+
export function duplicateResourceOrFail(
134+
application: string,
135+
resourceId: string,
136+
): string {
137+
const res = duplicateResource(application, resourceId);
138+
if (res.status !== 200) {
139+
fail(
140+
`Failed to launch duplication for resource ${resourceId} in application ${application}. Response: ${res.status} - ${res.body}`,
141+
);
142+
}
143+
return res.json("duplicateId") as string;
144+
}
145+
51146
/**
52147
* Represents a file entry in a ZIP archive
53148
*/
@@ -89,14 +184,23 @@ function readUInt16LE(bytes: Uint8Array, offset: number): number {
89184
* Read a little-endian 32-bit integer from a byte array
90185
*/
91186
function readUInt32LE(bytes: Uint8Array, offset: number): number {
92-
return bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24);
187+
return (
188+
bytes[offset] |
189+
(bytes[offset + 1] << 8) |
190+
(bytes[offset + 2] << 16) |
191+
(bytes[offset + 3] << 24)
192+
);
93193
}
94194

95195
/**
96196
* Convert bytes to string (ASCII/UTF-8)
97197
*/
98-
function bytesToString(bytes: Uint8Array, offset: number, length: number): string {
99-
let str = '';
198+
function bytesToString(
199+
bytes: Uint8Array,
200+
offset: number,
201+
length: number,
202+
): string {
203+
let str = "";
100204
for (let i = 0; i < length; i++) {
101205
str += String.fromCharCode(bytes[offset + i]);
102206
}
@@ -115,7 +219,11 @@ function toUint8Array(body: any): Uint8Array {
115219
return new Uint8Array(body);
116220
}
117221
// Check if it's an array-like object with numeric indices
118-
if (typeof body === 'object' && body.length !== undefined && typeof body[0] === 'number') {
222+
if (
223+
typeof body === "object" &&
224+
body.length !== undefined &&
225+
typeof body[0] === "number"
226+
) {
119227
const bytes = new Uint8Array(body.length);
120228
for (let i = 0; i < body.length; i++) {
121229
bytes[i] = body[i];
@@ -141,9 +249,14 @@ function findEndOfCentralDirectory(bytes: Uint8Array): number {
141249
// Search from the end of the file (EOCD is at the end)
142250
const maxSearchLength = Math.min(bytes.length, 65536 + 22); // Maximum comment length + EOCD size
143251
const searchStart = Math.max(0, bytes.length - maxSearchLength);
144-
252+
145253
for (let i = bytes.length - 22; i >= searchStart; i--) {
146-
if (bytes[i] === 0x50 && bytes[i + 1] === 0x4b && bytes[i + 2] === 0x05 && bytes[i + 3] === 0x06) {
254+
if (
255+
bytes[i] === 0x50 &&
256+
bytes[i + 1] === 0x4b &&
257+
bytes[i + 2] === 0x05 &&
258+
bytes[i + 3] === 0x06
259+
) {
147260
return i;
148261
}
149262
}
@@ -164,7 +277,9 @@ export function parseZip(data: any): ZipArchive {
164277
// Find End of Central Directory record
165278
const eocdOffset = findEndOfCentralDirectory(bytes);
166279
if (eocdOffset === -1) {
167-
throw new Error("Invalid ZIP file: End of Central Directory record not found");
280+
throw new Error(
281+
"Invalid ZIP file: End of Central Directory record not found",
282+
);
168283
}
169284

170285
// Parse EOCD
@@ -174,21 +289,27 @@ export function parseZip(data: any): ZipArchive {
174289

175290
// Validate offsets
176291
if (centralDirOffset >= bytes.length) {
177-
throw new Error(`Invalid ZIP: Central Directory offset (${centralDirOffset}) exceeds file size (${bytes.length})`);
292+
throw new Error(
293+
`Invalid ZIP: Central Directory offset (${centralDirOffset}) exceeds file size (${bytes.length})`,
294+
);
178295
}
179296

180297
// Parse Central Directory entries
181298
let offset = centralDirOffset;
182299
for (let i = 0; i < totalEntries; i++) {
183300
// Check if we have enough bytes to read the signature
184301
if (offset + 4 > bytes.length) {
185-
throw new Error(`Invalid ZIP: Not enough data for Central Directory entry ${i} at offset ${offset} (file size: ${bytes.length})`);
302+
throw new Error(
303+
`Invalid ZIP: Not enough data for Central Directory entry ${i} at offset ${offset} (file size: ${bytes.length})`,
304+
);
186305
}
187-
306+
188307
// Central Directory File Header signature: 0x02014b50
189308
const signature = readUInt32LE(bytes, offset);
190309
if (signature !== 0x02014b50) {
191-
throw new Error(`Invalid Central Directory entry ${i} at offset ${offset}: expected signature 0x02014b50, got 0x${signature.toString(16).padStart(8, '0')}`);
310+
throw new Error(
311+
`Invalid Central Directory entry ${i} at offset ${offset}: expected signature 0x02014b50, got 0x${signature.toString(16).padStart(8, "0")}`,
312+
);
192313
}
193314

194315
const compressionMethod = readUInt16LE(bytes, offset + 10);
@@ -199,7 +320,7 @@ export function parseZip(data: any): ZipArchive {
199320
const fileCommentLength = readUInt16LE(bytes, offset + 32);
200321

201322
const filename = bytesToString(bytes, offset + 46, filenameLength);
202-
const isDirectory = filename.endsWith('/');
323+
const isDirectory = filename.endsWith("/");
203324

204325
if (isDirectory) {
205326
directoryCount++;
@@ -231,11 +352,14 @@ export function parseZip(data: any): ZipArchive {
231352
* @param filename - Name of the file to extract
232353
* @returns The file data as Uint8Array, or null if not found
233354
*/
234-
export function extractFileFromZip(data: any, filename: string): Uint8Array | null {
355+
export function extractFileFromZip(
356+
data: any,
357+
filename: string,
358+
): Uint8Array | null {
235359
const bytes = toUint8Array(data);
236360
const archive = parseZip(data);
237361
const entry = archive.entries.get(filename);
238-
362+
239363
if (!entry || entry.isDirectory) {
240364
return null;
241365
}
@@ -247,23 +371,25 @@ export function extractFileFromZip(data: any, filename: string): Uint8Array | nu
247371
const filenameLength = readUInt16LE(bytes, i + 26);
248372
const extraFieldLength = readUInt16LE(bytes, i + 28);
249373
const localFilename = bytesToString(bytes, i + 30, filenameLength);
250-
374+
251375
if (localFilename === filename) {
252376
const compressionMethod = readUInt16LE(bytes, i + 8);
253377
const compressedSize = readUInt32LE(bytes, i + 18);
254378
const dataOffset = i + 30 + filenameLength + extraFieldLength;
255-
379+
256380
if (compressionMethod === 0) {
257381
// No compression - return data as-is
258382
return bytes.slice(dataOffset, dataOffset + compressedSize);
259383
} else {
260384
// Compressed data - k6 doesn't have built-in decompression
261-
throw new Error(`File "${filename}" uses compression method ${compressionMethod}. Decompression is not supported in k6. Use stored (uncompressed) files only.`);
385+
throw new Error(
386+
`File "${filename}" uses compression method ${compressionMethod}. Decompression is not supported in k6. Use stored (uncompressed) files only.`,
387+
);
262388
}
263389
}
264390
}
265391
}
266-
392+
267393
return null;
268394
}
269395

@@ -290,29 +416,29 @@ export function getZipTree(archive: ZipArchive): string[] {
290416
export function validateZipStructure(
291417
archive: ZipArchive,
292418
expectedFiles: string[],
293-
expectedDirs?: string[]
419+
expectedDirs?: string[],
294420
): { valid: boolean; missing: string[]; unexpected: string[] } {
295421
const missing: string[] = [];
296422
const unexpected: string[] = [];
297423
const allExpected = new Set([...expectedFiles, ...(expectedDirs || [])]);
298-
424+
299425
// Check for missing files
300426
for (const expected of allExpected) {
301427
if (!archive.entries.has(expected)) {
302428
missing.push(expected);
303429
}
304430
}
305-
431+
306432
// Check for unexpected files (optional - you can skip this if you want)
307433
for (const [filename] of archive.entries) {
308434
if (!allExpected.has(filename)) {
309435
unexpected.push(filename);
310436
}
311437
}
312-
438+
313439
return {
314440
valid: missing.length === 0,
315441
missing,
316442
unexpected,
317443
};
318-
}
444+
}

0 commit comments

Comments
 (0)