Commit fc89c0ec authored by CHRISTOPHE THERY's avatar CHRISTOPHE THERY
Browse files

task#105492 : managing FileSystem Access and Persistence

parent 261bf980
Pipeline #10985 passed with stage
in 47 seconds
<?php
namespace Pellicule;
use \Buzz\Client\FileGetContents;
use \Slim\Psr7\Factory\RequestFactory;
use \Slim\Psr7\Factory\ResponseFactory;
use \Fig\Http\Message\StatusCodeInterface;
class FileSystem {
use HttpClientAware;
use
HttpClientAware,
StormFileSystem;
protected static $_base_path = '/tmp/',
$_base_url;
......@@ -27,33 +29,63 @@ class FileSystem {
}
public function fileExists($path) {
return \file_exists(static::$_base_path . $path);
public function getBasePath() {
return static::$_base_path;
}
public function fileExists($path){
return $this->getFileSystem()->fileExists($path);
}
public function fileExtension($response){
foreach($response->getHeader('content-type') as $key => $value)
if ($extension = $this->_getExtensionFromContentType($value))
return ".".$extension;
}
protected function _getExtensionFromContentType($value){
$elements = explode("/", $value);
if (isset($elements[1])
&& in_array($elements[1],['jpg','bmp','gif','png','jpeg']))
return $elements[1];
return "";
}
public function download($url, $path, $headers = []) {
public function download($media) {
$url = $media->getUrl();
$path = $media->getFullsize();
$headers = $media->getProviderHeaders();
if (!$url || !$path)
return;
$directory = pathinfo(static::$_base_path . $path, PATHINFO_DIRNAME);
if (false === \file_exists($directory))
\mkdir($directory, 0755, true);
if (false === $this->fileExists($directory))
$this->getFileSystem()->mkdir($directory, 0755, true);
$request = (new RequestFactory())->createRequest('GET', $url);
foreach($headers as $key => $value)
$request = $request->withHeader($key, $value);
$client = new FileGetContents(new ResponseFactory(), ['allow_redirects' => true]);
if ($headers)
foreach($headers as $key => $value)
$request = $request->withHeader($key, $value);
$client = $this->newFileGetContents();
$response = $client
->sendRequest($request, ['timeout' => 4]);
if (($status = $response->getStatusCode()) != StatusCodeInterface::STATUS_OK) {
echo 'file was not downloaded error:'. $status . ' '. $response->getBody();
echo $url. 'file was not download error:'. $status . ' '. $response->getBody();
return $this;
}
file_put_contents(static::$_base_path . $path, $response->getBody());
$extension = $this->fileExtension($response);
$this->getFileSystem()
->filePutContents( static::$_base_path . $path . $extension, $response->getBody());
$media->setFullsize($path . $extension);
}
}
......@@ -3,6 +3,7 @@ namespace Pellicule;
use \Buzz\Browser;
use \Buzz\Client\Curl;
use \Buzz\Client\FileGetContents;
use \Slim\Psr7\Factory\ResponseFactory;
use \Slim\Psr7\Factory\RequestFactory;
......@@ -17,9 +18,15 @@ trait HttpClientAware {
return static::$_default_http_client;
$client = new Curl(new ResponseFactory(), static::_httpClientOptions());
$browser = new Browser($client, new RequestFactory());
return new Browser($client, new RequestFactory());
}
static public function newFileGetContents() {
if (static::$_default_http_client)
return static::$_default_http_client;
return $browser;
return new FileGetContents(new ResponseFactory(), ['allow_redirects' => true]);
}
......
......@@ -3,38 +3,35 @@ namespace Pellicule\Models;
use \Storm\Model\ModelAbstract;
class Media extends ModelAbstract {
use
\Pellicule\StormFileSystem,
\Pellicule\Traits\TimeSource;
const
SIZE_FULL = 'fullsize',
TYPE_COVER = 'cover',
TYPE_BACKCOVER = 'back_cover';
protected static $_file_system;
protected
$_table_name = 'media',
$_belongs_to = [ 'record' => ['model' => Record::class ] ],
$_default_attribute_values = [ 'id' => 0,
$_default_attribute_values = ['id' => 0,
'fullsize' => '',
'provider_headers' => '',
'created_at' => '',
'updated_at' => ''
],
$_provider_headers;
'updated_at' => ''],
$_provider_headers = [];
/** @category testing */
public static function setFileSystem($filesystem) {
static::$_file_system = $filesystem;
public function getProviderHeaders() {
return $this->_provider_headers;
}
public static function getFileSystem() {
return isset(static::$_file_system)
? static::$_file_system
: new \Pellicule\FileSystem();
public function setProviderHeaders($headers) {
$this->_provider_headers = $headers;
return $this;
}
......@@ -50,18 +47,14 @@ class Media extends ModelAbstract {
public function beforeSave() {
$datenow = (new \DateTime())->format('Y-m-d H:i:s');
$datenow = $this->getCurrentDateTime();
if (!$this->getCreatedAt())
$this->setCreatedAt($datenow);
$this->setUpdatedAt($datenow);
}
public function afterSave() {
if (!$this->fileExists())
$this->getFileSystem()->download($this->getUrl(),
$this->getFullsize(),
$this->getProviderHeaders());
$this->getPelliculeFileSystem()->download($this);
}
......@@ -69,19 +62,19 @@ class Media extends ModelAbstract {
return $this->setFullsize($this->_getFilePathFromIdentifier($identifier));
}
public function _getFilePathFromIdentifier($identifier) {
public function _getFilePathFromIdentifier($identifier = "") {
return '/' . implode('/',
array_merge([$this->getSize(),
array_merge(['images',
$this->getSize(),
$this->getType()],
$this->splitId($identifier)))
. '/'. $identifier .".";
. '/'. $identifier;
}
public function fileExists() {
return $this->isNew()
? false
: $this->getFileSystem()->fileExists($this->getFilePath());
return $this->getPelliculeFileSystem()->fileExists($this->getFullsize());
}
......@@ -98,11 +91,6 @@ class Media extends ModelAbstract {
}
public function getFilePath() {
return $this->getLocation() . $this->getLocalFileName();
}
public function splitId($identifier = "") {
return array_slice(str_split(sprintf('%04d',
($identifier ? $identifier : $this->getId()))),
......@@ -110,25 +98,7 @@ class Media extends ModelAbstract {
}
protected function getLocation() {
return $this->isNew()
? ''
: '/' . implode('/', array_merge([$this->getSize(),
$this->getType()],
$this->splitId())) . '/';
}
protected function getLocalFileName() {
return $this->isNew()
? ''
: $this->getId() . '.' . $this->getExtension();
}
protected function getExtension() {
return $this->hasUrl()
? pathinfo($this->getUrl(), PATHINFO_EXTENSION)
: '';
protected function getLocation($identifier = "") {
return $this->_getFilePathFromIdentifier($identifier);
}
}
<?php
namespace Pellicule;
trait StormFileSystem {
protected static $_file_system;
/** @category testing */
public static function setFileSystem($file_system) {
self::$_file_system = $file_system;
}
public static function getFileSystem() {
if (null !== self::$_file_system)
return self::$_file_system;
return new \Storm\FileSystem\Disk();
}
public static function getPelliculeFileSystem() {
if (null !== self::$_file_system)
return self::$_file_system;
return new \Pellicule\FileSystem();
}
}
\ No newline at end of file
<?php
/**
* Copyright (c) 2012, Agence Française Informatique (AFI). All rights reserved.
*
* BOKEH is free software; you can redistribute it and/or modify
* it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
* the Free Software Foundation.
*
* There are special exceptions to the terms and conditions of the AGPL as it
* is applied to this software (see README file).
*
* BOKEH is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
* along with BOKEH; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
* Encapsule les appels systeme temps pour pouvoir les surcharger pendant les tests
* @category testing
*/
namespace Pellicule;
class TimeSource {
const DAY_AND_HOURS_FORMAT = 'Y-m-d H:i:s';
/** @return int */
public function time() {
return time();
}
public function dateYmd() {
return $this->dateFormat('Y-m-d');
}
public function dateDayAndHours() {
return $this->dateFormat(static::DAY_AND_HOURS_FORMAT);
}
public function dateDayMonthYear() {
return $this->dateFormat('dmY');
}
public function dateFormat($format) {
return date($format, $this->time());
}
public function dateHttpHeader() {
return gmdate('D, d M Y H:i:s \G\M\T', $this->time());
}
public function date() {
$time = $this->time();
return $this->midnightTime(date('n', $time), date('j', $time), date('Y', $time));
}
public function mktime($hour, $minute, $second, $month, $day, $year) {
return mktime($hour, $minute, $second, $month, $day, $year);
}
public function nextDate() {
$time = $this->time();
return $this->midnightTime(date('n', $time),
date('j', $time) + 1,
date('Y', $time));
}
public function nextMonths($number_of_months=1) {
$time = $this->time();
return $this->midnightTime(date('n', $time) + $number_of_months,
date('j', $time),
date('Y', $time));
}
public function getMonth($month) {
$time = $this->time();
$year = date('m', $time) > $month ? date('Y', $time) + 1 : date('Y', $time);
return date('Y-m', $this->midnightTime($month, 1, $year));
}
protected function midnightTime($month, $day, $year) {
return mktime(0, 0, 0, $month, $day, $year);
}
public function lastYear() {
$time = $this->time();
return date('Y', $time) - 1 . date('-m-d', $time);
}
public function daysFrom($time) {
return (int)($this->hoursFrom($time) / 24);
}
public function hoursFrom($time) {
return (int)(($this->time() - $time) / 3660);
}
/**
* @var $min string '00:00'
* @var $max string '23:55'
* @var $step int
* @return array ['00:00' => '00h00', ...]
*/
public function getPossibleHours($min, $max, $step) {
if (!$this->isValidHourMinutes($min) || !$this->isValidHourMinutes($max))
return [];
list($min_h, $min_m) = explode(':', $min);
list($max_h, $max_m) = explode(':', $max);
if ($min_h > $max_h)
return [];
if (($min_h == $max_h) && ($min_m >= $max_m))
return [];
$date_time = new DateTime($min . ':00');
$current_day = $date_time->format('d');
$multioptions_array = [];
do {
$multioptions_array[$date_time->format('H:i')] = $date_time->format('H\hi');
$date_time->modify('+' . $step . ' min');
} while(($date_time->format('H:i') <= $max) && ($date_time->format('d') == $current_day));
return $multioptions_array;
}
public function isValidHourMinutes($value) {
return preg_match('/^(\d{2}):(\d{2})$/', $value, $matches)
? 24 > (int)$matches[1] && 60 > (int)$matches[2]
: false;
}
public function asDateTime() {
return DateTime::createFromFormat(static::DAY_AND_HOURS_FORMAT,
$this->dateDayAndHours());
}
}
\ No newline at end of file
<?php
/**
* Copyright (c) 2012, Agence Française Informatique (AFI). All rights reserved.
*
* BOKEH is free software; you can redistribute it and/or modify
* it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
* the Free Software Foundation.
*
* There are special exceptions to the terms and conditions of the AGPL as it
* is applied to this software (see README file).
*
* BOKEH is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
* along with BOKEH; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
namespace Pellicule\Traits;
trait TimeSource {
/** @var Pellicule\TimeSource */
protected static $_time_source;
/**
* @category testing
* @return int
*/
public function getCurrentTime() {
return self::getTimeSource()->time();
}
public static function getCurrentYear() {
return date('Y',self::getTimeSource()->time());
}
public static function getCurrentDate() {
return date('Y-m-d',self::getTimeSource()->time());
}
public static function getCurrentDateTime() {
return date('Y-m-d H:i:s', self::getTimeSource()->time());
}
public static function substractYearsToCurrentDate($days) {
return date('Y-m-d',strtotime('-'.(string)$days.' year', self::getTimeSource()->time()));
}
public static function addDaysToCurrentDate($days) {
$now = date('Y-m-d',strtotime((string)$days.' day', self::getTimeSource()->time()));
return $now;
}
/** @return \Pellicule\TimeSource */
public static function getTimeSource() {
if (null == self::$_time_source)
self::$_time_source = new \Pellicule\TimeSource();
return self::$_time_source;
}
/** @param $time_source \Pellicule\TimeSource */
public static function setTimeSource($time_source) {
self::$_time_source = $time_source;
}
}
......@@ -8,10 +8,11 @@ use Pellicule\Models\Media;
use Pellicule\Providers\Electre;
use Pellicule\Providers\Orb;
use Pellicule\FileSystem;
use \Storm\FileSystem\Volatile;
class FileSystemTest extends TestCase {
class FileSystemMediaDownloadTest extends TestCase {
protected $_file_system;
public function setUp(){
parent::setUp();
......@@ -20,10 +21,13 @@ class FileSystemTest extends TestCase {
$this->_http_client
->whenCalled('sendRequest')
->answers($this->_forgePSR7Response(['content' => file_get_contents(realpath(dirname(__FILE__)) . '/' .'imagefullsize.jpg')]));
->answers($this->_forgePSR7Response(['headers' => (new \Slim\Psr7\Headers)->addHeader('Content-Type','image/jpg'),
'content' => file_get_contents(realpath(dirname(__FILE__)) . '/' .'imagefullsize.jpg')]));
FileSystem::setFileSystem(new Volatile());
FileSystem::setBasePath('/tmp');
FileSystem::setDefaultHttpClient($this->_http_client);
$this->_file_system = new FileSystem();
$this->fixture(Record::class,
['id' => 1,
......@@ -31,26 +35,34 @@ class FileSystemTest extends TestCase {
'ean' => '9782259228190',
'ark' => 'https://catalogue.bnf.fr/ark:/12148/cb445155569']);
$this->fixture(Media::class,
['id'=> 135,
'record_id'=> 1,
'type' => 'cover',
'url' => 'http://image.org/city.jpg',
'provider' => 'Me',
'created_at' => '2020-01-13 08:00:00',
'updated_at' => '2020-01-16 08:00:00'
]);
(new Media())
->updateAttributes(['record_id'=> 1,
'type' => 'cover',
'url' => 'http://image.org/city.jpg',
'provider' => 'Me',
'fullsize' => '/images/fullsize/cover/2/2/5/9/2259228194',
'created_at' => '2020-01-13 08:00:00',
'updated_at' => '2020-01-16 08:00:00'
])
->save();
}
/** @test */
public function fullsizeCoverDirectoryShouldBeCreated() {
$this->assertTrue($this->_root->hasChild('fullsize/cover/2/2/5/9/'));
$this->assertTrue($this->_file_system->fileExists('/tmp/images/fullsize/cover/2/2/5/9/'));
}
/** @test */
public function fullsizeCoverFileShouldBeCreated() {
$this->assertTrue($this->_root->hasChild('fullsize/cover/2/2/5/9/2259228194.jpg'));
$this->assertTrue($this->_file_system->fileExists('/tmp/images/fullsize/cover/2/2/5/9/2259228194.jpg'));
}
/** @test */
public function mediaShouldHaveFullsizeWithExtension() {
$this->assertEquals('/images/fullsize/cover/2/2/5/9/2259228194.jpg',
Media::find(1)->getFullsize());
}
}
......@@ -6,6 +6,7 @@ use \Pellicule\Models\Record;
use \Pellicule\Models\Media;
use \Pellicule\FileSystem;
use \Pellicule\Providers\Provider;
use \Pellicule\Traits\TimeSource;
use \org\bovigo\vfs\vfsStream;
......@@ -22,6 +23,8 @@ class MediaLocalRecordTest extends TestCase {
->whenCalled('fileExists')->answers(true)
->whenCalled('baseUrl')->answers('https://localhost/pellicule/'));
Media::setTimeSource(new TimeSourceForTest('2020-01-21 08:00:00'));
$this->fixture(Record::class,
['id' => 1,
'isbn' => '9782259228190',
......@@ -35,7 +38,7 @@ class MediaLocalRecordTest extends TestCase {
'url' => 'http://image.org/city.jpg',
'provider' => 'Me',
'created_at' => '2020-01-13 08:00:00',
'updated_at' => (new \DateTime())->format('Y-m-d H:i:s')
'updated_at' => '2020-01-21 08:00:00'
]);
$this->fixture(Media::class,
......@@ -45,11 +48,15 @@ class MediaLocalRecordTest extends TestCase {
'url' => 'http://image.org/cityback.jpg',
'provider' => 'Me',
'created_at' => '2020-01-21 08:00:00',
'updated_at' => (new \DateTime())->format('Y-m-d H:i:s')
'updated_at' => '2020-01-21 08:00:00'