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(); $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.')); $this->session->flashError(t('Unable to upload the file.'));
} }
@@ -76,14 +76,13 @@ class File extends Base
{ {
$task = $this->getTask(); $task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id')); $file = $this->file->getById($this->request->getIntegerParam('file_id'));
$filename = FILES_DIR.$file['path'];
if ($file['task_id'] == $task['id'] && file_exists($filename)) { if ($file['task_id'] != $task['id']) {
$this->response->forceDownload($file['name']); $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
$this->response->binary(file_get_contents($filename));
} }
$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(); $task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id')); $file = $this->file->getById($this->request->getIntegerParam('file_id'));
$filename = FILES_DIR.$file['path'];
if ($file['task_id'] == $task['id'] && file_exists($filename)) { if ($file['task_id'] != $task['id']) {
$metadata = getimagesize($filename); $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
if (isset($metadata['mime'])) {
$this->response->contentType($metadata['mime']);
readfile($filename);
}
} }
$this->response->contentType($this->file->getImageMimeType($file['name']));
$this->objectStorage->passthru($file['path']);
} }
/** /**
@@ -134,17 +130,13 @@ class File extends Base
{ {
$task = $this->getTask(); $task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id')); $file = $this->file->getById($this->request->getIntegerParam('file_id'));
$filename = FILES_DIR.$file['path'];
if ($file['task_id'] == $task['id'] && file_exists($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('image/jpeg');
$this->file->generateThumbnail(
$filename,
$this->request->getIntegerParam('width'),
$this->request->getIntegerParam('height')
);
} }
$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; namespace Model;
use Event\FileEvent; use Event\FileEvent;
use Core\Tool;
use Core\ObjectStorage\ObjectStorageException;
/** /**
* File model * File model
@@ -47,14 +49,17 @@ class File extends Base
*/ */
public function remove($file_id) public function remove($file_id)
{ {
$file = $this->getbyId($file_id); try {
if (! empty($file)) { $file = $this->getbyId($file_id);
@unlink(FILES_DIR.$file['path']); $this->objectStorage->remove($file['path']);
return $this->db->table(self::TABLE)->eq('id', $file_id)->remove();
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) 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(); $results = array();
foreach ($files as $file) { foreach ($file_ids as $file_id) {
$results[] = $this->remove($file['id']); $results[] = $this->remove($file_id);
} }
return ! in_array(false, $results, true); return ! in_array(false, $results, true);
@@ -195,6 +200,30 @@ class File extends Base
return false; 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 * 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()); 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 * Handle file upload
* *
@@ -218,11 +259,13 @@ class File extends Base
* @param string $form_name File form name * @param string $form_name File form name
* @return bool * @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) { foreach ($_FILES[$form_name]['error'] as $key => $error) {
@@ -232,22 +275,27 @@ class File extends Base
$uploaded_filename = $_FILES[$form_name]['tmp_name'][$key]; $uploaded_filename = $_FILES[$form_name]['tmp_name'][$key];
$destination_filename = $this->generatePath($project_id, $task_id, $original_filename); $destination_filename = $this->generatePath($project_id, $task_id, $original_filename);
@mkdir(FILES_DIR.dirname($destination_filename), 0755, true); if ($this->isImage($original_filename)) {
$this->generateThumbnailFromFile($uploaded_filename, $destination_filename);
if (@move_uploaded_file($uploaded_filename, FILES_DIR.$destination_filename)) {
$results[] = $this->create(
$task_id,
$original_filename,
$destination_filename,
$_FILES[$form_name]['size'][$key]
);
} }
$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) 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'; $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); return $this->uploadContent($project_id, $task_id, $original_filename, $blob);
@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)
);
} }
/** /**
* Handle file upload (base64 encoded content) * Handle file upload (base64 encoded content)
* *
* @access public * @access public
* @param integer $project_id Project id * @param integer $project_id Project id
* @param integer $task_id Task id * @param integer $task_id Task id
* @param string $filename Filename * @param string $original_filename Filename
* @param string $blob Base64 encoded image * @param string $blob Base64 encoded file
* @return bool|integer * @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; 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 * @access public
* @param string $filename Source image * @param string $destination_filename
* @param integer $resize_width Desired image width * @param string $data
* @param integer $resize_height Desired image height
*/ */
public function generateThumbnail($filename, $resize_width, $resize_height) public function generateThumbnailFromData($destination_filename, &$data)
{ {
$metadata = getimagesize($filename); $temp_filename = tempnam(sys_get_temp_dir(), 'datafile');
$src_width = $metadata[0];
$src_height = $metadata[1];
$dst_y = 0;
$dst_x = 0;
if (empty($metadata['mime'])) { file_put_contents($temp_filename, $data);
return; $this->generateThumbnailFromFile($temp_filename, $destination_filename);
} unlink($temp_filename);
}
if ($resize_width == 0 && $resize_height == 0) { /**
$resize_width = 100; * Generate thumbnail from a blob
$resize_height = 100; *
} * @access public
* @param string $uploaded_filename
if ($resize_width > 0 && $resize_height == 0) { * @param string $destination_filename
$dst_width = $resize_width; */
$dst_height = floor($src_height * ($resize_width / $src_width)); public function generateThumbnailFromFile($uploaded_filename, $destination_filename)
$dst_image = imagecreatetruecolor($dst_width, $dst_height); {
} $thumbnail_filename = tempnam(sys_get_temp_dir(), 'thumbnail');
elseif ($resize_width == 0 && $resize_height > 0) { Tool::generateThumbnail($uploaded_filename, $thumbnail_filename);
$dst_width = floor($src_width * ($resize_height / $src_height)); $this->objectStorage->moveFile($thumbnail_filename, $this->getThumbnailPath($destination_filename));
$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);
} }
} }

