Add abstract storage layer

This commit is contained in:
Frederic Guillot 2015-09-16 21:32:22 -04:00
parent 8bc141a286
commit 62fd225cfb
9 changed files with 519 additions and 156 deletions

View File

@ -60,7 +60,7 @@ class File extends Base
{
$task = $this->getTask();
if (! $this->file->upload($task['project_id'], $task['id'], 'files')) {
if (! $this->file->uploadFiles($task['project_id'], $task['id'], 'files')) {
$this->session->flashError(t('Unable to upload the file.'));
}
@ -76,14 +76,13 @@ class File extends Base
{
$task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
$filename = FILES_DIR.$file['path'];
if ($file['task_id'] == $task['id'] && file_exists($filename)) {
$this->response->forceDownload($file['name']);
$this->response->binary(file_get_contents($filename));
if ($file['task_id'] != $task['id']) {
$this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
}
$this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
$this->response->forceDownload($file['name']);
$this->objectStorage->passthru($file['path']);
}
/**
@ -113,16 +112,13 @@ class File extends Base
{
$task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
$filename = FILES_DIR.$file['path'];
if ($file['task_id'] == $task['id'] && file_exists($filename)) {
$metadata = getimagesize($filename);
if (isset($metadata['mime'])) {
$this->response->contentType($metadata['mime']);
readfile($filename);
}
if ($file['task_id'] != $task['id']) {
$this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
}
$this->response->contentType($this->file->getImageMimeType($file['name']));
$this->objectStorage->passthru($file['path']);
}
/**
@ -134,17 +130,13 @@ class File extends Base
{
$task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
$filename = FILES_DIR.$file['path'];
if ($file['task_id'] == $task['id'] && file_exists($filename)) {
$this->response->contentType('image/jpeg');
$this->file->generateThumbnail(
$filename,
$this->request->getIntegerParam('width'),
$this->request->getIntegerParam('height')
);
if ($file['task_id'] != $task['id']) {
$this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
}
$this->response->contentType('image/jpeg');
$this->objectStorage->passthru($this->file->getThumbnailPath($file['path']));
}
/**

View File

@ -0,0 +1,150 @@
<?php
namespace Core\ObjectStorage;
/**
* Local File Storage
*
* @package ObjectStorage
* @author Frederic Guillot
*/
class FileStorage implements ObjectStorageInterface
{
/**
* Base path
*
* @access private
* @var string
*/
private $path = '';
/**
* Constructor
*
* @access public
* @param string $path
*/
public function __construct($path)
{
$this->path = $path;
}
/**
* Fetch object contents
*
* @access public
* @param string $key
* @return string
*/
public function get($key)
{
$filename = $this->path.DIRECTORY_SEPARATOR.$key;
if (! file_exists($filename)) {
throw new ObjectStorageException('File not found: '.$filename);
}
return file_get_contents($filename);
}
/**
* Save object
*
* @access public
* @param string $key
* @param string $blob
* @return string
*/
public function put($key, &$blob)
{
$this->createFolder($key);
if (file_put_contents($this->path.DIRECTORY_SEPARATOR.$key, $blob) === false) {
throw new ObjectStorageException('Unable to write the file: '.$this->path.DIRECTORY_SEPARATOR.$key);
}
}
/**
* Output directly object content
*
* @access public
* @param string $key
*/
public function passthru($key)
{
$filename = $this->path.DIRECTORY_SEPARATOR.$key;
if (! file_exists($filename)) {
throw new ObjectStorageException('File not found: '.$filename);
}
return readfile($filename);
}
/**
* Move local file to object storage
*
* @access public
* @param string $filename
* @param string $key
* @return boolean
*/
public function moveFile($src_filename, $key)
{
$this->createFolder($key);
$dst_filename = $this->path.DIRECTORY_SEPARATOR.$key;
if (! rename($src_filename, $dst_filename)) {
throw new ObjectStorageException('Unable to move the file: '.$src_filename.' to '.$dst_filename);
}
return true;
}
/**
* Move uploaded file to object storage
*
* @access public
* @param string $filename
* @param string $key
* @return boolean
*/
public function moveUploadedFile($filename, $key)
{
$this->createFolder($key);
return move_uploaded_file($filename, $this->path.DIRECTORY_SEPARATOR.$key);
}
/**
* Remove object
*
* @access public
* @param string $key
* @return boolean
*/
public function remove($key)
{
$filename = $this->path.DIRECTORY_SEPARATOR.$key;
if (file_exists($filename)) {
return unlink($filename);
}
return false;
}
/**
* Create object folder
*
* @access private
* @param string $key
*/
private function createFolder($key)
{
$folder = $this->path.DIRECTORY_SEPARATOR.dirname($key);
if (! is_dir($folder) && ! mkdir($folder, 0755, true)) {
throw new ObjectStorageException('Unable to create folder: '.$folder);
}
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Core\ObjectStorage;
use Exception;
class ObjectStorageException extends Exception
{
}

View File

@ -0,0 +1,68 @@
<?php
namespace Core\ObjectStorage;
/**
* Object Storage Interface
*
* @package ObjectStorage
* @author Frederic Guillot
*/
interface ObjectStorageInterface
{
/**
* Fetch object contents
*
* @access public
* @param string $key
* @return string
*/
public function get($key);
/**
* Save object
*
* @access public
* @param string $key
* @param string $blob
* @return string
*/
public function put($key, &$blob);
/**
* Output directly object content
*
* @access public
* @param string $key
*/
public function passthru($key);
/**
* Move local file to object storage
*
* @access public
* @param string $filename
* @param string $key
* @return boolean
*/
public function moveFile($filename, $key);
/**
* Move uploaded file to object storage
*
* @access public
* @param string $filename
* @param string $key
* @return boolean
*/
public function moveUploadedFile($filename, $key);
/**
* Remove object
*
* @access public
* @param string $key
* @return boolean
*/
public function remove($key);
}

View File

@ -72,4 +72,82 @@ class Tool
}
}
}
/**
* Generate a jpeg thumbnail from an image
*
* @static
* @access public
* @param string $src_file Source file image
* @param string $dst_file Destination file image
* @param integer $resize_width Desired image width
* @param integer $resize_height Desired image height
*/
public static function generateThumbnail($src_file, $dst_file, $resize_width = 250, $resize_height = 100)
{
$metadata = getimagesize($src_file);
$src_width = $metadata[0];
$src_height = $metadata[1];
$dst_y = 0;
$dst_x = 0;
if (empty($metadata['mime'])) {
return;
}
if ($resize_width == 0 && $resize_height == 0) {
$resize_width = 100;
$resize_height = 100;
}
if ($resize_width > 0 && $resize_height == 0) {
$dst_width = $resize_width;
$dst_height = floor($src_height * ($resize_width / $src_width));
$dst_image = imagecreatetruecolor($dst_width, $dst_height);
}
elseif ($resize_width == 0 && $resize_height > 0) {
$dst_width = floor($src_width * ($resize_height / $src_height));
$dst_height = $resize_height;
$dst_image = imagecreatetruecolor($dst_width, $dst_height);
}
else {
$src_ratio = $src_width / $src_height;
$resize_ratio = $resize_width / $resize_height;
if ($src_ratio <= $resize_ratio) {
$dst_width = $resize_width;
$dst_height = floor($src_height * ($resize_width / $src_width));
$dst_y = ($dst_height - $resize_height) / 2 * (-1);
}
else {
$dst_width = floor($src_width * ($resize_height / $src_height));
$dst_height = $resize_height;
$dst_x = ($dst_width - $resize_width) / 2 * (-1);
}
$dst_image = imagecreatetruecolor($resize_width, $resize_height);
}
switch ($metadata['mime']) {
case 'image/jpeg':
case 'image/jpg':
$src_image = imagecreatefromjpeg($src_file);
break;
case 'image/png':
$src_image = imagecreatefrompng($src_file);
break;
case 'image/gif':
$src_image = imagecreatefromgif($src_file);
break;
default:
return;
}
imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, 0, 0, $dst_width, $dst_height, $src_width, $src_height);
imagejpeg($dst_image, $dst_file);
imagedestroy($dst_image);
}
}

