<?php
namespace Croogo\FileManager\Model\Table;
use ArrayObject;
use Cake\Datasource\EntityInterface;
use Cake\Event\Event;
use Cake\Filesystem\Folder;
use Cake\Log\LogTrait;
use Cake\ORM\Query;
use Cake\Utility\Hash;
use Char0n\FFMpegPHP\Movie;
use Croogo\Core\Model\Table\CroogoTable;
use Croogo\FileManager\Utility\StorageManager;
use Exception;
use finfo;
use Intervention\Image\ImageManagerStatic as Image;
use InvalidArgumentException;
use RuntimeException;
/**
* Attachments Model
*
*/
class AttachmentsTable extends CroogoTable
{
use LogTrait;
/**
* @var array
*/
public $findMethods = [
'duplicate' => true,
'modelAttachments' => true,
'versions' => true,
];
/**
* @param array $config
* @return void
*/
public function initialize(array $config)
{
$this->setTable('attachments');
$this->hasOne('Assets', [
'className' => 'Croogo/FileManager.Assets',
'foreignKey' => 'foreign_key',
'dependent' => true,
'cascadeCallbacks' => true,
'conditions' => [
'parent_asset_id IS' => null,
'model' => 'Attachments',
],
]);
$this->addBehavior('Timestamp');
$this->addBehavior('Croogo/Core.Trackable');
$this->addBehavior('Search.Search');
//$this->addBehavior('Burzum/Imagine.Imagine');
$this->searchManager()
->add('title', 'Search.Like', [
'field' => $this->Assets->aliasField('filename'),
'before' => true,
'after' => true,
])
->add('search', 'Search.Callback', [
'callback' => [$this, 'filterAttachments'],
])
->add('filename', 'Search.Like', [
'field' => $this->Assets->aliasField('filename'),
'before' => true,
'after' => true,
])
->value('model', [
'field' => $this->Assets->AssetUsages->aliasField('model'),
])
->value('foreign_key', [
'field' => $this->Assets->AssetUsages->aliasField('foreign_key'),
])
->value('asset_id', [
'field' => $this->Assets->aliasField('id'),
])
->value('id', [
'field' => $this->aliasField('id'),
])
->value('type', [
'field' => $this->Assets->AssetUsages->aliasField('type'),
]);
}
/**
* @param $query
* @param $args
* @param $filter
*
* @return mixed
*/
public function filterAttachments($query, $args, $filter)
{
$conditions = [];
if (!empty($args['search'])) {
$filter = '%' . $args['search'] . '%';
$conditions = [
'OR' => [
$this->aliasField('title') . ' LIKE' => $filter,
$this->aliasField('excerpt') . ' LIKE' => $filter,
$this->aliasField('body') . ' LIKE' => $filter,
],
];
$query
->contain('Assets')
->orWhere($conditions);
}
return $query;
}
/**
* Find duplicates based on hash
*/
public function findDuplicate(Query $query, array $options)
{
if (empty($options['hash'])) {
return $query;
}
$hash = $options['hash'];
$query->where([
$this->aliasField('hash') => $hash,
]);
return $query;
}
/**
* @param Query $query
* @param array $options
*
* @return Query
*/
public function findModelAttachments(Query $query, array $options)
{
$model = $foreignKey = null;
if (isset($options['model'])) {
$model = $options['model'];
unset($options['model']);
}
if (isset($options['foreign_key'])) {
$foreignKey = $options['foreign_key'];
unset($options['foreign_key']);
}
$assetsJoinConditions = [
$this->Assets->aliasField('model') . ' = \'Attachments\'',
$this->Assets->aliasField('foreign_key') . ' = ' . $this->aliasField('id'),
];
$assetUsagesJoinConditions = [
$this->Assets->aliasField('id') . ' = ' . $this->Assets->AssetUsages->aliasField('asset_id'),
];
$this->associations()->remove('Assets');
$this->addAssociations([
'hasOne' => [
'Assets' => [
'className' => 'Croogo/FileManager.Assets',
'foreignKey' => false,
'conditions' => $assetsJoinConditions,
],
'AssetUsages' => [
'className' => 'Croogo/FileManager.AssetUsages',
'foreignKey' => false,
'conditions' => $assetUsagesJoinConditions,
],
]
]);
$query->contain('Assets');
$query->contain('AssetUsages');
if (isset($model) && isset($foreignKey)) {
$query->where([
$this->Assets->AssetUsages->aliasField('model') => $model,
$this->Assets->AssetUsages->aliasField('foreign_key') => $foreignKey,
]);
}
$query->formatResults([$this, 'getVideoPoster']);
return $query;
}
public function getVideoPoster($results) {
return $results->map(function($result) {
if (strstr($result['asset']['mime_type'], 'video') !== false) {
$poster = $this->Assets->find()
->select(['path'])
->matching('Attachments')
->where([
'parent_asset_id' => $result['asset']['id'],
'mime_type LIKE' => 'image/%',
])
->first();
if ($poster) {
$result['asset']['poster_path'] = $poster->path;
}
}
return $result;
});
}
/**
* @param Query $query
* @param array $options
*
* @return Query
*/
public function findVersions(Query $query, array $options)
{
$assetId = $model = $foreignKey = null;
if (isset($options['asset_id'])) {
$assetId = $options['asset_id'];
unset($options['asset_id']);
}
if (isset($options['search']['asset_id'])) {
$assetId = $options['search']['asset_id'];
unset($options['search']['asset_id']);
}
if (isset($options['model'])) {
$model = $options['model'];
unset($options['model']);
}
if (isset($options['foreign_key'])) {
$foreignKey = $options['foreign_key'];
unset($options['foreign_key']);
}
if (isset($options['all'])) {
$all = $options['all'];
unset($options['all']);
}
$assetsJoinConditions = [
$this->Assets->aliasField('model') . ' = \'Attachments\'',
$this->Assets->aliasField('foreign_key') . ' = ' . $this->aliasField('id'),
];
$assetUsagesJoinConditions = [
$this->Assets->aliasField('id') . ' = ' . $this->Assets->AssetUsages->aliasField('asset_id'),
];
$this->associations()->remove('Assets');
$this->addAssociations([
'hasOne' => [
'Assets' => [
'className' => 'Croogo/FileManager.Assets',
'foreignKey' => false,
'dependent' => true,
'conditions' => $assetsJoinConditions,
],
'AssetUsages' => [
'className' => 'Croogo/FileManager.AssetUsages',
'foreignKey' => false,
'conditions' => $assetUsagesJoinConditions,
],
]
]);
$query->contain('Assets');
if ($assetId && !isset($all)) {
$conditions = [
'OR' => [
$this->Assets->aliasField('id') => $assetId,
$this->Assets->aliasField('parent_asset_id') => $assetId,
],
];
$query->orWhere($conditions);
}
return $query;
}
/**
* @param Event $event
* @param EntityInterface $entity
* @param ArrayObject|null $options
*
* @return bool|string
*/
public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options = null)
{
if (!empty($entity->asset->file['name'])) {
$file = $entity->asset->file;
$attachment = $entity;
if (empty($attachment->title)) {
$attachment->title = $file['name'];
}
if (empty($attachment->slug)) {
$attachment->slug = $file['name'];
}
if (empty($attachment->hash)) {
if (empty($file['tmp_name'])) {
return 'Uploaded file is empty';
} else {
$attachment->hash = sha1_file($file['tmp_name']);
}
}
}
return true;
}
/**
* Create an Attachments data from $file
*
* @param $file string Path to file
* @return array|string Array of data or error message
* @throws InvalidArgumentException
*/
public function createFromFile($file)
{
if (!file_exists($file)) {
throw new InvalidArgumentException(__('Attachments::createFromFile(): {0} cannot be found', $file));
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$fp = fopen($file, 'r');
$stat = fstat($fp);
fclose($fp);
$hash = sha1_file($file);
$duplicate = isset($hash) ?
$this->find('duplicate', ['hash' => $hash])->toArray() :
false;
if ($duplicate) {
$firstDupe = $duplicate[0]->id;
return sprintf('%s is duplicate to asset: %s', str_replace(APP, '', $file), $firstDupe);
}
$path = str_replace(rtrim(WWW_ROOT, '/'), '', $file);
$sizeDelimiterPos = strrpos($path, '-');
$originalPath = substr($path, 0, $sizeDelimiterPos);
$extDelimiterPos = strrpos($path, '.');
$originalExt = substr($path, $extDelimiterPos);
$originalFile = $originalPath . $originalExt;
if (file_exists(WWW_ROOT . $originalFile)) {
// Found a possible parent file. Retrieve its parent and store
// the file path in a temp property asset_path so it gets picked up
// by _createImportTask() and runTask()
$attachment = $this->find()
->contain('Assets')
->where([
'import_path' => $originalFile,
])
->first();
if ($attachment) {
$attachment->asset_path = $path;
$attachment->setDirty('asset_path', false);
return $attachment;
}
}
$attachment = $this->newEntity([
'path' => $path,
'import_path' => $path,
'title' => basename($file),
'slug' => basename($file),
'mime_type' => $finfo->file($file),
'hash' => $hash,
'status' => true,
'created' => date('Y-m-d H:i:s', $stat[9]),
'modified' => date('Y-m-d H:i:s', time()),
]);
return $attachment;
}
/**
* Create Import task
*/
protected function _createImportTask($files, $options)
{
$data = [];
$copy = [];
$error = [];
foreach ($files as $file) {
$attachment = $this->createFromFile($file);
if ($attachment instanceof EntityInterface) {
$data[] = $attachment;
$copy[] = ['from' => $attachment->asset_path ?: $attachment->import_path];
$error[] = null;
} else {
$data[] = null;
$copy[] = null;
$error[] = $attachment;
}
}
return compact('data', 'copy', 'error');
}
/**
* Perform the actual import based on $task
*
* @param $task array Array of tasks
*/
public function runTask($task)
{
$imports = $errors = 0;
foreach ($task['copy'] as $i => $source) {
if (!$source) {
continue;
}
$file = WWW_ROOT . $source['from'];
$fp = fopen($file, 'r');
$stat = fstat($fp);
fclose($fp);
$pathinfo = pathinfo($file);
$width = $height = null;
if (strcmp($task['data'][$i]->mime_type, 'image') !== false) {
$sizeinfo = getimagesize($file);
$width = $sizeinfo[0];
$height = $sizeinfo[1];
}
$originalAsset = $task['data'][$i]->asset;
if ($originalAsset) {
$asset = $this->Assets->newEntity([
'parent_asset_id' => $originalAsset->id,
'model' => 'Attachments',
'foreign_key' => $originalAsset->foreign_key,
'adapter' => 'LegacyLocalAttachment',
'filename' => basename($file),
'filesize' => $stat['size'],
'width' => $width,
'height' => $height,
'hash' => sha1_file($file),
'extension' => $pathinfo['extension'],
'mime_type' => $originalAsset->mime_type,
'path' => $source['from'],
]);
$result = $this->Assets->save($asset, ['atomic' => true]);
} else {
$task['data'][$i]->asset = $this->Assets->newEntity([
'model' => 'Attachments',
'adapter' => 'LegacyLocalAttachment',
'filename' => basename($file),
'filesize' => $stat['size'],
'width' => $width,
'height' => $height,
'hash' => $task['data'][$i]->hash,
'extension' => $pathinfo['extension'],
'mime_type' => $task['data'][$i]->mime_type,
'path' => $source['from'],
]);
$result = $this->save($task['data'][$i], ['atomic' => true]);
}
if ($result) {
$imports++;
} else {
$errors++;
}
}
return compact('imports', 'errors');
}
/**
* Import files into the assets repository
*
* @param $dir array|string Path to import
* @param $regex string Regex to filter files to import
* @param $options array
* @throws InvalidArgumentException
*/
public function importTask($dirs = [], $regex = '.*', $options = [])
{
$options = Hash::merge([
'recursive' => true,
], $options);
foreach ($dirs as $dir) {
if (substr($dir, -1) === '/') {
$dir = substr($dir, 0, strlen($dir) - 1);
}
if (!is_dir($dir)) {
throw new InvalidArgumentException(__('{0} is not a directory', $dir));
}
$folder = new Folder($dir, false, false);
if ($options['recursive']) {
$files = $folder->findRecursive($regex, false);
} else {
$files = $folder->find($regex, false);
$files = array_map(
function ($v) use ($dir) {
return APP . $dir . '/' . $v;
},
$files
);
}
}
if (empty($files)) {
throw new Exception('importTask: cannot detect files to import');
}
return $this->_createImportTask($files, $options);
}
/**
* Create a video thumbnail
*
* @param int $id Attachment Id
* @param int $w New Width
* @param int $h New Height
* @param array $options Options array
*/
public function createVideoThumbnail($id, $w = null, $h = null, $options = [])
{
if (!class_exists('Char0n\FFMpegPHP\Movie')) {
throw new RunTimeException('Char0n\FFMpegPHP\Movie class not found');
}
$this->recursive = -1;
$attachment = $this->get($id, [
'contain' => ['Assets'],
]);
$asset =& $attachment->asset;
$path = rtrim(WWW_ROOT, '/') . $asset->path;
$info = pathinfo($asset->path);
$ind = sprintf('.resized-%dx%d.', $w, $h);
$filename = $info['filename'] . $ind . 'jpg';
$thumbnailPath = $info['dirname'] . DS . $filename;
$writePath = rtrim(WWW_ROOT, '/') . $thumbnailPath;
$ffmpeg = new Movie($path, null);
$frameCount = $ffmpeg->getFrameCount();
$width = $ffmpeg->getFrameWidth();
$height = $ffmpeg->getFrameHeight();
$posterFrameIndex = intval($frameCount / 4);
$frame = $ffmpeg->getFrame($posterFrameIndex, $w, $h);
imagejpeg($frame->toGDImage(), $writePath, 100);
$fp = fopen($writePath, 'r');
$stat = fstat($fp);
fclose($fp);
$adapter = $asset['adapter'];
$entity = $this->Assets->newEntity([
'filename' => $filename,
'path' => dirname($asset->path) . '/' . $filename,
'model' => $asset->model,
'extension' => 'jpg',
'parent_asset_id' => $asset->id,
'foreign_key' => $asset->foreign_key,
'adapter' => $adapter,
'mime_type' => 'image/jpeg',
'width' => $width,
'height' => $height,
'filesize' => $stat[7],
]);
$asset = $this->Assets->save($entity);
return $asset;
}
/**
* Copy an existing attachment and resize with width: $w and height: $h
*
* @param int $id Attachment Id
* @param int $w New Width
* @param int $h New Height
* @param array $options Options array
*/
public function createResized($id, $w, $h, $options = [])
{
$options = Hash::merge([
'uploadsDir' => 'assets',
], $options);
$attachment = $this->get($id, [
'contain' => ['Assets'],
]);
$asset = $attachment->asset;
$path = rtrim(WWW_ROOT, '/') . $asset->path;
$image = Image::make($path);
$stream = $image
->resize($w, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
})
->stream();
$newWidth = $image->width();
$newHeight = $image->height();
$info = pathinfo($asset->path);
$ind = sprintf('.resized-%dx%d.', $newWidth, $newHeight);
$uploadsDir = str_replace('/' . $options['uploadsDir'] . '/', '', dirname($asset['path'])) . '/';
$filename = $info['filename'] . $ind . $info['extension'];
$writePath = $uploadsDir . $filename;
$adapter = $asset->adapter;
$filesystem = StorageManager::adapter($adapter);
$filesystem->write($writePath, $stream);
$entity = $this->Assets->newEntity([
'filename' => $filename,
'path' => dirname($asset->path) . '/' . $filename,
'model' => $asset->model,
'extension' => $asset->extension,
'parent_asset_id' => $asset->id,
'foreign_key' => $asset->foreign_key,
'adapter' => $adapter,
'mime_type' => $asset->mime_type,
'width' => $newWidth,
'height' => $newHeight,
'filesize' => $image->filesize(),
'hash' => sha1($stream),
]);
$asset = $this->Assets->save($entity);
return $asset;
}
}