1d4265d07SGreg Roach<?php 2d4265d07SGreg Roach 3d4265d07SGreg Roach/** 4d4265d07SGreg Roach * webtrees: online genealogy 55bfc6897SGreg Roach * Copyright (C) 2022 webtrees development team 6d4265d07SGreg Roach * This program is free software: you can redistribute it and/or modify 7d4265d07SGreg Roach * it under the terms of the GNU General Public License as published by 8d4265d07SGreg Roach * the Free Software Foundation, either version 3 of the License, or 9d4265d07SGreg Roach * (at your option) any later version. 10d4265d07SGreg Roach * This program is distributed in the hope that it will be useful, 11d4265d07SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 12d4265d07SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13d4265d07SGreg Roach * GNU General Public License for more details. 14d4265d07SGreg Roach * You should have received a copy of the GNU General Public License 1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 16d4265d07SGreg Roach */ 17d4265d07SGreg Roach 18d4265d07SGreg Roachdeclare(strict_types=1); 19d4265d07SGreg Roach 20d4265d07SGreg Roachnamespace Fisharebest\Webtrees\Services; 21d4265d07SGreg Roach 22b5c53c7fSGreg Roachuse Fisharebest\Webtrees\Exceptions\FileUploadException; 23d4265d07SGreg Roachuse Fisharebest\Webtrees\FlashMessages; 24d4265d07SGreg Roachuse Fisharebest\Webtrees\I18N; 25f7cf8a15SGreg Roachuse Fisharebest\Webtrees\Registry; 26d4265d07SGreg Roachuse Fisharebest\Webtrees\Tree; 27b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator; 28d4265d07SGreg Roachuse Illuminate\Database\Capsule\Manager as DB; 2913aa75d8SGreg Roachuse Illuminate\Database\Query\Expression; 30c6bad570SGreg Roachuse Illuminate\Database\Query\JoinClause; 3113aa75d8SGreg Roachuse Illuminate\Support\Collection; 32d4265d07SGreg Roachuse InvalidArgumentException; 33f7cf8a15SGreg Roachuse League\Flysystem\FilesystemException; 34f7cf8a15SGreg Roachuse League\Flysystem\FilesystemOperator; 35782714c2SGreg Roachuse League\Flysystem\FilesystemReader; 36f7cf8a15SGreg Roachuse League\Flysystem\StorageAttributes; 37d4265d07SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 38d4265d07SGreg Roachuse RuntimeException; 39d4265d07SGreg Roach 40d4265d07SGreg Roachuse function array_combine; 41d4265d07SGreg Roachuse function array_diff; 42f9b64f46SGreg Roachuse function array_intersect; 4313aa75d8SGreg Roachuse function dirname; 44f9b64f46SGreg Roachuse function explode; 45d501c45dSGreg Roachuse function ini_get; 46d4265d07SGreg Roachuse function intdiv; 4781aa9d16SGreg Roachuse function is_float; 48d501c45dSGreg Roachuse function min; 49d4265d07SGreg Roachuse function pathinfo; 50d4265d07SGreg Roachuse function sha1; 51d4265d07SGreg Roachuse function sort; 52dec352c1SGreg Roachuse function str_contains; 5375e2e2b6SGreg Roachuse function strlen; 540ea5fc1cSGreg Roachuse function strtoupper; 5545fc2659SGreg Roachuse function strtr; 56d501c45dSGreg Roachuse function substr; 57d4265d07SGreg Roachuse function trim; 5846b03695SGreg Roach 59d4265d07SGreg Roachuse const PATHINFO_EXTENSION; 6081aa9d16SGreg Roachuse const PHP_INT_MAX; 61d4265d07SGreg Roachuse const UPLOAD_ERR_OK; 62d4265d07SGreg Roach 63d4265d07SGreg Roach/** 64d4265d07SGreg Roach * Managing media files. 65d4265d07SGreg Roach */ 66d4265d07SGreg Roachclass MediaFileService 67d4265d07SGreg Roach{ 6845fc2659SGreg Roach public const EXTENSION_TO_FORM = [ 690ea5fc1cSGreg Roach 'JPEG' => 'JPG', 700ea5fc1cSGreg Roach 'TIFF' => 'TIF', 7145fc2659SGreg Roach ]; 7245fc2659SGreg Roach 73c6bad570SGreg Roach private const IGNORE_FOLDERS = [ 74c6bad570SGreg Roach // Old versions of webtrees 75c6bad570SGreg Roach 'thumbs', 76c6bad570SGreg Roach 'watermarks', 77c6bad570SGreg Roach // Windows 78c6bad570SGreg Roach 'Thumbs.db', 79c6bad570SGreg Roach // Synology 80c6bad570SGreg Roach '@eaDir', 81c6bad570SGreg Roach // QNAP, 82c6bad570SGreg Roach '.@__thumb', 832c7d07c0SGreg Roach // WebDAV, 842c7d07c0SGreg Roach '_DAV', 85c6bad570SGreg Roach ]; 86c6bad570SGreg Roach 87d4265d07SGreg Roach /** 88d4265d07SGreg Roach * What is the largest file a user may upload? 89d4265d07SGreg Roach */ 90d4265d07SGreg Roach public function maxUploadFilesize(): string 91d4265d07SGreg Roach { 92d8809d62SGreg Roach $sizePostMax = $this->parseIniFileSize((string) ini_get('post_max_size')); 93d8809d62SGreg Roach $sizeUploadMax = $this->parseIniFileSize((string) ini_get('upload_max_filesize')); 94d501c45dSGreg Roach 95d501c45dSGreg Roach $bytes = min($sizePostMax, $sizeUploadMax); 96d4265d07SGreg Roach $kb = intdiv($bytes + 1023, 1024); 97d4265d07SGreg Roach 98d4265d07SGreg Roach return I18N::translate('%s KB', I18N::number($kb)); 99d4265d07SGreg Roach } 100d4265d07SGreg Roach 101d4265d07SGreg Roach /** 102d501c45dSGreg Roach * Returns the given size from an ini value in bytes. 103d501c45dSGreg Roach * 104284014f8SGreg Roach * @param string $size 105d501c45dSGreg Roach * 106d501c45dSGreg Roach * @return int 107d501c45dSGreg Roach */ 108284014f8SGreg Roach private function parseIniFileSize(string $size): int 109d501c45dSGreg Roach { 110d501c45dSGreg Roach $number = (int) $size; 111d501c45dSGreg Roach 11281aa9d16SGreg Roach $units = [ 11381aa9d16SGreg Roach 'g' => 1073741824, 11481aa9d16SGreg Roach 'G' => 1073741824, 11581aa9d16SGreg Roach 'm' => 1048576, 11681aa9d16SGreg Roach 'M' => 1048576, 11781aa9d16SGreg Roach 'k' => 1024, 11881aa9d16SGreg Roach 'K' => 1024, 11981aa9d16SGreg Roach ]; 12081aa9d16SGreg Roach 12181aa9d16SGreg Roach $number *= $units[substr($size, -1)] ?? 1; 12281aa9d16SGreg Roach 12381aa9d16SGreg Roach if (is_float($number)) { 12481aa9d16SGreg Roach // Probably a 32bit version of PHP, with an INI setting >= 2GB 12581aa9d16SGreg Roach return PHP_INT_MAX; 126d501c45dSGreg Roach } 12781aa9d16SGreg Roach 12881aa9d16SGreg Roach return $number; 129d501c45dSGreg Roach } 130d501c45dSGreg Roach 131d501c45dSGreg Roach /** 132d4265d07SGreg Roach * A list of media files not already linked to a media object. 133d4265d07SGreg Roach * 134d4265d07SGreg Roach * @param Tree $tree 135f7cf8a15SGreg Roach * @param FilesystemOperator $data_filesystem 136d4265d07SGreg Roach * 137bfe98399SGreg Roach * @return array<string> 138d4265d07SGreg Roach */ 139f7cf8a15SGreg Roach public function unusedFiles(Tree $tree, FilesystemOperator $data_filesystem): array 140d4265d07SGreg Roach { 141d4265d07SGreg Roach $used_files = DB::table('media_file') 142d4265d07SGreg Roach ->where('m_file', '=', $tree->id()) 143d4265d07SGreg Roach ->where('multimedia_file_refn', 'NOT LIKE', 'http://%') 144d4265d07SGreg Roach ->where('multimedia_file_refn', 'NOT LIKE', 'https://%') 145d4265d07SGreg Roach ->pluck('multimedia_file_refn') 146d4265d07SGreg Roach ->all(); 147d4265d07SGreg Roach 148c6bad570SGreg Roach $media_filesystem = $tree->mediaFilesystem($data_filesystem); 149782714c2SGreg Roach $disk_files = $this->allFilesOnDisk($media_filesystem, '', FilesystemReader::LIST_DEEP)->all(); 150d4265d07SGreg Roach $unused_files = array_diff($disk_files, $used_files); 151d4265d07SGreg Roach 152d4265d07SGreg Roach sort($unused_files); 153d4265d07SGreg Roach 154d4265d07SGreg Roach return array_combine($unused_files, $unused_files); 155d4265d07SGreg Roach } 156d4265d07SGreg Roach 157d4265d07SGreg Roach /** 158d4265d07SGreg Roach * Store an uploaded file (or URL), either to be added to a media object 159d4265d07SGreg Roach * or to create a media object. 160d4265d07SGreg Roach * 161d4265d07SGreg Roach * @param ServerRequestInterface $request 162d4265d07SGreg Roach * 163d4265d07SGreg Roach * @return string The value to be stored in the 'FILE' field of the media object. 164f7cf8a15SGreg Roach * @throws FilesystemException 165d4265d07SGreg Roach */ 166d4265d07SGreg Roach public function uploadFile(ServerRequestInterface $request): string 167d4265d07SGreg Roach { 168b55cbc6bSGreg Roach $tree = Validator::attributes($request)->tree(); 169d4265d07SGreg Roach 1706b9cb339SGreg Roach $data_filesystem = Registry::filesystem()->data(); 171a04bb9a2SGreg Roach 172b46c87bdSGreg Roach $params = (array) $request->getParsedBody(); 173d4265d07SGreg Roach $file_location = $params['file_location']; 174d4265d07SGreg Roach 175d4265d07SGreg Roach switch ($file_location) { 176d4265d07SGreg Roach case 'url': 177d4265d07SGreg Roach $remote = $params['remote']; 178d4265d07SGreg Roach 179dec352c1SGreg Roach if (str_contains($remote, '://')) { 180d4265d07SGreg Roach return $remote; 181d4265d07SGreg Roach } 182d4265d07SGreg Roach 183d4265d07SGreg Roach return ''; 184d4265d07SGreg Roach 185d4265d07SGreg Roach case 'unused': 186d4265d07SGreg Roach $unused = $params['unused']; 187d4265d07SGreg Roach 188f7cf8a15SGreg Roach if ($tree->mediaFilesystem($data_filesystem)->fileExists($unused)) { 189d4265d07SGreg Roach return $unused; 190d4265d07SGreg Roach } 191d4265d07SGreg Roach 192d4265d07SGreg Roach return ''; 193d4265d07SGreg Roach 194d4265d07SGreg Roach case 'upload': 195d4265d07SGreg Roach default: 196d4265d07SGreg Roach $folder = $params['folder']; 197d4265d07SGreg Roach $auto = $params['auto']; 198d4265d07SGreg Roach $new_file = $params['new_file']; 199d4265d07SGreg Roach 200b5c53c7fSGreg Roach $uploaded_file = $request->getUploadedFiles()['file'] ?? null; 201b5c53c7fSGreg Roach 202d4265d07SGreg Roach if ($uploaded_file === null || $uploaded_file->getError() !== UPLOAD_ERR_OK) { 203b5c53c7fSGreg Roach throw new FileUploadException($uploaded_file); 204d4265d07SGreg Roach } 205d4265d07SGreg Roach 206d4265d07SGreg Roach // The filename 207dec352c1SGreg Roach $new_file = strtr($new_file, ['\\' => '/']); 208dec352c1SGreg Roach if ($new_file !== '' && !str_contains($new_file, '/')) { 209d4265d07SGreg Roach $file = $new_file; 210d4265d07SGreg Roach } else { 211d4265d07SGreg Roach $file = $uploaded_file->getClientFilename(); 212d4265d07SGreg Roach } 213d4265d07SGreg Roach 214d4265d07SGreg Roach // The folder 215dec352c1SGreg Roach $folder = strtr($folder, ['\\' => '/']); 216d4265d07SGreg Roach $folder = trim($folder, '/'); 217d4265d07SGreg Roach if ($folder !== '') { 218d4265d07SGreg Roach $folder .= '/'; 219d4265d07SGreg Roach } 220d4265d07SGreg Roach 221d4265d07SGreg Roach // Generate a unique name for the file? 222f7cf8a15SGreg Roach if ($auto === '1' || $tree->mediaFilesystem($data_filesystem)->fileExists($folder . $file)) { 223d4265d07SGreg Roach $folder = ''; 224d4265d07SGreg Roach $extension = pathinfo($uploaded_file->getClientFilename(), PATHINFO_EXTENSION); 225d4265d07SGreg Roach $file = sha1((string) $uploaded_file->getStream()) . '.' . $extension; 226d4265d07SGreg Roach } 227d4265d07SGreg Roach 228d4265d07SGreg Roach try { 229f7cf8a15SGreg Roach $tree->mediaFilesystem($data_filesystem)->writeStream($folder . $file, $uploaded_file->getStream()->detach()); 230d4265d07SGreg Roach 231d4265d07SGreg Roach return $folder . $file; 232d4265d07SGreg Roach } catch (RuntimeException | InvalidArgumentException $ex) { 233d4265d07SGreg Roach FlashMessages::addMessage(I18N::translate('There was an error uploading your file.')); 234d4265d07SGreg Roach 235d4265d07SGreg Roach return ''; 236d4265d07SGreg Roach } 237d4265d07SGreg Roach } 238d4265d07SGreg Roach } 239d4265d07SGreg Roach 240d4265d07SGreg Roach /** 241d4265d07SGreg Roach * Convert the media file attributes into GEDCOM format. 242d4265d07SGreg Roach * 243d4265d07SGreg Roach * @param string $file 244d4265d07SGreg Roach * @param string $type 245d4265d07SGreg Roach * @param string $title 24645fc2659SGreg Roach * @param string $note 247d4265d07SGreg Roach * 248d4265d07SGreg Roach * @return string 249d4265d07SGreg Roach */ 25045fc2659SGreg Roach public function createMediaFileGedcom(string $file, string $type, string $title, string $note): string 251d4265d07SGreg Roach { 252d4265d07SGreg Roach $gedcom = '1 FILE ' . $file; 25345fc2659SGreg Roach 2542bba966dSGreg Roach if (str_contains($file, '://')) { 2552bba966dSGreg Roach $format = ''; 2562bba966dSGreg Roach } else { 2570ea5fc1cSGreg Roach $format = strtoupper(pathinfo($file, PATHINFO_EXTENSION)); 25845fc2659SGreg Roach $format = self::EXTENSION_TO_FORM[$format] ?? $format; 2592bba966dSGreg Roach } 26045fc2659SGreg Roach 26175e2e2b6SGreg Roach if ($format !== '' && strlen($format) <= 4) { 26245fc2659SGreg Roach $gedcom .= "\n2 FORM " . $format; 26345fc2659SGreg Roach } elseif ($type !== '') { 26445fc2659SGreg Roach $gedcom .= "\n2 FORM"; 265d4265d07SGreg Roach } 26645fc2659SGreg Roach 26745fc2659SGreg Roach if ($type !== '') { 26845fc2659SGreg Roach $gedcom .= "\n3 TYPE " . $type; 26945fc2659SGreg Roach } 27045fc2659SGreg Roach 271d4265d07SGreg Roach if ($title !== '') { 272d4265d07SGreg Roach $gedcom .= "\n2 TITL " . $title; 273d4265d07SGreg Roach } 274d4265d07SGreg Roach 27545fc2659SGreg Roach if ($note !== '') { 27645fc2659SGreg Roach // Convert HTML line endings to GEDCOM continuations 27745fc2659SGreg Roach $gedcom .= "\n1 NOTE " . strtr($note, ["\r\n" => "\n2 CONT "]); 27845fc2659SGreg Roach } 27945fc2659SGreg Roach 280d4265d07SGreg Roach return $gedcom; 281d4265d07SGreg Roach } 28213aa75d8SGreg Roach 28313aa75d8SGreg Roach /** 28413aa75d8SGreg Roach * Fetch a list of all files on disk (in folders used by any tree). 28513aa75d8SGreg Roach * 286f7cf8a15SGreg Roach * @param FilesystemOperator $filesystem $filesystem to search 287f7cf8a15SGreg Roach * @param string $folder Root folder 28813aa75d8SGreg Roach * @param bool $subfolders Include subfolders 28913aa75d8SGreg Roach * 29036779af1SGreg Roach * @return Collection<int,string> 29113aa75d8SGreg Roach */ 292f7cf8a15SGreg Roach public function allFilesOnDisk(FilesystemOperator $filesystem, string $folder, bool $subfolders): Collection 29313aa75d8SGreg Roach { 294f0448b68SGreg Roach try { 295f9b64f46SGreg Roach $files = $filesystem 296f9b64f46SGreg Roach ->listContents($folder, $subfolders) 297f9b64f46SGreg Roach ->filter(fn (StorageAttributes $attributes): bool => $attributes->isFile()) 298f9b64f46SGreg Roach ->filter(fn (StorageAttributes $attributes): bool => !$this->ignorePath($attributes->path())) 299f9b64f46SGreg Roach ->map(fn (StorageAttributes $attributes): string => $attributes->path()) 300f0448b68SGreg Roach ->toArray(); 301f0448b68SGreg Roach } catch (FilesystemException $ex) { 302f0448b68SGreg Roach $files = []; 303f0448b68SGreg Roach } 304f7cf8a15SGreg Roach 305f0448b68SGreg Roach return new Collection($files); 30613aa75d8SGreg Roach } 30713aa75d8SGreg Roach 30813aa75d8SGreg Roach /** 30913aa75d8SGreg Roach * Fetch a list of all files on in the database. 31013aa75d8SGreg Roach * 31113aa75d8SGreg Roach * @param string $media_folder Root folder 31213aa75d8SGreg Roach * @param bool $subfolders Include subfolders 31313aa75d8SGreg Roach * 31436779af1SGreg Roach * @return Collection<int,string> 31513aa75d8SGreg Roach */ 31613aa75d8SGreg Roach public function allFilesInDatabase(string $media_folder, bool $subfolders): Collection 31713aa75d8SGreg Roach { 31813aa75d8SGreg Roach $query = DB::table('media_file') 31913aa75d8SGreg Roach ->join('gedcom_setting', 'gedcom_id', '=', 'm_file') 32013aa75d8SGreg Roach ->where('setting_name', '=', 'MEDIA_DIRECTORY') 32113aa75d8SGreg Roach //->where('multimedia_file_refn', 'LIKE', '%/%') 32213aa75d8SGreg Roach ->where('multimedia_file_refn', 'NOT LIKE', 'http://%') 32313aa75d8SGreg Roach ->where('multimedia_file_refn', 'NOT LIKE', 'https://%') 32413aa75d8SGreg Roach ->where(new Expression('setting_value || multimedia_file_refn'), 'LIKE', $media_folder . '%') 32513aa75d8SGreg Roach ->select(new Expression('setting_value || multimedia_file_refn AS path')) 32613aa75d8SGreg Roach ->orderBy(new Expression('setting_value || multimedia_file_refn')); 32713aa75d8SGreg Roach 32813aa75d8SGreg Roach if (!$subfolders) { 32913aa75d8SGreg Roach $query->where(new Expression('setting_value || multimedia_file_refn'), 'NOT LIKE', $media_folder . '%/%'); 33013aa75d8SGreg Roach } 33113aa75d8SGreg Roach 33213aa75d8SGreg Roach return $query->pluck('path'); 33313aa75d8SGreg Roach } 33413aa75d8SGreg Roach 33513aa75d8SGreg Roach /** 336f9b64f46SGreg Roach * Generate a list of all folders used by a tree. 337f9b64f46SGreg Roach * 338f9b64f46SGreg Roach * @param Tree $tree 339f9b64f46SGreg Roach * 34036779af1SGreg Roach * @return Collection<int,string> 341f9b64f46SGreg Roach * @throws FilesystemException 342f9b64f46SGreg Roach */ 343f9b64f46SGreg Roach public function mediaFolders(Tree $tree): Collection 344f9b64f46SGreg Roach { 345f9b64f46SGreg Roach $folders = Registry::filesystem()->media($tree) 346782714c2SGreg Roach ->listContents('', FilesystemReader::LIST_DEEP) 347f9b64f46SGreg Roach ->filter(fn (StorageAttributes $attributes): bool => $attributes->isDir()) 348f9b64f46SGreg Roach ->filter(fn (StorageAttributes $attributes): bool => !$this->ignorePath($attributes->path())) 349f9b64f46SGreg Roach ->map(fn (StorageAttributes $attributes): string => $attributes->path()) 350f9b64f46SGreg Roach ->toArray(); 351f9b64f46SGreg Roach 352f9b64f46SGreg Roach return new Collection($folders); 353f9b64f46SGreg Roach } 354f9b64f46SGreg Roach 355f9b64f46SGreg Roach /** 35613aa75d8SGreg Roach * Generate a list of all folders in either the database or the filesystem. 35713aa75d8SGreg Roach * 358f7cf8a15SGreg Roach * @param FilesystemOperator $data_filesystem 35913aa75d8SGreg Roach * 36036779af1SGreg Roach * @return Collection<array-key,string> 361f7cf8a15SGreg Roach * @throws FilesystemException 36213aa75d8SGreg Roach */ 363f7cf8a15SGreg Roach public function allMediaFolders(FilesystemOperator $data_filesystem): Collection 36413aa75d8SGreg Roach { 36513aa75d8SGreg Roach $db_folders = DB::table('media_file') 366c6bad570SGreg Roach ->leftJoin('gedcom_setting', static function (JoinClause $join): void { 367c6bad570SGreg Roach $join 368c6bad570SGreg Roach ->on('gedcom_id', '=', 'm_file') 369c6bad570SGreg Roach ->where('setting_name', '=', 'MEDIA_DIRECTORY'); 370c6bad570SGreg Roach }) 37113aa75d8SGreg Roach ->where('multimedia_file_refn', 'NOT LIKE', 'http://%') 37213aa75d8SGreg Roach ->where('multimedia_file_refn', 'NOT LIKE', 'https://%') 373c6bad570SGreg Roach ->select(new Expression("COALESCE(setting_value, 'media/') || multimedia_file_refn AS path")) 37413aa75d8SGreg Roach ->pluck('path') 37513aa75d8SGreg Roach ->map(static function (string $path): string { 37613aa75d8SGreg Roach return dirname($path) . '/'; 37713aa75d8SGreg Roach }); 37813aa75d8SGreg Roach 379c6bad570SGreg Roach $media_roots = DB::table('gedcom') 380c6bad570SGreg Roach ->leftJoin('gedcom_setting', static function (JoinClause $join): void { 381c6bad570SGreg Roach $join 382c6bad570SGreg Roach ->on('gedcom.gedcom_id', '=', 'gedcom_setting.gedcom_id') 383c6bad570SGreg Roach ->where('setting_name', '=', 'MEDIA_DIRECTORY'); 384c6bad570SGreg Roach }) 385c6bad570SGreg Roach ->where('gedcom.gedcom_id', '>', '0') 386c6bad570SGreg Roach ->pluck(new Expression("COALESCE(setting_value, 'media/')")) 3878c627a69SGreg Roach ->uniqueStrict(); 38813aa75d8SGreg Roach 38913aa75d8SGreg Roach $disk_folders = new Collection($media_roots); 39013aa75d8SGreg Roach 39113aa75d8SGreg Roach foreach ($media_roots as $media_folder) { 392f9b64f46SGreg Roach $tmp = $data_filesystem 393782714c2SGreg Roach ->listContents($media_folder, FilesystemReader::LIST_DEEP) 394f9b64f46SGreg Roach ->filter(fn (StorageAttributes $attributes): bool => $attributes->isDir()) 395f9b64f46SGreg Roach ->filter(fn (StorageAttributes $attributes): bool => !$this->ignorePath($attributes->path())) 396f9b64f46SGreg Roach ->map(fn (StorageAttributes $attributes): string => $attributes->path() . '/') 397f7cf8a15SGreg Roach ->toArray(); 39813aa75d8SGreg Roach 39913aa75d8SGreg Roach $disk_folders = $disk_folders->concat($tmp); 40013aa75d8SGreg Roach } 40113aa75d8SGreg Roach 40213aa75d8SGreg Roach return $disk_folders->concat($db_folders) 4038c627a69SGreg Roach ->uniqueStrict() 404*6edebcedSGreg Roach ->sort(I18N::comparator()) 40513aa75d8SGreg Roach ->mapWithKeys(static function (string $folder): array { 40613aa75d8SGreg Roach return [$folder => $folder]; 40713aa75d8SGreg Roach }); 40813aa75d8SGreg Roach } 409f7cf8a15SGreg Roach 410f7cf8a15SGreg Roach /** 411f9b64f46SGreg Roach * Ignore special media folders. 41208b5db2aSGreg Roach * 41308b5db2aSGreg Roach * @param string $path 41408b5db2aSGreg Roach * 41508b5db2aSGreg Roach * @return bool 416f7cf8a15SGreg Roach */ 417c6bad570SGreg Roach private function ignorePath(string $path): bool 418f7cf8a15SGreg Roach { 419d8809d62SGreg Roach return array_intersect(self::IGNORE_FOLDERS, explode('/', $path)) !== []; 420f7cf8a15SGreg Roach } 421d4265d07SGreg Roach} 422