Created
December 11, 2020 08:03
-
-
Save Tech-Emiretus/abe4d765275b405d6fb16e63c421f975 to your computer and use it in GitHub Desktop.
FileStorage Wrapper for a SFTP server for a multitenant system.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| namespace FileStorage; | |
| use File; | |
| use Config; | |
| use Storage; | |
| use Exception; | |
| use App\Helpers\MimeType; | |
| use InvalidArgumentException; | |
| use Illuminate\Http\File as HttpFile; | |
| use League\Flysystem\FileNotFoundException; | |
| use Symfony\Component\HttpFoundation\StreamedResponse; | |
| class FileStorage | |
| { | |
| /** | |
| * The temp folder root used by the old s3 wrapper. | |
| * | |
| * @static | |
| * @var string | |
| */ | |
| public const TEMP_FOLDER_ROOT = '/s3temp/files/temp'; | |
| /** | |
| * The storage adapter or disk to be used for this object. | |
| * | |
| * @var \Illuminate\Filesystem\FilesystemAdapter | |
| */ | |
| private $storage; | |
| /** | |
| * Root folder to be used for this file transactions. | |
| * It is the company's id. | |
| * | |
| * @var string | |
| */ | |
| public $root; | |
| /** | |
| * The destination or current folder the specified file name | |
| * exists in or will be saved in. | |
| * | |
| * @var string | |
| */ | |
| public $savepath; | |
| /** | |
| * The absolute path of a file to be uploaded to the files server. | |
| * | |
| * @var string | |
| */ | |
| public $sourcepath; | |
| /** | |
| * Is the given source path a path for internal movement. | |
| * | |
| * @var bool | |
| */ | |
| public $is_internal_sourcepath = false; | |
| /** | |
| * The name (including the extension) of the file to be manipulated. | |
| * | |
| * @var string | |
| */ | |
| public $filename; | |
| /** | |
| * The temporary location used by old s3 wrapper to cache files. It is | |
| * needed in some places as a log path. | |
| * | |
| * @var string | |
| */ | |
| public $tempPath; | |
| /** | |
| * Instanciate this class into a new object. | |
| * We use the company id to set the root folder and you can | |
| * use a different disk instead of the default by specifying a disk. | |
| * | |
| * @param int|string $company_id | |
| * @param null|string $disk | |
| * @return void | |
| * | |
| * @throws \InvalidArgumentException | |
| */ | |
| public function __construct($company_id, $disk = null) | |
| { | |
| if ($disk && !Config::get('filesystems.disks.'. $disk)) { | |
| throw new InvalidArgumentException('The specified disk ('. $disk .') is invalid.'); | |
| } | |
| $this->storage = Storage::disk($disk); | |
| if (!$this->exists($company_id)) { | |
| throw new InvalidArgumentException('There is no root folder for the provided company ('. $company_id .').'); | |
| } | |
| $this->root = $company_id; | |
| $this->setTempFolderPath(); | |
| $this->createTempFolder(); | |
| } | |
| /** | |
| * Check if the specified file or folder exists. | |
| * | |
| * @param null|string $path | |
| * @return bool | |
| */ | |
| public function exists($path = null): bool | |
| { | |
| return $this->storage->exists($path ?: $this->getFilePath()); | |
| } | |
| /** | |
| * Get the specified file if it exists. | |
| * This returns the raw binary string of the file. | |
| * | |
| * @param null|string $path | |
| * @return string | |
| */ | |
| public function get($path = null): string | |
| { | |
| $file_path = $path ?: $this->getFilePath(); | |
| if (!$this->storage->exists($file_path)) { | |
| return ''; | |
| } | |
| return $this->storage->get($file_path); | |
| } | |
| /** | |
| * Get the mime type of the specified file. | |
| * | |
| * @param null|string $path | |
| * @return string | |
| * | |
| * @throws \League\Flysystem\FileNotFoundException | |
| */ | |
| public function mimeType($path = null): string | |
| { | |
| $file_path = $path ?: $this->getFilePath(); | |
| if (!$this->storage->exists($file_path)) { | |
| throw new FileNotFoundException($file_path); | |
| } | |
| return $this->storage->mimeType($file_path); | |
| } | |
| /** | |
| * Get the size of the specified file. | |
| * | |
| * @param null|string $path | |
| * @return int | |
| * | |
| * @throws \League\Flysystem\FileNotFoundException | |
| */ | |
| public function size($path = null): int | |
| { | |
| $file_path = $path ?: $this->getFilePath(); | |
| if (!$this->storage->exists($file_path)) { | |
| throw new FileNotFoundException($file_path); | |
| } | |
| return $this->storage->size($file_path); | |
| } | |
| /** | |
| * Get the meta data of the specified file. | |
| * | |
| * @param null|string $path | |
| * @return array | |
| * | |
| * @throws \League\Flysystem\FileNotFoundException | |
| */ | |
| public function metadata($path = null): array | |
| { | |
| $file_path = $path ?: $this->getFilePath(); | |
| if (!$this->storage->exists($file_path)) { | |
| throw new FileNotFoundException($file_path); | |
| } | |
| return $this->storage->getMetadata($file_path); | |
| } | |
| /** | |
| * Display file's content with the appropriate headers. | |
| * For viewing or downloading files. | |
| * | |
| * @param null|string $path | |
| * @return string | |
| */ | |
| public function displayFile($path = null): string | |
| { | |
| $file_path = $path ?: $this->getFilePath(); | |
| if ($file = $this->get($file_path)) { | |
| header('MIME-Version: 1.0'); | |
| header('Content-Description: File Transfer'); | |
| header('Content-Length: '. $this->size($file_path)); | |
| header('Content-Type: '. $this->mimeType($file_path)); | |
| header('Content-Transfer-Encoding: binary'); | |
| return $file; | |
| } | |
| return ''; | |
| } | |
| /** | |
| * This is a custom version of the laravel storage download method. | |
| * This method is suitable for downloading huge files. | |
| * | |
| * @param string|null $filename | |
| * @param array $headers | |
| * @return StreamedResponse | |
| */ | |
| public function download($filename = null, $headers = []): StreamedResponse | |
| { | |
| $response = new StreamedResponse; | |
| $path = $this->getFilePath(); | |
| $filename = $filename ?? basename($path); | |
| $disposition = $response->headers->makeDisposition('attachment', $filename); | |
| $response->headers->replace($headers + [ | |
| 'Content-Disposition' => $disposition, | |
| ]); | |
| $response->setCallback(function () use ($path) { | |
| $stream = $this->storage->readStream($path); | |
| while (!feof($stream)) { | |
| echo fread($stream, 2048); | |
| } | |
| fclose($stream); | |
| }); | |
| return $response; | |
| } | |
| /** | |
| * Upload or store the specified file to the server. | |
| * | |
| * @return null|string|bool | |
| * | |
| * @throws \InvalidArgumentException | |
| */ | |
| public function store() | |
| { | |
| $this->sourcepath = is_null($this->sourcepath) ? '' : $this->sourcepath; | |
| $sourcepath = static::stripUnwantedSlashes($this->sourcepath); | |
| if (!File::isFile($sourcepath)) { | |
| throw new InvalidArgumentException('The soure path ('. $sourcepath .') of the file to be uploaded is invalid.'); | |
| } | |
| return $this->storage->putFileAs( | |
| $this->getFilePath(true), | |
| new HttpFile($sourcepath), | |
| $this->filename | |
| ); | |
| } | |
| /** | |
| * Delete a file from the server. | |
| * | |
| * @param null|string $path | |
| * @return bool | |
| * | |
| * @throws \League\Flysystem\FileNotFoundException|\InvalidArgumentException | |
| */ | |
| public function delete($path = null): bool | |
| { | |
| $file_path = $path ?: $this->getFilePath(); | |
| if (!$this->storage->exists($file_path)) { | |
| throw new FileNotFoundException($file_path); | |
| } | |
| if ($this->isDirectory($file_path)) { | |
| throw new InvalidArgumentException('The specified file path is a directory.'); | |
| } | |
| return $this->storage->delete($file_path); | |
| } | |
| /** | |
| * Delete a directory and all its files from the server. | |
| * If no path is provided, it deletes the path given to the save path property. | |
| * | |
| * @param null|string $path | |
| * @return bool | |
| * | |
| * @throws \League\Flysystem\FileNotFoundException|\InvalidArgumentException|\Exception | |
| */ | |
| public function deleteDirectory($path = null): bool | |
| { | |
| $file_path = $path ?: $this->getFilePath(true); | |
| if (!$this->storage->exists($file_path)) { | |
| throw new FileNotFoundException($file_path); | |
| } | |
| if ($this->isFile($file_path)) { | |
| throw new InvalidArgumentException('The specified file path is a file.'); | |
| } | |
| if ($this->isCompanyRootDirectory($file_path)) { | |
| throw new Exception('The root directory cannot be deleted.'); | |
| } | |
| return $this->storage->deleteDirectory($file_path); | |
| } | |
| /** | |
| * Check if the provided path is a directory. | |
| * | |
| * @param string $path | |
| * @return bool | |
| */ | |
| public function isDirectory(string $path): bool | |
| { | |
| try { | |
| return $this->metadata($path)['type'] === 'dir'; | |
| } catch (Exception $e) { | |
| return false; | |
| } | |
| } | |
| /** | |
| * Check if the provided path is a file. | |
| * | |
| * @param string $path | |
| * @return bool | |
| */ | |
| public function isFile(string $path): bool | |
| { | |
| try { | |
| return $this->metadata($path)['type'] === 'file'; | |
| } catch (Exception $e) { | |
| return false; | |
| } | |
| } | |
| /** | |
| * Is the provided path the root folder. | |
| * | |
| * @param string $path | |
| * @return bool | |
| */ | |
| public function isCompanyRootDirectory(string $path): bool | |
| { | |
| return $file_path === $this->root; | |
| } | |
| /** | |
| * Get the absolute path to the file specified. | |
| * | |
| * @param bool only_folder | |
| * @return string | |
| */ | |
| public function getFilePath(bool $only_folder = false): string | |
| { | |
| $path_components = [$this->root, $this->savepath]; | |
| if (!$only_folder) { | |
| $path_components[] = $this->filename; | |
| } | |
| return static::stripUnwantedSlashes( | |
| implode('/', array_filter($path_components)) | |
| ); | |
| } | |
| /** | |
| * Create the temporary folder for files. | |
| * | |
| * @return bool | |
| */ | |
| public function createTempFolder(): bool | |
| { | |
| return is_dir($this->tempPath) ?: mkdir($this->tempPath, 0755, true); | |
| } | |
| /** | |
| * Delete a temporary file. | |
| * | |
| * @param null|string $path | |
| * @return void | |
| */ | |
| public function deleteTempFile($path = null) | |
| { | |
| $file_path = $path ?: static::stripUnwantedSlashes($this->tempPath .'/'. $this->filename); | |
| if (file_exists($file_path)) { | |
| @unlink($file_path); | |
| } | |
| } | |
| /** | |
| * Check if the source path provided exists. | |
| * | |
| * @return bool | |
| */ | |
| public function sourcePathExists(): bool | |
| { | |
| return $this->is_internal_sourcepath | |
| ? $this->exists($this->getSourcePath()) | |
| : file_exists($this->getSourcePath()); | |
| } | |
| /** | |
| * Copy a file internally from the source path to the save path. | |
| * The source path must be internal. The file can be renamed by providing | |
| * a new file name. | |
| * | |
| * @param null|string $new_file_name | |
| * @return bool | |
| */ | |
| public function copyFileInternally($new_file_name = null): bool | |
| { | |
| $destination = $new_file_name | |
| ? static::stripUnwantedSlashes($this->getFilePath(true) .'/'. $new_file_name) | |
| : $this->getFilePath(); | |
| if (!$this->is_internal_sourcepath) { | |
| throw new InvalidArgumentException('The source path must be an internal path.'); | |
| } | |
| if (!$this->sourcePathExists()) { | |
| throw new FileNotFoundException($this->getSourcePath()); | |
| } | |
| return $this->storage->copy($this->getSourcePath(), $destination); | |
| } | |
| /** | |
| * Move a file internally from the source path to the save path. | |
| * The source path must be internal. The file can be renamed by providing | |
| * a new file name. | |
| * | |
| * @param null|string $new_file_name | |
| * @return bool | |
| */ | |
| public function moveFileInternally($new_file_name = null): bool | |
| { | |
| $destination = $new_file_name | |
| ? static::stripUnwantedSlashes($this->getFilePath(true) .'/'. $new_file_name) | |
| : $this->getFilePath(); | |
| if (!$this->is_internal_sourcepath) { | |
| throw new InvalidArgumentException('The source path must be an internal path.'); | |
| } | |
| if (!$this->sourcePathExists()) { | |
| throw new FileNotFoundException($this->getSourcePath()); | |
| } | |
| return $this->storage->move($this->getSourcePath(), $destination); | |
| } | |
| /** | |
| * Removes the temp folder and all its files completely. | |
| * | |
| * @param string $path | |
| * @return void | |
| */ | |
| public static function removeUserTempFilesAndFolders($path) | |
| { | |
| $path = static::stripUnwantedSlashes($path); | |
| if (is_dir($path)) { | |
| foreach (@scandir($path) as $file) { | |
| $absolute_path = static::stripUnwantedSlashes($path .'/'. $file); | |
| if (in_array($file, ['.', '..'])) { | |
| continue; | |
| } | |
| if (is_dir($absolute_path)) { | |
| static::removeUserTempFilesAndFolders($absolute_path); | |
| } else { | |
| @unlink($absolute_path); | |
| } | |
| } | |
| @rmdir($path); | |
| } | |
| } | |
| /** | |
| * Save the specified file temporary in our set temp path. | |
| * | |
| * @param null|string $path | |
| * @return bool | |
| */ | |
| public function saveFileTemporary($path = null): bool | |
| { | |
| $file_path = $path ?: $this->getFilePath(); | |
| if (!$this->exists($file_path)) { | |
| return false; | |
| } | |
| return file_put_contents( | |
| static::stripUnwantedSlashes($this->tempPath .'/'. $this->filename), | |
| $this->get($file_path) | |
| ) !== false; | |
| } | |
| /** | |
| * Return the full temporary path of the file. | |
| * | |
| * @return string | |
| */ | |
| public function getFileTempPath(): string | |
| { | |
| return static::stripUnwantedSlashes($this->tempPath .'/'. $this->filename); | |
| } | |
| /** | |
| * Search for a file in the root folder. | |
| * This search is done recursively. | |
| * | |
| * @return bool | |
| */ | |
| public function search(): bool | |
| { | |
| $allFiles = $this->storage->allFiles($this->root); | |
| $allDirectories = $this->storage->allDirectories($this->root) ?? []; | |
| // Add the root itself as the first directory to search | |
| array_unshift($allDirectories, $this->root); | |
| foreach ($allDirectories as $directory) { | |
| $file_path = static::stripUnwantedSlashes($directory .'/'. $this->filename); | |
| if (in_array($file_path, $allFiles)) { | |
| $this->savepath = str_replace($this->root, '', $directory); | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /** | |
| * Get the absoluate file path from the root of the sftp | |
| * folder to the current root folder for the file. | |
| * | |
| * @return string | |
| */ | |
| protected function getAbsoluteRootPath(): string | |
| { | |
| $sftp_config = config('filesystems.disks.sftp'); | |
| if (!sftp_config) { | |
| return null; | |
| } | |
| return static::stripUnwantedSlashes($sftp_config['root'] .'/'. $this->root); | |
| } | |
| /** | |
| * Set the temporary folder path. | |
| * | |
| * @return void | |
| */ | |
| protected function setTempFolderPath() | |
| { | |
| $path = static::TEMP_FOLDER_ROOT; | |
| $path .= array_get($_REQUEST, 'prjct') | |
| ? '/'. $this->root .'/'. $_REQUEST['prjct'] | |
| : '/'. $this->root; | |
| $this->tempPath = static::stripUnwantedSlashes($path); | |
| } | |
| /** | |
| * Strip Unwanted slashes from the specified string. | |
| * Replace numerous slashes with a single slash. | |
| * | |
| * @param string $string | |
| * @return string | |
| */ | |
| public static function stripUnwantedSlashes(string $string): string | |
| { | |
| return preg_replace('/(\/){2,}|(\\\){2,}/', '/', $string); | |
| } | |
| /** | |
| * Get the source path of the file to be uploaded. | |
| * | |
| * @return string | |
| */ | |
| protected function getSourcePath(): string | |
| { | |
| $path_components = [$this->sourcepath]; | |
| if ($this->is_internal_sourcepath) { | |
| array_unshift($path_components, $this->root); | |
| } | |
| return static::stripUnwantedSlashes( | |
| implode('/', array_filter($path_components)) | |
| ); | |
| } | |
| /** | |
| * Usually we store our files with the absolute url of the fileviewer. | |
| * Eg. https://.../fileviewer.php?file=signature_1.png&type=signature | |
| * We need to return the file name `signature_1.png` from the url. | |
| * | |
| * @param string $url | |
| * @return string | |
| */ | |
| public static function getFileNameFromUrl(string $url): string | |
| { | |
| $url = str_replace('&', '&', htmlspecialchars_decode($url)); | |
| if (($file_pos = strpos($url, 'file=')) === false) { | |
| return ''; | |
| } | |
| $start_pos = $file_pos + strlen('file='); | |
| // Sometimes `?(timestamp)` is attached to the filename | |
| // to prevent caching. So it can be the end pos. | |
| $end_pos = strpos($url, '?', $start_pos) === false | |
| ? strpos($url, '&', $start_pos) | |
| : strpos($url, '?', $start_pos); | |
| if ($end_pos === false) { | |
| return ''; | |
| } | |
| return substr($url, $start_pos, $end_pos - $start_pos); | |
| } | |
| /** | |
| * Get the file name to be downloaded. It should try and guess | |
| * the file extension if the specified name does not have an extension. | |
| * | |
| * It also replaces all special characters with an underscore | |
| * | |
| * @param string|null $filename | |
| * @return string | |
| */ | |
| public function getDownloadName(string $filename = null): string | |
| { | |
| $filename = $filename ?? $this->filename; | |
| if (!static::hasFileExtension($filename)) { | |
| $ext = $this->getFileExtension(); | |
| $filename .= $ext ? '.'. $ext : ''; | |
| } | |
| return preg_replace('/[^ a-zA-Z0-9.()-]+/', '_', $filename); | |
| } | |
| /** | |
| * Check if the file name has an extension. | |
| * | |
| * @static | |
| * @param string $filename | |
| * @return bool | |
| */ | |
| public static function hasFileExtension(string $filename): bool | |
| { | |
| $file_parts = pathinfo($filename); | |
| return !!array_get($file_parts, 'extension'); | |
| } | |
| /** | |
| * Get the file extension based on the mime type else guess it | |
| * from the system name of the file. | |
| * | |
| * @return string|null | |
| */ | |
| public function getFileExtension(): ?string | |
| { | |
| $ext = MimeType::getExtensionFromMimeType($this->mimeType()); | |
| if (!$ext) { | |
| $file_parts = pathinfo($this->filename); | |
| $ext = array_get($file_parts, 'extension'); | |
| } | |
| return $ext; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment