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; } }