View File

@ -3,6 +3,8 @@
namespace Model;
use Event\FileEvent;
use Core\Tool;
use Core\ObjectStorage\ObjectStorageException;
/**
* File model
@ -47,14 +49,17 @@ class File extends Base
*/
public function remove($file_id)
{
$file = $this->getbyId($file_id);
try {
if (! empty($file)) {
@unlink(FILES_DIR.$file['path']);
return $this->db->table(self::TABLE)->eq('id', $file_id)->remove();
$file = $this->getbyId($file_id);
$this->objectStorage->remove($file['path']);
return $this->db->table(self::TABLE)->eq('id', $file['id'])->remove();
}
catch (ObjectStorageException $e) {
$this->logger->error($e->getMessage());
return false;
}
return false;
}
/**
@ -66,11 +71,11 @@ class File extends Base
*/
public function removeAll($task_id)
{
$files = $this->getAll($task_id);
$file_ids = $this->db->table(self::TABLE)->eq('task_id', $task_id)->asc('id')->findAllByColumn('id');
$results = array();
foreach ($files as $file) {
$results[] = $this->remove($file['id']);
foreach ($file_ids as $file_id) {
$results[] = $this->remove($file_id);
}
return ! in_array(false, $results, true);
@ -195,6 +200,30 @@ class File extends Base
return false;
}
/**
* Return the image mimetype based on the file extension
*
* @access public
* @param $filename
* @return string
*/
public function getImageMimeType($filename)
{
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
switch ($extension) {
case 'jpeg':
case 'jpg':
return 'image/jpeg';
case 'png':
return 'image/png';
case 'gif':
return 'image/gif';
default:
return 'image/jpeg';
}
}
/**
* Generate the path for a new filename
*
@ -209,6 +238,18 @@ class File extends Base
return $project_id.DIRECTORY_SEPARATOR.$task_id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time());
}
/**
* Generate the path for a thumbnails
*
* @access public
* @param string $key Storage key
* @return string
*/
public function getThumbnailPath($key)
{
return 'thumbnails'.DIRECTORY_SEPARATOR.$key;
}
/**
* Handle file upload
*
@ -218,11 +259,13 @@ class File extends Base
* @param string $form_name File form name
* @return bool
*/
public function upload($project_id, $task_id, $form_name)
public function uploadFiles($project_id, $task_id, $form_name)
{
$results = array();
try {
if (! empty($_FILES[$form_name])) {
if (empty($_FILES[$form_name])) {
return false;
}
foreach ($_FILES[$form_name]['error'] as $key => $error) {
@ -232,22 +275,27 @@ class File extends Base
$uploaded_filename = $_FILES[$form_name]['tmp_name'][$key];
$destination_filename = $this->generatePath($project_id, $task_id, $original_filename);
@mkdir(FILES_DIR.dirname($destination_filename), 0755, true);
if (@move_uploaded_file($uploaded_filename, FILES_DIR.$destination_filename)) {
$results[] = $this->create(
$task_id,
$original_filename,
$destination_filename,
$_FILES[$form_name]['size'][$key]
);
if ($this->isImage($original_filename)) {
$this->generateThumbnailFromFile($uploaded_filename, $destination_filename);
}
$this->objectStorage->moveUploadedFile($uploaded_filename, $destination_filename);
$this->create(
$task_id,
$original_filename,
$destination_filename,
$_FILES[$form_name]['size'][$key]
);
}
}
}
return ! in_array(false, $results, true);
return true;
}
catch (ObjectStorageException $e) {
$this->logger->error($e->getMessage());
return false;
}
}
/**
@ -261,129 +309,77 @@ class File extends Base
*/
public function uploadScreenshot($project_id, $task_id, $blob)
{
$data = base64_decode($blob);
if (empty($data)) {
return false;
}
$original_filename = e('Screenshot taken %s', dt('%B %e, %Y at %k:%M %p', time())).'.png';
$destination_filename = $this->generatePath($project_id, $task_id, $original_filename);
@mkdir(FILES_DIR.dirname($destination_filename), 0755, true);
@file_put_contents(FILES_DIR.$destination_filename, $data);
return $this->create(
$task_id,
$original_filename,
$destination_filename,
strlen($data)
);
return $this->uploadContent($project_id, $task_id, $original_filename, $blob);
}
/**
* Handle file upload (base64 encoded content)
*
* @access public
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param string $filename Filename
* @param string $blob Base64 encoded image
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param string $original_filename Filename
* @param string $blob Base64 encoded file
* @return bool|integer
*/
public function uploadContent($project_id, $task_id, $filename, $blob)
public function uploadContent($project_id, $task_id, $original_filename, $blob)
{
$data = base64_decode($blob);
try {
if (empty($data)) {
$data = base64_decode($blob);
if (empty($data)) {
return false;
}
$destination_filename = $this->generatePath($project_id, $task_id, $original_filename);
$this->objectStorage->put($destination_filename, $data);
if ($this->isImage($original_filename)) {
$this->generateThumbnailFromData($destination_filename, $data);
}
return $this->create(
$task_id,
$original_filename,
$destination_filename,
strlen($data)
);
}
catch (ObjectStorageException $e) {
$this->logger->error($e->getMessage());
return false;
}
$destination_filename = $this->generatePath($project_id, $task_id, $filename);
@mkdir(FILES_DIR.dirname($destination_filename), 0755, true);
@file_put_contents(FILES_DIR.$destination_filename, $data);
return $this->create(
$task_id,
$filename,
$destination_filename,
strlen($data)
);
}
/**
* Generate a jpeg thumbnail from an image (output directly the image)
* Generate thumbnail from a blob
*
* @access public
* @param string $filename Source image
* @param integer $resize_width Desired image width
* @param integer $resize_height Desired image height
* @param string $destination_filename
* @param string $data
*/
public function generateThumbnail($filename, $resize_width, $resize_height)
public function generateThumbnailFromData($destination_filename, &$data)
{
$metadata = getimagesize($filename);
$src_width = $metadata[0];
$src_height = $metadata[1];
$dst_y = 0;
$dst_x = 0;
$temp_filename = tempnam(sys_get_temp_dir(), 'datafile');
if (empty($metadata['mime'])) {
return;
}
file_put_contents($temp_filename, $data);
$this->generateThumbnailFromFile($temp_filename, $destination_filename);
unlink($temp_filename);
}
if ($resize_width == 0 && $resize_height == 0) {
$resize_width = 100;
$resize_height = 100;
}
if ($resize_width > 0 && $resize_height == 0) {
$dst_width = $resize_width;
$dst_height = floor($src_height * ($resize_width / $src_width));
$dst_image = imagecreatetruecolor($dst_width, $dst_height);
}
elseif ($resize_width == 0 && $resize_height > 0) {
$dst_width = floor($src_width * ($resize_height / $src_height));
$dst_height = $resize_height;
$dst_image = imagecreatetruecolor($dst_width, $dst_height);
}
else {
$src_ratio = $src_width / $src_height;
$resize_ratio = $resize_width / $resize_height;
if ($src_ratio <= $resize_ratio) {
$dst_width = $resize_width;
$dst_height = floor($src_height * ($resize_width / $src_width));
$dst_y = ($dst_height - $resize_height) / 2 * (-1);
}
else {
$dst_width = floor($src_width * ($resize_height / $src_height));
$dst_height = $resize_height;
$dst_x = ($dst_width - $resize_width) / 2 * (-1);
}
$dst_image = imagecreatetruecolor($resize_width, $resize_height);
}
switch ($metadata['mime']) {
case 'image/jpeg':
case 'image/jpg':
$src_image = imagecreatefromjpeg($filename);
break;
case 'image/png':
$src_image = imagecreatefrompng($filename);
break;
case 'image/gif':
$src_image = imagecreatefromgif($filename);
break;
default:
return;
}
imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, 0, 0, $dst_width, $dst_height, $src_width, $src_height);
imagejpeg($dst_image);
/**
* Generate thumbnail from a blob
*
* @access public
* @param string $uploaded_filename
* @param string $destination_filename
*/
public function generateThumbnailFromFile($uploaded_filename, $destination_filename)
{
$thumbnail_filename = tempnam(sys_get_temp_dir(), 'thumbnail');
Tool::generateThumbnail($uploaded_filename, $thumbnail_filename);
$this->objectStorage->moveFile($thumbnail_filename, $this->getThumbnailPath($destination_filename));
}
}

View File

@ -2,6 +2,7 @@
namespace ServiceProvider;
use Core\ObjectStorage\FileStorage;
use Core\Paginator;
use Core\OAuth2;
use Core\Tool;
@ -106,5 +107,9 @@ class ClassProvider implements ServiceProviderInterface
$container['htmlConverter'] = function($c) {
return new HtmlConverter(array('strip_tags' => true));
};
$container['objectStorage'] = function($c) {
return new FileStorage(FILES_DIR);
};
}
}

