Source of file FlysystemAssetStore.php
Size: 60,812 Bytes - Last Modified: 2021-12-23T10:27:40+00:00
/var/www/docs.ssmods.com/process/src/src/Flysystem/FlysystemAssetStore.php
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706 | <?php namespace SilverStripe\Assets\Flysystem; use Generator; use InvalidArgumentException; use League\Flysystem\Directory; use League\Flysystem\Exception as FlysystemException; use League\Flysystem\Filesystem; use League\Flysystem\Util; use LogicException; use SilverStripe\Assets\File; use SilverStripe\Assets\FilenameParsing\FileIDHelper; use SilverStripe\Assets\FilenameParsing\FileResolutionStrategy; use SilverStripe\Assets\FilenameParsing\HashFileIDHelper; use SilverStripe\Assets\FilenameParsing\ParsedFileID; use SilverStripe\Assets\Storage\AssetNameGenerator; use SilverStripe\Assets\Storage\AssetStore; use SilverStripe\Assets\Storage\AssetStoreRouter; use SilverStripe\Assets\Storage\FileHashingService; use SilverStripe\Control\Controller; use SilverStripe\Control\Director; use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPStreamResponse; use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Extensible; use SilverStripe\Core\Flushable; use SilverStripe\Core\Injector\Injector; use SilverStripe\Security\Security; use SilverStripe\Versioned\Versioned; /** * Asset store based on flysystem Filesystem as a backend */ class FlysystemAssetStore implements AssetStore, AssetStoreRouter, Flushable { use Configurable; use Extensible; /** * Session key to use for user grants */ const GRANTS_SESSION = 'AssetStore_Grants'; /** * @var Filesystem */ private $publicFilesystem = null; /** * Filesystem to use for protected files * * @var Filesystem */ private $protectedFilesystem = null; /** * File resolution strategy to use with the public adapter. * @var FileResolutionStrategy */ private $publicResolutionStrategy = null; /** * File resolution strategy to use with the protected adapter. * @var FileResolutionStrategy */ private $protectedResolutionStrategy = null; /** * Enable to use legacy filename behaviour (omits hash and uses the natural filename). * * This setting was only required for SilverStripe prior to the 4.4.0 release. * This release re-introduced natural filenames as the default mode for public files. * See https://docs.silverstripe.org/en/4/developer_guides/files/file_migration/ * and https://docs.silverstripe.org/en/4/changelogs/4.4.0/ for details. * * If you have migrated to 4.x prior to the 4.4.0 release with this setting turned on, * the setting won't have any effect starting with this release. * * If you have migrated to 4.x prior to the 4.4.0 release with this setting turned off, * we recommend that you run the file migration task as outlined * in https://docs.silverstripe.org/en/4/changelogs/4.4.0/ * * @config * @deprecated 1.4.0 * @var bool */ private static $legacy_filenames = false; /** * Flag if empty folders are allowed. * If false, empty folders are cleared up when their contents are deleted. * * @config * @var bool */ private static $keep_empty_dirs = false; /** * Set HTTP error code for requests to secure denied assets. * Note that this defaults to 404 to prevent information disclosure * of secure files * * @config * @var int */ private static $denied_response_code = 404; /** * Set HTTP error code to use for missing secure assets * * @config * @var int */ private static $missing_response_code = 404; /** * Define the HTTP Response code for request that should be temporarily redirected to a different URL. Defaults to * 302. * @config * @var int */ private static $redirect_response_code = 302; /** * Define the HTTP Response code for request that should be permanently redirected to a different URL. Defaults to * 301. * @config * @var int */ private static $permanent_redirect_response_code = 301; /** * Custom headers to add to all custom file responses * * @config * @var array */ private static $file_response_headers = [ 'Cache-Control' => 'private' ]; /** * Assign new flysystem backend * * @param Filesystem $filesystem * @throws InvalidArgumentException * @return $this */ public function setPublicFilesystem(Filesystem $filesystem) { if (!$filesystem->getAdapter() instanceof PublicAdapter) { throw new InvalidArgumentException("Configured adapter must implement PublicAdapter"); } $this->publicFilesystem = $filesystem; return $this; } /** * Get the currently assigned flysystem backend * * @return Filesystem * @throws LogicException */ public function getPublicFilesystem() { if (!$this->publicFilesystem) { throw new LogicException("Filesystem misconfiguration error"); } return $this->publicFilesystem; } /** * Assign filesystem to use for non-public files * * @param Filesystem $filesystem * @throws InvalidArgumentException * @return $this */ public function setProtectedFilesystem(Filesystem $filesystem) { if (!$filesystem->getAdapter() instanceof ProtectedAdapter) { throw new InvalidArgumentException("Configured adapter must implement ProtectedAdapter"); } $this->protectedFilesystem = $filesystem; return $this; } /** * Get filesystem to use for non-public files * * @return Filesystem * @throws LogicException */ public function getProtectedFilesystem() { if (!$this->protectedFilesystem) { throw new LogicException("Filesystem misconfiguration error"); } return $this->protectedFilesystem; } /** * @return FileResolutionStrategy * @internal This API has not been formalised yet. */ public function getPublicResolutionStrategy() { if (!$this->publicResolutionStrategy) { $this->publicResolutionStrategy = Injector::inst()->get(FileResolutionStrategy::class . '.public'); } if (!$this->publicResolutionStrategy) { throw new LogicException("Filesystem misconfiguration error"); } return $this->publicResolutionStrategy; } /** * @param FileResolutionStrategy $publicResolutionStrategy * @internal This API has not been formalised yet. */ public function setPublicResolutionStrategy(FileResolutionStrategy $publicResolutionStrategy) { $this->publicResolutionStrategy = $publicResolutionStrategy; } /** * @return FileResolutionStrategy * @throws LogicException * @internal This API has not been formalised yet. */ public function getProtectedResolutionStrategy() { if (!$this->protectedResolutionStrategy) { $this->protectedResolutionStrategy = Injector::inst()->get(FileResolutionStrategy::class . '.protected'); } if (!$this->protectedResolutionStrategy) { throw new LogicException("Filesystem misconfiguration error"); } return $this->protectedResolutionStrategy; } /** * @param FileResolutionStrategy $protectedResolutionStrategy * @internal This API has not been formalised yet. */ public function setProtectedResolutionStrategy(FileResolutionStrategy $protectedResolutionStrategy) { $this->protectedResolutionStrategy = $protectedResolutionStrategy; } /** * Return the store that contains the given fileID * * @param string $fileID Internal file identifier * @deprecated 1.4.0 * @return Filesystem */ protected function getFilesystemFor($fileID) { return $this->applyToFileIDOnFilesystem( function (ParsedFileID $parsedFileID, Filesystem $fs) { return $fs; }, $fileID ); } /** * Generic method to apply an action to a file regardless of what FileSystem it's on. The action to perform should * be provided as a closure expecting the following signature: * ``` * function(ParsedFileID $parsedFileID, FileSystem $fs, FileResolutionStrategy $strategy, $visibility) * ``` * * `applyToFileOnFilesystem` will try to following steps and call the closure if they are succesfull: * 1. Look for the file on the public filesystem using the explicit fileID provided. * 2. Look for the file on the protected filesystem using the explicit fileID provided. * 3. Look for the file on the public filesystem using the public resolution strategy. * 4. Look for the file on the protected filesystem using the protected resolution strategy. * * If the closure returns `false`, `applyToFileOnFilesystem` will carry on and try the follow up steps. * * Any other value the closure returns (including `null`) will be returned to the calling function. * * @param callable $callable Action to apply. * @param string|array|ParsedFileID $fileID File identication. Can be a string, a file tuple or a ParsedFileID * @param bool $strictHashCheck * @return mixed */ private function applyToFileOnFilesystem(callable $callable, ParsedFileID $parsedFileID, $strictHashCheck = true) { $publicSet = [ $this->getPublicFilesystem(), $this->getPublicResolutionStrategy(), self::VISIBILITY_PUBLIC ]; $protectedSet = [ $this->getProtectedFilesystem(), $this->getProtectedResolutionStrategy(), self::VISIBILITY_PROTECTED ]; /** @var FileHashingService $hasher */ $hasher = Injector::inst()->get(FileHashingService::class); /** @var Filesystem $fs */ /** @var FileResolutionStrategy $strategy */ /** @var string $visibility */ // First we try to search for exact file id string match foreach ([$publicSet, $protectedSet] as $set) { list($fs, $strategy, $visibility) = $set; // Get a FileID string based on the type of FileID $fileID = $strategy->buildFileID($parsedFileID); if ($fs->has($fileID)) { // Let's try validating the hash of our file if ($parsedFileID->getHash()) { $mainFileID = $strategy->buildFileID($strategy->stripVariant($parsedFileID)); if (!$fs->has($mainFileID)) { // The main file doesn't exists ... this is kind of weird. continue; } $actualHash = $hasher->computeFromFile($mainFileID, $fs); if (!$hasher->compare($actualHash, $parsedFileID->getHash())) { continue; } } // We already have a ParsedFileID, we just need to set the matching file ID string $closesureParsedFileID = $parsedFileID->setFileID($fileID); $response = $callable( $closesureParsedFileID, $fs, $strategy, $visibility ); if ($response !== false) { return $response; } } } // Let's fall back to using our FileResolution strategy to see if our FileID matches alternative formats foreach ([$publicSet, $protectedSet] as $set) { list($fs, $strategy, $visibility) = $set; $closesureParsedFileID = $strategy->searchForTuple($parsedFileID, $fs, $strictHashCheck); if ($closesureParsedFileID) { $response = $callable($closesureParsedFileID, $fs, $strategy, $visibility); if ($response !== false) { return $response; } } } return null; } /** * Equivalent to `applyToFileOnFilesystem`, only it expects a `fileID1 string instead of a ParsedFileID. * * @param callable $callable Action to apply. * @param string $fileID * @param bool $strictHashCheck * @return mixed */ private function applyToFileIDOnFilesystem(callable $callable, $fileID, $strictHashCheck = true) { $publicSet = [ $this->getPublicFilesystem(), $this->getPublicResolutionStrategy(), self::VISIBILITY_PUBLIC ]; $protectedSet = [ $this->getProtectedFilesystem(), $this->getProtectedResolutionStrategy(), self::VISIBILITY_PROTECTED ]; /** @var Filesystem $fs */ /** @var FileResolutionStrategy $strategy */ /** @var string $visibility */ // First we try to search for exact file id string match foreach ([$publicSet, $protectedSet] as $set) { list($fs, $strategy, $visibility) = $set; if ($fs->has($fileID)) { $parsedFileID = $strategy->resolveFileID($fileID, $fs); if ($parsedFileID) { $response = $callable( $parsedFileID, $fs, $strategy, $visibility ); if ($response !== false) { return $response; } } } } // Let's fall back to using our FileResolution strategy to see if our FileID matches alternative formats foreach ([$publicSet, $protectedSet] as $set) { list($fs, $strategy, $visibility) = $set; $parsedFileID = $strategy->resolveFileID($fileID, $fs); if ($parsedFileID) { $response = $callable($parsedFileID, $fs, $strategy, $visibility); if ($response !== false) { return $response; } } } return null; } public function getCapabilities() { return [ 'visibility' => [ self::VISIBILITY_PUBLIC, self::VISIBILITY_PROTECTED ], 'conflict' => [ self::CONFLICT_EXCEPTION, self::CONFLICT_OVERWRITE, self::CONFLICT_RENAME, self::CONFLICT_USE_EXISTING ] ]; } public function getVisibility($filename, $hash) { return $this->applyToFileOnFilesystem( function (ParsedFileID $parsedFileID, Filesystem $fs, FileResolutionStrategy $strategy, $visibility) { return $visibility; }, new ParsedFileID($filename, $hash) ); } public function getAsStream($filename, $hash, $variant = null) { return $this->applyToFileOnFilesystem( function (ParsedFileID $parsedFileID, FileSystem $fs, FileResolutionStrategy $strategy, $visibility) { return $fs->readStream($parsedFileID->getFileID()); }, new ParsedFileID($filename, $hash, $variant) ); } public function getAsString($filename, $hash, $variant = null) { return $this->applyToFileOnFilesystem( function (ParsedFileID $parsedFileID, FileSystem $fs, FileResolutionStrategy $strategy, $visibility) { return $fs->read($parsedFileID->getFileID()); }, new ParsedFileID($filename, $hash, $variant) ); } public function getAsURL($filename, $hash, $variant = null, $grant = true) { $tuple = new ParsedFileID($filename, $hash, $variant); // Check with filesystem this asset exists in $public = $this->getPublicFilesystem(); $protected = $this->getProtectedFilesystem(); if ($parsedFileID = $this->getPublicResolutionStrategy()->searchForTuple($tuple, $public)) { /** @var PublicAdapter $publicAdapter */ $publicAdapter = $public->getAdapter(); return $publicAdapter->getPublicUrl($parsedFileID->getFileID()); } if ($parsedFileID = $this->getProtectedResolutionStrategy()->searchForTuple($tuple, $protected)) { if ($grant) { $this->grant($parsedFileID->getFilename(), $parsedFileID->getHash()); } /** @var ProtectedAdapter $protectedAdapter */ $protectedAdapter = $protected->getAdapter(); return $protectedAdapter->getProtectedUrl($parsedFileID->getFileID()); } $fileID = $this->getPublicResolutionStrategy()->buildFileID($tuple); /** @var PublicAdapter $publicAdapter */ $publicAdapter = $public->getAdapter(); return $publicAdapter->getPublicUrl($fileID); } public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $config = []) { // Validate this file exists if (!file_exists($path)) { throw new InvalidArgumentException("$path does not exist"); } // Get filename to save to if (empty($filename)) { $filename = basename($path); } $stream = fopen($path, 'r'); if ($stream === false) { throw new InvalidArgumentException("$path could not be opened for reading"); } try { return $this->setFromStream($stream, $filename, $hash, $variant, $config); } finally { if (is_resource($stream)) { fclose($stream); } } } public function setFromString($data, $filename, $hash = null, $variant = null, $config = []) { $stream = fopen('php://temp', 'r+'); fwrite($stream, $data); rewind($stream); try { return $this->setFromStream($stream, $filename, $hash, $variant, $config); } finally { if (is_resource($stream)) { fclose($stream); } } } public function setFromStream($stream, $filename, $hash = null, $variant = null, $config = []) { if (empty($filename)) { throw new InvalidArgumentException('$filename can not be empty'); } // If the stream isn't rewindable, write to a temporary filename if (!$this->isSeekableStream($stream)) { $path = $this->getStreamAsFile($stream); $result = $this->setFromLocalFile($path, $filename, $hash, $variant, $config); unlink($path); return $result; } /** @var FileHashingService $hasher */ $hasher = Injector::inst()->get(FileHashingService::class); // When saving original filename, generate hash if (!$hash && !$variant) { $hash = $hasher->computeFromStream($stream); } // Callback for saving content $callback = function (Filesystem $filesystem, $fileID) use ($stream, $hasher, $hash, $variant) { // If there's already a file where we want to write and that file has the same sha1 hash as our source file // We just let the existing file sit there pretend to have writen it. This avoid a weird edge case where // We try to move an existing file to its own location which causes us to override the file with zero bytes if ($filesystem->has($fileID)) { $newHash = $hasher->computeFromStream($stream); $oldHash = $hasher->computeFromFile($fileID, $filesystem); if ($newHash === $oldHash) { return true; } } $result = $filesystem->putStream($fileID, $stream); // If we have an hash for a main file, let's pre-warm our file hashing cache. if ($hash || !$variant) { $hasher->set($fileID, $filesystem, $hash); } return $result; }; // Submit to conflict check return $this->writeWithCallback($callback, $filename, $hash, $variant, $config); } public function delete($filename, $hash) { $response = false; $this->applyToFileOnFilesystem( function (ParsedFileID $pfid, Filesystem $fs, FileResolutionStrategy $strategy) use (&$response) { $response = $this->deleteFromFileStore($pfid, $fs, $strategy) || $response; return false; }, new ParsedFileID($filename, $hash) ); return $response; } public function rename($filename, $hash, $newName) { if (empty($newName)) { throw new InvalidArgumentException("Cannot write to empty filename"); } if ($newName === $filename) { return $filename; } return $this->applyToFileOnFilesystem( function (ParsedFileID $parsedFileID, Filesystem $fs, FileResolutionStrategy $strategy) use ($newName) { $destParsedFileID = $parsedFileID->setFilename($newName); // Move all variants around foreach ($strategy->findVariants($parsedFileID, $fs) as $originParsedFileID) { $origin = $originParsedFileID->getFileID(); $destination = $strategy->buildFileID( $destParsedFileID->setVariant($originParsedFileID->getVariant()) ); /** @var FileHashingService $hasher */ $hasher = Injector::inst()->get(FileHashingService::class); if ($origin !== $destination) { if ($fs->has($destination)) { $fs->delete($origin); // Invalidate hash of delete file $hasher->invalidate($origin, $fs); } else { $fs->rename($origin, $destination); // Move cached hash value to new location $hasher->move($origin, $fs, $destination); } $this->truncateDirectory(dirname($origin), $fs); } } // Build and parsed non-variant file ID so we can figure out what the new name file name is $cleanFilename = $strategy->parseFileID( $strategy->buildFileID($destParsedFileID) )->getFilename(); return $cleanFilename; }, new ParsedFileID($filename, $hash) ); } public function copy($filename, $hash, $newName) { if (empty($newName)) { throw new InvalidArgumentException("Cannot write to empty filename"); } if ($newName === $filename) { return $filename; } /** @var ParsedFileID $newParsedFiledID */ $newParsedFiledID = $newParsedFiledID = $this->applyToFileOnFilesystem( function (ParsedFileID $pfid, Filesystem $fs, FileResolutionStrategy $strategy) use ($newName) { $newName = $strategy->cleanFilename($newName); foreach ($strategy->findVariants($pfid, $fs) as $variantParsedFileID) { $fromFileID = $variantParsedFileID->getFileID(); $toFileID = $strategy->buildFileID($variantParsedFileID->setFilename($newName)); if ($fromFileID !== $toFileID) { if (!$fs->has($toFileID)) { $fs->copy($fromFileID, $toFileID); // Set hash value for new file /** @var FileHashingService $hasher */ $hasher = Injector::inst()->get(FileHashingService::class); if ($hash = $hasher->get($fromFileID, $fs)) { $hasher->set($toFileID, $fs, $hash); } } } } return $pfid->setFilename($newName); }, new ParsedFileID($filename, $hash) ); return $newParsedFiledID ? $newParsedFiledID->getFilename(): null; } /** * Delete the given file (and any variants) in the given {@see Filesystem} * * @param string $fileID * @param Filesystem $filesystem * @return bool True if a file was deleted * @deprecated 1.4.0 */ protected function deleteFromFilesystem($fileID, Filesystem $filesystem) { $deleted = false; foreach ($this->findVariants($fileID, $filesystem) as $nextID) { $filesystem->delete($nextID); $deleted = true; } return $deleted; } /** * Delete the given file (and any variants) in the given {@see Filesystem} * @param ParsedFileID $parsedFileID * @param Filesystem $filesystem * @param FileResolutionStrategy $strategy * @return bool */ protected function deleteFromFileStore(ParsedFileID $parsedFileID, Filesystem $fs, FileResolutionStrategy $strategy) { /** @var FileHashingService $hasher */ $hasher = Injector::inst()->get(FileHashingService::class); $deleted = false; /** @var ParsedFileID $parsedFileIDToDel */ foreach ($strategy->findVariants($parsedFileID, $fs) as $parsedFileIDToDel) { $fs->delete($parsedFileIDToDel->getFileID()); $deleted = true; $hasher->invalidate($parsedFileIDToDel->getFileID(), $fs); } // Truncate empty dirs $this->truncateDirectory(dirname($parsedFileID->getFileID()), $fs); return $deleted; } /** * Clear directory if it's empty * * @param string $dirname Name of directory * @param Filesystem $filesystem */ protected function truncateDirectory($dirname, Filesystem $filesystem) { if ($dirname && ltrim($dirname, '.') && !$this->config()->get('keep_empty_dirs') && !$filesystem->listContents($dirname) ) { $filesystem->deleteDir($dirname); $this->truncateDirectory(dirname($dirname), $filesystem); } } /** * Returns an iterable {@see Generator} of all files / variants for the given $fileID in the given $filesystem * This includes the empty (no) variant. * * @param string $fileID ID of original file to compare with. * @param Filesystem $filesystem * @return Generator */ protected function findVariants($fileID, Filesystem $filesystem) { $dirname = ltrim(dirname($fileID), '.'); foreach ($filesystem->listContents($dirname) as $next) { if ($next['type'] !== 'file') { continue; } $nextID = $next['path']; // Compare given file to target, omitting variant if ($fileID === $this->removeVariant($nextID)) { yield $nextID; } } } public function publish($filename, $hash) { if ($this->getVisibility($filename, $hash) === AssetStore::VISIBILITY_PUBLIC) { // The file is already publish return; } $parsedFileID = new ParsedFileID($filename, $hash); $protected = $this->getProtectedFilesystem(); $public = $this->getPublicFilesystem(); $this->moveBetweenFileStore( $parsedFileID, $protected, $this->getProtectedResolutionStrategy(), $public, $this->getPublicResolutionStrategy() ); } /** * Similar to publish, only any existing files that would be overriden by publishing will be moved back to the * protected store. * @param $filename * @param $hash */ public function swapPublish($filename, $hash) { if ($this->getVisibility($filename, $hash) === AssetStore::VISIBILITY_PUBLIC) { // The file is already publish return; } /** @var FileHashingService $hasher */ $hasher = Injector::inst()->get(FileHashingService::class); $parsedFileID = new ParsedFileID($filename, $hash); $from = $this->getProtectedFilesystem(); $to = $this->getPublicFilesystem(); $fromStrategy = $this->getProtectedResolutionStrategy(); $toStrategy = $this->getPublicResolutionStrategy(); // Contain a list of temporary file that needs to be move to the $from store once we are done. // Look for files that might be overriden by publishing to destination store, those need to be stashed away /** @var ParsedFileID $variantParsedFileID */ $swapFileIDStr = $toStrategy->buildFileID($parsedFileID); $swapFiles = []; if ($to->has($swapFileIDStr)) { $swapParsedFileID = $toStrategy->resolveFileID($swapFileIDStr, $to); foreach ($toStrategy->findVariants($swapParsedFileID, $to) as $variantParsedFileID) { $toFileID = $variantParsedFileID->getFileID(); $fromFileID = $fromStrategy->buildFileID($variantParsedFileID); // Cache destination file into the origin store under a `.swap` directory $stream = $to->readStream($toFileID); $from->putStream('.swap/' . $fromFileID, $stream); if (is_resource($stream)) { fclose($stream); } $swapFiles[] = $variantParsedFileID->setFileID($fromFileID); // Blast existing variants from the destination $to->delete($toFileID); $hasher->move($toFileID, $to, '.swap/' . $fromFileID, $from); $this->truncateDirectory(dirname($toFileID), $to); } } // Let's find all the variants on the origin store ... those need to be moved to the destination /** @var ParsedFileID $variantParsedFileID */ foreach ($fromStrategy->findVariants($parsedFileID, $from) as $variantParsedFileID) { // Copy via stream $fromFileID = $variantParsedFileID->getFileID(); $toFileID = $toStrategy->buildFileID($variantParsedFileID); $stream = $from->readStream($fromFileID); $to->putStream($toFileID, $stream); if (is_resource($stream)) { fclose($stream); } // Remove the origin file and keep the file ID $from->delete($fromFileID); $hasher->move($fromFileID, $from, $toFileID, $to); $this->truncateDirectory(dirname($fromFileID), $from); } foreach ($swapFiles as $variantParsedFileID) { $fileID = $variantParsedFileID->getFileID(); $from->rename('.swap/' . $fileID, $fileID); $hasher->move('.swap/' . $fileID, $from, $fileID); } $from->deleteDir('.swap'); } public function protect($filename, $hash) { if ($this->getVisibility($filename, $hash) === AssetStore::VISIBILITY_PROTECTED) { // The file is already protected return; } $parsedFileID = new ParsedFileID($filename, $hash); $protected = $this->getProtectedFilesystem(); $public = $this->getPublicFilesystem(); $expectedPublicFileID = $this->getPublicResolutionStrategy()->buildFileID($parsedFileID); $this->moveBetweenFileStore( $parsedFileID, $public, $this->getPublicResolutionStrategy(), $protected, $this->getProtectedResolutionStrategy() ); } /** * Move a file (and its associative variants) between filesystems * * @param string $fileID * @param Filesystem $from * @param Filesystem $to * @deprecated 1.4.0 */ protected function moveBetweenFilesystems($fileID, Filesystem $from, Filesystem $to) { foreach ($this->findVariants($fileID, $from) as $nextID) { // Copy via stream $stream = $from->readStream($nextID); $to->putStream($nextID, $stream); if (is_resource($stream)) { fclose($stream); } $from->delete($nextID); } // Truncate empty dirs $this->truncateDirectory(dirname($fileID), $from); } /** * Move a file and its associated variant from one file store to another adjusting the file name format. * @param ParsedFileID $parsedFileID * @param Filesystem $from * @param FileResolutionStrategy $fromStrategy * @param Filesystem $to * @param FileResolutionStrategy $toStrategy */ protected function moveBetweenFileStore( ParsedFileID $parsedFileID, Filesystem $from, FileResolutionStrategy $fromStrategy, Filesystem $to, FileResolutionStrategy $toStrategy, $swap = false ) { /** @var FileHashingService $hasher */ $hasher = Injector::inst()->get(FileHashingService::class); // Let's find all the variants on the origin store ... those need to be moved to the destination /** @var ParsedFileID $variantParsedFileID */ foreach ($fromStrategy->findVariants($parsedFileID, $from) as $variantParsedFileID) { // Copy via stream $fromFileID = $variantParsedFileID->getFileID(); $toFileID = $toStrategy->buildFileID($variantParsedFileID); $stream = $from->readStream($fromFileID); $to->putStream($toFileID, $stream); if (is_resource($stream)) { fclose($stream); } // Remove the origin file and keep the file ID $idsToDelete[] = $fromFileID; $from->delete($fromFileID); $hasher->move($fromFileID, $from, $toFileID, $to); $this->truncateDirectory(dirname($fromFileID), $from); } } public function grant($filename, $hash) { $fileID = $this->getFileID($filename, $hash); $session = Controller::curr()->getRequest()->getSession(); $granted = $session->get(self::GRANTS_SESSION) ?: []; $granted[$fileID] = true; $session->set(self::GRANTS_SESSION, $granted); } public function revoke($filename, $hash) { $fileID = $this->getFileID($filename, $hash); if (!$fileID) { $fileID = $this->getProtectedResolutionStrategy()->buildFileID(new ParsedFileID($filename, $hash)); } $session = Controller::curr()->getRequest()->getSession(); $granted = $session->get(self::GRANTS_SESSION) ?: []; unset($granted[$fileID]); if ($granted) { $session->set(self::GRANTS_SESSION, $granted); } else { $session->clear(self::GRANTS_SESSION); } } public function canView($filename, $hash) { $canView = $this->applyToFileOnFilesystem( function (ParsedFileID $parsedFileID, Filesystem $fs, FileResolutionStrategy $strategy, $visibility) { if ($visibility === AssetStore::VISIBILITY_PROTECTED) { // Can't return false directly otherwise applyToFileOnFilesystem will keep looking return $this->isGranted($parsedFileID) ?: null; } return true; }, new ParsedFileID($filename, $hash) ); return $canView === true; } /** * Determine if a grant exists for the given FileID * * @param string|ParsedFileID $fileID * @return bool */ protected function isGranted($fileID) { // Since permissions are applied to the non-variant only, // map back to the original file before checking $parsedFileID = $this->getProtectedResolutionStrategy()->stripVariant($fileID); // Make sure our File ID got understood if ($parsedFileID && $originalID = $parsedFileID->getFileID()) { $session = Controller::curr()->getRequest()->getSession(); $granted = $session->get(self::GRANTS_SESSION) ?: []; if (!empty($granted[$originalID])) { return true; } if ($member = Security::getCurrentUser()) { $params = ['FileFilename' => $parsedFileID->getFilename()]; if (File::singleton()->hasExtension(Versioned::class)) { $file = Versioned::withVersionedMode(function () use ($params) { Versioned::set_stage(Versioned::DRAFT); return File::get()->filter($params)->first(); }); } else { $file = File::get()->filter($params)->first(); } if ($file) { return (bool) $file->canView($member); } } return false; } // Our file ID didn't make sense return false; } /** * get sha1 hash from stream * * @param resource $stream * @return string str1 hash * @deprecated 4.4.0 Use FileHashingService::computeFromStream() instead */ protected function getStreamSHA1($stream) { return Injector::inst() ->get(FileHashingService::class) ->computeFromStream($stream); } /** * Get stream as a file * * @param resource $stream * @return string Filename of resulting stream content * @throws FlysystemException */ protected function getStreamAsFile($stream) { // Get temporary file and name $file = tempnam(sys_get_temp_dir(), 'ssflysystem'); $buffer = fopen($file, 'w'); if (!$buffer) { throw new FlysystemException("Could not create temporary file"); } // Transfer from given stream Util::rewindStream($stream); stream_copy_to_stream($stream, $buffer); if (!fclose($buffer)) { throw new FlysystemException("Could not write stream to temporary file"); } return $file; } /** * Determine if this stream is seekable * * @param resource $stream * @return bool True if this stream is seekable */ protected function isSeekableStream($stream) { return Util::isSeekableStream($stream); } /** * Invokes the conflict resolution scheme on the given content, and invokes a callback if * the storage request is approved. * * @param callable $callback Will be invoked and passed a fileID if the file should be stored * @param string $filename Name for the resulting file * @param string $hash SHA1 of the original file content * @param string $variant Variant to write * @param array $config Write options. {@see AssetStore} * @return array Tuple associative array (Filename, Hash, Variant) * @throws FlysystemException */ protected function writeWithCallback($callback, $filename, $hash, $variant = null, $config = []) { // Set default conflict resolution $conflictResolution = empty($config['conflict']) ? $this->getDefaultConflictResolution($variant) : $config['conflict']; // Validate parameters if ($variant && $conflictResolution === AssetStore::CONFLICT_RENAME) { // As variants must follow predictable naming rules, they should not be dynamically renamed throw new InvalidArgumentException("Rename cannot be used when writing variants"); } if (!$filename) { throw new InvalidArgumentException("Filename is missing"); } if (!$hash) { throw new InvalidArgumentException("File hash is missing"); } $parsedFileID = new ParsedFileID($filename, $hash, $variant); $fsObjs = $this->applyToFileOnFilesystem( function ( ParsedFileID $noVariantParsedFileID, Filesystem $fs, FileResolutionStrategy $strategy, $visibility ) use ($parsedFileID) { $parsedFileID = $strategy->generateVariantFileID($parsedFileID, $fs); if ($parsedFileID) { return [$parsedFileID, $fs, $strategy, $visibility]; } // Keep looking return false; }, $parsedFileID->setVariant('') ); if ($fsObjs) { list($parsedFileID, $fs, $strategy, $visibility) = $fsObjs; $targetFileID = $parsedFileID->getFileID(); } else { if (isset($config['visibility']) && $config['visibility'] === self::VISIBILITY_PUBLIC) { $fs = $this->getPublicFilesystem(); $strategy = $this->getPublicResolutionStrategy(); $visibility = self::VISIBILITY_PUBLIC; } else { $fs = $this->getProtectedFilesystem(); $strategy = $this->getProtectedResolutionStrategy(); $visibility = self::VISIBILITY_PROTECTED; } $targetFileID = $strategy->buildFileID($parsedFileID); } // If overwrite is requested, simply put if ($conflictResolution === AssetStore::CONFLICT_OVERWRITE || !$fs->has($targetFileID)) { $parsedFileID = $parsedFileID->setFileID($targetFileID); } elseif ($conflictResolution === static::CONFLICT_EXCEPTION) { throw new InvalidArgumentException("File already exists at path {$targetFileID}"); } elseif ($conflictResolution === static::CONFLICT_RENAME) { foreach ($this->fileGeneratorFor($targetFileID) as $candidate) { if (!$fs->has($candidate)) { $parsedFileID = $strategy->parseFileID($candidate)->setHash($hash); break; } } } else { // Use exists file if (empty($variant)) { // If deferring to the existing file, return the sha of the existing file, // unless we are writing a variant (which has the same hash value as its original file) /** @var FileHashingService $hasher */ $hasher = Injector::inst()->get(FileHashingService::class); $hash = $hasher->computeFromFile($targetFileID, $fs); $parsedFileID = $parsedFileID->setHash($hash); } return $parsedFileID->getTuple(); } // Submit and validate result $result = $callback($fs, $parsedFileID->getFileID()); if (!$result) { throw new FlysystemException("Could not save {$filename}"); } return $parsedFileID->getTuple(); } /** * Choose a default conflict resolution * * @param string $variant * @return string */ protected function getDefaultConflictResolution($variant) { return AssetStore::CONFLICT_OVERWRITE; } /** * Determine if legacy filenames should be used. This no longuer makes any difference with the introduction of * FileResolutionStrategies. * @deprecated 1.4.0 * @return bool */ protected function useLegacyFilenames() { return $this->config()->get('legacy_filenames'); } public function getMetadata($filename, $hash, $variant = null) { // If `applyToFileOnFilesystem` calls our closure we'll know for sure that a file exists return $this->applyToFileOnFilesystem( function (ParsedFileID $parsedFileID, Filesystem $fs) { return $fs->getMetadata($parsedFileID->getFileID()); }, new ParsedFileID($filename, $hash, $variant) ); } public function getMimeType($filename, $hash, $variant = null) { // If `applyToFileOnFilesystem` calls our closure we'll know for sure that a file exists return $this->applyToFileOnFilesystem( function (ParsedFileID $parsedFileID, Filesystem $fs) { return $fs->getMimetype($parsedFileID->getFileID()); }, new ParsedFileID($filename, $hash, $variant) ); } public function exists($filename, $hash, $variant = null) { if (empty($filename) || empty($hash)) { return false; } // If `applyToFileOnFilesystem` calls our closure we'll know for sure that a file exists return $this->applyToFileOnFilesystem( function (ParsedFileID $parsedFileID, Filesystem $fs, FileResolutionStrategy $strategy) use ($hash) { $parsedFileID = $strategy->stripVariant($parsedFileID); if ($parsedFileID && $originalFileID = $parsedFileID->getFileID()) { if ($fs->has($originalFileID)) { /** @var FileHashingService $hasher */ $hasher = Injector::inst()->get(FileHashingService::class); $actualHash = $hasher->computeFromFile($originalFileID, $fs); // If the hash of the file doesn't match we return false, because we want to keep looking. return $hasher->compare($actualHash, $hash); } } return false; }, new ParsedFileID($filename, $hash, $variant) ) ?: false; } /** * Determine the path that should be written to, given the conflict resolution scheme * * @param string $conflictResolution * @param string $fileID * @return string|false Safe filename to write to. If false, then don't write, and use existing file. * @throws InvalidArgumentException */ protected function resolveConflicts($conflictResolution, $fileID) { // If overwrite is requested, simply put if ($conflictResolution === AssetStore::CONFLICT_OVERWRITE) { return $fileID; } // Otherwise, check if this exists $exists = $this->applyToFileIDOnFilesystem(function () { return true; }, $fileID); if (!$exists) { return $fileID; } // Flysystem defaults to use_existing switch ($conflictResolution) { // Throw tantrum case static::CONFLICT_EXCEPTION: { throw new InvalidArgumentException("File already exists at path {$fileID}"); } // Rename case static::CONFLICT_RENAME: { foreach ($this->fileGeneratorFor($fileID) as $candidate) { $exists = $this->applyToFileIDOnFilesystem(function () { return true; }, $candidate); if (!$exists) { return $candidate; } } throw new InvalidArgumentException("File could not be renamed with path {$fileID}"); } // Use existing file case static::CONFLICT_USE_EXISTING: default: { return false; } } } /** * Get an asset renamer for the given filename. * * @param string $fileID Adapter specific identifier for this file/version * @return AssetNameGenerator */ protected function fileGeneratorFor($fileID) { return Injector::inst()->createWithArgs(AssetNameGenerator::class, [$fileID]); } /** * Performs filename cleanup before sending it back. * * This name should not contain hash or variants. * * @param string $filename * @return string * @deprecated 1.4.0 */ protected function cleanFilename($filename) { /** @var FileIDHelper $helper */ $helper = Injector::inst()->get(HashFileIDHelper::class); return $helper->cleanFilename($filename); } /** * Get Filename and Variant from FileID * * @param string $fileID * @return array * @deprecated 1.4.0 */ protected function parseFileID($fileID) { /** @var ParsedFileID $parsedFileID */ $parsedFileID = $this->getProtectedResolutionStrategy()->parseFileID($fileID); return $parsedFileID ? $parsedFileID->getTuple() : null; } /** * Given a FileID, map this back to the original filename, trimming variant and hash * * @param string $fileID Adapter specific identifier for this file/version * @return string Filename for this file, omitting hash and variant * @deprecated 1.4.0 */ protected function getOriginalFilename($fileID) { $parsedFiledID = $this->getPublicResolutionStrategy()->parseFileID($fileID); return $parsedFiledID ? $parsedFiledID->getFilename() : null; } /** * Get variant from this file * * @param string $fileID * @return string * @deprecated 1.4.0 */ protected function getVariant($fileID) { $parsedFiledID = $this->getPublicResolutionStrategy()->parseFileID($fileID); return $parsedFiledID ? $parsedFiledID->getVariant() : null; } /** * Remove variant from a fileID * * @param string $fileID * @return string FileID without variant * @deprecated */ protected function removeVariant($fileID) { $parsedFiledID = $this->getPublicResolutionStrategy()->parseFileID($fileID); if ($parsedFiledID) { return $this->getPublicResolutionStrategy()->buildFileID($parsedFiledID->setVariant('')); } return $fileID; } /** * Map file tuple (hash, name, variant) to a filename to be used by flysystem * * The resulting file will look something like my/directory/EA775CB4D4/filename__variant.jpg * * @param string $filename Name of file * @param string $hash Hash of original file * @param string $variant (if given) * @return string Adapter specific identifier for this file/version */ protected function getFileID($filename, $hash, $variant = null) { $parsedFileID = new ParsedFileID($filename, $hash, $variant); $fileID = $this->applyToFileOnFilesystem( function (ParsedFileID $parsedFileID) { return $parsedFileID->getFileID(); }, $parsedFileID ); // We couldn't find a file matching the requested critera if (!$fileID) { // Default to using the file ID format of the protected store $fileID = $this->getProtectedResolutionStrategy()->buildFileID($parsedFileID); } return $fileID; } /** * Ensure each adapter re-generates its own server configuration files */ public static function flush() { // Ensure that this instance is constructed on flush, thus forcing // bootstrapping of necessary .htaccess / web.config files $instance = singleton(AssetStore::class); if ($instance instanceof FlysystemAssetStore) { $public = $instance->getPublicFilesystem(); if ($public instanceof Filesystem) { $publicAdapter = $public->getAdapter(); if ($publicAdapter instanceof AssetAdapter) { $publicAdapter->flush(); } } $protected = $instance->getProtectedFilesystem(); if ($protected instanceof Filesystem) { $protectedAdapter = $protected->getAdapter(); if ($protectedAdapter instanceof AssetAdapter) { $protectedAdapter->flush(); } } } } public function getResponseFor($asset) { /** @var HTTPResponse $response */ /** @var array $context */ [$response, $context] = $this->generateResponseFor($asset); // Give a chance to extensions to tweak the response $this->extend('updateResponse', $response, $asset, $context); return $response; } /** * Build a response for getResponseFor along with some context information for the `updateResponse` hook. * @param string $asset * @return array HTTPResponse and some surronding context */ private function generateResponseFor(string $asset): array { $public = $this->getPublicFilesystem(); $protected = $this->getProtectedFilesystem(); $publicStrategy = $this->getPublicResolutionStrategy(); $protectedStrategy = $this->getPublicResolutionStrategy(); // If the file exists on the public store, we just straight return it. if ($public->has($asset)) { return [ $this->createResponseFor($public, $asset), ['visibility' => self::VISIBILITY_PUBLIC] ]; } // If the file exists in the protected store and the user has been explicitely granted access to it if ($protected->has($asset) && $this->isGranted($asset)) { $parsedFileID = $protectedStrategy->resolveFileID($asset, $protected); if ($this->canView($parsedFileID->getFilename(), $parsedFileID->getHash())) { return [ $this->createResponseFor($protected, $asset), ['visibility' => self::VISIBILITY_PROTECTED, 'parsedFileID' => $parsedFileID] ]; } // Let's not deny if the file is in the protected store, but is not granted. // We might be able to redirect to a live version. } // Check if we can find a URL to redirect to if ($parsedFileID = $publicStrategy->softResolveFileID($asset, $public)) { $redirectFileID = $parsedFileID->getFileID(); $permanentFileID = $publicStrategy->buildFileID($parsedFileID); // If our redirect FileID is equal to the permanent file ID, this URL will never change $code = $redirectFileID === $permanentFileID ? $this->config()->get('permanent_redirect_response_code') : $this->config()->get('redirect_response_code'); return [ $this->createRedirectResponse($redirectFileID, $code), ['visibility' => self::VISIBILITY_PUBLIC, 'parsedFileID' => $parsedFileID] ]; } // Deny if file is protected and denied if ($protected->has($asset)) { return [ $this->createDeniedResponse(), ['visibility' => self::VISIBILITY_PROTECTED] ]; } // We've looked everywhere and couldn't find a file return [$this->createMissingResponse(), []]; } /** * Generate an {@see HTTPResponse} for the given file from the source filesystem * @param Filesystem $flysystem * @param string $fileID * @return HTTPResponse */ protected function createResponseFor(Filesystem $flysystem, $fileID) { // Block directory access if ($flysystem->get($fileID) instanceof Directory) { return $this->createDeniedResponse(); } // Create streamable response $stream = $flysystem->readStream($fileID); $size = $flysystem->getSize($fileID); $mime = $flysystem->getMimetype($fileID); $response = HTTPStreamResponse::create($stream, $size) ->addHeader('Content-Type', $mime); // Add standard headers $headers = $this->config()->get('file_response_headers'); foreach ($headers as $header => $value) { $response->addHeader($header, $value); } return $response; } /** * Redirect browser to specified file ID on the public store. Assumes an existence check for the fileID has * already occured. * @note This was introduced as a patch and will be rewritten/remove in SS4.4. * @param string $fileID * @param int $code * @return HTTPResponse */ private function createRedirectResponse($fileID, $code) { $response = new HTTPResponse(null, $code); /** @var PublicAdapter $adapter */ $adapter = $this->getPublicFilesystem()->getAdapter(); $response->addHeader('Location', $adapter->getPublicUrl($fileID)); return $response; } /** * Generate a response for requests to a denied protected file * * @return HTTPResponse */ protected function createDeniedResponse() { $code = (int)$this->config()->get('denied_response_code'); return $this->createErrorResponse($code); } /** * Generate a response for missing file requests * * @return HTTPResponse */ protected function createMissingResponse() { $code = (int)$this->config()->get('missing_response_code'); return $this->createErrorResponse($code); } /** * Create a response with the given error code * * @param int $code * @return HTTPResponse */ protected function createErrorResponse($code) { $response = new HTTPResponse('', $code); // Show message in dev if (!Director::isLive()) { $response->setBody($response->getStatusDescription()); } return $response; } public function normalisePath($fileID) { return $this->applyToFileIDOnFilesystem( function (...$args) { return $this->normaliseToDefaultPath(...$args); }, $fileID ); } public function normalise($filename, $hash) { return $this->applyToFileOnFilesystem( function (...$args) { return $this->normaliseToDefaultPath(...$args); }, new ParsedFileID($filename, $hash) ); } /** * Given a parsed file ID, move the matching file and all its variants to the default position as defined by the * provided strategy. * @param ParsedFileID $pfid * @param Filesystem $fs * @param FileResolutionStrategy $strategy * @return array List of new file names with the old name as the key * @throws \League\Flysystem\FileExistsException * @throws \League\Flysystem\FileNotFoundException */ private function normaliseToDefaultPath(ParsedFileID $pfid, Filesystem $fs, FileResolutionStrategy $strategy) { $ops = []; /** @var FileHashingService $hasher */ $hasher = Injector::inst()->get(FileHashingService::class); // Let's make sure we are using a valid file name $cleanFilename = $strategy->cleanFilename($pfid->getFilename()); // Check if our cleaned filename is different from the original filename if ($cleanFilename !== $pfid->getFilename()) { // We need to build a new filename that doesn't conflict with any existing file $fileID = $strategy->buildFileID($pfid->setVariant('')->setFilename($cleanFilename)); if ($fs->has($fileID)) { foreach ($this->fileGeneratorFor($fileID) as $candidate) { if (!$fs->has($candidate)) { $cleanFilename = $strategy->parseFileID($candidate)->getFilename(); break; } } } } // Let's move all the variants foreach ($strategy->findVariants($pfid, $fs) as $variantPfid) { $origin = $variantPfid->getFileID(); $targetVariantFileID = $strategy->buildFileID($variantPfid->setFilename($cleanFilename)); if ($targetVariantFileID !== $origin) { if ($fs->has($targetVariantFileID)) { $fs->delete($origin); $hasher->invalidate($origin, $fs); } else { $fs->rename($origin, $targetVariantFileID); $hasher->move($origin, $fs, $targetVariantFileID); $ops[$origin] = $targetVariantFileID; } $this->truncateDirectory(dirname($origin), $fs); } } // Our strategy will have cleaned up the name $pfid = $pfid->setFilename($cleanFilename); return array_merge($pfid->getTuple(), ['Operations' => $ops]); } } |