Initial
This commit is contained in:
46
system/Files/Exceptions/FileException.php
Normal file
46
system/Files/Exceptions/FileException.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Files\Exceptions;
|
||||
|
||||
use CodeIgniter\Exceptions\DebugTraceableTrait;
|
||||
use CodeIgniter\Exceptions\ExceptionInterface;
|
||||
use RuntimeException;
|
||||
|
||||
class FileException extends RuntimeException implements ExceptionInterface
|
||||
{
|
||||
use DebugTraceableTrait;
|
||||
|
||||
public static function forUnableToMove(?string $from = null, ?string $to = null, ?string $error = null)
|
||||
{
|
||||
return new static(lang('Files.cannotMove', [$from, $to, $error]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws when an item is expected to be a directory but is not or is missing.
|
||||
*
|
||||
* @param string $caller The method causing the exception
|
||||
*/
|
||||
public static function forExpectedDirectory(string $caller)
|
||||
{
|
||||
return new static(lang('Files.expectedDirectory', [$caller]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws when an item is expected to be a file but is not or is missing.
|
||||
*
|
||||
* @param string $caller The method causing the exception
|
||||
*/
|
||||
public static function forExpectedFile(string $caller)
|
||||
{
|
||||
return new static(lang('Files.expectedFile', [$caller]));
|
||||
}
|
||||
}
|
||||
26
system/Files/Exceptions/FileNotFoundException.php
Normal file
26
system/Files/Exceptions/FileNotFoundException.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Files\Exceptions;
|
||||
|
||||
use CodeIgniter\Exceptions\DebugTraceableTrait;
|
||||
use CodeIgniter\Exceptions\ExceptionInterface;
|
||||
use RuntimeException;
|
||||
|
||||
class FileNotFoundException extends RuntimeException implements ExceptionInterface
|
||||
{
|
||||
use DebugTraceableTrait;
|
||||
|
||||
public static function forFileNotFound(string $path)
|
||||
{
|
||||
return new static(lang('Files.fileNotFound', [$path]));
|
||||
}
|
||||
}
|
||||
186
system/Files/File.php
Normal file
186
system/Files/File.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Files;
|
||||
|
||||
use CodeIgniter\Files\Exceptions\FileException;
|
||||
use CodeIgniter\Files\Exceptions\FileNotFoundException;
|
||||
use Config\Mimes;
|
||||
use ReturnTypeWillChange;
|
||||
use SplFileInfo;
|
||||
|
||||
/**
|
||||
* Wrapper for PHP's built-in SplFileInfo, with goodies.
|
||||
*/
|
||||
class File extends SplFileInfo
|
||||
{
|
||||
/**
|
||||
* The files size in bytes
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $size;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $originalMimeType;
|
||||
|
||||
/**
|
||||
* Run our SplFileInfo constructor with an optional verification
|
||||
* that the path is really a file.
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function __construct(string $path, bool $checkFile = false)
|
||||
{
|
||||
if ($checkFile && ! is_file($path)) {
|
||||
throw FileNotFoundException::forFileNotFound($path);
|
||||
}
|
||||
|
||||
parent::__construct($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the file size.
|
||||
*
|
||||
* Implementations SHOULD return the value stored in the "size" key of
|
||||
* the file in the $_FILES array if available, as PHP calculates this based
|
||||
* on the actual size transmitted.
|
||||
*
|
||||
* @return false|int The file size in bytes, or false on failure
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function getSize()
|
||||
{
|
||||
return $this->size ?? ($this->size = parent::getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the file size by unit.
|
||||
*
|
||||
* @return false|int|string
|
||||
*/
|
||||
public function getSizeByUnit(string $unit = 'b')
|
||||
{
|
||||
switch (strtolower($unit)) {
|
||||
case 'kb':
|
||||
return number_format($this->getSize() / 1024, 3);
|
||||
|
||||
case 'mb':
|
||||
return number_format(($this->getSize() / 1024) / 1024, 3);
|
||||
|
||||
default:
|
||||
return $this->getSize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to determine the file extension based on the trusted
|
||||
* getType() method. If the mime type is unknown, will return null.
|
||||
*/
|
||||
public function guessExtension(): ?string
|
||||
{
|
||||
return Mimes::guessExtensionFromType($this->getMimeType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the media type of the file. SHOULD not use information from
|
||||
* the $_FILES array, but should use other methods to more accurately
|
||||
* determine the type of file, like finfo, or mime_content_type().
|
||||
*
|
||||
* @return string The media type we determined it to be.
|
||||
*/
|
||||
public function getMimeType(): string
|
||||
{
|
||||
if (! function_exists('finfo_open')) {
|
||||
return $this->originalMimeType ?? 'application/octet-stream'; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mimeType = finfo_file($finfo, $this->getRealPath() ?: $this->__toString());
|
||||
finfo_close($finfo);
|
||||
|
||||
return $mimeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random names based on a simple hash and the time, with
|
||||
* the correct file extension attached.
|
||||
*/
|
||||
public function getRandomName(): string
|
||||
{
|
||||
$extension = $this->getExtension();
|
||||
$extension = empty($extension) ? '' : '.' . $extension;
|
||||
|
||||
return time() . '_' . bin2hex(random_bytes(10)) . $extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a file to a new location.
|
||||
*
|
||||
* @return File
|
||||
*/
|
||||
public function move(string $targetPath, ?string $name = null, bool $overwrite = false)
|
||||
{
|
||||
$targetPath = rtrim($targetPath, '/') . '/';
|
||||
$name = $name ?? $this->getBaseName();
|
||||
$destination = $overwrite ? $targetPath . $name : $this->getDestination($targetPath . $name);
|
||||
|
||||
$oldName = $this->getRealPath() ?: $this->__toString();
|
||||
|
||||
if (! @rename($oldName, $destination)) {
|
||||
$error = error_get_last();
|
||||
|
||||
throw FileException::forUnableToMove($this->getBasename(), $targetPath, strip_tags($error['message']));
|
||||
}
|
||||
|
||||
@chmod($destination, 0777 & ~umask());
|
||||
|
||||
return new self($destination);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the destination path for the move operation where overwriting is not expected.
|
||||
*
|
||||
* First, it checks whether the delimiter is present in the filename, if it is, then it checks whether the
|
||||
* last element is an integer as there may be cases that the delimiter may be present in the filename.
|
||||
* For the all other cases, it appends an integer starting from zero before the file's extension.
|
||||
*/
|
||||
public function getDestination(string $destination, string $delimiter = '_', int $i = 0): string
|
||||
{
|
||||
if ($delimiter === '') {
|
||||
$delimiter = '_';
|
||||
}
|
||||
|
||||
while (is_file($destination)) {
|
||||
$info = pathinfo($destination);
|
||||
$extension = isset($info['extension']) ? '.' . $info['extension'] : '';
|
||||
|
||||
if (strpos($info['filename'], $delimiter) !== false) {
|
||||
$parts = explode($delimiter, $info['filename']);
|
||||
|
||||
if (is_numeric(end($parts))) {
|
||||
$i = end($parts);
|
||||
array_pop($parts);
|
||||
$parts[] = ++$i;
|
||||
$destination = $info['dirname'] . DIRECTORY_SEPARATOR . implode($delimiter, $parts) . $extension;
|
||||
} else {
|
||||
$destination = $info['dirname'] . DIRECTORY_SEPARATOR . $info['filename'] . $delimiter . ++$i . $extension;
|
||||
}
|
||||
} else {
|
||||
$destination = $info['dirname'] . DIRECTORY_SEPARATOR . $info['filename'] . $delimiter . ++$i . $extension;
|
||||
}
|
||||
}
|
||||
|
||||
return $destination;
|
||||
}
|
||||
}
|
||||
367
system/Files/FileCollection.php
Normal file
367
system/Files/FileCollection.php
Normal file
@@ -0,0 +1,367 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Files;
|
||||
|
||||
use CodeIgniter\Files\Exceptions\FileException;
|
||||
use CodeIgniter\Files\Exceptions\FileNotFoundException;
|
||||
use Countable;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
use IteratorAggregate;
|
||||
|
||||
/**
|
||||
* File Collection Class
|
||||
*
|
||||
* Representation for a group of files, with utilities for locating,
|
||||
* filtering, and ordering them.
|
||||
*/
|
||||
class FileCollection implements Countable, IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* The current list of file paths.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $files = [];
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Support Methods
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolves a full path and verifies it is an actual directory.
|
||||
*
|
||||
* @throws FileException
|
||||
*/
|
||||
final protected static function resolveDirectory(string $directory): string
|
||||
{
|
||||
if (! is_dir($directory = set_realpath($directory))) {
|
||||
$caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1];
|
||||
|
||||
throw FileException::forExpectedDirectory($caller['function']);
|
||||
}
|
||||
|
||||
return $directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a full path and verifies it is an actual file.
|
||||
*
|
||||
* @throws FileException
|
||||
*/
|
||||
final protected static function resolveFile(string $file): string
|
||||
{
|
||||
if (! is_file($file = set_realpath($file))) {
|
||||
$caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1];
|
||||
|
||||
throw FileException::forExpectedFile($caller['function']);
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes files that are not part of the given directory (recursive).
|
||||
*
|
||||
* @param string[] $files
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
final protected static function filterFiles(array $files, string $directory): array
|
||||
{
|
||||
$directory = self::resolveDirectory($directory);
|
||||
|
||||
return array_filter($files, static function (string $value) use ($directory): bool {
|
||||
return strpos($value, $directory) === 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns any files whose `basename` matches the given pattern.
|
||||
*
|
||||
* @param string[] $files
|
||||
* @param string $pattern Regex or pseudo-regex string
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
final protected static function matchFiles(array $files, string $pattern): array
|
||||
{
|
||||
// Convert pseudo-regex into their true form
|
||||
if (@preg_match($pattern, '') === false) {
|
||||
$pattern = str_replace(
|
||||
['#', '.', '*', '?'],
|
||||
['\#', '\.', '.*', '.'],
|
||||
$pattern
|
||||
);
|
||||
$pattern = "#{$pattern}#";
|
||||
}
|
||||
|
||||
return array_filter($files, static function ($value) use ($pattern) {
|
||||
return (bool) preg_match($pattern, basename($value));
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Class Core
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Loads the Filesystem helper and adds any initial files.
|
||||
*
|
||||
* @param string[] $files
|
||||
*/
|
||||
public function __construct(array $files = [])
|
||||
{
|
||||
helper(['filesystem']);
|
||||
|
||||
$this->add($files)->define();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies any initial inputs after the constructor.
|
||||
* This method is a stub to be implemented by child classes.
|
||||
*/
|
||||
protected function define(): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimizes and returns the current file list.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function get(): array
|
||||
{
|
||||
$this->files = array_unique($this->files);
|
||||
sort($this->files, SORT_STRING);
|
||||
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file list directly, files are still subject to verification.
|
||||
* This works as a "reset" method with [].
|
||||
*
|
||||
* @param string[] $files The new file list to use
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function set(array $files)
|
||||
{
|
||||
$this->files = [];
|
||||
|
||||
return $this->addFiles($files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an array/single file or directory to the list.
|
||||
*
|
||||
* @param string|string[] $paths
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function add($paths, bool $recursive = true)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if (! is_string($path)) {
|
||||
throw new InvalidArgumentException('FileCollection paths must be strings.');
|
||||
}
|
||||
|
||||
try {
|
||||
// Test for a directory
|
||||
self::resolveDirectory($path);
|
||||
} catch (FileException $e) {
|
||||
return $this->addFile($path);
|
||||
}
|
||||
|
||||
$this->addDirectory($path, $recursive);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// File Handling
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Verifies and adds files to the list.
|
||||
*
|
||||
* @param string[] $files
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addFiles(array $files)
|
||||
{
|
||||
foreach ($files as $file) {
|
||||
$this->addFile($file);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies and adds a single file to the file list.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addFile(string $file)
|
||||
{
|
||||
$this->files[] = self::resolveFile($file);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes files from the list.
|
||||
*
|
||||
* @param string[] $files
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function removeFiles(array $files)
|
||||
{
|
||||
$this->files = array_diff($this->files, $files);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single file from the list.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function removeFile(string $file)
|
||||
{
|
||||
return $this->removeFiles([$file]);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Directory Handling
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Verifies and adds files from each
|
||||
* directory to the list.
|
||||
*
|
||||
* @param string[] $directories
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addDirectories(array $directories, bool $recursive = false)
|
||||
{
|
||||
foreach ($directories as $directory) {
|
||||
$this->addDirectory($directory, $recursive);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies and adds all files from a directory.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addDirectory(string $directory, bool $recursive = false)
|
||||
{
|
||||
$directory = self::resolveDirectory($directory);
|
||||
|
||||
// Map the directory to depth 2 to so directories become arrays
|
||||
foreach (directory_map($directory, 2, true) as $key => $path) {
|
||||
if (is_string($path)) {
|
||||
$this->addFile($directory . $path);
|
||||
} elseif ($recursive && is_array($path)) {
|
||||
$this->addDirectory($directory . $key, true);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Filtering
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Removes any files from the list that match the supplied pattern
|
||||
* (within the optional scope).
|
||||
*
|
||||
* @param string $pattern Regex or pseudo-regex string
|
||||
* @param string|null $scope The directory to limit the scope
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function removePattern(string $pattern, ?string $scope = null)
|
||||
{
|
||||
if ($pattern === '') {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Start with all files or those in scope
|
||||
$files = $scope === null ? $this->files : self::filterFiles($this->files, $scope);
|
||||
|
||||
// Remove any files that match the pattern
|
||||
return $this->removeFiles(self::matchFiles($files, $pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps only the files from the list that match
|
||||
* (within the optional scope).
|
||||
*
|
||||
* @param string $pattern Regex or pseudo-regex string
|
||||
* @param string|null $scope A directory to limit the scope
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function retainPattern(string $pattern, ?string $scope = null)
|
||||
{
|
||||
if ($pattern === '') {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Start with all files or those in scope
|
||||
$files = $scope === null ? $this->files : self::filterFiles($this->files, $scope);
|
||||
|
||||
// Matches the pattern within the scoped files and remove their inverse.
|
||||
return $this->removeFiles(array_diff($files, self::matchFiles($files, $pattern)));
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Interface Methods
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the current number of files in the collection.
|
||||
* Fulfills Countable.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Yields as an Iterator for the current files.
|
||||
* Fulfills IteratorAggregate.
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return Generator<File>
|
||||
*/
|
||||
public function getIterator(): Generator
|
||||
{
|
||||
foreach ($this->get() as $file) {
|
||||
yield new File($file, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user