diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 0d3ff49..96bd6a1 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -130,7 +130,7 @@ jobs: id: stan run: | set +e - vendor/bin/phpstan analyse --error-format=json --no-progress > phpstan.json 2>&1 + vendor/bin/phpstan analyse --error-format=json --no-progress > phpstan.json EXIT_CODE=$? # Also run for GitHub format @@ -181,11 +181,14 @@ jobs: id: psalm run: | set +e - vendor/bin/psalm --output-format=json --show-info=false > psalm.json 2>&1 + vendor/bin/psalm --output-format=json --show-info=false > psalm.json EXIT_CODE=$? # Generate SARIF for GitHub Security - vendor/bin/psalm --output-format=sarif --show-info=false > psalm.sarif 2>&1 || true + vendor/bin/psalm --output-format=sarif --show-info=false > psalm.sarif || true + + # Sanitize SARIF: Ensure all location coordinates are at least 1 (Psalm can output 0) + jq '(.runs[].results[].locations[].physicalLocation.region | select(.startLine < 1).startLine) |= 1 | (.runs[].results[].locations[].physicalLocation.region | select(.startColumn < 1).startColumn) |= 1 | (.runs[].results[].locations[].physicalLocation.region | select(.endLine < 1).endLine) |= 1 | (.runs[].results[].locations[].physicalLocation.region | select(.endColumn < 1).endColumn) |= 1' psalm.sarif > psalm_clean.sarif && mv psalm_clean.sarif psalm.sarif ERRORS=$(jq 'length' psalm.json 2>/dev/null || echo "0") echo "errors=${ERRORS}" >> $GITHUB_OUTPUT @@ -200,7 +203,7 @@ jobs: - name: Upload Psalm SARIF if: always() - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 with: sarif_file: psalm.sarif category: psalm @@ -242,7 +245,7 @@ jobs: id: fmt run: | set +e - OUTPUT=$(vendor/bin/pint --test --format=json 2>&1) + OUTPUT=$(vendor/bin/pint --test --format=json) EXIT_CODE=$? echo "$OUTPUT" > pint.json @@ -292,7 +295,7 @@ jobs: id: audit run: | set +e - composer audit --format=json > audit.json 2>&1 + composer audit --format=json > audit.json EXIT_CODE=$? VULNS=$(jq '.advisories | to_entries | map(.value | length) | add // 0' audit.json 2>/dev/null || echo "0") diff --git a/src/Core/Media/Thumbnail/LazyThumbnail.php b/src/Core/Media/Thumbnail/LazyThumbnail.php index 0c42e53..3a5c8e6 100644 --- a/src/Core/Media/Thumbnail/LazyThumbnail.php +++ b/src/Core/Media/Thumbnail/LazyThumbnail.php @@ -369,10 +369,41 @@ public function deleteAll(string $sourcePath): int */ public function purgeStale(int $maxAgeDays = 30): int { + /** @var \Illuminate\Filesystem\FilesystemAdapter $disk */ $disk = $this->getThumbnailDisk(); $cutoff = now()->subDays($maxAgeDays)->timestamp; $deleted = 0; + try { + // Optimization: Use listContents to get metadata in one go + // This avoids N+1 calls to lastModified() which is slow on remote storage + $contents = $disk->listContents($this->thumbnailPrefix, true); + + foreach ($contents as $attributes) { + if ($attributes instanceof \League\Flysystem\FileAttributes) { + try { + $timestamp = $attributes->lastModified(); + + if ($timestamp !== null && $timestamp < $cutoff) { + if ($disk->delete($attributes->path())) { + $deleted++; + } + } + } catch (\Throwable $e) { + // Skip files where metadata cannot be read + continue; + } + } + } + + return $deleted; + } catch (\Throwable $e) { + Log::warning('LazyThumbnail: listContents optimization failed, falling back to slow method', [ + 'error' => $e->getMessage(), + ]); + } + + // Fallback for disks that don't support listContents or other errors $files = $disk->allFiles($this->thumbnailPrefix); foreach ($files as $file) {