Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Composer run action PHPC_CS
uses: MayMeowHQ/composer-run-action@v3
uses: MayMeowHQ/composer-run-action@v8.4
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version v8.4 for this GitHub Action is unusual. GitHub Actions typically use either major version tags (e.g., v3, v8) or full semantic versions (e.g., v3.2.1). Please verify that v8.4 is a valid and existing tag in the MayMeowHQ/composer-run-action repository.

Copilot uses AI. Check for mistakes.
with:
composer_script: 'cs-check'

Expand All @@ -21,7 +21,17 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Composer run action PHPStan
uses: MayMeowHQ/composer-run-action@v3
uses: MayMeowHQ/composer-run-action@v8.4
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version v8.4 for this GitHub Action is unusual. GitHub Actions typically use either major version tags (e.g., v3, v8) or full semantic versions (e.g., v3.2.1). Please verify that v8.4 is a valid and existing tag in the MayMeowHQ/composer-run-action repository.

Copilot uses AI. Check for mistakes.
with:
composer_script: 'stan'
memory_limit: '1024M'

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Composer run action PHPUnit
uses: MayMeowHQ/composer-run-action@v8.4
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version v8.4 for this GitHub Action is unusual. GitHub Actions typically use either major version tags (e.g., v3, v8) or full semantic versions (e.g., v3.2.1). Please verify that v8.4 is a valid and existing tag in the MayMeowHQ/composer-run-action repository.

Copilot uses AI. Check for mistakes.
with:
composer_script: 'test'
memory_limit: '1024M'
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"scripts": {
"cs-check": "phpcs --colors -p src/ tests/",
"cs-fix": "phpcbf --colors -p src/ tests/",
"stan": "phpstan analyse"
"stan": "phpstan analyse",
"test": "vendor/bin/phpunit"
},
"config": {
"allow-plugins": {
Expand Down
9 changes: 0 additions & 9 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,6 @@
</testsuite>
</testsuites>

<!-- Setup a listener for fixtures -->
<listeners>
<listener class="Cake\TestSuite\Fixture\FixtureInjector">
<arguments>
<object class="Cake\TestSuite\Fixture\FixtureManager"/>
</arguments>
</listener>
</listeners>

<filter>
<whitelist>
<directory suffix=".php">src/</directory>
Expand Down
33 changes: 27 additions & 6 deletions src/Controller/Component/DownloadComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace FileUpload\Controller\Component;

use Cake\Controller\Component;
use Cake\Http\Exception\HttpException;
use FileUpload\File\StoredFileInterface;
use FileUpload\Storage\StorageManagerInterface;

Expand All @@ -17,26 +18,46 @@ class DownloadComponent extends Component
*
* @var array
*/
protected array $_defaultConfig = [
];
protected array $_defaultConfig = [];

private ?StorageManagerInterface $storageManager = null;

/**
* Returns stored file info and content
*
* @param string $fileName Name of file without path or slashes
* @return \FileUpload\File\StoredFileInterface Stored file object
* @throws \Cake\Http\Exception\HttpException When storage manager is misconfigured
*/
public function getFile(string $fileName): StoredFileInterface
{
$sm = $this->_getStorageManager();

return $sm->pull($fileName);
return $this->_getStorageManager()->pull($fileName);
}

protected function _getStorageManager(): StorageManagerInterface
{
if ($this->storageManager instanceof StorageManagerInterface) {
return $this->storageManager;
}

$sm = $this->getConfig('managerClass');

return new $sm($this->getConfig());
if (!is_string($sm) || $sm === '') {
throw new HttpException('Storage manager class is not configured.');
}

if (!class_exists($sm)) {
throw new HttpException(sprintf('Storage manager class "%s" does not exist.', $sm));
}

$storageManager = new $sm($this->getConfig());

if (!$storageManager instanceof StorageManagerInterface) {
throw new HttpException(sprintf('Storage manager must implement %s.', StorageManagerInterface::class));
}

$this->storageManager = $storageManager;

return $this->storageManager;
}
}
43 changes: 33 additions & 10 deletions src/Controller/Component/UploadComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Cake\Http\Exception\HttpException;
use FileUpload\File\UploadedFileDecorator;
use FileUpload\Storage\StorageManagerInterface;
use Psr\Http\Message\UploadedFileInterface;

/**
* Upload component
Expand All @@ -25,31 +26,53 @@ class UploadComponent extends Component
];

/**
* Allowed stoage types
*
* @var string[]
* @var \FileUpload\Storage\StorageManagerInterface|null
*/
protected array $_allowedStorageTypes = [
'local', 's3',
];
private ?StorageManagerInterface $storageManager = null;

/**
* Uploading File to storage and returns info of that file
*
* @param \Cake\Http\ServerRequest $serverRequest Server Request
* @throws HttpException
* @param \Cake\Controller\Controller $controller Controller instance
* @throws \Cake\Http\Exception\HttpException When upload data is missing or misconfigured
*/
public function getFile(Controller $controller): UploadedFileDecorator
{
$uploadedFile = $controller->getRequest()->getData($this->getConfig('fieldName'));

if (!$uploadedFile instanceof UploadedFileInterface) {
throw new HttpException('Uploaded file data is missing or invalid.');
}

$sm = $this->_getStorageManager();

return $sm->put($controller->getRequest()->getData($this->getConfig('fieldName')));
return $sm->put($uploadedFile);
}

protected function _getStorageManager(): StorageManagerInterface
{
if ($this->storageManager instanceof StorageManagerInterface) {
return $this->storageManager;
}

$sm = $this->getConfig('managerClass');

return new $sm($this->getConfig());
if (!is_string($sm) || $sm === '') {
throw new HttpException('Storage manager class is not configured.');
}

if (!class_exists($sm)) {
throw new HttpException(sprintf('Storage manager class "%s" does not exist.', $sm));
}

$storageManager = new $sm($this->getConfig());

if (!$storageManager instanceof StorageManagerInterface) {
throw new HttpException(sprintf('Storage manager must implement %s.', StorageManagerInterface::class));
}

$this->storageManager = $storageManager;

return $this->storageManager;
}
Comment on lines 52 to 77
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _getStorageManager() method (lines 57-82) is duplicated in both UploadComponent and DownloadComponent. Consider extracting this logic into a shared trait or base class to reduce code duplication and improve maintainability.

Copilot uses AI. Check for mistakes.
}
4 changes: 4 additions & 0 deletions src/File/PathUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ public static function fileNameSanitize(string $filename): string
$filename = pathinfo($filename, PATHINFO_FILENAME);
$filename = Text::slug($filename);

