Skip to content

Commit 411590c

Browse files
committed
chore: update project version to 2.1.9 and upgrade dependencies
- Incremented project version to `2.1.9` in `composer.json`. - Updated `composer.lock` with new content hash and upgraded dependencies, including `qase/qase-api-client` to `1.1.6` and `phpunit/phpunit` to `9.6.30`. - Enhanced `uploadAttachment` method in `ApiClientV1` to support multiple attachments with validation and batching. - Updated `ClientInterface` to reflect changes in the `uploadAttachment` method signature. - Improved attachment handling in `ApiClientV2` to utilize the new batch upload functionality.
1 parent 56c0b4e commit 411590c

File tree

5 files changed

+241
-40
lines changed

5 files changed

+241
-40
lines changed

composer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
},
3030
"require": {
3131
"php": "^8.0",
32-
"qase/qase-api-client": "^1.1.0",
33-
"qase/qase-api-v2-client": "^1.1.0",
32+
"qase/qase-api-client": "^1.1.5",
33+
"qase/qase-api-v2-client": "^1.1.2",
3434
"ramsey/uuid": "^4.7"
3535
},
3636
"require-dev": {
@@ -40,7 +40,7 @@
4040
"scripts": {
4141
"test": "phpunit"
4242
},
43-
"version": "2.1.8",
43+
"version": "2.1.9",
4444
"config": {
4545
"platform": {
4646
"php": "8.0"

composer.lock

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Client/ApiClientV1.php

Lines changed: 200 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -184,35 +184,219 @@ public function isTestRunExist(string $code, int $runId): bool
184184
}
185185
}
186186

187-
public function uploadAttachment(string $code, Attachment $attachment): ?string
187+
/**
188+
* Upload one or multiple attachments
189+
*
190+
* Limitations:
191+
* - Up to 32 MB per file
192+
* - Up to 128 MB per single request
193+
* - Up to 20 files per single request
194+
*
195+
* @param string $code Project code
196+
* @param Attachment|Attachment[] $attachments Single attachment or array of attachments
197+
* @return string|string[]|null Hash(es) of uploaded attachment(s) or null on failure
198+
*/
199+
public function uploadAttachment(string $code, Attachment|array $attachments): string|array|null
188200
{
201+
// Normalize to array
202+
$attachmentsArray = is_array($attachments) ? $attachments : [$attachments];
203+
204+
// Check for empty array
205+
if (empty($attachmentsArray)) {
206+
$this->logger->warning('Empty attachments array provided');
207+
return is_array($attachments) ? [] : null;
208+
}
209+
189210
try {
190-
$this->logger->debug('Upload attachment: ' . json_encode($attachment));
211+
$this->logger->debug('Upload ' . count($attachmentsArray) . ' attachment(s)');
212+
213+
// Filter and validate individual file constraints (skip invalid files)
214+
$validAttachments = $this->filterValidAttachments($attachmentsArray);
215+
216+
if (empty($validAttachments)) {
217+
$this->logger->warning('No valid attachments to upload after filtering');
218+
return is_array($attachments) ? [] : null;
219+
}
191220

221+
// Split into batches
222+
$batches = $this->splitIntoBatches($validAttachments);
223+
$this->logger->debug('Split into ' . count($batches) . ' batch(es)');
224+
225+
$allHashes = [];
192226
$attachApi = new AttachmentsApi($this->client, $this->clientConfig);
193-
if ($attachment->path) {
194-
$attachmentId = $attachApi->uploadAttachment($code, new SplFileObject($attachment->path));
195-
} elseif ($attachment->content) {
196-
$filepath = rtrim(getcwd(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $attachment->title;
197-
if (file_put_contents($filepath, $attachment->content) === false) {
198-
$this->logger->error('Can not save attachment: ' . $filepath);
199-
return null;
227+
228+
// Upload each batch
229+
foreach ($batches as $batchIndex => $batch) {
230+
$this->logger->debug('Uploading batch ' . ($batchIndex + 1) . '/' . count($batches) . ' with ' . count($batch) . ' file(s)');
231+
232+
$fileObjects = [];
233+
$tempFiles = [];
234+
235+
// Prepare files for this batch
236+
foreach ($batch as $attachment) {
237+
if ($attachment->path) {
238+
$fileObjects[] = new SplFileObject($attachment->path);
239+
} elseif ($attachment->content) {
240+
$filepath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('qase_attach_', true) . '_' . ($attachment->title ?? 'attachment');
241+
if (file_put_contents($filepath, $attachment->content) === false) {
242+
$this->logger->error('Can not save attachment: ' . $filepath);
243+
// Clean up already created temp files
244+
foreach ($tempFiles as $tempFile) {
245+
if (file_exists($tempFile)) {
246+
unlink($tempFile);
247+
}
248+
}
249+
return null;
250+
}
251+
$fileObjects[] = new SplFileObject($filepath);
252+
$tempFiles[] = $filepath;
253+
} else {
254+
$this->logger->error('Attachment has neither path nor content');
255+
// Clean up temp files
256+
foreach ($tempFiles as $tempFile) {
257+
if (file_exists($tempFile)) {
258+
unlink($tempFile);
259+
}
260+
}
261+
return null;
262+
}
200263
}
201-
$attachmentId = $attachApi->uploadAttachment($code, new SplFileObject($filepath));
202-
if (unlink($filepath) === false) {
203-
$this->logger->error('Can not remove attachment: ' . $filepath);
264+
265+
// Upload batch
266+
$result = $attachApi->uploadAttachment($code, $fileObjects);
267+
268+
// Clean up temp files for this batch
269+
foreach ($tempFiles as $tempFile) {
270+
if (file_exists($tempFile) && unlink($tempFile) === false) {
271+
$this->logger->error('Can not remove temporary attachment: ' . $tempFile);
272+
}
273+
}
274+
275+
// Extract hashes from result
276+
if ($result && $result->getResult()) {
277+
foreach ($result->getResult() as $attachmentResult) {
278+
$allHashes[] = $attachmentResult->getHash();
279+
}
204280
}
205-
} else {
206-
return null;
207281
}
208282

209-
return $attachmentId->getResult()[0]->getHash();
283+
// Return single hash if single attachment was provided, array otherwise
284+
if (!is_array($attachments)) {
285+
return $allHashes[0] ?? null;
286+
}
287+
288+
return $allHashes;
210289
} catch (Exception $e) {
211-
$this->logger->error('Failed to upload attachment: ' . $e->getMessage());
290+
$this->logger->error('Failed to upload attachment(s): ' . $e->getMessage());
212291
return null;
213292
}
214293
}
215294

295+
/**
296+
* Filter attachments and return only valid ones, logging errors for invalid files
297+
*
298+
* @param Attachment[] $attachments
299+
* @return Attachment[] Array of valid attachments
300+
*/
301+
private function filterValidAttachments(array $attachments): array
302+
{
303+
$maxFileSize = 32 * 1024 * 1024; // 32 MB
304+
$validAttachments = [];
305+
306+
foreach ($attachments as $index => $attachment) {
307+
$fileSize = 0;
308+
$fileName = $attachment->title ?? $attachment->path ?? "attachment at index {$index}";
309+
310+
// Check if file has path or content
311+
if ($attachment->path) {
312+
if (!file_exists($attachment->path)) {
313+
$this->logger->warning("Skipping attachment '{$fileName}': file not found: {$attachment->path}");
314+
continue;
315+
}
316+
$fileSize = filesize($attachment->path);
317+
} elseif ($attachment->content) {
318+
$fileSize = strlen($attachment->content);
319+
} else {
320+
$this->logger->warning("Skipping attachment '{$fileName}': has neither path nor content");
321+
continue;
322+
}
323+
324+
// Check file size
325+
if ($fileSize > $maxFileSize) {
326+
$fileSizeMB = round($fileSize / 1024 / 1024, 2);
327+
$this->logger->warning("Skipping attachment '{$fileName}': file size {$fileSizeMB} MB exceeds maximum of 32 MB per file");
328+
continue;
329+
}
330+
331+
$validAttachments[] = $attachment;
332+
}
333+
334+
$skippedCount = count($attachments) - count($validAttachments);
335+
if ($skippedCount > 0) {
336+
$this->logger->info("Filtered out {$skippedCount} invalid attachment(s), proceeding with " . count($validAttachments) . " valid attachment(s)");
337+
}
338+
339+
return $validAttachments;
340+
}
341+
342+
/**
343+
* Split attachments into batches respecting API constraints
344+
*
345+
* @param Attachment[] $attachments
346+
* @return Attachment[][] Array of batches
347+
*/
348+
private function splitIntoBatches(array $attachments): array
349+
{
350+
$maxFilesPerBatch = 20;
351+
$maxSizePerBatch = 128 * 1024 * 1024; // 128 MB
352+
353+
$batches = [];
354+
$currentBatch = [];
355+
$currentBatchSize = 0;
356+
357+
foreach ($attachments as $attachment) {
358+
// Get file size (files should be validated already, but add safety check)
359+
$fileSize = 0;
360+
if ($attachment->path) {
361+
if (file_exists($attachment->path)) {
362+
$fileSize = filesize($attachment->path);
363+
} else {
364+
$this->logger->warning("Skipping attachment in batch: file not found: {$attachment->path}");
365+
continue;
366+
}
367+
} elseif ($attachment->content) {
368+
$fileSize = strlen($attachment->content);
369+
} else {
370+
$this->logger->warning("Skipping attachment in batch: has neither path nor content");
371+
continue;
372+
}
373+
374+
// Check if we need to start a new batch
375+
$wouldExceedFileLimit = count($currentBatch) >= $maxFilesPerBatch;
376+
$wouldExceedSizeLimit = ($currentBatchSize + $fileSize) > $maxSizePerBatch;
377+
378+
if ($wouldExceedFileLimit || $wouldExceedSizeLimit) {
379+
// Save current batch and start a new one
380+
if (!empty($currentBatch)) {
381+
$batches[] = $currentBatch;
382+
$currentBatch = [];
383+
$currentBatchSize = 0;
384+
}
385+
}
386+
387+
// Add file to current batch
388+
$currentBatch[] = $attachment;
389+
$currentBatchSize += $fileSize;
390+
}
391+
392+
// Add the last batch if it's not empty
393+
if (!empty($currentBatch)) {
394+
$batches[] = $currentBatch;
395+
}
396+
397+
return $batches;
398+
}
399+
216400
public function sendResults(string $code, int $runId, array $results): void
217401
{
218402
// Use Api V2 client for sending results

src/Client/ApiClientV2.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,20 @@ private function convertRelations(Relation $relation): ResultRelations
146146

147147
private function convertAttachments(array $attachments): array
148148
{
149-
$hashes = [];
150-
foreach ($attachments as $item) {
151-
$result = $this->uploadAttachment($this->config->getProject(), $item);
152-
if ($result) {
153-
$hashes[] = $result;
154-
}
149+
if (empty($attachments)) {
150+
return [];
151+
}
152+
153+
// Upload all attachments at once (method handles validation and batching if needed)
154+
$result = $this->uploadAttachment($this->config->getProject(), $attachments);
155+
156+
if (is_array($result)) {
157+
return $result;
158+
} elseif (is_string($result)) {
159+
return [$result];
155160
}
156161

157-
return $hashes;
162+
return [];
158163
}
159164

160165
public function runUpdateExternalIssue(string $code, string $type, array $links): void

src/Interfaces/ClientInterface.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,19 @@ public function completeTestRun(string $code, int $runId): void;
2121

2222
public function isTestRunExist(string $code, int $runId): bool;
2323

24-
public function uploadAttachment(string $code, Attachment $attachment): ?string;
24+
/**
25+
* Upload one or multiple attachments
26+
*
27+
* Limitations:
28+
* - Up to 32 MB per file
29+
* - Up to 128 MB per single request
30+
* - Up to 20 files per single request
31+
*
32+
* @param string $code Project code
33+
* @param Attachment|Attachment[] $attachments Single attachment or array of attachments
34+
* @return string|string[]|null Hash(es) of uploaded attachment(s) or null on failure
35+
*/
36+
public function uploadAttachment(string $code, Attachment|array $attachments): string|array|null;
2537

2638
public function sendResults(string $code, int $runId, array $results): void;
2739

0 commit comments

Comments
 (0)