FileService.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. <?php
  2. namespace App\Services;
  3. use App\Models\File;
  4. use Exception;
  5. use FilesystemIterator;
  6. use Illuminate\Support\Facades\Storage;
  7. use Illuminate\Support\Str;
  8. use RecursiveDirectoryIterator;
  9. use RecursiveIteratorIterator;
  10. use RuntimeException;
  11. use ZipArchive;
  12. class FileService
  13. {
  14. /**
  15. * @param $path
  16. * @param $file
  17. * @return File
  18. */
  19. public function saveUploadedFile($path, $file): File
  20. {
  21. $originalName = $this->sanitizeOriginalName((string)$file->getClientOriginalName());
  22. $storedFilename = $this->buildStoredFilename($originalName);
  23. $relativePath = $this->buildUniquePath($path, $storedFilename);
  24. try {
  25. Storage::disk('public')->put($relativePath, $file->getContent());
  26. } catch (\Throwable $e) {
  27. throw new RuntimeException('Не удалось сохранить файл. Проверьте имя файла и повторите попытку.', 0, $e);
  28. }
  29. return File::query()->create([
  30. 'link' => url('/storage/' . $relativePath),
  31. 'path' => $relativePath,
  32. 'user_id' => auth()->user()->id,
  33. 'original_name' => $originalName,
  34. 'mime_type' => $file->getClientMimeType(),
  35. ]);
  36. }
  37. private function sanitizeOriginalName(string $originalName): string
  38. {
  39. $name = basename(str_replace('\\', '/', $originalName));
  40. if (class_exists('\Normalizer')) {
  41. $normalized = \Normalizer::normalize($name, \Normalizer::FORM_C);
  42. if (is_string($normalized)) {
  43. $name = $normalized;
  44. }
  45. }
  46. // Remove control/format unicode symbols (e.g. zero-width, LRM/RLM), then normalize spaces.
  47. $name = preg_replace('/\p{C}+/u', '', $name) ?? '';
  48. $name = preg_replace('/\p{Z}+/u', ' ', $name) ?? '';
  49. $name = trim($name);
  50. return $name === '' ? 'file' : $name;
  51. }
  52. private function buildStoredFilename(string $originalName): string
  53. {
  54. $baseName = pathinfo($originalName, PATHINFO_FILENAME);
  55. $extension = pathinfo($originalName, PATHINFO_EXTENSION);
  56. $baseName = preg_replace('/[<>:|?*\/\\\\]+/u', '_', $baseName) ?? '';
  57. $baseName = preg_replace('/\s+/u', ' ', $baseName) ?? '';
  58. $baseName = trim($baseName, " .\t\n\r\0\x0B");
  59. if ($baseName === '' || $baseName === '.' || $baseName === '..') {
  60. $baseName = 'file';
  61. }
  62. if (function_exists('mb_substr')) {
  63. $baseName = mb_substr($baseName, 0, 150);
  64. } else {
  65. $baseName = substr($baseName, 0, 150);
  66. }
  67. $extension = preg_replace('/[^A-Za-z0-9]+/', '', $extension) ?? '';
  68. return $extension === '' ? $baseName : $baseName . '.' . $extension;
  69. }
  70. private function buildUniquePath(string $directory, string $filename): string
  71. {
  72. $directory = trim($directory, '/');
  73. $basePath = $directory === '' ? $filename : $directory . '/' . $filename;
  74. if (!Storage::disk('public')->exists($basePath)) {
  75. return $basePath;
  76. }
  77. $baseName = pathinfo($filename, PATHINFO_FILENAME);
  78. $extension = pathinfo($filename, PATHINFO_EXTENSION);
  79. for ($index = 1; $index < 10000; $index++) {
  80. $candidateName = $baseName . ' (' . $index . ')';
  81. if ($extension !== '') {
  82. $candidateName .= '.' . $extension;
  83. }
  84. $candidatePath = $directory === '' ? $candidateName : $directory . '/' . $candidateName;
  85. if (!Storage::disk('public')->exists($candidatePath)) {
  86. return $candidatePath;
  87. }
  88. }
  89. return $basePath;
  90. }
  91. /**
  92. * @param string $path
  93. * @param string $archiveName
  94. * @param int|null $userId
  95. * @return File
  96. * @throws Exception
  97. */
  98. public function createZipArchive(string $path, string $archiveName, ?int $userId = null): File
  99. {
  100. $fullPath = storage_path('app/public/' . $path);
  101. $archivePath = $this->archivePath($path, $archiveName);
  102. $archiveFullPath = Storage::disk('public')->path($archivePath);
  103. Storage::disk('public')->makeDirectory(dirname($archivePath));
  104. $zip = new ZipArchive();
  105. if ($zip->open($archiveFullPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
  106. throw new Exception('Cant create zip archive!');
  107. }
  108. $files = new RecursiveIteratorIterator(
  109. new RecursiveDirectoryIterator($fullPath, FilesystemIterator::SKIP_DOTS),
  110. RecursiveIteratorIterator::LEAVES_ONLY
  111. );
  112. foreach ($files as $file) {
  113. $filePath = $file->getRealPath();
  114. $relativePath = substr($filePath, strlen($fullPath) + 1);
  115. if (!$file->isDir()) {
  116. $zip->addFile($filePath, $relativePath);
  117. } else {
  118. $zip->addEmptyDir($relativePath);
  119. }
  120. }
  121. $zip->close();
  122. $fileModel = File::query()->updateOrCreate([
  123. 'link' => url('/storage/') . '/' . $archivePath,
  124. ], [
  125. 'path' => $archivePath,
  126. 'user_id' => $userId ?? auth()->user()->id,
  127. 'original_name' => $archiveName,
  128. 'mime_type' => 'application/zip',
  129. 'is_generated' => true,
  130. ]);
  131. return $fileModel;
  132. }
  133. private function archivePath(string $path, string $archiveName): string
  134. {
  135. $normalizedPath = trim($path, '/');
  136. if (Str::endsWith($normalizedPath, '/tmp')) {
  137. $normalizedPath = (string) Str::beforeLast($normalizedPath, '/tmp');
  138. }
  139. return $normalizedPath . '/' . $archiveName;
  140. }
  141. }