Split FileHandler & FileUtils even better

FileHandler is now for the specific storage pattern, FileUtils is for general metadata and thumbnail handling
master
Vitaliy Filippov 2019-05-03 01:37:10 +03:00
parent cbcfe42667
commit ec3d63d6c5
3 changed files with 159 additions and 131 deletions

View File

@ -1,9 +1,9 @@
<?php <?php
/** /**
* Простой слой загрузки файлов на сервер - "голая" версия * Simple file upload layer. Handles file metadata and storage
* Версия 2018-01-14 * Version 2019-05-01
* (c) Виталий Филиппов 2018 * (c) Vitaliy Filippov 2018+
*/ */
class File class File
@ -67,14 +67,29 @@ class File
return FileHandler::getThumb($file, $max_width, $height, false, self::CROP_X); return FileHandler::getThumb($file, $max_width, $height, false, self::CROP_X);
} }
protected static function doUpload($row)
{
$exist = App::$db->select($this->table, '*', [ 'sha1' => $row['sha1'] ], NULL, MS_ROW);
if ($exist)
{
$exist['props'] = json_decode($exist['props'], true);
return $exist;
}
$row['id'] = App::$db->insert_row($this->table, [
'props' => json_encode($row['props'], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),
'added' => time(),
] + $row);
return $row;
}
public static function upload(LocalFile $localFile, $allowedFormats = File::ANYTHING) public static function upload(LocalFile $localFile, $allowedFormats = File::ANYTHING)
{ {
return FileHandler::upload($localFile, $allowedFormats); return self::doUpload(FileHandler::upload($localFile, $allowedFormats));
} }
public static function uploadUrl($url, $allowedFormats = File::ONLY_IMAGES, $curl_options = []) public static function uploadUrl($url, $allowedFormats = File::ONLY_IMAGES, $curl_options = [])
{ {
return FileHandler::uploadUrl($url, $allowedFormats, $curl_options); return self::doUpload(FileHandler::uploadUrl($url, $allowedFormats, $curl_options));
} }
public static function deleteFiles($where) public static function deleteFiles($where)

View File

