File upload layer extracted & evolved from numerous projects
commit
2812bab85d
|
@ -0,0 +1,124 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Простой слой загрузки файлов на сервер - привязка к DBObject.php
|
||||||
|
* Версия 2018-01-14
|
||||||
|
* (c) Виталий Филиппов 2018
|
||||||
|
*/
|
||||||
|
|
||||||
|
class File extends DBObject
|
||||||
|
{
|
||||||
|
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 $table = 'files';
|
||||||
|
public static $clean = [], $dirty = [];
|
||||||
|
public static $fields = [
|
||||||
|
'id' => false,
|
||||||
|
'user_id' => false,
|
||||||
|
'sha1' => false,
|
||||||
|
'format' => false,
|
||||||
|
'mimetype' => false,
|
||||||
|
'size' => true,
|
||||||
|
'width' => true,
|
||||||
|
'height' => true,
|
||||||
|
'added' => true,
|
||||||
|
'props' => true,
|
||||||
|
];
|
||||||
|
public static $joins = [
|
||||||
|
'user' => 'User',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function get_disk_path()
|
||||||
|
{
|
||||||
|
return FileHandler::getPath(false, $this->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function get_raw_url()
|
||||||
|
{
|
||||||
|
return FileHandler::getPath(true, $this->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function get_fsize_ru()
|
||||||
|
{
|
||||||
|
return FileUtils::sizeString($this->data['size'], 'ru');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function get_fsize_en()
|
||||||
|
{
|
||||||
|
return FileUtils::sizeString($this->data['size'], 'en');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function get_url()
|
||||||
|
{
|
||||||
|
return App::url('api', [ 'action' => 'Files.thumb', 'sha1' => $this->data['sha1'] ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function get_gps()
|
||||||
|
{
|
||||||
|
return FileHandler::getGPS($this->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getThumb($width, $height, $force = false, $crop = false, $alignY = 0.5)
|
||||||
|
{
|
||||||
|
return FileHandler::getThumb($this->data, $width, $height, $force, $crop, $alignY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cropThumb($width, $height, $alignY = 0.5)
|
||||||
|
{
|
||||||
|
return FileHandler::getThumb($this->data, $width, $height, false, self::CROP_XY, $alignY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cropYThumb($width, $max_height, $alignY = 0.5)
|
||||||
|
{
|
||||||
|
return FileHandler::getThumb($this->data, $width, $max_height, false, self::CROP_Y, $alignY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cropXThumb($max_width, $height)
|
||||||
|
{
|
||||||
|
return FileHandler::getThumb($this->data, $max_width, $height, false, self::CROP_X);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function upload(LocalFile $localFile, $allowedFormats = File::ANYTHING)
|
||||||
|
{
|
||||||
|
$file = new File();
|
||||||
|
$file->data = FileHandler::upload($localFile, $allowedFormats);
|
||||||
|
return $file->data ? $file : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function uploadUrl($url, $allowedFormats = File::ONLY_IMAGES, $curl_options = [])
|
||||||
|
{
|
||||||
|
$file = new File();
|
||||||
|
$file->data = FileHandler::uploadUrl($url, $allowedFormats, $curl_options);
|
||||||
|
return $file->data ? $file : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete()
|
||||||
|
{
|
||||||
|
return FileHandler::deleteFiles([ 'id' => $this->data['id'] ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function newFromRow($row)
|
||||||
|
{
|
||||||
|
$obj = parent::newFromRow($row);
|
||||||
|
if ($obj)
|
||||||
|
{
|
||||||
|
$obj->data['props'] = json_decode($obj->data['props'], true);
|
||||||
|
}
|
||||||
|
return $obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveMe()
|
||||||
|
{
|
||||||
|
throw new Exception('File objects are immutable');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Простой слой загрузки файлов на сервер - "голая" версия
|
||||||
|
* Версия 2018-01-14
|
||||||
|
* (c) Виталий Филиппов 2018
|
||||||
|
*/
|
||||||
|
|
||||||
|
class File
|
||||||
|
{
|
||||||
|
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 $table = 'files';
|
||||||
|
|
||||||
|
public static function getDiskPath($file)
|
||||||
|
{
|
||||||
|
return FileHandler::getPath(false, $file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getUrl($file)
|
||||||
|
{
|
||||||
|
return FileHandler::getPath(true, $file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getThumbPath($file, $type)
|
||||||
|
{
|
||||||
|
return FileHandler::getThumbPath(false, $file, $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getGPS($file)
|
||||||
|
{
|
||||||
|
return FileHandler::getGPS($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSizeString($file, $lang = 'ru')
|
||||||
|
{
|
||||||
|
return FileUtils::sizeString($file['size'], $lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getThumb($file, $width, $height, $force = false, $crop = false, $alignY = 0.5)
|
||||||
|
{
|
||||||
|
return FileHandler::getThumb($file, $width, $height, $force, $crop, $alignY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function cropThumb($file, $width, $height, $alignY = 0.5)
|
||||||
|
{
|
||||||
|
return FileHandler::getThumb($file, $width, $height, false, self::CROP_XY, $alignY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function cropYThumb($file, $width, $max_height, $alignY = 0.5)
|
||||||
|
{
|
||||||
|
return FileHandler::getThumb($file, $width, $max_height, false, self::CROP_Y, $alignY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function cropXThumb($file, $max_width, $height)
|
||||||
|
{
|
||||||
|
return FileHandler::getThumb($file, $max_width, $height, false, self::CROP_X);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function upload(LocalFile $localFile, $allowedFormats = File::ANYTHING)
|
||||||
|
{
|
||||||
|
return FileHandler::upload($localFile, $allowedFormats);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function uploadUrl($url, $allowedFormats = File::ONLY_IMAGES, $curl_options = [])
|
||||||
|
{
|
||||||
|
return FileHandler::uploadUrl($url, $allowedFormats, $curl_options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function deleteFiles($where)
|
||||||
|
{
|
||||||
|
return FileHandler::deleteFiles($where);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,204 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Простой слой загрузки файлов на сервер
|
||||||
|
* Версия 2018-01-14
|
||||||
|
* (c) Виталий Филиппов 2018
|
||||||
|
*/
|
||||||
|
|
||||||
|
class FileHandler
|
||||||
|
{
|
||||||
|
public static $thumbPath = 'thumb/';
|
||||||
|
|
||||||
|
public static function base($web_or_fs = true)
|
||||||
|
{
|
||||||
|
static $p, $l;
|
||||||
|
if (!$p)
|
||||||
|
{
|
||||||
|
$p = App::$config['files_path'];
|
||||||
|
if ($p{0} != '/')
|
||||||
|
{
|
||||||
|
$p = '/'.$p;
|
||||||
|
}
|
||||||
|
$l = App::$config['local_path'];
|
||||||
|
if ($l{strlen($l)-1} == '/')
|
||||||
|
{
|
||||||
|
$l = substr($l, 0, -1);
|
||||||
|
}
|
||||||
|
if ($p{strlen($p)-1} != '/')
|
||||||
|
{
|
||||||
|
$p .= '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $web_or_fs ? App::domain().$p : $l.$p;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPath($web_or_fs, $file)
|
||||||
|
{
|
||||||
|
$s = $file['sha1'];
|
||||||
|
return FileHandler::base($web_or_fs) . '/' . substr($s, 0, 1) . '/' . substr($s, 0, 2) . '/' . $s . '.' . $file['format'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getThumbPath($web_or_fs, $file, $type)
|
||||||
|
{
|
||||||
|
$s = $file['sha1'];
|
||||||
|
$ext = $file['format'] == 'jpg' || $file['format'] == 'png' || $file['format'] == 'gif'
|
||||||
|
? $file['format'] : 'jpg';
|
||||||
|
return FileHandler::base($web_or_fs) . self::$thumbPath . '/' . $type . '/' .substr($s, 0, 1) . '/' . substr($s, 0, 2) . '/' . $s . '.' . $ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 upload(LocalFile $localFile, $allowedFormats = File::ANYTHING)
|
||||||
|
{
|
||||||
|
$tmp_name = $localFile->getLocalPath();
|
||||||
|
$props = FileUtils::getProps($allowedFormats, $tmp_name, $localFile->getFileName());
|
||||||
|
$row = [
|
||||||
|
'id' => NULL,
|
||||||
|
'added' => time(),
|
||||||
|
'user_id' => App::$user['id'] ?: NULL,
|
||||||
|
] + $props + [ 'props' => [] ];
|
||||||
|
$exist = App::$db->select(File::$table, '*', [ 'sha1' => $row['sha1'] ], NULL, MS_ROW);
|
||||||
|
if ($exist)
|
||||||
|
{
|
||||||
|
$exist['props'] = json_decode($exist['props'], true);
|
||||||
|
return $exist;
|
||||||
|
}
|
||||||
|
$row['id'] = App::$db->insert_row(File::$table, [
|
||||||
|
'props' => json_encode($row['props'], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),
|
||||||
|
] + $row);
|
||||||
|
$fn = FileHandler::getPath(false, $row);
|
||||||
|
FileUtils::mkpath(dirname($fn), true);
|
||||||
|
$m = $localFile->shouldMove ? 'rename' : 'copy';
|
||||||
|
if (!@$m($tmp_name, $fn))
|
||||||
|
{
|
||||||
|
$error = error_get_last();
|
||||||
|
throw new Exception($error['message']);
|
||||||
|
}
|
||||||
|
chmod($fn, 0666 & ~umask());
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
static $uploadCurl;
|
||||||
|
|
||||||
|
public static function uploadUrl($url, $flags = File::ONLY_IMAGES, $curl_options = [])
|
||||||
|
{
|
||||||
|
if (!$url)
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
$file = NULL;
|
||||||
|
if (substr($url, 0, 2) == '//')
|
||||||
|
{
|
||||||
|
$url = "http:$url";
|
||||||
|
}
|
||||||
|
if (!self::$uploadCurl)
|
||||||
|
{
|
||||||
|
// Reuse handle to use keepalive when possible
|
||||||
|
self::$uploadCurl = curl_init();
|
||||||
|
}
|
||||||
|
curl_setopt_array(self::$uploadCurl, [
|
||||||
|
CURLOPT_URL => $url,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
] + $curl_options);
|
||||||
|
$s = curl_exec(self::$uploadCurl);
|
||||||
|
if ($s)
|
||||||
|
{
|
||||||
|
$tmp = tempnam(sys_get_temp_dir(), 'upl');
|
||||||
|
file_put_contents($tmp, $s);
|
||||||
|
unset($s);
|
||||||
|
$file = File::upload(new LocalFile($tmp, true), $flags);
|
||||||
|
@unlink($tmp);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Log it as E_USER_NOTICE
|
||||||
|
trigger_error(curl_error(self::$uploadCurl));
|
||||||
|
}
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function deleteFiles($where)
|
||||||
|
{
|
||||||
|
$files = App::$db->select(File::$table, '*', $where);
|
||||||
|
foreach ($existing as $e)
|
||||||
|
{
|
||||||
|
$disk_name = FileHandler::getPath(false, $e);
|
||||||
|
if (file_exists($disk_name))
|
||||||
|
{
|
||||||
|
// Remove old file
|
||||||
|
// FIXME unlink thumbnails
|
||||||
|
unlink($disk_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or generate a thumbnail and return its URL
|
||||||
|
*/
|
||||||
|
public static function getThumb($file, $width, $height, $force = false, $crop = false, $alignY = 0.5)
|
||||||
|
{
|
||||||
|
$size = FileUtils::getThumbSize($file, $width, $height, $crop);
|
||||||
|
if (!$size)
|
||||||
|
{
|
||||||
|
return FileHandler::getPath(true, $file);
|
||||||
|
}
|
||||||
|
list($width, $height) = $size;
|
||||||
|
if (!$crop)
|
||||||
|
{
|
||||||
|
$type = intval($width);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$p = $crop == File::CROP_Y ? 'cy' : ($crop == File::CROP_X ? 'cx' : 'c');
|
||||||
|
$type = intval($width).'x'.intval($height).'_'.$alignY;
|
||||||
|
}
|
||||||
|
$fn = FileHandler::getThumbPath(false, $file, $type);
|
||||||
|
if (!file_exists($fn) || $force)
|
||||||
|
{
|
||||||
|
if (substr($file['mimetype'], 0, 6) === 'video/')
|
||||||
|
{
|
||||||
|
$sourcefn = FileHandler::getThumbPath(false, $file, 'src');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$sourcefn = FileHandler::getPath(false, $file);
|
||||||
|
}
|
||||||
|
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 == File::CROP_X || $crop == File::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 FileHandler::getThumbPath(true, $file, $type);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,427 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class FileUtils
|
||||||
|
{
|
||||||
|
public static $quality = 90;
|
||||||
|
public static $exifProps = [
|
||||||
|
'Model',
|
||||||
|
'Orientation',
|
||||||
|
'DateTime',
|
||||||
|
'DateTimeOriginal',
|
||||||
|
'ExposureTime',
|
||||||
|
'FNumber',
|
||||||
|
'ISOSpeedRatings',
|
||||||
|
'FocalLength',
|
||||||
|
'FocalLengthIn35mmFilm',
|
||||||
|
'GPSLongitude',
|
||||||
|
'GPSLongitudeRef',
|
||||||
|
'GPSLatitude',
|
||||||
|
'GPSLatitudeRef',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function magick()
|
||||||
|
{
|
||||||
|
static $class;
|
||||||
|
if (!$class)
|
||||||
|
{
|
||||||
|
// Use GraphicsMagick if possible
|
||||||
|
$class = class_exists('Gmagick') ? 'Gmagick' : 'Imagick';
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
*/
|
||||||
|
public static function getProps($allowed_types, $fs_path, $orig_name)
|
||||||
|
{
|
||||||
|
if (!file_exists($fs_path))
|
||||||
|
{
|
||||||
|
throw new UserException('no-file-uploaded');
|
||||||
|
}
|
||||||
|
$mime = self::checkMime($fs_path);
|
||||||
|
if (!$mime)
|
||||||
|
{
|
||||||
|
throw new UserException('file-mime-type-unknown');
|
||||||
|
}
|
||||||
|
elseif (preg_match(App::$config['mime_blacklist'], $mime))
|
||||||
|
{
|
||||||
|
// Prevent uploads with dangerous MIME types
|
||||||
|
throw new UserException('file-mime-type-blacklisted');
|
||||||
|
}
|
||||||
|
$stdProps = [
|
||||||
|
'mimetype' => $mime,
|
||||||
|
'sha1' => sha1_file($fs_path),
|
||||||
|
'size' => filesize($fs_path),
|
||||||
|
'format' => '',
|
||||||
|
'width' => 0,
|
||||||
|
'height' => 0,
|
||||||
|
];
|
||||||
|
$flag = File::ONLY_BINARY;
|
||||||
|
$props = NULL;
|
||||||
|
if ($mime == 'application/x-shockwave-flash')
|
||||||
|
{
|
||||||
|
$props = self::getSWFProps($fs_path);
|
||||||
|
if ($props)
|
||||||
|
$flag = File::ONLY_SWF;
|
||||||
|
}
|
||||||
|
elseif (substr($mime, 0, 6) == 'image/')
|
||||||
|
{
|
||||||
|
$props = self::getImageProps($fs_path);
|
||||||
|
if ($props)
|
||||||
|
$flag = File::ONLY_IMAGES;
|
||||||
|
}
|
||||||
|
elseif (substr($mime, 0, 6) == 'video/')
|
||||||
|
{
|
||||||
|
$props = VideoUtils::getVideoProps($fs_path);
|
||||||
|
if ($props)
|
||||||
|
$flag = File::ONLY_VIDEO;
|
||||||
|
}
|
||||||
|
if (!($allowed_types & $flag))
|
||||||
|
{
|
||||||
|
throw new UserException('file-type-denied', [ 'allowed' => $allowed_types, 'type' => $flag ]);
|
||||||
|
}
|
||||||
|
$props = ($props ?: []) + $stdProps;
|
||||||
|
if (!$props['format'])
|
||||||
|
{
|
||||||
|
if (!empty($props['filename']))
|
||||||
|
$orig_name = preg_replace('#^.*/#is', '', $props['filename']);
|
||||||
|
$p = strrpos($orig_name, '.');
|
||||||
|
if ($p)
|
||||||
|
$props['format'] = strtolower(substr($orig_name, $p+1));
|
||||||
|
}
|
||||||
|
unset($props['filename']);
|
||||||
|
return $props;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get image properties + compress uploaded BMP images to JPEG
|
||||||
|
*/
|
||||||
|
protected static function getImageProps($fn)
|
||||||
|
{
|
||||||
|
$format = NULL;
|
||||||
|
$props = [];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$exif = @exif_read_data($fn);
|
||||||
|
if ($exif)
|
||||||
|
{
|
||||||
|
foreach (self::$exifProps as $p)
|
||||||
|
{
|
||||||
|
if (isset($exif[$p]))
|
||||||
|
{
|
||||||
|
$props[$p] = $exif[$p];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// FIXME Сначала проверять ширину/высоту, не читая саму картинку в память
|
||||||
|
$im = static::magick();
|
||||||
|
$im->readImage($fn);
|
||||||
|
$format = strtolower($im->getImageFormat());
|
||||||
|
if ($format == 'jpeg')
|
||||||
|
{
|
||||||
|
$format = 'jpg';
|
||||||
|
}
|
||||||
|
if ($format != 'png' &&
|
||||||
|
$format != 'gif' &&
|
||||||
|
$format != 'jpeg' &&
|
||||||
|
$format != 'jpg' &&
|
||||||
|
$format != 'pdf' &&
|
||||||
|
$format != 'djvu' &&
|
||||||
|
$format != 'djv' &&
|
||||||
|
($format != 'ico' || $im->getImageWidth() > 48 || $im->getImageHeight() > 48)) // bmp, tiff, psd...
|
||||||
|
{
|
||||||
|
$im->mergeImageLayers(Imagick::LAYERMETHOD_FLATTEN);
|
||||||
|
if (!empty($props['Orientation']))
|
||||||
|
{
|
||||||
|
self::fixOrientation($im, $props['Orientation']);
|
||||||
|
unset($props['Orientation']);
|
||||||
|
}
|
||||||
|
$im->setCompressionQuality(static::$quality);
|
||||||
|
$im->setImageFormat('jpeg');
|
||||||
|
$format = 'jpg';
|
||||||
|
$fn = tempnam(sys_get_temp_dir(), 'imguniq');
|
||||||
|
$im->writeImage($fn);
|
||||||
|
}
|
||||||
|
$width = $im->getImageWidth();
|
||||||
|
$height = $im->getImageHeight();
|
||||||
|
if (!empty($props['Orientation']) && $props['Orientation'] >= 5)
|
||||||
|
{
|
||||||
|
$t = $width;
|
||||||
|
$width = $height;
|
||||||
|
$height = $t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception $e)
|
||||||
|
{
|
||||||
|
// Not an image
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'filename' => $fn,
|
||||||
|
'width' => $width,
|
||||||
|
'height' => $height,
|
||||||
|
'format' => $format,
|
||||||
|
'props' => $props,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get MIME type of a file
|
||||||
|
*/
|
||||||
|
protected static function checkMime($filename)
|
||||||
|
{
|
||||||
|
$mime = false;
|
||||||
|
if (class_exists('finfo'))
|
||||||
|
{
|
||||||
|
static $finfo;
|
||||||
|
if (!$finfo)
|
||||||
|
{
|
||||||
|
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||||
|
}
|
||||||
|
if ($finfo)
|
||||||
|
{
|
||||||
|
$mime = $finfo->file($filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif (function_exists('mime_content_type'))
|
||||||
|
{
|
||||||
|
$mime = mime_content_type($filename);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$mime = shell_exec("file ".escapeshellarg($filename));
|
||||||
|
}
|
||||||
|
return $mime;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function getSWFProps($fn)
|
||||||
|
{
|
||||||
|
$size = shell_exec("swfdump -X -Y ".escapeshellarg($fn));
|
||||||
|
if (preg_match('/-X (\d+) -Y (\d+)/', $size, $m))
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'format' => 'swf',
|
||||||
|
'width' => $m[1],
|
||||||
|
'height' => $m[2],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix ImageMagick image $im orientation based on EXIF orientation $orient
|
||||||
|
*/
|
||||||
|
public static function fixOrientation($im, $orient)
|
||||||
|
{
|
||||||
|
$orient = intval($orient);
|
||||||
|
if ($orient > 1 && $orient <= 8)
|
||||||
|
{
|
||||||
|
if ($orient == 2)
|
||||||
|
{
|
||||||
|
$im->flopImage();
|
||||||
|
}
|
||||||
|
elseif ($orient == 3)
|
||||||
|
{
|
||||||
|
$im->rotateImage($im instanceof Imagick ? new ImagickPixel() : new GmagickPixel(), 180);
|
||||||
|
}
|
||||||
|
elseif ($orient == 4)
|
||||||
|
{
|
||||||
|
$im->flipImage();
|
||||||
|
}
|
||||||
|
elseif ($orient == 5)
|
||||||
|
{
|
||||||
|
if (method_exists($im, 'transposeImage'))
|
||||||
|
{
|
||||||
|
$im->transposeImage();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// GraphicsMagick doesn't have Transpose
|
||||||
|
$im->rotateImage($im instanceof Imagick ? new ImagickPixel() : new GmagickPixel(), 90);
|
||||||
|
$im->flopImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($orient == 6)
|
||||||
|
{
|
||||||
|
$im->rotateImage($im instanceof Imagick ? new ImagickPixel() : new GmagickPixel(), 90);
|
||||||
|
}
|
||||||
|
elseif ($orient == 7)
|
||||||
|
{
|
||||||
|
if (method_exists($im, 'transverseImage'))
|
||||||
|
{
|
||||||
|
$im->transverseImage();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// GraphicsMagick doesn't have Transverse
|
||||||
|
$im->rotateImage($im instanceof Imagick ? new ImagickPixel() : new GmagickPixel(), 270);
|
||||||
|
$im->flopImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($orient == 8)
|
||||||
|
{
|
||||||
|
$im->rotateImage($im instanceof Imagick ? new ImagickPixel() : new GmagickPixel(), 270);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate thumbnail image width and height
|
||||||
|
*/
|
||||||
|
public static function thumbnailSize($cw, $ch, $maxw, $maxh)
|
||||||
|
{
|
||||||
|
if ($maxw <= 0 && $maxh <= 0 || $cw <= 0 || $ch <= 0)
|
||||||
|
{
|
||||||
|
return [ $cw, $ch ];
|
||||||
|
}
|
||||||
|
if ($maxw > 0 && ($maxh <= 0 || $maxh/$ch >= $maxw/$cw))
|
||||||
|
{
|
||||||
|
$nw = $maxw;
|
||||||
|
$nh = $ch * $maxw / $cw;
|
||||||
|
}
|
||||||
|
elseif ($maxh > 0 && ($maxw <= 0 || $maxh/$ch < $maxw/$cw))
|
||||||
|
{
|
||||||
|
$nw = $cw * $maxh / $ch;
|
||||||
|
$nh = $maxh;
|
||||||
|
}
|
||||||
|
return [ intval($nw), intval($nh) ];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return file size description
|
||||||
|
*/
|
||||||
|
static $sizeSuffix = [
|
||||||
|
'Кб' => [
|
||||||
|
'bytes' => 'bytes',
|
||||||
|
'Kb' => 'Кб',
|
||||||
|
'Mb' => 'Мб',
|
||||||
|
'Gb' => 'Гб',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
public static function sizeString($bytes, $lang = 'ru')
|
||||||
|
{
|
||||||
|
$r = $bytes;
|
||||||
|
if (is_numeric($r))
|
||||||
|
{
|
||||||
|
if ($r >= 0 && $r < 1024)
|
||||||
|
$r = $r . ' ' . (isset(self::$sizeString[$lang]['bytes']) ? self::$sizeString[$lang]['bytes'] : 'bytes');
|
||||||
|
elseif ($r >= 1024 && $r < 1024*1024)
|
||||||
|
$r = sprintf('%.2f ', $r/1024) . (isset(self::$sizeString[$lang]['Kb']) ? self::$sizeString[$lang]['Kb'] : 'Kb');
|
||||||
|
elseif ($r >= 1024*1024 && $r < 1024*1024*1024)
|
||||||
|
$r = sprintf('%.2f ', $r/1024/1024) . (isset(self::$sizeString[$lang]['Mb']) ? self::$sizeString[$lang]['Mb'] : 'Mb');
|
||||||
|
elseif ($r >= 1024*1024*1024)
|
||||||
|
$r = sprintf('%.2f ', $r/1024/1024/1024) . (isset(self::$sizeString[$lang]['Gb']) ? self::$sizeString[$lang]['Gb'] : 'Gb');
|
||||||
|
elseif ($r < 0)
|
||||||
|
$r = sprintf('%.2f ', 2-($r/1024/1024/1024)) . (isset(self::$sizeString[$lang]['Gb']) ? self::$sizeString[$lang]['Gb'] : 'Gb');
|
||||||
|
}
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively create a directory
|
||||||
|
*/
|
||||||
|
public static function mkpath($path, $throw_error = true)
|
||||||
|
{
|
||||||
|
if (is_dir($path) || @mkdir($path, 0777, true))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ($throw_error)
|
||||||
|
{
|
||||||
|
$error = error_get_last();
|
||||||
|
throw new Exception("Failed to create path $path: ".$error['message']);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get thumbnail size for $file
|
||||||
|
*/
|
||||||
|
public static function getThumbSize($file, $width, $height, $crop = false)
|
||||||
|
{
|
||||||
|
if (empty($file['id']))
|
||||||
|
return NULL;
|
||||||
|
if ($width < 0)
|
||||||
|
$width = 0;
|
||||||
|
if ($height < 0)
|
||||||
|
$height = 0;
|
||||||
|
if (!$width || !$height)
|
||||||
|
$crop = false;
|
||||||
|
if (!$file['width'] || !$width && !$height ||
|
||||||
|
(!$width || $file['width'] <= $width) &&
|
||||||
|
(!$height || $file['height'] < $height) && !$crop ||
|
||||||
|
($file['format'] == 'swf' || $file['format'] == 'video'))
|
||||||
|
return NULL;
|
||||||
|
if (!$crop)
|
||||||
|
return self::thumbnailSize($file['width'], $file['height'], $width, $height);
|
||||||
|
return [ $width, $height ];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert ImageMagick image $im to a thumbnail
|
||||||
|
*/
|
||||||
|
public static function makeThumb($im, $width, $height, $crop, $orientation, $alignY)
|
||||||
|
{
|
||||||
|
$cl = get_class($im);
|
||||||
|
if ($crop)
|
||||||
|
{
|
||||||
|
$iw = $im->getImageWidth();
|
||||||
|
$ih = $im->getImageHeight();
|
||||||
|
if ($crop == File::CROP_Y && $ih*$width/$iw < $height)
|
||||||
|
{
|
||||||
|
$height = $ih*$width/$iw;
|
||||||
|
}
|
||||||
|
elseif ($crop == File::CROP_X && $iw*$height/$ih < $width)
|
||||||
|
{
|
||||||
|
$width = $iw*$height/$ih;
|
||||||
|
}
|
||||||
|
if ($width/$height < $iw/$ih)
|
||||||
|
{
|
||||||
|
$cw = intval($width*$ih/$height);
|
||||||
|
$im->cropImage($cw, $ih, intval(($iw-$cw)/2), 0);
|
||||||
|
}
|
||||||
|
elseif ($width/$height > $iw/$ih)
|
||||||
|
{
|
||||||
|
$ch = intval($height*$iw/$width);
|
||||||
|
$im->cropImage($iw, $ch, 0, intval(($ih-$ch)*$alignY));
|
||||||
|
}
|
||||||
|
$im->resizeImage($width, $height, $cl::FILTER_LANCZOS, 1);
|
||||||
|
$im->setImagePage($width, $height, 0, 0); /* for gif cropping */
|
||||||
|
$iw = $im->getImageWidth();
|
||||||
|
$ih = $im->getImageHeight();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$im->resizeImage($width, $height, $cl::FILTER_LANCZOS, 1);
|
||||||
|
}
|
||||||
|
$im->stripImage();
|
||||||
|
if ($orientation !== NULL)
|
||||||
|
{
|
||||||
|
self::fixOrientation($im, $orientation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class LocalFile
|
||||||
|
{
|
||||||
|
var $shouldMove = false;
|
||||||
|
var $name;
|
||||||
|
|
||||||
|
function __construct($name, $shouldMove = false)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
$this->shouldMove = $shouldMove;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocalPath()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileName()
|
||||||
|
{
|
||||||
|
return basename($this->name);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class PostedFile extends LocalFile
|
||||||
|
{
|
||||||
|
var $shouldMove = true;
|
||||||
|
|
||||||
|
function __construct($name)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
if (!is_uploaded_file(@$_FILES[$this->name]['tmp_name']))
|
||||||
|
{
|
||||||
|
throw new Exception("No POSTed file with name $name");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static function newFromName($name)
|
||||||
|
{
|
||||||
|
if (is_uploaded_file(@$_FILES[$name]['tmp_name']))
|
||||||
|
{
|
||||||
|
return new self($name);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __destruct()
|
||||||
|
{
|
||||||
|
$tmp_name = $_FILES[$this->name]['tmp_name'];
|
||||||
|
if (@is_uploaded_file($tmp_name))
|
||||||
|
{
|
||||||
|
// Unlink temporary upload
|
||||||
|
@unlink($tmp_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocalPath()
|
||||||
|
{
|
||||||
|
return $_FILES[$this->name]['tmp_name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileName()
|
||||||
|
{
|
||||||
|
if (isset($_FILES[$this->name]['name']))
|
||||||
|
{
|
||||||
|
$name = trim($_FILES[$this->name]['name']);
|
||||||
|
$name = preg_replace('#^.*[/\\\\]#is', '', $name);
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class VideoUtils
|
||||||
|
{
|
||||||
|
public static function getVideoProps($fn)
|
||||||
|
{
|
||||||
|
if (empty(App::$config['video_converter']))
|
||||||
|
{
|
||||||
|
throw new Exception('Video converter is not configured on the server');
|
||||||
|
}
|
||||||
|
$probe = App::$config['video_converter'];
|
||||||
|
$cmd = $probe['probe'];
|
||||||
|
$cmd = str_replace('$input', escapeshellarg($fn), $cmd);
|
||||||
|
$out = shell_exec($cmd);
|
||||||
|
$data = [];
|
||||||
|
foreach ([ 'format', 'size', 'duration', 'video_format', 'audio_format' ] as $k)
|
||||||
|
{
|
||||||
|
if (preg_match_all($probe['probe_'.$k], $out, $m, PREG_PATTERN_ORDER))
|
||||||
|
{
|
||||||
|
unset($m[0]);
|
||||||
|
$data[$k] = $m;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$data[$k] = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($data['format'] && $data['size'] && $data['duration'])
|
||||||
|
{
|
||||||
|
$dur = $data['duration'][1][0];
|
||||||
|
$colon = explode(':', $dur);
|
||||||
|
if (count($colon) > 1)
|
||||||
|
{
|
||||||
|
$dur = 0;
|
||||||
|
for ($mul = 1, $i = 0; $i < count($colon); $i++, $mul *= 60)
|
||||||
|
$dur += $colon[count($colon)-1-$i] * $mul;
|
||||||
|
}
|
||||||
|
// Пустой формат видео означает, что файл ещё нужно обработать (минимум qt-faststart и вытащить тамбик)
|
||||||
|
return [
|
||||||
|
'format' => 'video',
|
||||||
|
'width' => $data['size'][1][0],
|
||||||
|
'height' => $data['size'][2][0],
|
||||||
|
'props' => [
|
||||||
|
'duration' => $dur,
|
||||||
|
'video_format' => @$data['video_format'][1],
|
||||||
|
'audio_format' => @$data['audio_format'][1],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function extractVideoPreview($file, $redirect_output = '')
|
||||||
|
{
|
||||||
|
$conv = App::$config['video_converter'];
|
||||||
|
$ss = $file['props']['duration'] * $conv['preview_moment'];
|
||||||
|
$fn = FileHandler::getPath(false, $file);
|
||||||
|
$out = FileHandler::getThumbPath(false, $file, 'src');
|
||||||
|
FileUtils::mkpath(dirname($out), true);
|
||||||
|
$cmd = $conv['extract_frame'];
|
||||||
|
$cmd = str_replace([ '$input', '$output', '$position' ], [ escapeshellarg($fn), escapeshellarg($out), ceil($ss) ], $cmd);
|
||||||
|
system($cmd . ($redirect_output ? ' &> '.$redirect_output : ''));
|
||||||
|
if (!file_exists($out) || !filesize($out))
|
||||||
|
{
|
||||||
|
throw new Exception('Failed to extract video frame for preview');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function runConvertJobDaemon($redirect_output = '')
|
||||||
|
{
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
$file = App::$db->selectRow(File::$table, '*', [ 'format' => 'video' ], [ 'LIMIT' => 1 ]);
|
||||||
|
if ($file)
|
||||||
|
{
|
||||||
|
self::convertVideoAndUpdate($file, $redirect_output);
|
||||||
|
}
|
||||||
|
App::$db->commitAll();
|
||||||
|
sleep(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function convertVideoAndUpdate($file, $redirect_output = '')
|
||||||
|
{
|
||||||
|
$props = self::convertVideo($file, $redirect_output);
|
||||||
|
if (!$props)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::extractVideoPreview($props + $file, $redirect_output);
|
||||||
|
$update = $props;
|
||||||
|
if (isset($update['props']))
|
||||||
|
{
|
||||||
|
$update['props'] = json_encode($update['props']);
|
||||||
|
}
|
||||||
|
App::$db->update(File::$table, $update, [ 'id' => $file['id'] ]);
|
||||||
|
if (isset($props['sha1']) && $props['sha1'] != $file['sha1'])
|
||||||
|
{
|
||||||
|
@unlink(FileHandler::getPath(false, $file));
|
||||||
|
}
|
||||||
|
elseif (isset($props['format']) && $props['format'] != $file['format'])
|
||||||
|
{
|
||||||
|
@rename(FileHandler::getPath(false, $file), FileHandler::getPath(false, $props + $file));
|
||||||
|
}
|
||||||
|
return $props + $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function convertVideo($file, $redirect_output = '')
|
||||||
|
{
|
||||||
|
// Краткая сводка:
|
||||||
|
// Flash понимает форматы файлов MP4/FLV с видео+аудио FLV+MP3 или H.264+AAC
|
||||||
|
// HTML5 кроссбраузерно понимает форматы MP4 с видео H.264 и аудио MP3/AAC, либо WebM с видео VP8 и аудио Vorbis
|
||||||
|
// Для MP4 надо делать qt-faststart, если MOOV ATOM не находится в начале видеофайла
|
||||||
|
//
|
||||||
|
// Итак, если формат WebM+VP8+Vorbis или FLV+FLV+MP3 или FLV+H.264+AAC, видео можно вообще не конвертировать
|
||||||
|
// Если формат MP4+H.264+MP3/AAC, надо проверить/сделать qt-faststart
|
||||||
|
// Любой другой формат надо перегнать в MP4+H.264+AAC
|
||||||
|
if ($file['format'] != 'video')
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (empty(App::$config['video_converter']))
|
||||||
|
{
|
||||||
|
throw new Exception('Video converter is not configured on the server');
|
||||||
|
}
|
||||||
|
$conv = App::$config['video_converter'];
|
||||||
|
$vf = $file['props']['video_format'][0];
|
||||||
|
$af = $file['props']['audio_format'][0];
|
||||||
|
if ($file['mimetype'] == 'video/x-flv' && ($vf == 'flv1' && $af == 'mp3' || $vf == 'h264' && $af == 'aac'))
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'format' => 'flv',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
elseif ($file['mimetype'] == 'video/webm' && $vf == 'vp8' && $af == 'vorbis')
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'format' => 'webm',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$fn = FileHandler::getPath(false, $file);
|
||||||
|
$out = tempnam(sys_get_temp_dir(), 'ffc');
|
||||||
|
if ($file['mimetype'] == 'video/mp4' && $vf == 'h264' && ($af == 'mp3' || $af == 'aac'))
|
||||||
|
{
|
||||||
|
$cmd = $conv['qt_faststart'].' '.escapeshellarg($fn).' '.escapeshellarg($out);
|
||||||
|
system($cmd . ($redirect_output ? ' &> '.$redirect_output : ''));
|
||||||
|
if (!file_exists($out))
|
||||||
|
{
|
||||||
|
// Уже было faststart, всё ок
|
||||||
|
return [
|
||||||
|
'format' => 'mp4',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$cmd = $conv['convert'];
|
||||||
|
$cmd = str_replace([ '$input', '$output' ], [ escapeshellarg($fn), escapeshellarg($out) ], $cmd);
|
||||||
|
system($cmd . ($redirect_output ? ' &> '.$redirect_output : ''));
|
||||||
|
if (!file_exists($out))
|
||||||
|
{
|
||||||
|
// Не сконвертировалось, плохо
|
||||||
|
throw new Exception('Failed to convert video to MP4');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Сконвертированный файл
|
||||||
|
$update = FileUtils::getProps(File::ONLY_VIDEO, $out, $out);
|
||||||
|
$update['format'] = 'mp4';
|
||||||
|
$newfn = FileHandler::getPath(false, $update + $file);
|
||||||
|
FileUtils::mkpath(dirname($newfn), true);
|
||||||
|
if (!@rename($out, $newfn))
|
||||||
|
{
|
||||||
|
$error = error_get_last();
|
||||||
|
throw new Exception($error['message']);
|
||||||
|
}
|
||||||
|
// FIXME Теоретически может получиться так, что полученный файл будет дубликатом, и запрос свалится
|
||||||
|
// (Может ли ffmpeg дважды выдать одинаковый файл? Тогда будет проблемка)
|
||||||
|
return $update;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$config += [
|
||||||
|
'local_path' => dirname(dirname(__DIR__)),
|
||||||
|
'files_path' => '/files/',
|
||||||
|
'mime_blacklist' => '#'.
|
||||||
|
// HTML may contain cookie-stealing JavaScript and web bugs
|
||||||
|
'^text/(html|(x-)?javascript)$|^application/x-shellscript$'.
|
||||||
|
// PHP/Perl/Bash/etc scripts may execute arbitrary code on the server
|
||||||
|
'|php|perl|python|bash|x-c?sh(e|$)'.
|
||||||
|
// Client-side hazards on Internet Explorer
|
||||||
|
'|^text/scriptlet$|^application/x-msdownload$'.
|
||||||
|
// Windows metafile, client-side vulnerability on some systems
|
||||||
|
'|^application/x-msmetafile$'.
|
||||||
|
'#is',
|
||||||
|
'video_converter' => [
|
||||||
|
'extract_frame' => '/usr/bin/ffmpeg -ss \'$position\' -i $input -vframes 1 -f image2 -y $output 2>&1',
|
||||||
|
'preview_moment' => '0.05',
|
||||||
|
'probe' => '/usr/bin/ffmpeg -i $input 2>&1',
|
||||||
|
'probe_format' => '/Input #\d+, (\S+), from/is',
|
||||||
|
'format_aliases' => [ 'mov,mp4,m4a,3gp,3g2,mj2' => 'mp4', 'matroska' => 'mkv', 'matroska,webm' => 'mkv' ],
|
||||||
|
'probe_size' => '/Stream.*Video.*,\s+(\d+)x(\d+)/is', // [1] == width, [2] == height
|
||||||
|
'probe_duration' => '/Duration: ([\d:]+)/is',
|
||||||
|
'probe_video_format' => '/Stream.*Video:\s*(\S+)/is',
|
||||||
|
'probe_audio_format' => '/Stream.*Audio:\s*(\S+)/is',
|
||||||
|
'qt_faststart' => '/usr/bin/qt-faststart',
|
||||||
|
'convert' => '/usr/bin/ffmpeg -i $input -vcodec h264 -qmax 28 -acodec aac -movflags faststart -y $output',
|
||||||
|
],
|
||||||
|
];
|
|
@ -0,0 +1,29 @@
|
||||||
|
-- Файлы
|
||||||
|
create table if not exists files (
|
||||||
|
id serial not null primary key,
|
||||||
|
added bigint not null,
|
||||||
|
user_id int,
|
||||||
|
sha1 varchar(40) not null,
|
||||||
|
format varchar(40) not null,
|
||||||
|
mimetype varchar(1024) not null,
|
||||||
|
size bigint not null,
|
||||||
|
width int not null default 0,
|
||||||
|
height int not null default 0,
|
||||||
|
props jsonb not null default '{}'::jsonb,
|
||||||
|
foreign key (user_id) references users (id) on delete set null on update cascade
|
||||||
|
);
|
||||||
|
|
||||||
|
create unique index on files (sha1);
|
||||||
|
create index on files (format);
|
||||||
|
create index on files (user_id);
|
||||||
|
|
||||||
|
comment on table files is 'Файлы';
|
||||||
|
comment on column files.added is 'UNIX время загрузки';
|
||||||
|
comment on column files.user_id is 'ID пользователя-владельца';
|
||||||
|
comment on column files.sha1 is 'SHA1 хеш';
|
||||||
|
comment on column files.format is 'Формат (расширение)';
|
||||||
|
comment on column files.mimetype is 'MIME-тип (реальный формат)';
|
||||||
|
comment on column files.size is 'Размер файла';
|
||||||
|
comment on column files.width is 'Ширина';
|
||||||
|
comment on column files.height is 'Высота';
|
||||||
|
comment on column files.propdata is 'Дополнительные свойства (EXIF, параметры видео)';
|
Loading…
Reference in New Issue