| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- <?php
- namespace App\Services;
- use App\Models\File;
- use Exception;
- use FilesystemIterator;
- use Illuminate\Support\Facades\Storage;
- use Illuminate\Support\Str;
- use RecursiveDirectoryIterator;
- use RecursiveIteratorIterator;
- use RuntimeException;
- use ZipArchive;
- class FileService
- {
- /**
- * @param $path
- * @param $file
- * @return File
- */
- public function saveUploadedFile($path, $file): File
- {
- $originalName = $this->sanitizeOriginalName((string)$file->getClientOriginalName());
- $storedFilename = $this->buildStoredFilename($originalName);
- $relativePath = $this->buildUniquePath($path, $storedFilename);
- try {
- Storage::disk('public')->put($relativePath, $file->getContent());
- } catch (\Throwable $e) {
- throw new RuntimeException('Не удалось сохранить файл. Проверьте имя файла и повторите попытку.', 0, $e);
- }
- $fileModel = File::query()->create([
- 'link' => url('/storage/' . $relativePath),
- 'path' => $relativePath,
- 'user_id' => auth()->user()->id,
- 'original_name' => $originalName,
- 'mime_type' => $file->getClientMimeType(),
- ]);
- $this->ensureThumbnail($fileModel);
- return $fileModel;
- }
- public function ensureThumbnail(File $file, bool $overwrite = false): bool
- {
- if (!$this->isImageFile($file) || !$file->path || $this->isThumbnailPath($file->path)) {
- return false;
- }
- $disk = Storage::disk('public');
- if (!$disk->exists($file->path)) {
- return false;
- }
- $thumbnailPath = $this->thumbnailPath($file->path);
- if ($disk->exists($thumbnailPath) && !$overwrite) {
- return true;
- }
- return $this->createThumbnail($file->path, $thumbnailPath);
- }
- public function deleteFileWithThumbnail(File $file): void
- {
- if ($file->path) {
- Storage::disk('public')->delete($file->path);
- if (!$this->isThumbnailPath($file->path)) {
- Storage::disk('public')->delete($this->thumbnailPath($file->path));
- }
- }
- }
- public function thumbnailPath(string $path): string
- {
- $directory = pathinfo($path, PATHINFO_DIRNAME);
- $filename = pathinfo($path, PATHINFO_FILENAME);
- $extension = pathinfo($path, PATHINFO_EXTENSION);
- $thumbnailName = $filename . '.thumbnail' . ($extension !== '' ? '.' . $extension : '');
- return $directory === '.' ? $thumbnailName : $directory . '/' . $thumbnailName;
- }
- public function isThumbnailPath(string $path): bool
- {
- return Str::contains(pathinfo($path, PATHINFO_FILENAME), '.thumbnail');
- }
- public function isImageFile(File $file): bool
- {
- if (in_array((string) $file->mime_type, ['image/jpeg', 'image/png', 'image/webp'], true)) {
- return true;
- }
- return in_array(Str::lower(pathinfo((string) $file->path, PATHINFO_EXTENSION)), ['jpg', 'jpeg', 'png', 'webp'], true);
- }
- private function createThumbnail(string $sourcePath, string $thumbnailPath): bool
- {
- if (!extension_loaded('gd')) {
- return false;
- }
- $disk = Storage::disk('public');
- $sourceFullPath = $disk->path($sourcePath);
- $imageInfo = @getimagesize($sourceFullPath);
- if (!$imageInfo || empty($imageInfo[0]) || empty($imageInfo[1])) {
- return false;
- }
- [$width, $height, $type] = $imageInfo;
- $source = match ($type) {
- IMAGETYPE_JPEG => function_exists('\imagecreatefromjpeg') ? @\imagecreatefromjpeg($sourceFullPath) : false,
- IMAGETYPE_PNG => function_exists('\imagecreatefrompng') ? @\imagecreatefrompng($sourceFullPath) : false,
- IMAGETYPE_WEBP => function_exists('\imagecreatefromwebp') ? @\imagecreatefromwebp($sourceFullPath) : false,
- default => false,
- };
- if (!$source) {
- return false;
- }
- $maxSide = $this->thumbnailMaxSide();
- $scale = min(1, $maxSide / max($width, $height));
- $thumbnailWidth = max(1, (int) round($width * $scale));
- $thumbnailHeight = max(1, (int) round($height * $scale));
- $thumbnail = \imagecreatetruecolor($thumbnailWidth, $thumbnailHeight);
- if (in_array($type, [IMAGETYPE_PNG, IMAGETYPE_WEBP], true)) {
- \imagealphablending($thumbnail, false);
- \imagesavealpha($thumbnail, true);
- }
- \imagecopyresampled($thumbnail, $source, 0, 0, 0, 0, $thumbnailWidth, $thumbnailHeight, $width, $height);
- $disk->makeDirectory(dirname($thumbnailPath));
- $thumbnailFullPath = $disk->path($thumbnailPath);
- $saved = match ($type) {
- IMAGETYPE_JPEG => function_exists('\imagejpeg') && \imagejpeg($thumbnail, $thumbnailFullPath, 82),
- IMAGETYPE_PNG => function_exists('\imagepng') && \imagepng($thumbnail, $thumbnailFullPath, 7),
- IMAGETYPE_WEBP => function_exists('\imagewebp') && \imagewebp($thumbnail, $thumbnailFullPath, 82),
- default => false,
- };
- \imagedestroy($source);
- \imagedestroy($thumbnail);
- return (bool) $saved;
- }
- private function thumbnailMaxSide(): int
- {
- return max(1, (int) config('filesystems.photo_thumbnail_max_side', 150));
- }
- private function sanitizeOriginalName(string $originalName): string
- {
- $name = basename(str_replace('\\', '/', $originalName));
- if (class_exists('\Normalizer')) {
- $normalized = \Normalizer::normalize($name, \Normalizer::FORM_C);
- if (is_string($normalized)) {
- $name = $normalized;
- }
- }
- // Remove control/format unicode symbols (e.g. zero-width, LRM/RLM), then normalize spaces.
- $name = preg_replace('/\p{C}+/u', '', $name) ?? '';
- $name = preg_replace('/\p{Z}+/u', ' ', $name) ?? '';
- $name = trim($name);
- return $name === '' ? 'file' : $name;
- }
- private function buildStoredFilename(string $originalName): string
- {
- $baseName = pathinfo($originalName, PATHINFO_FILENAME);
- $extension = pathinfo($originalName, PATHINFO_EXTENSION);
- $baseName = preg_replace('/[<>:|?*\/\\\\]+/u', '_', $baseName) ?? '';
- $baseName = preg_replace('/\s+/u', ' ', $baseName) ?? '';
- $baseName = trim($baseName, " .\t\n\r\0\x0B");
- if ($baseName === '' || $baseName === '.' || $baseName === '..') {
- $baseName = 'file';
- }
- if (function_exists('mb_substr')) {
- $baseName = mb_substr($baseName, 0, 150);
- } else {
- $baseName = substr($baseName, 0, 150);
- }
- $extension = preg_replace('/[^A-Za-z0-9]+/', '', $extension) ?? '';
- return $extension === '' ? $baseName : $baseName . '.' . $extension;
- }
- private function buildUniquePath(string $directory, string $filename): string
- {
- $directory = trim($directory, '/');
- $basePath = $directory === '' ? $filename : $directory . '/' . $filename;
- if (!Storage::disk('public')->exists($basePath)) {
- return $basePath;
- }
- $baseName = pathinfo($filename, PATHINFO_FILENAME);
- $extension = pathinfo($filename, PATHINFO_EXTENSION);
- for ($index = 1; $index < 10000; $index++) {
- $candidateName = $baseName . ' (' . $index . ')';
- if ($extension !== '') {
- $candidateName .= '.' . $extension;
- }
- $candidatePath = $directory === '' ? $candidateName : $directory . '/' . $candidateName;
- if (!Storage::disk('public')->exists($candidatePath)) {
- return $candidatePath;
- }
- }
- return $basePath;
- }
- /**
- * @param string $path
- * @param string $archiveName
- * @param int|null $userId
- * @return File
- * @throws Exception
- */
- public function createZipArchive(string $path, string $archiveName, ?int $userId = null): File
- {
- $fullPath = storage_path('app/public/' . $path);
- $archivePath = $this->archivePath($path, $archiveName);
- $archiveFullPath = Storage::disk('public')->path($archivePath);
- Storage::disk('public')->makeDirectory(dirname($archivePath));
- $zip = new ZipArchive();
- if ($zip->open($archiveFullPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
- throw new Exception('Cant create zip archive!');
- }
- $files = new RecursiveIteratorIterator(
- new RecursiveDirectoryIterator($fullPath, FilesystemIterator::SKIP_DOTS),
- RecursiveIteratorIterator::LEAVES_ONLY
- );
- foreach ($files as $file) {
- $filePath = $file->getRealPath();
- $relativePath = substr($filePath, strlen($fullPath) + 1);
- if (!$file->isDir()) {
- $zip->addFile($filePath, $relativePath);
- } else {
- $zip->addEmptyDir($relativePath);
- }
- }
- $zip->close();
- $fileModel = File::query()->updateOrCreate([
- 'link' => url('/storage/') . '/' . $archivePath,
- ], [
- 'path' => $archivePath,
- 'user_id' => $userId ?? auth()->user()->id,
- 'original_name' => $archiveName,
- 'mime_type' => 'application/zip',
- 'is_generated' => true,
- ]);
- return $fileModel;
- }
- private function archivePath(string $path, string $archiveName): string
- {
- $normalizedPath = trim($path, '/');
- if (Str::endsWith($normalizedPath, '/tmp')) {
- $normalizedPath = (string) Str::beforeLast($normalizedPath, '/tmp');
- }
- return $normalizedPath . '/' . $archiveName;
- }
- }
|