From 7e6cd67d0fa405a27a61eaaf767a6dd90bd024df Mon Sep 17 00:00:00 2001 From: busgurlu Date: Thu, 16 Sep 2021 21:28:59 +0300 Subject: [PATCH 01/27] cache images vary for webp --- src/Controller/AttachmentsController.php | 75 ++++++++++++++++-------- 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/src/Controller/AttachmentsController.php b/src/Controller/AttachmentsController.php index 58c720e..750534e 100644 --- a/src/Controller/AttachmentsController.php +++ b/src/Controller/AttachmentsController.php @@ -211,10 +211,17 @@ function ($v, $k) { $file = new File($cacheFile); $response = $this->response->withFile($cacheFile, ['download' => false, 'name' => (isset($attachment) ? $attachment->filename : null)]) + ->withVary('Accept') ->withType($file->mime()) - ->withCache('-1 minute', '+1 month') - ->withExpires('+1 month') + ->withCache('-1 minute', '+6 month') + ->withExpires('+6 month') + ->withMustRevalidate(false) ->withModified($file->lastChange()); + + if ($options['type'] == IMAGETYPE_WEBP) { + $response = $response->withSharable(false); + } + if ($response->checkNotModified($this->request)) { return $response; } @@ -228,8 +235,21 @@ public function file($id, $name = null) if (!file_exists($attachment->path)) { throw new \Exception("File {$attachment->path} cannot be read."); } - $response = $this->response->withType($attachment->filetype) - ->withFile($attachment->path, ['download' => false, 'name' => $attachment->filename]); + $file = new File($attachment->filetype); + + $response = $this->response->withFile($attachment->path, + ['download' => false, 'name' => $attachment->filename]) + ->withType($attachment->filetype) + ->withCache('-1 minute', '+6 month') + ->withExpires('+6 month') + ->withMustRevalidate(false) + ->withModified($file->lastChange()) + ->withSharable(true); + + if ($response->checkNotModified($this->request)) { + return $response; + } + return $response; } @@ -239,8 +259,20 @@ public function download($id, $name = null) if (!file_exists($attachment->path)) { throw new \Exception("File {$attachment->path} cannot be read."); } - $response = $this->response->withType($attachment->filetype) - ->withFile($attachment->path, ['download' => true, 'name' => $attachment->filename]); + $file = new File($attachment->filetype); + + $response = $this->response->withFile($attachment->path, + ['download' => true, 'name' => $attachment->filename]) + ->withType($attachment->filetype) + ->withCache('-1 minute', '+6 month') + ->withExpires('+6 month') + ->withMustRevalidate(false) + ->withModified($file->lastChange()); + + if ($response->checkNotModified($this->request)) { + return $response; + } + return $response; } @@ -314,13 +346,13 @@ public function stream($id, $name = null) ob_get_clean(); header("Content-Type: video/mp4"); header("Cache-Control: max-age=311040000, public"); - header("Expires: ".gmdate('D, d M Y H:i:s', time()+311040000) . ' GMT'); - header("Last-Modified: ".gmdate('D, d M Y H:i:s', @filemtime($attachment->path)) . ' GMT' ); + header("Expires: " . gmdate('D, d M Y H:i:s', time() + 311040000) . ' GMT'); + header("Last-Modified: " . gmdate('D, d M Y H:i:s', @filemtime($attachment->path)) . ' GMT'); $this->start = 0; - $this->size = filesize($attachment->path); - $this->end = $this->size - 1; + $this->size = filesize($attachment->path); + $this->end = $this->size - 1; - header("Accept-Ranges: 0-".$this->end); + header("Accept-Ranges: 0-" . $this->end); //set header if (isset($_SERVER['HTTP_RANGE'])) { @@ -335,7 +367,7 @@ public function stream($id, $name = null) } if ($range == '-') { $c_start = $this->size - substr($range, 1); - }else{ + } else { $range = explode('-', $range); $c_start = $range[0]; $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $c_end; @@ -351,19 +383,17 @@ public function stream($id, $name = null) $length = $this->end - $this->start + 1; fseek($this->stream, $this->start); header('HTTP/1.1 206 Partial Content'); - header("Content-Length: ".$length); - header("Content-Range: bytes $this->start-$this->end/".$this->size); - } - else - { - header("Content-Length: ".$this->size); + header("Content-Length: " . $length); + header("Content-Range: bytes $this->start-$this->end/" . $this->size); + } else { + header("Content-Length: " . $this->size); } //stream $i = $this->start; set_time_limit(0); - while(!feof($this->stream) && $i <= $this->end) { + while (!feof($this->stream) && $i <= $this->end) { $bytesToRead = $this->buffer; - if(($i+$bytesToRead) > $this->end) { + if (($i + $bytesToRead) > $this->end) { $bytesToRead = $this->end - $i + 1; } $data = @stream_get_contents($this->stream, $bytesToRead, intval($i)); @@ -389,14 +419,13 @@ public function editImage($id) if ($this->Attachments->replaceFile($id, $tempPath)) { $this->Flash->success(__('Image modified.')); $redirectTo = $this->getRequest()->getSession()->consume('Attachment.redirectAfter'); - if($redirectTo) { + if ($redirectTo) { return $this->redirect($redirectTo); } } else { $this->Flash->error(__('Image could not be saved. Please, try again.')); } - } - else { + } else { $this->getRequest()->getSession()->write('Attachment.redirectAfter', $this->referer()); } $this->set('image', $image); From c3b25c102024ba49ecead2a6efecf7b9dd5a7248 Mon Sep 17 00:00:00 2001 From: busgurlu Date: Sun, 7 Nov 2021 21:32:32 +0300 Subject: [PATCH 02/27] sub attachments for subtitles, etc. --- src/Model/Entity/Attachment.php | 3 +++ src/Model/Table/AttachmentsTable.php | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/Model/Entity/Attachment.php b/src/Model/Entity/Attachment.php index 73f75f1..87c1d42 100644 --- a/src/Model/Entity/Attachment.php +++ b/src/Model/Entity/Attachment.php @@ -6,6 +6,7 @@ use Cake\Filesystem\Folder; use Cake\I18n\Number; use Cake\Core\Configure; +use JeremyHarris\LazyLoad\ORM\LazyLoadEntityTrait; use Uskur\Attachments\Model\Entity\DetailsTrait; /** @@ -23,6 +24,8 @@ class Attachment extends Entity { use DetailsTrait; + use LazyLoadEntityTrait; + /** * Fields that can be mass assigned using newEntity() or patchEntity(). diff --git a/src/Model/Table/AttachmentsTable.php b/src/Model/Table/AttachmentsTable.php index 822bd3b..551be5a 100644 --- a/src/Model/Table/AttachmentsTable.php +++ b/src/Model/Table/AttachmentsTable.php @@ -42,6 +42,20 @@ public function initialize(array $config) 'scope' => ['model', 'foreign_key'], 'start' => 1, ]); + + $this->belongsTo('ParentAttachment', [ + 'className' => 'Uskur/Attachments.Attachments', + 'foreignKey' => 'foreign_key', + 'conditions' => ['SubAttachments.model'=>'Attachments'], + 'joinType' => 'LEFT' + ]); + $this->hasMany('SubAttachments', [ + 'className' => 'Uskur/Attachments.Attachments', + 'foreignKey' => 'foreign_key', + 'conditions' => ['SubAttachments.model'=>'Attachments'], + 'dependent' => true + ]); + } /** From 517a0f82e07f078fb6c64fe5fb97b4294f1ea406 Mon Sep 17 00:00:00 2001 From: busgurlu Date: Fri, 3 Mar 2023 16:59:38 +0300 Subject: [PATCH 03/27] setBackgroundColor to white for PDFs --- src/Controller/AttachmentsController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controller/AttachmentsController.php b/src/Controller/AttachmentsController.php index 750534e..e26e275 100644 --- a/src/Controller/AttachmentsController.php +++ b/src/Controller/AttachmentsController.php @@ -155,6 +155,7 @@ function ($v, $k) { if ($attachment->filetype === 'application/pdf') { $imagePath = "/tmp/" . uniqid(); $imagick = new \Imagick("{$attachment->path}[0]"); + $imagick->setBackgroundColor('white'); $imagick->setImageFormat('jpg'); file_put_contents($imagePath, $imagick); } From 85201afdda275d7aa4987104a9546121f86258e4 Mon Sep 17 00:00:00 2001 From: busgurlu Date: Fri, 3 Mar 2023 17:55:16 +0300 Subject: [PATCH 04/27] fix pdf preview --- src/Controller/AttachmentsController.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Controller/AttachmentsController.php b/src/Controller/AttachmentsController.php index e26e275..2fb2194 100644 --- a/src/Controller/AttachmentsController.php +++ b/src/Controller/AttachmentsController.php @@ -155,8 +155,11 @@ function ($v, $k) { if ($attachment->filetype === 'application/pdf') { $imagePath = "/tmp/" . uniqid(); $imagick = new \Imagick("{$attachment->path}[0]"); + $imagick->setResolution(300, 300); $imagick->setBackgroundColor('white'); $imagick->setImageFormat('jpg'); + $imagick->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN); + $imagick->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE); file_put_contents($imagePath, $imagick); } $image = new ImageResize($imagePath); From d9cad6d4885cae12ee8733a41c0a52715f061b08 Mon Sep 17 00:00:00 2001 From: busgurlu Date: Wed, 16 Aug 2023 21:35:34 +0300 Subject: [PATCH 05/27] add 1:1 ratio --- src/Template/Attachments/edit_image.ctp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Template/Attachments/edit_image.ctp b/src/Template/Attachments/edit_image.ctp index ff28fda..348a9c5 100644 --- a/src/Template/Attachments/edit_image.ctp +++ b/src/Template/Attachments/edit_image.ctp @@ -23,6 +23,7 @@ $this->start('context-menu'); ?> + From ca96849e8813ffdc4af94ed3b87de4e1a83db7fb Mon Sep 17 00:00:00 2001 From: busgurlu Date: Wed, 17 Apr 2024 11:31:03 +0300 Subject: [PATCH 06/27] fix deprecated use, reformat --- src/Model/Table/AttachmentsTable.php | 66 ++++++++++++++++------------ 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/src/Model/Table/AttachmentsTable.php b/src/Model/Table/AttachmentsTable.php index 551be5a..b088617 100644 --- a/src/Model/Table/AttachmentsTable.php +++ b/src/Model/Table/AttachmentsTable.php @@ -46,16 +46,15 @@ public function initialize(array $config) $this->belongsTo('ParentAttachment', [ 'className' => 'Uskur/Attachments.Attachments', 'foreignKey' => 'foreign_key', - 'conditions' => ['SubAttachments.model'=>'Attachments'], - 'joinType' => 'LEFT' + 'conditions' => ['SubAttachments.model' => 'Attachments'], + 'joinType' => 'LEFT', ]); $this->hasMany('SubAttachments', [ 'className' => 'Uskur/Attachments.Attachments', 'foreignKey' => 'foreign_key', - 'conditions' => ['SubAttachments.model'=>'Attachments'], - 'dependent' => true + 'conditions' => ['SubAttachments.model' => 'Attachments'], + 'dependent' => true, ]); - } /** @@ -64,11 +63,11 @@ public function initialize(array $config) * @param \Cake\Validation\Validator $validator Validator instance. * @return \Cake\Validation\Validator */ - public function validationDefault(Validator $validator) + public function validationDefault(Validator $validator): Validator { $validator ->uuid('id') - ->allowEmptyString('id', 'create'); + ->allowEmptyString('id', null, 'create'); $validator ->allowEmptyString('filename'); @@ -98,16 +97,20 @@ public function buildRules(RulesChecker $rules) } /** - * Save one Attachemnt + * Save one Attachment * * @param EntityInterface $entity Entity * @param string $upload Upload - * @return boolean + * @param array $allowed_types Allowed types + * @param array $details Details + * @return bool|EntityInterface + * @throws \Exception */ public function addUpload($entity, $upload, $allowed_types = [], $details = []) { - if (!empty($allowed_types) && !in_array($upload['type'], $allowed_types)) + if (!empty($allowed_types) && !in_array($upload['type'], $allowed_types)) { throw new \Exception("File type not allowed."); + } if (!file_exists($upload['tmp_name'])) { throw new \Exception("File {$upload['tmp_name']} does not exist."); } @@ -117,28 +120,31 @@ public function addUpload($entity, $upload, $allowed_types = [], $details = []) $file = new File($upload['tmp_name']); $info = $file->info(); $attachment = $this->newEntity([ - 'model' => $entity->source(), + 'model' => $entity->getSource(), 'foreign_key' => $entity->id, 'filename' => $upload['name'], 'size' => $info['filesize'], 'filetype' => $info['mime'], 'md5' => $file->md5(true), - 'tmpPath' => $upload['tmp_name'] + 'tmpPath' => $upload['tmp_name'], ]); - if ($details) + if ($details) { $attachment->details = json_encode($details); + } // if the same thing return existing - $existing = $this->find('all') + $existing = $this->find() ->where([ 'filename' => $attachment->filename, 'model' => $attachment->model, 'foreign_key' => $attachment->foreign_key, 'md5' => $attachment->md5, 'details' => $attachment->details])->first(); - if ($existing) return $existing; - + if ($existing) { + return $existing; + } $save = $this->save($attachment); + return ($save) ? true : false; } @@ -153,23 +159,26 @@ public function addFile($entity, $filePath, $details = []) 'size' => $info['filesize'], 'filetype' => $info['mime'], 'md5' => $file->md5(true), - 'tmpPath' => $filePath + 'tmpPath' => $filePath, ]); - if ($details) + if ($details) { $attachment->details = json_encode($details); + } // if the same thing return existing - $existing = $this->find('all') + $existing = $this->find() ->where([ 'filename' => $attachment->filename, 'model' => $attachment->model, 'foreign_key' => $attachment->foreign_key, 'md5' => $attachment->md5, 'details' => $attachment->details])->first(); - if ($existing) return $existing; - + if ($existing) { + return $existing; + } $save = $this->save($attachment); + return ($save) ? $attachment : false; } @@ -215,20 +224,21 @@ public function getAttachmentsOfArticle($articleId, $type = 'image') $attachments = $this->find('all', [ 'conditions' => [ 'Attachments.article_id' => $articleId, - 'Attachments.filetype LIKE' => "$type/%" + 'Attachments.filetype LIKE' => "$type/%", ], - 'contain' => [] + 'contain' => [], ]); + return $attachments; } /** * Replace file - * @param $id - * @param $path - * @return bool + * @param string $id Attachment ID + * @param string $tmpPath Path to the new file + * @return bool|Attachment */ - public function replaceFile($id, $tmpPath) + public function replaceFile(string $id, string $tmpPath) { $currentAttachment = $this->get($id); $this->delete($currentAttachment); @@ -241,7 +251,7 @@ public function replaceFile($id, $tmpPath) 'size' => $file->size(), 'filetype' => $file->mime(), 'md5' => $file->md5(true), - 'tmpPath' => $tmpPath + 'tmpPath' => $tmpPath, ]); return $this->save($attachment) ? $attachment : false; From a68aa7f0073719943eb81e14d1333bbd7e730e57 Mon Sep 17 00:00:00 2001 From: burak Date: Sat, 22 Jun 2024 12:54:17 +0300 Subject: [PATCH 07/27] s3 initial --- composer.json | 3 +- config/attachments.php | 7 ++- src/Model/Entity/Attachment.php | 79 +++++++++++++++++++++++++--- src/Model/Table/AttachmentsTable.php | 68 +++++++++++++++++++++++- 4 files changed, 146 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 4210b54..097517b 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,8 @@ "php": ">=5.6", "cakephp/cakephp": ">=3.4 <4.0.0", "admad/cakephp-sequence": "^2.0", - "gumlet/php-image-resize": "^1.9" + "gumlet/php-image-resize": "^1.9", + "aws/aws-sdk-php": "^3.314" }, "require-dev": { "phpunit/phpunit": "*" diff --git a/config/attachments.php b/config/attachments.php index a1d4736..d7ea701 100755 --- a/config/attachments.php +++ b/config/attachments.php @@ -13,7 +13,12 @@ $config = [ 'Attachment' => [ - 'path' => '/tmp/filestorage' + 'path' => '/tmp/filestorage', + 's3-endpoint' => false, + 's3-region' => '', + 's3-key' => '', + 's3-secret' => '', + 's3-bucket' => '', ] ]; diff --git a/src/Model/Entity/Attachment.php b/src/Model/Entity/Attachment.php index 87c1d42..f243783 100644 --- a/src/Model/Entity/Attachment.php +++ b/src/Model/Entity/Attachment.php @@ -1,4 +1,5 @@ false, ]; - protected $_virtual = ['details_array','readable_size','readable_created']; + protected $_virtual = ['details_array', 'readable_size', 'readable_created']; protected function _getPath() { - $targetDir = Configure::read('Attachment.path').DS.substr($this->_properties['md5'],0,2); - $folder = new Folder(); - if (!$folder->create($targetDir)) { - throw new \Exception("Folder {$targetDir} could not be created."); - } + $targetDir = Configure::read('Attachment.path') . DS . substr($this->_properties['md5'], 0, 2); + + $filePath = $targetDir . DS . $this->_properties['md5']; + + $folder = new Folder(); + if (!file_exists($filePath) && !$folder->create($targetDir)) { + throw new \Exception("Folder {$targetDir} could not be created."); + } - return $targetDir.DS.$this->_properties['md5']; + if (!file_exists($filePath) && Configure::read('Attachment.s3-endpoint')) { + $config = + [ + 'version' => 'latest', + 'region' => Configure::read('Attachment.s3-region'), + 'endpoint' => Configure::read('Attachment.s3-endpoint'), + 'credentials' => + [ + 'key' => Configure::read('Attachment.s3-key'), + 'secret' => Configure::read('Attachment.s3-secret'), + ], + ]; + $s3client = new \Aws\S3\S3Client($config); + try { + $s3client->getObject( + [ + 'Bucket' => Configure::read('Attachment.s3-bucket'), + 'Key' => $this->s3_path, + 'SaveAs' => $filePath, + ] + ); + } catch (\Exception $e) { + return false; + } + } + + return $filePath; } protected function _getReadableSize() @@ -69,4 +102,36 @@ protected function _getExtension() $pathinfo = pathinfo($this->filename); return $pathinfo['extension']; } + + //s3 path + protected function _getS3Path() + { + return substr($this->_properties['md5'], 0, 2) . '/' . $this->_properties['md5']; + } + + protected function _getS3Attributes() + { + if (Configure::read('Attachment.s3-endpoint')) { + $config = + [ + 'version' => 'latest', + 'region' => Configure::read('Attachment.s3-region'), + 'endpoint' => Configure::read('Attachment.s3-endpoint'), + 'credentials' => + [ + 'key' => Configure::read('Attachment.s3-key'), + 'secret' => Configure::read('Attachment.s3-secret'), + ], + ]; + $s3client = new \Aws\S3\S3Client($config); + try { + return $s3client->getObjectAttributes( + Configure::read('Attachment.s3-bucket'), + $this->s3_path + ); + } catch (\Exception $e) { + return false; + } + } + } } diff --git a/src/Model/Table/AttachmentsTable.php b/src/Model/Table/AttachmentsTable.php index b088617..c3827bc 100644 --- a/src/Model/Table/AttachmentsTable.php +++ b/src/Model/Table/AttachmentsTable.php @@ -2,6 +2,7 @@ namespace Uskur\Attachments\Model\Table; +use Cake\Core\Configure; use Uskur\Attachments\Model\Entity\Attachment; use ArrayObject; use Cake\ORM\Entity; @@ -22,6 +23,9 @@ class AttachmentsTable extends Table { + private $s3client = false; + private $s3bucket = false; + /** * Initialize method * @@ -55,6 +59,22 @@ public function initialize(array $config) 'conditions' => ['SubAttachments.model' => 'Attachments'], 'dependent' => true, ]); + + if (Configure::read('Attachment.s3-endpoint')) { + $config = + [ + 'version' => 'latest', + 'region' => Configure::read('Attachment.s3-region'), + 'endpoint' => Configure::read('Attachment.s3-endpoint'), + 'credentials' => + [ + 'key' => Configure::read('Attachment.s3-key'), + 'secret' => Configure::read('Attachment.s3-secret'), + ], + ]; + $this->s3client = new \Aws\S3\S3Client($config); + $this->s3bucket = Configure::read('Attachment.s3-bucket'); + } } /** @@ -205,6 +225,17 @@ public function afterSave(Event $event, Attachment $attachment, \ArrayObject $op throw new \Exception("File {$attachment->tmpPath} could not be copied to {$attachment->path}"); } } + if ($this->s3bucket !== false) { + try { + $result = $this->s3client->putObject([ + 'Bucket' => $this->s3bucket, + 'Key' => $attachment->s3_path, + 'SourceFile' => $attachment->path, + ]); + } catch (\Exception $e) { + throw new \Exception("File {$attachment->tmpPath} could not be moved to S3 bucket"); + } + } $attachment->tmpPath = null; } } @@ -212,9 +243,16 @@ public function afterSave(Event $event, Attachment $attachment, \ArrayObject $op public function afterDelete(Event $event, Attachment $attachment, \ArrayObject $options) { if (file_exists($attachment->get('path'))) { - $otherExisting = $this->find('all', ['conditions' => ['Attachments.md5' => $attachment->md5]])->count(); + $otherExisting = $this->find()->where(['Attachments.md5' => $attachment->md5])->count(); if ($otherExisting == 0) { - unlink($attachment->get('path')); + if ($this->s3bucket !== false) { + $this->s3client->deleteObject([ + 'Bucket' => $this->s3bucket, + 'Key' => $attachment->s3_path, + ]); + } else { + unlink($attachment->get('path')); + } } } } @@ -256,4 +294,30 @@ public function replaceFile(string $id, string $tmpPath) return $this->save($attachment) ? $attachment : false; } + + public function moveFilesToS3($limit = 100) + { + $moved = 0; + $attachments = $this->find(); + foreach ($attachments as $attachment) { + if($moved >= $limit) { + break; + } + if ($attachment->path && file_exists($attachment->path)) { + if ($this->s3bucket !== false && !$this->s3client->objectExists($this->s3bucket, $attachment->s3_path)) { + try { + $result = $this->s3client->putObject([ + 'Bucket' => $this->s3bucket, + 'Key' => $attachment->s3_path, + 'SourceFile' => $attachment->path, + ]); + } catch (\Exception $e) { + throw new \Exception("File {$attachment->tmpPath} could not be moved to S3 bucket"); + } + $moved++; + } + $attachment->tmpPath = null; + } + } + } } From 92c16fe7c40455838cda51bff1dbe2c1d118c76e Mon Sep 17 00:00:00 2001 From: burak Date: Sat, 22 Jun 2024 13:16:49 +0300 Subject: [PATCH 08/27] fix call --- src/Model/Table/AttachmentsTable.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Model/Table/AttachmentsTable.php b/src/Model/Table/AttachmentsTable.php index c3827bc..b65f790 100644 --- a/src/Model/Table/AttachmentsTable.php +++ b/src/Model/Table/AttachmentsTable.php @@ -297,6 +297,9 @@ public function replaceFile(string $id, string $tmpPath) public function moveFilesToS3($limit = 100) { + if($this->s3bucket == false) { + throw new \Exception("S3 bucket not configured"); + } $moved = 0; $attachments = $this->find(); foreach ($attachments as $attachment) { @@ -304,7 +307,7 @@ public function moveFilesToS3($limit = 100) break; } if ($attachment->path && file_exists($attachment->path)) { - if ($this->s3bucket !== false && !$this->s3client->objectExists($this->s3bucket, $attachment->s3_path)) { + if (!$this->s3client->doesObjectExistV2($this->s3bucket, $attachment->s3_path)) { try { $result = $this->s3client->putObject([ 'Bucket' => $this->s3bucket, @@ -312,11 +315,10 @@ public function moveFilesToS3($limit = 100) 'SourceFile' => $attachment->path, ]); } catch (\Exception $e) { - throw new \Exception("File {$attachment->tmpPath} could not be moved to S3 bucket"); + throw new \Exception("File {$attachment->path} could not be moved to S3 bucket"); } $moved++; } - $attachment->tmpPath = null; } } } From ee20fb2c9d6d8ee9b69ffeb67eea3ca27b626cb9 Mon Sep 17 00:00:00 2001 From: burak Date: Sat, 22 Jun 2024 13:31:16 +0300 Subject: [PATCH 09/27] access externally --- src/Model/Table/AttachmentsTable.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/Table/AttachmentsTable.php b/src/Model/Table/AttachmentsTable.php index b65f790..2426331 100644 --- a/src/Model/Table/AttachmentsTable.php +++ b/src/Model/Table/AttachmentsTable.php @@ -23,8 +23,8 @@ class AttachmentsTable extends Table { - private $s3client = false; - private $s3bucket = false; + public $s3client = false; + public $s3bucket = false; /** * Initialize method From 99ece5baec5e58faa25edae6a6f2131b87a05bcf Mon Sep 17 00:00:00 2001 From: busgurlu Date: Fri, 28 Jun 2024 21:06:42 +0300 Subject: [PATCH 10/27] take action if tmppath exists --- src/Model/Table/AttachmentsTable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Table/AttachmentsTable.php b/src/Model/Table/AttachmentsTable.php index 2426331..4732f32 100644 --- a/src/Model/Table/AttachmentsTable.php +++ b/src/Model/Table/AttachmentsTable.php @@ -216,7 +216,7 @@ public function afterSave(Event $event, Attachment $attachment, \ArrayObject $op { if ($attachment->tmpPath) { $path = $attachment->get('path'); - if (is_uploaded_file($attachment->tmpPath)) { + if (is_uploaded_file($attachment->tmpPath) && file_exists($attachment->tmpPath)) { if (!move_uploaded_file($attachment->tmpPath, $path)) { throw new \Exception("Temporary file {$attachment->tmpPath} could not be moved to {$attachment->path}"); } From 7763d9df49ebf663d20f1473a637f1edd6286d64 Mon Sep 17 00:00:00 2001 From: busgurlu Date: Fri, 28 Jun 2024 21:26:44 +0300 Subject: [PATCH 11/27] check for tmpPath --- src/Model/Entity/Attachment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Entity/Attachment.php b/src/Model/Entity/Attachment.php index f243783..f43c089 100644 --- a/src/Model/Entity/Attachment.php +++ b/src/Model/Entity/Attachment.php @@ -58,7 +58,7 @@ protected function _getPath() throw new \Exception("Folder {$targetDir} could not be created."); } - if (!file_exists($filePath) && Configure::read('Attachment.s3-endpoint')) { + if (!file_exists($filePath) && Configure::read('Attachment.s3-endpoint') && is_null($this->tmpPath)) { $config = [ 'version' => 'latest', From e04b68dcb19c62ff8a39a48ce48e8f1cd82df4c2 Mon Sep 17 00:00:00 2001 From: busgurlu Date: Tue, 10 Sep 2024 20:33:15 +0300 Subject: [PATCH 12/27] delete from path as well --- src/Model/Table/AttachmentsTable.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Model/Table/AttachmentsTable.php b/src/Model/Table/AttachmentsTable.php index 4732f32..1b2c5fc 100644 --- a/src/Model/Table/AttachmentsTable.php +++ b/src/Model/Table/AttachmentsTable.php @@ -250,7 +250,8 @@ public function afterDelete(Event $event, Attachment $attachment, \ArrayObject $ 'Bucket' => $this->s3bucket, 'Key' => $attachment->s3_path, ]); - } else { + } + if (file_exists($attachment->get('path'))) { unlink($attachment->get('path')); } } From c86366285d76e611c2d4c41b5849c9aef9fc836d Mon Sep 17 00:00:00 2001 From: Burak USGURLU Date: Mon, 5 May 2025 11:12:56 +0300 Subject: [PATCH 13/27] Update gumlet --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 097517b..e10cc38 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "php": ">=5.6", "cakephp/cakephp": ">=3.4 <4.0.0", "admad/cakephp-sequence": "^2.0", - "gumlet/php-image-resize": "^1.9", + "gumlet/php-image-resize": "^2.0", "aws/aws-sdk-php": "^3.314" }, "require-dev": { From 66d4b64f1e76fe39b57dcd452910de6bb852fc99 Mon Sep 17 00:00:00 2001 From: busgurlu Date: Mon, 5 May 2025 11:54:51 +0300 Subject: [PATCH 14/27] crop image to requested size after resizing with fill-color --- src/Controller/AttachmentsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/AttachmentsController.php b/src/Controller/AttachmentsController.php index 2fb2194..9812d7d 100644 --- a/src/Controller/AttachmentsController.php +++ b/src/Controller/AttachmentsController.php @@ -140,7 +140,6 @@ function ($v, $k) { array_keys($options) )); $cacheFile = $cacheFolder . DS . md5($id . $cacheKey); - if (!file_exists($cacheFile)) { if (!file_exists($cacheFolder)) { mkdir($cacheFolder); @@ -193,6 +192,7 @@ function ($v, $k) { //delete temp image unlink($tempImage); }); + $image->crop($options['w'], $options['h']); } elseif ($options['w'] && $options['h'] && $options['c']) { $image->crop($options['w'], $options['h'], $options['e']); } elseif ($options['w'] && $options['h']) { From 187a21722428e3ea685c0c3bf0297e21c2572d3d Mon Sep 17 00:00:00 2001 From: busgurlu Date: Mon, 5 May 2025 12:54:09 +0300 Subject: [PATCH 15/27] validate parameters and convert quality for png --- src/Controller/AttachmentsController.php | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/Controller/AttachmentsController.php b/src/Controller/AttachmentsController.php index 9812d7d..51c98b1 100644 --- a/src/Controller/AttachmentsController.php +++ b/src/Controller/AttachmentsController.php @@ -115,6 +115,37 @@ public function image($id) $options = []; foreach ($validOptions as $option) { if ($this->request->getQuery($option)) { + //validate quality + if ($option == 'q' && ( + !is_numeric($this->request->getQuery($option)) || + $this->request->getQuery($option) > 100) || + $this->request->getQuery($option) < 0 + ) { + throw new \Exception("Invalid quality parameter."); + } + //validate height and width + if (($option == 'w' || $option == 'h') && ( + !is_numeric($this->request->getQuery($option)) || + $this->request->getQuery($option) < 0 + )) { + throw new \Exception("Invalid height/width parameter."); + } + //validate crop and enlarge + if (($option == 'c' || $option == 'e') && ( + $this->request->getQuery($option) != 0 && + $this->request->getQuery($option) != 1 + )) { + throw new \Exception("Invalid crop/enlarge parameter."); + } + //validate mode + if ($option == 'm' && $this->request->getQuery($option) != 'fill') { + throw new \Exception("Invalid mode parameter."); + } + //validate fill color + if ($option == 'fc' && !preg_match('/^[a-f0-9]{6}$/i', $this->request->getQuery($option))) { + throw new \Exception("Invalid fill color parameter."); + } + $options[$option] = $this->request->getQuery($option); } //default fill color to white elseif ($option == 'fc') { @@ -206,6 +237,10 @@ function ($v, $k) { //preserve PNG for transparency if ($attachment->filetype == 'image/png' && $options['type'] != IMAGETYPE_WEBP) { $options['type'] = IMAGETYPE_PNG; + //modify quality imagejpeg to imagepng + if (!is_null($options['q'])) { + $options['q'] = (int) round((100 - $options['q']) / 10); + } } $image->save($cacheFile, $options['type'], $options['q']); } From 6026168a0d802546d54db86478f3bd5947d4360a Mon Sep 17 00:00:00 2001 From: busgurlu Date: Thu, 14 Aug 2025 13:42:16 +0300 Subject: [PATCH 16/27] include 4/3 ratio --- src/Template/Attachments/edit_image.ctp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Template/Attachments/edit_image.ctp b/src/Template/Attachments/edit_image.ctp index 348a9c5..c0481cb 100644 --- a/src/Template/Attachments/edit_image.ctp +++ b/src/Template/Attachments/edit_image.ctp @@ -21,6 +21,7 @@ $this->start('context-menu'); ?>
+ From fbcdbae158b50c015ca51c20e2da6a5785bf7da2 Mon Sep 17 00:00:00 2001 From: busgurlu Date: Sat, 23 Aug 2025 13:45:57 +0300 Subject: [PATCH 17/27] fix fill issues --- src/Controller/AttachmentsController.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Controller/AttachmentsController.php b/src/Controller/AttachmentsController.php index 51c98b1..5cbcc67 100644 --- a/src/Controller/AttachmentsController.php +++ b/src/Controller/AttachmentsController.php @@ -171,6 +171,9 @@ function ($v, $k) { array_keys($options) )); $cacheFile = $cacheFolder . DS . md5($id . $cacheKey); + if(file_exists($cacheFile)) { + unlink($cacheFile); + } if (!file_exists($cacheFile)) { if (!file_exists($cacheFolder)) { mkdir($cacheFolder); @@ -200,7 +203,7 @@ function ($v, $k) { $image->save($tempImage, IMAGETYPE_JPEG); $image = new ImageResize($imagePath); - $image->resize($options['w'], $options['h']); + $image->resize($options['w'], $options['h'], true); $image->addFilter(function ($imageDesc) use ($options, $tempImage) { list($r, $g, $b) = sscanf($options['fc'], "%02x%02x%02x"); $backgroundColor = imagecolorallocate($imageDesc, $r, $g, $b); @@ -223,7 +226,6 @@ function ($v, $k) { //delete temp image unlink($tempImage); }); - $image->crop($options['w'], $options['h']); } elseif ($options['w'] && $options['h'] && $options['c']) { $image->crop($options['w'], $options['h'], $options['e']); } elseif ($options['w'] && $options['h']) { From e25b3dd9286cc72596ea4a33a357068e034157e7 Mon Sep 17 00:00:00 2001 From: busgurlu Date: Sat, 23 Aug 2025 13:46:53 +0300 Subject: [PATCH 18/27] remove debug mode --- src/Controller/AttachmentsController.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Controller/AttachmentsController.php b/src/Controller/AttachmentsController.php index 5cbcc67..2839583 100644 --- a/src/Controller/AttachmentsController.php +++ b/src/Controller/AttachmentsController.php @@ -171,9 +171,6 @@ function ($v, $k) { array_keys($options) )); $cacheFile = $cacheFolder . DS . md5($id . $cacheKey); - if(file_exists($cacheFile)) { - unlink($cacheFile); - } if (!file_exists($cacheFile)) { if (!file_exists($cacheFolder)) { mkdir($cacheFolder); From 564f79fe7a2ec3a5e186af70af083016552ac90f Mon Sep 17 00:00:00 2001 From: busgurlu Date: Sat, 6 Sep 2025 15:38:03 +0300 Subject: [PATCH 19/27] copy attachment to new entity --- src/Model/Entity/Attachment.php | 1 + src/Model/Table/AttachmentsTable.php | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/Model/Entity/Attachment.php b/src/Model/Entity/Attachment.php index f43c089..42a7a63 100644 --- a/src/Model/Entity/Attachment.php +++ b/src/Model/Entity/Attachment.php @@ -24,6 +24,7 @@ * @property string $extension * @property string $s3_path * @property array $s3_attributes + * @property int $sequence */ class Attachment extends Entity { diff --git a/src/Model/Table/AttachmentsTable.php b/src/Model/Table/AttachmentsTable.php index 1b2c5fc..446e47d 100644 --- a/src/Model/Table/AttachmentsTable.php +++ b/src/Model/Table/AttachmentsTable.php @@ -323,4 +323,17 @@ public function moveFilesToS3($limit = 100) } } } + + public function copyAttachment($id, $entity) + { + $currentAttachment = $this->get($id); + $newAttachmentData = $currentAttachment->toArray(); + unset($newAttachmentData['id']); + unset($newAttachmentData['created']); + unset($newAttachmentData['sequence']); + $newAttachmentData['model'] = $entity->getSource(); + $newAttachmentData['foreign_key'] = $entity->id; + $newAttachment = $this->newEntity($newAttachmentData); + return $this->save($newAttachment) ? $newAttachment : false; + } } From cf668f07446caf385f6579c70ae77e7be649f89b Mon Sep 17 00:00:00 2001 From: busgurlu Date: Tue, 7 Oct 2025 18:32:02 +0300 Subject: [PATCH 20/27] require imagick --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index e10cc38..0a19200 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "type": "cakephp-plugin", "require": { "php": ">=5.6", + "ext-imagick": "*", "cakephp/cakephp": ">=3.4 <4.0.0", "admad/cakephp-sequence": "^2.0", "gumlet/php-image-resize": "^2.0", From b8cad00f23d8300bf3d9c5f0f381b4401bfd0073 Mon Sep 17 00:00:00 2001 From: busgurlu Date: Mon, 13 Oct 2025 17:28:30 +0300 Subject: [PATCH 21/27] new index --- ...13171500_AddSequenceIndexToAttachments.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 config/Migrations/20251013171500_AddSequenceIndexToAttachments.php diff --git a/config/Migrations/20251013171500_AddSequenceIndexToAttachments.php b/config/Migrations/20251013171500_AddSequenceIndexToAttachments.php new file mode 100644 index 0000000..a436d0d --- /dev/null +++ b/config/Migrations/20251013171500_AddSequenceIndexToAttachments.php @@ -0,0 +1,19 @@ +table('attachments'); + $table->addIndex(['model', 'foreign_key', 'sequence'], ['name' => 'model_foreign_key_sequence']); + $table->update(); + } +} From 547adaab82e3b5345bb0b96c81a39ddb6876fb26 Mon Sep 17 00:00:00 2001 From: Burak USGURLU Date: Tue, 4 Nov 2025 17:14:45 +0300 Subject: [PATCH 22/27] Update src/Model/Table/AttachmentsTable.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Model/Table/AttachmentsTable.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/Table/AttachmentsTable.php b/src/Model/Table/AttachmentsTable.php index 446e47d..0ea0afa 100644 --- a/src/Model/Table/AttachmentsTable.php +++ b/src/Model/Table/AttachmentsTable.php @@ -23,8 +23,8 @@ class AttachmentsTable extends Table { - public $s3client = false; - public $s3bucket = false; + protected $s3client = false; + protected $s3bucket = false; /** * Initialize method From 140ad18849fc830eefe772387ced64680d2015a9 Mon Sep 17 00:00:00 2001 From: Burak USGURLU Date: Tue, 4 Nov 2025 17:15:09 +0300 Subject: [PATCH 23/27] Update src/Model/Table/AttachmentsTable.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Model/Table/AttachmentsTable.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Model/Table/AttachmentsTable.php b/src/Model/Table/AttachmentsTable.php index 0ea0afa..3aeb1c8 100644 --- a/src/Model/Table/AttachmentsTable.php +++ b/src/Model/Table/AttachmentsTable.php @@ -296,6 +296,13 @@ public function replaceFile(string $id, string $tmpPath) return $this->save($attachment) ? $attachment : false; } + /** + * Move files to S3 storage. + * + * @param int $limit The maximum number of files to move. + * @return void + * @throws \Exception If the S3 bucket is not configured or a file cannot be moved. + */ public function moveFilesToS3($limit = 100) { if($this->s3bucket == false) { From f2803cb3fe65091bf370fe64dd51d356651b577a Mon Sep 17 00:00:00 2001 From: Burak USGURLU Date: Tue, 4 Nov 2025 17:15:39 +0300 Subject: [PATCH 24/27] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Model/Table/AttachmentsTable.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Model/Table/AttachmentsTable.php b/src/Model/Table/AttachmentsTable.php index 3aeb1c8..d8533a1 100644 --- a/src/Model/Table/AttachmentsTable.php +++ b/src/Model/Table/AttachmentsTable.php @@ -305,13 +305,13 @@ public function replaceFile(string $id, string $tmpPath) */ public function moveFilesToS3($limit = 100) { - if($this->s3bucket == false) { + if ($this->s3bucket == false) { throw new \Exception("S3 bucket not configured"); } $moved = 0; $attachments = $this->find(); foreach ($attachments as $attachment) { - if($moved >= $limit) { + if ($moved >= $limit) { break; } if ($attachment->path && file_exists($attachment->path)) { @@ -331,6 +331,13 @@ public function moveFilesToS3($limit = 100) } } + /** + * Copies an attachment to a new entity. + * + * @param string $id Attachment ID + * @param \Cake\Datasource\EntityInterface $entity The target entity + * @return bool|\Uskur\Attachments\Model\Entity\Attachment + */ public function copyAttachment($id, $entity) { $currentAttachment = $this->get($id); From d29a25b217380c53a6463a8f0ce84e7a93792f19 Mon Sep 17 00:00:00 2001 From: Burak USGURLU Date: Tue, 4 Nov 2025 17:16:06 +0300 Subject: [PATCH 25/27] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Template/Attachments/edit_image.ctp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Template/Attachments/edit_image.ctp b/src/Template/Attachments/edit_image.ctp index c0481cb..2ebebef 100644 --- a/src/Template/Attachments/edit_image.ctp +++ b/src/Template/Attachments/edit_image.ctp @@ -21,7 +21,7 @@ $this->start('context-menu'); ?>
- + From a38ba5b371e3e34943a6c1dac0d769e0c1e5ec0a Mon Sep 17 00:00:00 2001 From: busgurlu Date: Fri, 21 Nov 2025 16:20:17 +0300 Subject: [PATCH 26/27] update deprecated calls --- config/Migrations/20170126125021_AttachmentsInitial.php | 2 +- src/Model/Behavior/AttachmentsBehavior.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/Migrations/20170126125021_AttachmentsInitial.php b/config/Migrations/20170126125021_AttachmentsInitial.php index e7445ac..211cae7 100644 --- a/config/Migrations/20170126125021_AttachmentsInitial.php +++ b/config/Migrations/20170126125021_AttachmentsInitial.php @@ -52,6 +52,6 @@ public function up() public function down() { - $this->dropTable('attachments'); + $this->table('attachments')->drop()->save(); } } diff --git a/src/Model/Behavior/AttachmentsBehavior.php b/src/Model/Behavior/AttachmentsBehavior.php index aef499e..7c83a3a 100644 --- a/src/Model/Behavior/AttachmentsBehavior.php +++ b/src/Model/Behavior/AttachmentsBehavior.php @@ -189,7 +189,7 @@ protected function _clearTag($attachment, $tag) if ($existingTag === $tag) { unset($attachmentWithExclusiveTag->tags[$key]); $attachmentWithExclusiveTag->tags = array_values($attachmentWithExclusiveTag->tags); - $attachmentWithExclusiveTag->dirty('tags', true); + $attachmentWithExclusiveTag->setDirty('tags'); break; } } From 12db687b6f60bada906bd4a45cf04409af398377 Mon Sep 17 00:00:00 2001 From: Burak USGURLU Date: Wed, 25 Feb 2026 18:56:32 +0300 Subject: [PATCH 27/27] constrict version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0a19200..42c75cb 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "cakephp/cakephp": ">=3.4 <4.0.0", "admad/cakephp-sequence": "^2.0", "gumlet/php-image-resize": "^2.0", - "aws/aws-sdk-php": "^3.314" + "aws/aws-sdk-php": ">=3.314 <3.368" }, "require-dev": { "phpunit/phpunit": "*"