File: /var/www/heifetz/heifetz-app/models/File.php
<?php
namespace Models;
use App\Exceptions\CrmException;
use Core\DbLib\NewDbModel;
use Core\Models\Acl;
use Core\Models\CoreHelper;
use Core\Models\ErrorLog;
use Exception;
use finfo;
use Gumlet\ImageResize;
use Gumlet\ImageResizeException;
use Helpers\ArrayHelper;
use Helpers\Data;
use Helpers\FileHelper;
use JetBrains\PhpStorm\NoReturn;
use Traits\ModelAddByArrayReturnsId;
use Traits\ModelGetQuery;
/**
* @property int $id
* @property string $name
* @property string $extension
* @property int $size
* @property string $path
* @property string $created_at
* @property string $updated_at
* @property string $deleted_at
*/
class File extends NewDbModel
{
use ModelGetQuery;
use ModelAddByArrayReturnsId;
static string $tableName = 'files';
protected static bool $hasSoftDelete = true;
const STORAGE_TYPE_LOCAL = 0;
public static array $storageTypes = [
self::STORAGE_TYPE_LOCAL => 'Локальное хранилище',
];
const LOCAL_STORAGE_TYPES = [
self::STORAGE_TYPE_LOCAL,
];
static array $imageExtensions = ['jpg', 'jpeg', 'gif', 'png'];
public static function upload($fileData, $typeId = null)
{
$tempFile = $fileData['tmp_name'];
if (empty(filesize($tempFile))) {
return 0;
}
$pathInfo = pathinfo($fileData['name']);
$targetPath = UPLOADS_DIR . '/';
$targetPath = str_replace('//', '/', $targetPath);
$targetPath .= date('Y/m/d/');
$extension = $pathInfo['extension'] ?? '';
if (mb_strlen($extension) > 5) {
$extension = '';
}
$fileName = md5($fileData['name']) . '.' . $extension;
$fileName = date('YmdHis') . str_replace(' ', '_', $fileName);
$targetFile = $targetPath . $fileName;
$targetFile = str_replace('//', '/', $targetFile);
try {
FileHelper::checkAndCreateDir($targetPath, 0777);
move_uploaded_file($tempFile, $targetFile);
} catch (Exception) {
throw new CrmException('Ошибка загрузки файла');
}
chmod($targetFile, 0775);
$file = new self();
$file->name = $fileData['name'];
$file->extension = $extension;
$file->size = (int) filesize($targetFile);
$file->path = str_replace(ROOT, '', $targetFile);
$file->save();
return $file->id;
}
public static function uploadMultiply($fieldName, $typeIdName = null)
{
$files = [];
if (empty($_FILES[$fieldName]['name']) || empty($_FILES[$fieldName]['tmp_name'])) {
return $files;
}
if (isset($typeIdName)) {
$typesIds = Data::getVar($typeIdName);
}
if (!is_array($_FILES[$fieldName]['name']) || !is_array($_FILES[$fieldName]['tmp_name'])) {
$_FILES[$fieldName]['tmp_name'][] = $_FILES[$fieldName]['tmp_name'];
$_FILES[$fieldName]['name'][] = $_FILES[$fieldName]['name'];
}
foreach ($_FILES[$fieldName]['name'] as $key => $value) {
if (empty($_FILES[$fieldName]['name'][$key]) || empty($_FILES[$fieldName]['tmp_name'][$key])) {
continue;
}
$fileData = [
'tmp_name' => $_FILES[$fieldName]['tmp_name'][$key],
'name' => $_FILES[$fieldName]['name'][$key],
];
$fileId = self::upload($fileData, $typesIds[$key] ?? null);
if (!empty($fileId)) {
$files[] = $fileId;
}
}
return $files;
}
public static function preRender($list)
{
$files = [];
foreach ($list as $row) {
$files[] = [
'filesize' => Data::humanSize($row->size),
'basename' => $row->name,
'link' => '/' . Acl::PRESENTER_FILES . $row->path,
'key' => $row->id,
'delete_request' => $row->delete_request ?? null,
];
}
return $files;
}
public static function saveFile($name, $fileContent, $subPath = null, $path = ROOT . '/storage/', int $storageType = self::STORAGE_TYPE_LOCAL, bool $generateName = false)
{
$path .= $subPath ? '/' . $subPath . '/' : '';
$path .= date('Y/m/d/');
$pathInfo = pathinfo($name);
if ($generateName) {
$oldName = $name;
$name = md5($name) . '.' . $pathInfo['extension'];
$name = date('YmdHis') . str_replace(' ', '_', $name);
}
$targetFile = str_replace('//', '/', $path . $name);
try {
FileHelper::checkAndCreateDir($path, 0777);
file_put_contents($targetFile, $fileContent);
} catch (Exception) {
throw new CrmException('Ошибка загрузки файла');
}
$fileSize = filesize($targetFile);
if (empty($fileSize)) {
return false;
}
return self::create(
[
'name' => $generateName ? $oldName : basename($targetFile),
'extension' => $pathInfo['extension'],
'size' => $fileSize,
'path' => str_replace('//', '/', str_replace(ROOT, '/', $targetFile)),
'storage_type' => $storageType,
'company_id' => CoreHelper::$companyId,
'created_at' => date('Y-m-d H:i:s'),
]
);
}
public static function isImage($file)
{
if (is_object($file)) {
return in_array(mb_strtolower($file->extension), self::$imageExtensions);
}
$path_parts = pathinfo($file);
if (isset($path_parts['extension'])) {
return in_array(mb_strtolower($path_parts['extension']), self::$imageExtensions);
}
return false;
}
#[NoReturn]
public static function rangeDownload(self $file, string $filename): void
{
$isUrl = filter_var($filename, FILTER_VALIDATE_URL);
$length = 0;
$size = 0;
$fp = fopen($filename, 'rb');
if ($isUrl === false) {
$size = filesize($filename);
$length = $size;
} else {
$responseHeaders = get_headers($filename, true);
$size = $responseHeaders['Content-Length'];
$length = $size;
}
$start = 0;
$end = $size - 1;
header("Accept-Ranges: 0-$length");
if (isset($_SERVER['HTTP_RANGE'])) {
$c_end = $end;
[, $range] = explode('=', $_SERVER['HTTP_RANGE'], 2);
if (str_contains($range, ',')) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
exit;
}
if ($range == '-') {
$c_start = $size - substr($range, 1);
} else {
$range = explode('-', $range);
$c_start = $range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
}
$c_end = ($c_end > $end) ? $end : $c_end;
if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
exit;
}
$start = $c_start;
$end = $c_end;
$length = $end - $start + 1; // Calculate new content length
$meta = stream_get_meta_data($fp);
if ($meta['seekable']) {
fseek($fp, $start);
}
header('HTTP/1.1 206 Partial Content');
}
header("Content-Range: bytes $start-$end/$size");
header("Content-Length: $length");
$buffer = 1024 * 8;
while (!feof($fp) && ($p = ftell($fp)) <= $end) {
if ($p + $buffer > $end) {
$buffer = $end - $p + 1;
}
set_time_limit(0); // Reset time limit for big files
echo fread($fp, $buffer);
flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
}
fclose($fp);
exit;
}
#[NoReturn]
public static function load(?self $file, string $filename, ?string $realFileName = null, bool $view = false): void
{
$realFilename = $realFileName ?? $filename;
$isUrl = filter_var($filename, FILTER_VALIDATE_URL);
// Открываем искомый файл
$f = fopen($filename, 'r');
if (!empty($f)) {
if ($isUrl === false) {
$headers = [mime_content_type($filename), filesize($filename)];
} else {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$type = $finfo->buffer(file_get_contents($filename));
$responseHeaders = get_headers($filename, true);
$headers = [$type, $responseHeaders['Content-Length']];
}
header($_SERVER['SERVER_PROTOCOL'] . ' 200 OK');
header('Content-Type: ' . $headers[0]);
header('Content-Length: ' . $headers[1]);
if (!$view && !empty($filename)) {
header('Content-Disposition: attachment; filename="' . $realFilename . '"');
}
if (!empty($file) && 'mp3' === $file->extension) {
header('Accept-Ranges: bytes');
header('Transfer-Encoding: chunked');
}
while (!feof($f)) {
// Читаем килобайтный блок, отдаем его в вывод и сбрасываем в буфер
echo fread($f, 1024);
flush();
}
// Закрываем файл
fclose($f);
} else {
header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
header('Status: 404 Not Found');
echo '404 Not Found';
}
exit;
}
#[NoReturn]
public static function download(?self $file, string $filename, ?string $realFileName = null, string $mimetype = 'application/octet-stream'): void
{
$isUrl = filter_var($filename, FILTER_VALIDATE_URL);
$headers = [];
// Открываем искомый файл
$f = fopen($filename, 'r');
if (!empty($filename)) {
if ($isUrl === false) {
$headers = [
gmdate('r', filemtime($filename)),
sprintf('%x-%x-%x', fileinode($filename), filesize($filename), filemtime($filename)),
filesize($filename),
$realFileName ?? basename($filename),
];
} else {
$responseHeaders = get_headers($filename, true);
$headers = [
$responseHeaders['Last-Modified'],
$responseHeaders['ETag'],
$responseHeaders['Content-Length'],
$realFileName ?? $file->name,
];
}
header($_SERVER["SERVER_PROTOCOL"] . ' 200 OK');
header('Content-Type: ' . $mimetype);
header('Last-Modified: ' . $headers[0]);
header('ETag: ' . $headers[1]);
header('Content-Length: ' . $headers[2]);
header('Connection: close');
header('Content-Disposition: attachment; filename="' . $headers[3] . '";');
while (!feof($f)) {
// Читаем килобайтный блок, отдаем его в вывод и сбрасываем в буфер
echo fread($f, 1024);
flush();
}
// Закрываем файл
fclose($f);
} else {
header($_SERVER["SERVER_PROTOCOL"] . ' 404 Not Found');
header('Status: 404 Not Found');
echo '404 Not Found';
}
exit;
}
/** @deprecated use FilesRepository::getDownloadLink() */
public static function getDownloadLink(object|array|string $file, bool $view = false): string
{
if (is_object($file)) {
$file = ArrayHelper::toArray($file);
}
if (is_string($file)) {
return '/' . Acl::PRESENTER_FILES . $file . ($view ? '?view=1' : '');
}
return '/' . Acl::PRESENTER_FILES . $file['path'] . '?file_id=' . $file['id'] . ($view ? '&view=1' : '');
}
public static function rename($fileId, $name)
{
self::update(['name' => $name], ['id' => $fileId]);
UserLogs::logUpdate(self::$tableName, $fileId, 'name', $name);
}
public static function changeType($fileId, $typeId)
{
self::update(['type_id' => $typeId], ['id' => $fileId]);
UserLogs::logUpdate(self::$tableName, $fileId, 'type_id', $typeId);
}
/**
* @param int|self $file
* @param int $width
* @param int $height
*
* @return string
*/
public static function getThumb($file, int $width, int $height): ?string
{
if (!is_object($file)) {
$file = self::find($file);
}
if (!File::isImage($file)) {
return null;
}
$thumbDir = '/thumbnails/' . $file->id . '/';
$thumbFileName = $width . 'x' . $height . '-' . basename($file->path);
if (!file_exists(TEMP_DIR . $thumbDir . $thumbFileName)) {
$fileName = str_replace('//', '/', ROOT . '/' . $file->path);
if (!file_exists($fileName) || filesize($fileName) > 6 * 1024 * 1024) {
return '';
}
try {
$image = new ImageResize($fileName);
$image->resizeToBestFit($width, $height);
FileHelper::checkAndCreateDir(TEMP_DIR . $thumbDir);
$image->save(TEMP_DIR . $thumbDir . $thumbFileName);
} catch (Exception $exception) {
$exception = new CrmException($exception->getMessage() . ' ' . print_r($file, true));
ErrorLog::processException($exception);
return null;
}
}
return '/files/tmp' . $thumbDir . $thumbFileName;
}
}