View File

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

View File

@@ -11,7 +11,7 @@
<li> <li>
<?php if (function_exists('imagecreatetruecolor')): ?> <?php if (function_exists('imagecreatetruecolor')): ?>
<div class="img_container"> <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> </div>
<?php endif ?> <?php endif ?>
<p> <p>

View File

@@ -9,6 +9,17 @@ use Model\Project;
class FileTest extends Base 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() public function testCreation()
{ {
$p = new Project($this->container); $p = new Project($this->container);
@@ -85,13 +96,32 @@ class FileTest extends Base
public function testUploadScreenshot() public function testUploadScreenshot()
{ {
$p = new Project($this->container); $p = new Project($this->container);
$f = new File($this->container);
$tc = new TaskCreation($this->container); $tc = new TaskCreation($this->container);
$this->assertEquals(1, $p->create(array('name' => 'test'))); $this->assertEquals(1, $p->create(array('name' => 'test')));
$this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => '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); $file = $f->getById(1);
$this->assertNotEmpty($file); $this->assertNotEmpty($file);
@@ -113,7 +143,18 @@ class FileTest extends Base
$this->assertEquals(1, $p->create(array('name' => 'test'))); $this->assertEquals(1, $p->create(array('name' => 'test')));
$this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => '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); $file = $f->getById(1);
$this->assertNotEmpty($file); $this->assertNotEmpty($file);
@@ -170,9 +211,33 @@ class FileTest extends Base
$this->assertEquals(1, $p->create(array('name' => 'test'))); $this->assertEquals(1, $p->create(array('name' => 'test')));
$this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => '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(1, $f->create(1, 'B.pdf', '/tmp/foo1', 10));
$this->assertEquals(2, $f->create(1, 'A.png', '/tmp/foo', 10)); $this->assertEquals(2, $f->create(1, 'A.png', '/tmp/foo2', 10));
$this->assertEquals(3, $f->create(1, 'D.doc', '/tmp/foo', 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)); $this->assertTrue($f->remove(2));