View File

@ -11,7 +11,7 @@
<li>
<?php if (function_exists('imagecreatetruecolor')): ?>
<div class="img_container">
<img src="<?= $this->url->href('file', 'thumbnail', array('width' => 250, 'height' => 100, 'file_id' => $file['id'], 'project_id' => $task['project_id'], 'task_id' => $file['task_id'])) ?>" alt="<?= $this->e($file['name']) ?>"/>
<img src="<?= $this->url->href('file', 'thumbnail', array('file_id' => $file['id'], 'project_id' => $task['project_id'], 'task_id' => $file['task_id'])) ?>" alt="<?= $this->e($file['name']) ?>"/>
</div>
<?php endif ?>
<p>

View File

@ -9,6 +9,17 @@ use Model\Project;
class FileTest extends Base
{
public function setUp()
{
parent::setUp();
$this->container['objectStorage'] = $this
->getMockBuilder('\Core\ObjectStorage\FileStorage')
->setConstructorArgs(array($this->container))
->setMethods(array('put', 'moveFile', 'remove'))
->getMock();
}
public function testCreation()
{
$p = new Project($this->container);
@ -85,13 +96,32 @@ class FileTest extends Base
public function testUploadScreenshot()
{
$p = new Project($this->container);
$f = new File($this->container);
$tc = new TaskCreation($this->container);
$this->assertEquals(1, $p->create(array('name' => 'test')));
$this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test')));
$this->assertEquals(1, $f->uploadScreenshot(1, 1, base64_encode('image data')));
$data = base64_encode('image data');
$f = $this
->getMockBuilder('\Model\File')
->setConstructorArgs(array($this->container))
->setMethods(array('generateThumbnailFromData'))
->getMock();
$this->container['objectStorage']
->expects($this->once())
->method('put')
->with(
$this->stringContains('1/1/'),
$this->equalTo(base64_decode($data))
)
->will($this->returnValue(true));
$f->expects($this->once())
->method('generateThumbnailFromData');
$this->assertEquals(1, $f->uploadScreenshot(1, 1, $data));
$file = $f->getById(1);
$this->assertNotEmpty($file);
@ -113,7 +143,18 @@ class FileTest extends Base
$this->assertEquals(1, $p->create(array('name' => 'test')));
$this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test')));
$this->assertEquals(1, $f->uploadContent(1, 1, 'my file.pdf', base64_encode('file data')));
$data = base64_encode('file data');
$this->container['objectStorage']
->expects($this->once())
->method('put')
->with(
$this->stringContains('1/1/'),
$this->equalTo(base64_decode($data))
)
->will($this->returnValue(true));
$this->assertEquals(1, $f->uploadContent(1, 1, 'my file.pdf', $data));
$file = $f->getById(1);
$this->assertNotEmpty($file);
@ -170,9 +211,33 @@ class FileTest extends Base
$this->assertEquals(1, $p->create(array('name' => 'test')));
$this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test')));
$this->assertEquals(1, $f->create(1, 'B.pdf', '/tmp/foo', 10));
$this->assertEquals(2, $f->create(1, 'A.png', '/tmp/foo', 10));
$this->assertEquals(3, $f->create(1, 'D.doc', '/tmp/foo', 10));
$this->assertEquals(1, $f->create(1, 'B.pdf', '/tmp/foo1', 10));
$this->assertEquals(2, $f->create(1, 'A.png', '/tmp/foo2', 10));
$this->assertEquals(3, $f->create(1, 'D.doc', '/tmp/foo3', 10));
$this->container['objectStorage']
->expects($this->at(0))
->method('remove')
->with(
$this->equalTo('/tmp/foo2')
)
->will($this->returnValue(true));
$this->container['objectStorage']
->expects($this->at(1))
->method('remove')
->with(
$this->equalTo('/tmp/foo1')
)
->will($this->returnValue(true));
$this->container['objectStorage']
->expects($this->at(2))
->method('remove')
->with(
$this->equalTo('/tmp/foo3')
)
->will($this->returnValue(true));
$this->assertTrue($f->remove(2));