@ -1,35 +1,22 @@
<?php <?php
/** /**
* Простой слой загрузки файлов на сервер * Simple file upload layer. Handles file metadata and storage
* Версия 2018-09-10 * FileHandler: part that handles a specific disk storage layout
* (c) Виталий Филиппов 2018 *
* Version 2019-05-01
* (c) Vitaliy Filippov 2018+
*/ */
class FileHandler class FileHandler
{ {
const ONLY_BINARY = 1;
const ONLY_IMAGES = 2;
const ONLY_SWF = 4;
const ONLY_VIDEO = 8;
const IMAGES_VIDEO = 10;
const ANY_MEDIA = 14;
const ANYTHING = 15;
const CROP_XY = 1;
const CROP_Y = 2;
const CROP_X = 3;
public static $uploadCurl; public static $uploadCurl;
public function __construct($db, $config) public function __construct($config)
{ {
$this->db = $db;
$this->config = $config + [ $this->config = $config + [
'basedir' => '', 'basedir' => '',
'baseurl' => '', 'baseurl' => '',
'table' => 'files',
'get_user_id' => NULL,
'mime_blacklist' => '#'. 'mime_blacklist' => '#'.
// HTML may contain cookie-stealing JavaScript and web bugs // HTML may contain cookie-stealing JavaScript and web bugs
'^text/(html|(x-)?javascript)$|^application/x-shellscript$'. '^text/(html|(x-)?javascript)$|^application/x-shellscript$'.
@ -60,40 +47,15 @@ class FileHandler
$this->config['thumb_path'] . '/' . $type . '/' .substr($s, 0, 1) . '/' . substr($s, 0, 2) . '/' . $s . '.' . $ext; $this->config['thumb_path'] . '/' . $type . '/' .substr($s, 0, 1) . '/' . substr($s, 0, 2) . '/' . $s . '.' . $ext;
} }
public function getGPS($file) public function upload($localFile, $allowedFormats = FileUtils::ANYTHING)
{
$props = $file['props'];
if (empty($props['GPSLatitude']) && empty($props['GPSLongitude']))
{
return NULL;
}
$latitude = FileUtils::exifGPS($props['GPSLatitude'], $props['GPSLatitudeRef']);
$longitude = FileUtils::exifGPS($props['GPSLongitude'], $props['GPSLongitudeRef']);
return [ $latitude, $longitude ];
}
public function upload(LocalFile $localFile, $allowedFormats = FileHandler::ANYTHING)
{ {
$tmp_name = $localFile->getLocalPath(); $tmp_name = $localFile->getLocalPath();
$props = FileUtils::getProps($allowedFormats, $this->config['mime_blacklist'], $tmp_name, $localFile->getFileName()); $props = FileUtils::getProps($allowedFormats, $this->config['mime_blacklist'], $tmp_name, $localFile->getFileName());
$row = [ $fn = $this->getPath(false, $props);
'id' => NULL, if (file_exists($fn) && filesize($fn) == $props['size'])
'added' => time(),
] + $props + [ 'props' => [] ];
if ($this->config['get_user_id'])
{ {
$row['user_id'] = $this->config['get_user_id']() ?: NULL; return $props;
} }
$exist = $this->db->select($this->config['table'], '*', [ 'sha1' => $row['sha1'] ], NULL, MS_ROW);
if ($exist)
{
$exist['props'] = json_decode($exist['props'], true);
return $exist;
}
$row['id'] = $this->db->insert_row($this->config['table'], [
'props' => json_encode($row['props'], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),
] + $row);
$fn = $this->getPath(false, $row);
FileUtils::mkpath(dirname($fn), true); FileUtils::mkpath(dirname($fn), true);
$m = $localFile->shouldMove ? 'rename' : 'copy'; $m = $localFile->shouldMove ? 'rename' : 'copy';
if (!@$m($tmp_name, $fn)) if (!@$m($tmp_name, $fn))
@ -102,10 +64,10 @@ class FileHandler
throw new Exception($error['message']); throw new Exception($error['message']);
} }
chmod($fn, 0666 & ~umask()); chmod($fn, 0666 & ~umask());
return $row; return $props;
} }
public function uploadUrl($url, $flags = FileHandler::ONLY_IMAGES, $curl_options = []) public function uploadUrl($url, $flags = FileUtils::ONLY_IMAGES, $curl_options = [])
{ {
if (!$url) if (!$url)
{ {
@ -142,17 +104,20 @@ class FileHandler
return $file; return $file;
} }
public function deleteFiles($where) public function deleteFile($e)
{ {
$files = $this->db->select($this->config['table'], '*', $where); $disk_name = $this->getPath(false, $e);
foreach ($existing as $e) if (file_exists($disk_name))
{ {
$disk_name = $this->getPath(false, $e); // Remove old file
if (file_exists($disk_name)) unlink($disk_name);
}
$thumb_glob = $this->getThumbPath(false, $e, '*');
foreach (glob($thumb_glob) as $thumb)
{
if (file_exists($thumb))
{ {
// Remove old file unlink($thumb);
// FIXME unlink thumbnails
unlink($disk_name);
} }
} }
} }
@ -162,22 +127,12 @@ class FileHandler
*/ */
public function getThumb($file, $width, $height, $force = false, $crop = false, $alignY = 0.5) public function getThumb($file, $width, $height, $force = false, $crop = false, $alignY = 0.5)
{ {
$size = FileUtils::getThumbSize($file, $width, $height, $crop); $type = FileUtils::getThumbType($file, $width, $height, $crop, $alignY);
if (!$size) if (!$type)
{ {
return $this->getPath(true, $file); return NULL;
} }
list($width, $height) = $size; $fn = $this->getThumbPath(false, $file, $type['type']);
if (!$crop)
{
$type = intval($width);
}
else
{
$p = $crop == FileHandler::CROP_Y ? 'cy' : ($crop == FileHandler::CROP_X ? 'cx' : 'c');
$type = intval($width).'x'.intval($height).'_'.$alignY;
}
$fn = $this->getThumbPath(false, $file, $type);
if (!file_exists($fn) || $force) if (!file_exists($fn) || $force)
{ {
if (substr($file['mimetype'], 0, 6) === 'video/') if (substr($file['mimetype'], 0, 6) === 'video/')
@ -188,29 +143,9 @@ class FileHandler
{ {
$sourcefn = $this->getPath(false, $file); $sourcefn = $this->getPath(false, $file);
} }
try if (!FileUtils::generateThumbnail($sourcefn, $fn, $file, $type['width'], $type['height'], $crop, $alignY))
{ {
$im = FileUtils::magick(); return NULL;
$im->readImage($sourcefn);
$props = $file['props'];
if (!empty($props['Orientation']) && $props['Orientation'] > 5)
{
/* swap width & height */
$t = $width;
$width = $height;
$height = $t;
if ($crop == FileHandler::CROP_X || $crop == FileHandler::CROP_Y)
$crop = 5-$crop;
}
FileUtils::makeThumb($im, $width, $height, $crop, isset($props['Orientation']) ? $props['Orientation'] : NULL, $alignY);
$im->setCompressionQuality(FileUtils::$quality);
FileUtils::mkpath(dirname($fn));
$im->writeImage($fn);
}
catch (Exception $e)
{
trigger_error("$e");
return false;
} }
} }
return $this->getThumbPath(true, $file, $type); return $this->getThumbPath(true, $file, $type);

View File

@ -1,13 +1,27 @@
<?php <?php
/** /**
* Простой слой загрузки файлов на сервер * Simple file upload layer. Handles file metadata and storage
* Версия 2018-09-10 * FileUtils: part that handles image metadata and thumbnails
* (c) Виталий Филиппов 2018 *
* Version 2019-05-01
* (c) Vitaliy Filippov 2018+
*/ */
class FileUtils class FileUtils
{ {
const ONLY_BINARY = 1;
const ONLY_IMAGES = 2;
const ONLY_SWF = 4;
const ONLY_VIDEO = 8;
const IMAGES_VIDEO = 10;
const ANY_MEDIA = 14;
const ANYTHING = 15;
const CROP_XY = 1;
const CROP_Y = 2;
const CROP_X = 3;
public static $quality = 90; public static $quality = 90;
public static $exifProps = [ public static $exifProps = [
'Model', 'Model',
@ -36,29 +50,6 @@ class FileUtils
return new $class(); return new $class();
} }
public static function exifGPS($coordinate, $hemisphere)
{
for ($i = 0; $i < 3; $i++)
{
$part = explode('/', $coordinate[$i]);
if (count($part) == 1)
{
$coordinate[$i] = $part[0];
}
elseif (count($part) == 2)
{
$coordinate[$i] = floatval($part[0])/floatval($part[1]);
}
else
{
$coordinate[$i] = 0;
}
}
list($degrees, $minutes, $seconds) = $coordinate;
$sign = ($hemisphere == 'W' || $hemisphere == 'S') ? -1 : 1;
return $sign * ($degrees + $minutes/60 + $seconds/3600);
}
/** /**
* Get image or file properties (content SHA1, width, height, selected EXIF properties) * Get image or file properties (content SHA1, width, height, selected EXIF properties)
*/ */
@ -86,25 +77,25 @@ class FileUtils
'width' => 0, 'width' => 0,
'height' => 0, 'height' => 0,
]; ];
$flag = FileHandler::ONLY_BINARY; $flag = FileUtils::ONLY_BINARY;
$props = NULL; $props = NULL;
if ($mime == 'application/x-shockwave-flash') if ($mime == 'application/x-shockwave-flash')
{ {
$props = self::getSWFProps($fs_path); $props = self::getSWFProps($fs_path);
if ($props) if ($props)
$flag = FileHandler::ONLY_SWF; $flag = FileUtils::ONLY_SWF;
} }
elseif (substr($mime, 0, 6) == 'image/') elseif (substr($mime, 0, 6) == 'image/')
{ {
$props = self::getImageProps($fs_path); $props = self::getImageProps($fs_path);
if ($props) if ($props)
$flag = FileHandler::ONLY_IMAGES; $flag = FileUtils::ONLY_IMAGES;
} }
elseif (substr($mime, 0, 6) == 'video/') elseif (substr($mime, 0, 6) == 'video/')
{ {
$props = VideoUtils::getVideoProps($fs_path); $props = VideoUtils::getVideoProps($fs_path);
if ($props) if ($props)
$flag = FileHandler::ONLY_VIDEO; $flag = FileUtils::ONLY_VIDEO;
} }
if (!($allowed_types & $flag)) if (!($allowed_types & $flag))
{ {
@ -295,6 +286,41 @@ class FileUtils
} }
} }
public static function getGPS($file)
{
$props = $file['props'];
if (empty($props['GPSLatitude']) && empty($props['GPSLongitude']))
{
return NULL;
}
$latitude = FileUtils::exifGPS($props['GPSLatitude'], $props['GPSLatitudeRef']);
$longitude = FileUtils::exifGPS($props['GPSLongitude'], $props['GPSLongitudeRef']);
return [ $latitude, $longitude ];
}
public static function exifGPS($coordinate, $hemisphere)
{
for ($i = 0; $i < 3; $i++)
{
$part = explode('/', $coordinate[$i]);
if (count($part) == 1)
{
$coordinate[$i] = $part[0];
}
elseif (count($part) == 2)
{
$coordinate[$i] = floatval($part[0])/floatval($part[1]);
}
else
{
$coordinate[$i] = 0;
}
}
list($degrees, $minutes, $seconds) = $coordinate;
$sign = ($hemisphere == 'W' || $hemisphere == 'S') ? -1 : 1;
return $sign * ($degrees + $minutes/60 + $seconds/3600);
}
/** /**
* Calculate thumbnail image width and height * Calculate thumbnail image width and height
*/ */
@ -397,11 +423,11 @@ class FileUtils
{ {
$iw = $im->getImageWidth(); $iw = $im->getImageWidth();
$ih = $im->getImageHeight(); $ih = $im->getImageHeight();
if ($crop == FileHandler::CROP_Y && $ih*$width/$iw < $height) if ($crop == FileUtils::CROP_Y && $ih*$width/$iw < $height)
{ {
$height = $ih*$width/$iw; $height = $ih*$width/$iw;
} }
elseif ($crop == FileHandler::CROP_X && $iw*$height/$ih < $width) elseif ($crop == FileUtils::CROP_X && $iw*$height/$ih < $width)
{ {
$width = $iw*$height/$ih; $width = $iw*$height/$ih;
} }
@ -430,4 +456,56 @@ class FileUtils
self::fixOrientation($im, $orientation); self::fixOrientation($im, $orientation);
} }
} }
/**
* Get thumbnail type
*/
public static function getThumbType($file, $width, $height, $crop = false, $alignY = 0.5)
{
$size = FileUtils::getThumbSize($file, $width, $height, $crop);
if (!$size)
{
return NULL;
}
list($width, $height) = $size;
if (!$crop)
{
$type = intval($width);
}
else
{
$p = $crop == FileUtils::CROP_Y ? 'cy' : ($crop == FileUtils::CROP_X ? 'cx' : 'c');
$type = intval($width).'x'.intval($height).'_'.$alignY;
}
return [ 'width' => $width, 'height' => $height, 'type' => $type ];
}
public static function generateThumbnail($sourcefn, $thumbfn, $file, $width, $height, $crop = false, $alignY = 0.5)
{
try
{
$im = FileUtils::magick();
$im->readImage($sourcefn);
$props = $file['props'];
if (!empty($props['Orientation']) && $props['Orientation'] > 5)
{
/* swap width & height */
$t = $width;
$width = $height;
$height = $t;
if ($crop == FileUtils::CROP_X || $crop == FileUtils::CROP_Y)
$crop = 5-$crop;
}
FileUtils::makeThumb($im, $width, $height, $crop, isset($props['Orientation']) ? $props['Orientation'] : NULL, $alignY);
$im->setCompressionQuality(FileUtils::$quality);
FileUtils::mkpath(dirname($thumbfn));
$im->writeImage($thumbfn);
}
catch (Exception $e)
{
trigger_error("$e");
return false;
}
return true;
}
} }