if ($ext === '') {
return $filename;
}

return $filename . '.' . $ext;
}
}
47 changes: 38 additions & 9 deletions src/File/StoredFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,57 @@

namespace FileUpload\File;

use FileUpload\Exceptions\FileContentException;

class StoredFile implements StoredFileInterface
{
protected string $mimeType;
private ?string $content = null;

private ?string $mimeType = null;

public function __construct(
protected string $file,
private string $file,
) {
}

public function getContent(): string
public function getContent(): string
{
$stream = fopen($this->file, 'rb');
$content = stream_get_contents($stream);
if ($this->content !== null) {
return $this->content;
}

$finfo = finfo_open();
$this->mimeType = finfo_buffer($finfo, $content, FILEINFO_MIME_TYPE);
finfo_close($finfo);
$content = @file_get_contents($this->file);

return $content;
if ($content === false) {
throw new FileContentException(sprintf('Cannot load content of file "%s"', $this->file));
}

$this->content = $content;
$this->mimeType = $this->detectMimeType($content);

return $this->content;
}

public function getMimeType(): string
{
if ($this->mimeType === null) {
$this->mimeType = $this->detectMimeType($this->getContent());
}

return $this->mimeType;
}

private function detectMimeType(string $content): string
{
$finfo = finfo_open(FILEINFO_MIME_TYPE);

if ($finfo === false) {
return 'application/octet-stream';
}

$mimeType = finfo_buffer($finfo, $content) ?: 'application/octet-stream';
finfo_close($finfo);

return $mimeType;
}
}
16 changes: 9 additions & 7 deletions src/File/UploadedFileDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@

namespace FileUpload\File;

use Laminas\Diactoros\UploadedFile;
use Psr\Http\Message\UploadedFileInterface;

class UploadedFileDecorator
{
public function __construct(
protected UploadedFile $originalData,
protected UploadedFileInterface $originalData,
protected string $storageType,
protected array $options = []
) {
}

public function getOriginalData(): UploadedFile
public function getOriginalData(): UploadedFileInterface
{
return $this->originalData;
}
Expand All @@ -24,15 +24,17 @@ public function getStorageType(): string
return $this->storageType;
}

public function get(string $key): string
public function get(string $key, mixed $default = null): mixed
{
return $this->options[$key] ?? "";
return $this->options[$key] ?? $default;
}

public function getFileName(): string
{
if (isset($this->options['fileName'])) {
return $this->options['fileName'];
$fileName = $this->get('fileName');

if (is_string($fileName) && $fileName !== '') {
return $fileName;
}

return $this->originalData->getClientFilename();
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getClientFilename() method from PSR-7's UploadedFileInterface returns string|null, but the getFileName() method has a return type of string. This could cause a type error if getClientFilename() returns null. Consider handling the null case explicitly, e.g., return $this->originalData->getClientFilename() ?? '';

Suggested change
return $this->originalData->getClientFilename();
return $this->originalData->getClientFilename() ?? '';

Copilot uses AI. Check for mistakes.
Expand Down
Loading
Loading