@allurereport/service 3.14.0 → 3.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/model.d.ts CHANGED
@@ -13,8 +13,21 @@ export type AllureServiceApiClientConfig = UploadReportConfig & {
13
13
  export type UploadReportPayload = {
14
14
  reportUuid: string;
15
15
  pluginId?: string;
16
- files: Record<string, string>;
17
- onProgress?: () => void;
16
+ files: Record<string, string> | Record<string, string>[];
17
+ onProgress?: (uploadedFilesDelta?: number) => void;
18
+ uploadBatchMaxBytes?: number;
19
+ };
20
+ export type UploadReportFilePayload = {
21
+ filename: string;
22
+ file?: Buffer;
23
+ filepath?: string;
24
+ signal?: AbortSignal;
25
+ };
26
+ export type UploadReportFilesPayload = {
27
+ reportUuid: string;
28
+ pluginId?: string;
29
+ files: UploadReportFilePayload[];
30
+ signal?: AbortSignal;
18
31
  };
19
32
  export type UploadReportResult = {
20
33
  indexHref?: string;
@@ -40,19 +53,15 @@ export interface AllureServiceApiClient {
40
53
  reportUuid: string;
41
54
  pluginId?: string;
42
55
  }): Promise<unknown>;
43
- addReportAsset(payload: {
44
- filename: string;
45
- file?: Buffer;
46
- filepath?: string;
56
+ addReportAsset(payload: UploadReportFilePayload): Promise<unknown>;
57
+ addReportAssets?(payload: {
58
+ files: UploadReportFilePayload[];
47
59
  signal?: AbortSignal;
48
60
  }): Promise<unknown>;
49
- addReportFile(payload: {
61
+ addReportFile(payload: UploadReportFilePayload & {
50
62
  reportUuid: string;
51
63
  pluginId?: string;
52
- filename: string;
53
- file?: Buffer;
54
- filepath?: string;
55
- signal?: AbortSignal;
56
64
  }): Promise<string>;
65
+ addReportFiles?(payload: UploadReportFilesPayload): Promise<Record<string, string>>;
57
66
  uploadReport(payload: UploadReportPayload): Promise<UploadReportResult>;
58
67
  }
package/dist/service.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type HistoryDataPoint } from "@allurereport/core-api";
2
- import { type AllureServiceApiClient, type AllureServiceApiClientConfig, type UploadReportPayload } from "./model.js";
2
+ import { type AllureServiceApiClient, type AllureServiceApiClientConfig, type UploadReportFilePayload, type UploadReportFilesPayload, type UploadReportPayload } from "./model.js";
3
3
  export declare class AllureServiceClient implements AllureServiceApiClient {
4
4
  #private;
5
5
  readonly config: AllureServiceApiClientConfig;
@@ -23,19 +23,17 @@ export declare class AllureServiceClient implements AllureServiceApiClient {
23
23
  reportUuid: string;
24
24
  pluginId?: string;
25
25
  }): Promise<unknown>;
26
- addReportAsset(payload: {
27
- filename: string;
28
- file?: Buffer;
29
- filepath?: string;
26
+ addReportAssets(payload: {
27
+ files: UploadReportFilePayload[];
30
28
  signal?: AbortSignal;
31
29
  }): Promise<unknown>;
32
- addReportFile(payload: {
30
+ addReportAsset(payload: UploadReportFilePayload): Promise<unknown>;
31
+ addReportFiles(payload: UploadReportFilesPayload): Promise<{
32
+ [k: string]: string;
33
+ }>;
34
+ addReportFile(payload: UploadReportFilePayload & {
33
35
  reportUuid: string;
34
36
  pluginId?: string;
35
- filename: string;
36
- file?: Buffer;
37
- filepath?: string;
38
- signal?: AbortSignal;
39
37
  }): Promise<string>;
40
38
  uploadReport(payload: UploadReportPayload): Promise<import("./model.js").UploadReportResult>;
41
39
  }
package/dist/service.js CHANGED
@@ -10,11 +10,11 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
12
  var _AllureServiceClient_client, _AllureServiceClient_url;
13
- import { readFile } from "node:fs/promises";
14
13
  import { join as joinPosix } from "node:path/posix";
15
14
  import { ALLURE_SERVICE_STORAGE_PREFIX, } from "./model.js";
16
15
  import { createServiceHttpClient, uploadReport } from "./utils/http.js";
17
16
  import { parseServiceToken } from "./utils/token.js";
17
+ import { createUploadForm } from "./utils/upload.js";
18
18
  const UPLOAD_CONTENT_TYPE = "application/octet-stream";
19
19
  const createUploadBlob = (content) => new Blob([content], { type: UPLOAD_CONTENT_TYPE });
20
20
  const createReportUrl = (baseUrl, reportUuid) => `${baseUrl}/${reportUuid}`;
@@ -79,18 +79,12 @@ export class AllureServiceClient {
79
79
  },
80
80
  });
81
81
  }
82
- async addReportAsset(payload) {
83
- const { filename, file, filepath, signal } = payload;
84
- if (!file && !filepath) {
85
- throw new Error("File or filepath is required");
86
- }
87
- let content = file;
88
- if (!content) {
89
- content = signal ? await readFile(filepath, { signal }) : await readFile(filepath);
82
+ async addReportAssets(payload) {
83
+ const { files, signal } = payload;
84
+ if (files.length === 0) {
85
+ return undefined;
90
86
  }
91
- const form = new FormData();
92
- form.set("filename", filename);
93
- form.set("file", createUploadBlob(content), filename);
87
+ const { form } = await createUploadForm(files, createUploadBlob, signal);
94
88
  return __classPrivateFieldGet(this, _AllureServiceClient_client, "f").post("/api/assets/upload", {
95
89
  body: form,
96
90
  headers: {
@@ -99,19 +93,15 @@ export class AllureServiceClient {
99
93
  ...(signal ? { signal } : {}),
100
94
  });
101
95
  }
102
- async addReportFile(payload) {
103
- const { reportUuid, filename, file, filepath, pluginId, signal } = payload;
104
- const reportFilename = pluginId ? joinPosix(pluginId, filename) : filename;
105
- if (!file && !filepath) {
106
- throw new Error("File or filepath is required");
107
- }
108
- let content = file;
109
- if (!content) {
110
- content = signal ? await readFile(filepath, { signal }) : await readFile(filepath);
96
+ async addReportAsset(payload) {
97
+ return this.addReportAssets({ files: [payload], ...(payload.signal ? { signal: payload.signal } : {}) });
98
+ }
99
+ async addReportFiles(payload) {
100
+ const { reportUuid, pluginId, files, signal } = payload;
101
+ if (files.length === 0) {
102
+ return {};
111
103
  }
112
- const form = new FormData();
113
- form.set("filename", reportFilename);
114
- form.set("file", createUploadBlob(content), reportFilename);
104
+ const { entries, form } = await createUploadForm(files, createUploadBlob, signal, (filename) => pluginId ? joinPosix(pluginId, filename) : filename);
115
105
  await __classPrivateFieldGet(this, _AllureServiceClient_client, "f").post(`/api/reports/${reportUuid}/upload`, {
116
106
  body: form,
117
107
  headers: {
@@ -119,16 +109,31 @@ export class AllureServiceClient {
119
109
  },
120
110
  ...(signal ? { signal } : {}),
121
111
  });
122
- return createReportFileUrl(__classPrivateFieldGet(this, _AllureServiceClient_url, "f"), reportUuid, reportFilename);
112
+ return Object.fromEntries(entries.map(({ filename, reportFilename }) => [
113
+ filename,
114
+ createReportFileUrl(__classPrivateFieldGet(this, _AllureServiceClient_url, "f"), reportUuid, reportFilename),
115
+ ]));
116
+ }
117
+ async addReportFile(payload) {
118
+ const result = await this.addReportFiles({
119
+ reportUuid: payload.reportUuid,
120
+ pluginId: payload.pluginId,
121
+ files: [payload],
122
+ ...(payload.signal ? { signal: payload.signal } : {}),
123
+ });
124
+ return result[payload.filename];
123
125
  }
124
126
  async uploadReport(payload) {
127
+ const { uploadBatchMaxBytes: _uploadBatchMaxBytes, ...uploadPayload } = payload;
125
128
  return uploadReport({
126
- ...payload,
129
+ ...uploadPayload,
127
130
  uploadConcurrency: this.config.uploadConcurrency,
128
131
  uploadMaxAttempts: this.config.uploadMaxAttempts,
129
132
  uploadMaxSimultaneousFailures: this.config.uploadMaxSimultaneousFailures,
130
133
  addReportAsset: this.addReportAsset.bind(this),
134
+ addReportAssets: this.addReportAssets.bind(this),
131
135
  addReportFile: this.addReportFile.bind(this),
136
+ addReportFiles: this.addReportFiles.bind(this),
132
137
  });
133
138
  }
134
139
  }
package/dist/testops.d.ts CHANGED
@@ -25,6 +25,15 @@ export declare class AllureTestOpsClient implements AllureServiceApiClient {
25
25
  filepath?: string;
26
26
  signal?: AbortSignal;
27
27
  }): Promise<unknown>;
28
+ addReportAssets(payload: {
29
+ files: {
30
+ filename: string;
31
+ file?: Buffer;
32
+ filepath?: string;
33
+ signal?: AbortSignal;
34
+ }[];
35
+ signal?: AbortSignal;
36
+ }): Promise<unknown>;
28
37
  addReportFile(payload: {
29
38
  reportUuid: string;
30
39
  pluginId?: string;
@@ -33,5 +42,18 @@ export declare class AllureTestOpsClient implements AllureServiceApiClient {
33
42
  filepath?: string;
34
43
  signal?: AbortSignal;
35
44
  }): Promise<string>;
45
+ addReportFiles(payload: {
46
+ reportUuid: string;
47
+ pluginId?: string;
48
+ files: {
49
+ filename: string;
50
+ file?: Buffer;
51
+ filepath?: string;
52
+ signal?: AbortSignal;
53
+ }[];
54
+ signal?: AbortSignal;
55
+ }): Promise<{
56
+ [k: string]: string;
57
+ }>;
36
58
  uploadReport(payload: UploadReportPayload): Promise<import("./model.js").UploadReportResult>;
37
59
  }
package/dist/testops.js CHANGED
@@ -10,12 +10,13 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
12
  var _AllureTestOpsClient_instances, _AllureTestOpsClient_url, _AllureTestOpsClient_accessToken, _AllureTestOpsClient_projectId, _AllureTestOpsClient_client, _AllureTestOpsClient_authorizedClient;
13
- import { readFile } from "node:fs/promises";
14
13
  import { extname, join as joinPosix } from "node:path/posix";
15
14
  import { ALLURE_SERVICE_TESTOPS_PREFIX, } from "./model.js";
16
15
  import { createServiceHttpClient, uploadReport } from "./utils/http.js";
17
16
  import { parseServiceToken } from "./utils/token.js";
17
+ import { createUploadForm } from "./utils/upload.js";
18
18
  const DEFAULT_UPLOAD_CONTENT_TYPE = "application/octet-stream";
19
+ const UPLOAD_BATCH_MAX_BYTES = 8 * 1024 * 1024;
19
20
  const CONTENT_TYPES = {
20
21
  ".css": "text/css",
21
22
  ".gif": "image/gif",
@@ -90,17 +91,8 @@ export class AllureTestOpsClient {
90
91
  return client.delete(`/api/test-report/${reportUuid}`);
91
92
  }
92
93
  async addReportAsset(payload) {
93
- const { filename, file, filepath, signal } = payload;
94
- if (!file && !filepath) {
95
- throw new Error("File or filepath is required");
96
- }
97
- let content = file;
98
- if (!content) {
99
- content = signal ? await readFile(filepath, { signal }) : await readFile(filepath);
100
- }
101
- const form = new FormData();
102
- form.set("filename", filename);
103
- form.set("file", createUploadBlob(content, filename), filename);
94
+ const { file, filepath, filename, signal } = payload;
95
+ const { form } = await createUploadForm([{ filename, file, filepath, signal }], createUploadBlob, signal);
104
96
  const client = await __classPrivateFieldGet(this, _AllureTestOpsClient_instances, "m", _AllureTestOpsClient_authorizedClient).call(this);
105
97
  return client.post("/api/test-report/upload", {
106
98
  body: form,
@@ -110,19 +102,25 @@ export class AllureTestOpsClient {
110
102
  ...(signal ? { signal } : {}),
111
103
  });
112
104
  }
105
+ async addReportAssets(payload) {
106
+ const { files, signal } = payload;
107
+ if (files.length === 0) {
108
+ return undefined;
109
+ }
110
+ const { form } = await createUploadForm(files, createUploadBlob, signal);
111
+ const client = await __classPrivateFieldGet(this, _AllureTestOpsClient_instances, "m", _AllureTestOpsClient_authorizedClient).call(this);
112
+ return client.post("/api/test-report/upload/batch", {
113
+ body: form,
114
+ headers: {
115
+ "Content-Type": "multipart/form-data",
116
+ },
117
+ ...(signal ? { signal } : {}),
118
+ });
119
+ }
113
120
  async addReportFile(payload) {
114
- const { reportUuid, filename, file, filepath, pluginId, signal } = payload;
121
+ const { reportUuid, pluginId, filename, file, filepath, signal } = payload;
115
122
  const reportFilename = pluginId ? joinPosix(pluginId, filename) : filename;
116
- if (!file && !filepath) {
117
- throw new Error("File or filepath is required");
118
- }
119
- let content = file;
120
- if (!content) {
121
- content = signal ? await readFile(filepath, { signal }) : await readFile(filepath);
122
- }
123
- const form = new FormData();
124
- form.set("filename", reportFilename);
125
- form.set("file", createUploadBlob(content, reportFilename), reportFilename);
123
+ const { form } = await createUploadForm([{ filename, file, filepath, signal }], createUploadBlob, signal, () => reportFilename);
126
124
  const client = await __classPrivateFieldGet(this, _AllureTestOpsClient_instances, "m", _AllureTestOpsClient_authorizedClient).call(this);
127
125
  await client.post(`/api/test-report/${reportUuid}/upload`, {
128
126
  body: form,
@@ -133,14 +131,36 @@ export class AllureTestOpsClient {
133
131
  });
134
132
  return createReportFileUrl(__classPrivateFieldGet(this, _AllureTestOpsClient_url, "f"), reportUuid, reportFilename);
135
133
  }
134
+ async addReportFiles(payload) {
135
+ const { reportUuid, pluginId, files, signal } = payload;
136
+ if (files.length === 0) {
137
+ return {};
138
+ }
139
+ const { entries, form } = await createUploadForm(files, createUploadBlob, signal, (filename) => pluginId ? joinPosix(pluginId, filename) : filename);
140
+ const client = await __classPrivateFieldGet(this, _AllureTestOpsClient_instances, "m", _AllureTestOpsClient_authorizedClient).call(this);
141
+ await client.post(`/api/test-report/${reportUuid}/upload/batch`, {
142
+ body: form,
143
+ headers: {
144
+ "Content-Type": "multipart/form-data",
145
+ },
146
+ ...(signal ? { signal } : {}),
147
+ });
148
+ return Object.fromEntries(entries.map(({ filename, reportFilename }) => [
149
+ filename,
150
+ createReportFileUrl(__classPrivateFieldGet(this, _AllureTestOpsClient_url, "f"), reportUuid, reportFilename),
151
+ ]));
152
+ }
136
153
  async uploadReport(payload) {
137
154
  return uploadReport({
138
155
  ...payload,
139
156
  uploadConcurrency: this.config.uploadConcurrency,
140
157
  uploadMaxAttempts: this.config.uploadMaxAttempts,
141
158
  uploadMaxSimultaneousFailures: this.config.uploadMaxSimultaneousFailures,
159
+ uploadBatchMaxBytes: UPLOAD_BATCH_MAX_BYTES,
142
160
  addReportAsset: this.addReportAsset.bind(this),
161
+ addReportAssets: this.addReportAssets.bind(this),
143
162
  addReportFile: this.addReportFile.bind(this),
163
+ addReportFiles: this.addReportFiles.bind(this),
144
164
  });
145
165
  }
146
166
  }
@@ -1,5 +1,5 @@
1
1
  import { type AxiosRequestConfig } from "axios";
2
- import type { UploadReportConfig, UploadReportPayload, UploadReportResult } from "../model.js";
2
+ import type { UploadReportConfig, UploadReportFilePayload, UploadReportFilesPayload, UploadReportPayload, UploadReportResult } from "../model.js";
3
3
  export declare class KnownError extends Error {
4
4
  status?: number;
5
5
  constructor(message: string, status?: number);
@@ -40,16 +40,14 @@ export declare const createServiceHttpClient: (serviceUrl: string, params?: {
40
40
  };
41
41
  export type HttpClient = ReturnType<typeof createServiceHttpClient>;
42
42
  export declare const uploadReport: (payload: UploadReportPayload & UploadReportConfig & {
43
- addReportAsset: (payload: {
44
- filename: string;
45
- filepath: string;
43
+ addReportAsset: (payload: UploadReportFilePayload) => Promise<unknown>;
44
+ addReportAssets?: (payload: {
45
+ files: UploadReportFilePayload[];
46
46
  signal?: AbortSignal;
47
47
  }) => Promise<unknown>;
48
- addReportFile: (payload: {
48
+ addReportFile: (payload: UploadReportFilePayload & {
49
49
  reportUuid: string;
50
50
  pluginId?: string;
51
- filename: string;
52
- filepath: string;
53
- signal?: AbortSignal;
54
51
  }) => Promise<string>;
52
+ addReportFiles?: (payload: UploadReportFilesPayload) => Promise<Record<string, string>>;
55
53
  }) => Promise<UploadReportResult>;
@@ -1,3 +1,4 @@
1
+ import { stat } from "node:fs/promises";
1
2
  import axios, { isAxiosError } from "axios";
2
3
  import { isReportDataFile } from "./files.js";
3
4
  export class KnownError extends Error {
@@ -24,6 +25,57 @@ const stringifyErrorObject = (value) => {
24
25
  return entries.length > 0 ? `{ ${entries.join(", ")} }` : undefined;
25
26
  }
26
27
  };
28
+ const MAX_UPLOAD_BATCH_FILES = 32;
29
+ const createUploadFileBatches = (files) => {
30
+ const entries = Object.entries(files);
31
+ const batches = [];
32
+ let batch = {};
33
+ let batchFiles = 0;
34
+ const flushBatch = () => {
35
+ if (batchFiles === 0) {
36
+ return;
37
+ }
38
+ batches.push(batch);
39
+ batch = {};
40
+ batchFiles = 0;
41
+ };
42
+ for (const [filename, filepath] of entries) {
43
+ if (batchFiles >= MAX_UPLOAD_BATCH_FILES) {
44
+ flushBatch();
45
+ }
46
+ batch[filename] = filepath;
47
+ batchFiles += 1;
48
+ }
49
+ flushBatch();
50
+ return batches;
51
+ };
52
+ const createUploadFileBatchesBySize = async (files, maxBytes) => {
53
+ const entries = Object.entries(files);
54
+ const batches = [];
55
+ let batch = {};
56
+ let batchFiles = 0;
57
+ let batchBytes = 0;
58
+ const flushBatch = () => {
59
+ if (batchFiles === 0) {
60
+ return;
61
+ }
62
+ batches.push(batch);
63
+ batch = {};
64
+ batchFiles = 0;
65
+ batchBytes = 0;
66
+ };
67
+ for (const [filename, filepath] of entries) {
68
+ const fileBytes = (await stat(filepath)).size;
69
+ if (batchFiles >= MAX_UPLOAD_BATCH_FILES || (batchBytes > 0 && batchBytes + fileBytes > maxBytes)) {
70
+ flushBatch();
71
+ }
72
+ batch[filename] = filepath;
73
+ batchFiles += 1;
74
+ batchBytes += fileBytes;
75
+ }
76
+ flushBatch();
77
+ return batches;
78
+ };
27
79
  export const formatResponseErrorData = (data) => {
28
80
  if (data === undefined || data === null || data === "") {
29
81
  return undefined;
@@ -116,21 +168,21 @@ export const createServiceHttpClient = (serviceUrl, params) => {
116
168
  delete: sendRequest("delete"),
117
169
  };
118
170
  };
119
- const uploadWithRetry = async (filename, uploadAbortController, failedUploads, maxAttempts, maxSimultaneousFailures, uploadFn) => {
171
+ const uploadWithRetry = async (batchId, uploadAbortController, failedUploads, maxAttempts, maxSimultaneousFailures, uploadFn) => {
120
172
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
121
173
  if (uploadAbortController.signal.aborted) {
122
174
  return false;
123
175
  }
124
176
  try {
125
177
  await uploadFn();
126
- failedUploads.delete(filename);
178
+ failedUploads.delete(batchId);
127
179
  return true;
128
180
  }
129
181
  catch (error) {
130
182
  if (uploadAbortController.signal.aborted) {
131
183
  return false;
132
184
  }
133
- failedUploads.add(filename);
185
+ failedUploads.add(batchId);
134
186
  if (failedUploads.size > maxSimultaneousFailures || attempt >= maxAttempts) {
135
187
  throw error;
136
188
  }
@@ -139,9 +191,18 @@ const uploadWithRetry = async (filename, uploadAbortController, failedUploads, m
139
191
  return false;
140
192
  };
141
193
  export const uploadReport = async (payload) => {
142
- const { reportUuid, pluginId, files, onProgress, addReportAsset, addReportFile, uploadConcurrency, uploadMaxAttempts, uploadMaxSimultaneousFailures, } = payload;
143
- const fileEntries = Object.entries(files);
144
- if (fileEntries.length === 0) {
194
+ const { reportUuid, pluginId, files, onProgress, uploadBatchMaxBytes, uploadConcurrency, uploadMaxAttempts, uploadMaxSimultaneousFailures, } = payload;
195
+ let fileBatches;
196
+ if (uploadBatchMaxBytes === undefined) {
197
+ fileBatches = Array.isArray(files) ? files.flatMap(createUploadFileBatches) : createUploadFileBatches(files);
198
+ }
199
+ else if (Array.isArray(files)) {
200
+ fileBatches = (await Promise.all(files.map((fileBatch) => createUploadFileBatchesBySize(fileBatch, uploadBatchMaxBytes)))).flat();
201
+ }
202
+ else {
203
+ fileBatches = await createUploadFileBatchesBySize(files, uploadBatchMaxBytes);
204
+ }
205
+ if (fileBatches.length === 0) {
145
206
  return {
146
207
  hrefs: {},
147
208
  };
@@ -150,46 +211,119 @@ export const uploadReport = async (payload) => {
150
211
  const failedUploads = new Set();
151
212
  const hrefs = {};
152
213
  let indexHref;
153
- let nextFileIndex = 0;
154
- const uploadNext = async () => {
155
- while (!uploadAbortController.signal.aborted) {
156
- const fileIndex = nextFileIndex++;
157
- if (fileIndex >= fileEntries.length) {
158
- return;
214
+ let nextBatchIndex = 0;
215
+ const uploadBatches = [];
216
+ const canUploadReportFilesInBatch = !!payload.addReportFiles;
217
+ const canUploadAssetsInBatch = !!payload.addReportAssets;
218
+ for (const [batchIndex, batchFiles] of fileBatches.entries()) {
219
+ const reportDataFiles = [];
220
+ const assetFiles = [];
221
+ for (const [filename, filepath] of Object.entries(batchFiles)) {
222
+ const filePayload = { filename, filepath, signal: uploadAbortController.signal };
223
+ if (isReportDataFile(filename)) {
224
+ reportDataFiles.push(filePayload);
159
225
  }
160
- const [filename, filepath] = fileEntries[fileIndex];
161
- let fileUrl;
162
- const uploaded = await uploadWithRetry(filename, uploadAbortController, failedUploads, uploadMaxAttempts, uploadMaxSimultaneousFailures, async () => {
163
- if (isReportDataFile(filename)) {
164
- fileUrl = await addReportFile({
165
- reportUuid,
166
- pluginId,
167
- filename,
168
- filepath,
169
- signal: uploadAbortController.signal,
226
+ else {
227
+ assetFiles.push(filePayload);
228
+ }
229
+ }
230
+ if (reportDataFiles.length > 0) {
231
+ if (canUploadReportFilesInBatch) {
232
+ uploadBatches.push({
233
+ batchId: `batch-${batchIndex}-report`,
234
+ kind: "report",
235
+ files: reportDataFiles,
236
+ });
237
+ }
238
+ else {
239
+ reportDataFiles.forEach((filePayload, fileIndex) => {
240
+ uploadBatches.push({
241
+ batchId: `batch-${batchIndex}-report-${fileIndex}`,
242
+ kind: "report",
243
+ files: [filePayload],
170
244
  });
245
+ });
246
+ }
247
+ }
248
+ if (assetFiles.length > 0) {
249
+ if (canUploadAssetsInBatch) {
250
+ uploadBatches.push({
251
+ batchId: `batch-${batchIndex}-asset`,
252
+ kind: "asset",
253
+ files: assetFiles,
254
+ });
255
+ }
256
+ else {
257
+ assetFiles.forEach((filePayload, fileIndex) => {
258
+ uploadBatches.push({
259
+ batchId: `batch-${batchIndex}-asset-${fileIndex}`,
260
+ kind: "asset",
261
+ files: [filePayload],
262
+ });
263
+ });
264
+ }
265
+ }
266
+ }
267
+ const uploadFileBatch = async ({ kind, files: batchFiles }) => {
268
+ if (batchFiles.length === 0 || uploadAbortController.signal.aborted) {
269
+ return;
270
+ }
271
+ if (kind === "report") {
272
+ if (payload.addReportFiles) {
273
+ const batchResult = await payload.addReportFiles({
274
+ reportUuid,
275
+ pluginId,
276
+ files: batchFiles,
277
+ signal: uploadAbortController.signal,
278
+ });
279
+ for (const [filename, fileUrl] of Object.entries(batchResult)) {
280
+ hrefs[filename] = fileUrl;
281
+ if (filename === "index.html") {
282
+ indexHref = fileUrl;
283
+ }
171
284
  }
172
- else {
173
- await addReportAsset({
174
- filename,
175
- filepath,
176
- signal: uploadAbortController.signal,
285
+ }
286
+ else {
287
+ for (const filePayload of batchFiles) {
288
+ const fileUrl = await payload.addReportFile({
289
+ reportUuid,
290
+ pluginId,
291
+ ...filePayload,
177
292
  });
293
+ hrefs[filePayload.filename] = fileUrl;
294
+ if (filePayload.filename === "index.html") {
295
+ indexHref = fileUrl;
296
+ }
178
297
  }
298
+ }
299
+ }
300
+ else if (payload.addReportAssets) {
301
+ await payload.addReportAssets({
302
+ files: batchFiles,
303
+ signal: uploadAbortController.signal,
179
304
  });
180
- if (!uploaded || uploadAbortController.signal.aborted) {
305
+ }
306
+ else {
307
+ for (const filePayload of batchFiles) {
308
+ await payload.addReportAsset(filePayload);
309
+ }
310
+ }
311
+ onProgress?.(batchFiles.length);
312
+ };
313
+ const uploadNext = async () => {
314
+ while (!uploadAbortController.signal.aborted) {
315
+ const batchIndex = nextBatchIndex++;
316
+ if (batchIndex >= uploadBatches.length) {
181
317
  return;
182
318
  }
183
- if (fileUrl) {
184
- hrefs[filename] = fileUrl;
185
- if (filename === "index.html") {
186
- indexHref = fileUrl;
187
- }
319
+ const batch = uploadBatches[batchIndex];
320
+ const uploaded = await uploadWithRetry(batch.batchId, uploadAbortController, failedUploads, uploadMaxAttempts, uploadMaxSimultaneousFailures, async () => uploadFileBatch(batch));
321
+ if (!uploaded || uploadAbortController.signal.aborted) {
322
+ return;
188
323
  }
189
- onProgress?.();
190
324
  }
191
325
  };
192
- const uploadTasks = Array.from({ length: Math.min(uploadConcurrency, fileEntries.length) }, () => uploadNext());
326
+ const uploadTasks = Array.from({ length: Math.min(uploadConcurrency, uploadBatches.length) }, () => uploadNext());
193
327
  try {
194
328
  await Promise.all(uploadTasks);
195
329
  }
@@ -0,0 +1,10 @@
1
+ import type { UploadReportFilePayload } from "../model.js";
2
+ export declare const readUploadContent: (payload: UploadReportFilePayload) => Promise<Buffer<ArrayBufferLike>>;
3
+ export declare const createUploadForm: (files: UploadReportFilePayload[], createUploadBlob: (content: Buffer, filename: string) => Blob, signal?: AbortSignal, mapFilename?: (filename: string) => string) => Promise<{
4
+ entries: {
5
+ filename: string;
6
+ reportFilename: string;
7
+ content: Buffer<ArrayBufferLike>;
8
+ }[];
9
+ form: import("undici-types").FormData;
10
+ }>;
@@ -0,0 +1,27 @@
1
+ import { readFile } from "node:fs/promises";
2
+ export const readUploadContent = async (payload) => {
3
+ const { file, filepath, signal } = payload;
4
+ if (file) {
5
+ return file;
6
+ }
7
+ if (!filepath) {
8
+ throw new Error("File or filepath is required");
9
+ }
10
+ return signal ? readFile(filepath, { signal }) : readFile(filepath);
11
+ };
12
+ export const createUploadForm = async (files, createUploadBlob, signal, mapFilename) => {
13
+ const entries = await Promise.all(files.map(async ({ filename, ...filePayload }) => {
14
+ const reportFilename = mapFilename ? mapFilename(filename) : filename;
15
+ return {
16
+ filename,
17
+ reportFilename,
18
+ content: await readUploadContent({ filename, ...filePayload, signal: signal ?? filePayload.signal }),
19
+ };
20
+ }));
21
+ const form = new FormData();
22
+ for (const { reportFilename, content } of entries) {
23
+ form.append("filename", reportFilename);
24
+ form.append("file", createUploadBlob(content, reportFilename), reportFilename);
25
+ }
26
+ return { entries, form };
27
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allurereport/service",
3
- "version": "3.14.0",
3
+ "version": "3.14.2",
4
4
  "description": "Allure Service API",
5
5
  "keywords": [
6
6
  "allure",
@@ -30,8 +30,8 @@
30
30
  "lint:fix": "oxlint --import-plugin --fix src test features stories"
31
31
  },
32
32
  "dependencies": {
33
- "@allurereport/core-api": "3.14.0",
34
- "@allurereport/plugin-api": "3.14.0",
33
+ "@allurereport/core-api": "3.14.2",
34
+ "@allurereport/plugin-api": "3.14.2",
35
35
  "axios": "^1.18.1",
36
36
  "open": "^11.0.0"
37
37
  },