Skip to content

Instantly share code, notes, and snippets.

@Tech-Emiretus
Created December 11, 2020 08:03
Show Gist options
  • Select an option

  • Save Tech-Emiretus/abe4d765275b405d6fb16e63c421f975 to your computer and use it in GitHub Desktop.

Select an option

Save Tech-Emiretus/abe4d765275b405d6fb16e63c421f975 to your computer and use it in GitHub Desktop.
FileStorage Wrapper for a SFTP server for a multitenant system.
<?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('&amp;', '&', 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