add trashes

This commit is contained in:
Diskette Guy
2025-07-21 15:40:51 +07:00
parent c8f2944770
commit 07171f5b5a
848 changed files with 134166 additions and 0 deletions
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* 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;
/**
* Provides a domain-level interface for broad capture
* of all Files-related exceptions.
*
* catch (\CodeIgniter\Files\Exceptions\ExceptionInterface) { ... }
*/
interface ExceptionInterface extends \CodeIgniter\Exceptions\ExceptionInterface
{
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* 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\RuntimeException;
class FileException extends RuntimeException implements ExceptionInterface
{
use DebugTraceableTrait;
/**
* @return static
*/
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
*
* @return static
*/
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
*
* @return static
*/
public static function forExpectedFile(string $caller)
{
return new static(lang('Files.expectedFile', [$caller]));
}
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* 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\RuntimeException;
class FileNotFoundException extends RuntimeException implements ExceptionInterface
{
use DebugTraceableTrait;
/**
* @return static
*/
public static function forFileNotFound(string $path)
{
return new static(lang('Files.fileNotFound', [$path]));
}
}
+227
View File
@@ -0,0 +1,227 @@
<?php
declare(strict_types=1);
/**
* 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 CodeIgniter\I18n\Time;
use Config\Mimes;
use ReturnTypeWillChange;
use SplFileInfo;
/**
* Wrapper for PHP's built-in SplFileInfo, with goodies.
*
* @see \CodeIgniter\Files\FileTest
*/
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. A RuntimeException will be thrown if the file
* does not exist or an error occurs.
*
* @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, calculated in IEC standards with 1024 as base value.
*
* @phpstan-param positive-int $precision
*/
public function getSizeByBinaryUnit(FileSizeUnit $unit = FileSizeUnit::B, int $precision = 3): int|string
{
return $this->getSizeByUnitInternal(1024, $unit, $precision);
}
/**
* Retrieve the file size by unit, calculated in metric standards with 1000 as base value.
*
* @phpstan-param positive-int $precision
*/
public function getSizeByMetricUnit(FileSizeUnit $unit = FileSizeUnit::B, int $precision = 3): int|string
{
return $this->getSizeByUnitInternal(1000, $unit, $precision);
}
/**
* Retrieve the file size by unit.
*
* @deprecated 4.6.0 Use getSizeByBinaryUnit() or getSizeByMetricUnit() instead
*
* @return false|int|string
*/
public function getSizeByUnit(string $unit = 'b')
{
return match (strtolower($unit)) {
'kb' => $this->getSizeByBinaryUnit(FileSizeUnit::KB),
'mb' => $this->getSizeByBinaryUnit(FileSizeUnit::MB),
default => $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
{
// naively get the path extension using pathinfo
$pathinfo = pathinfo($this->getRealPath() ?: $this->__toString()) + ['extension' => ''];
$proposedExtension = $pathinfo['extension'];
return Mimes::guessExtensionFromType($this->getMimeType(), $proposedExtension);
}
/**
* 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::now()->getTimestamp() . '_' . 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 ??= $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 (str_contains($info['filename'], $delimiter)) {
$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;
}
private function getSizeByUnitInternal(int $fileSizeBase, FileSizeUnit $unit, int $precision): int|string
{
$exponent = $unit->value;
$divider = $fileSizeBase ** $exponent;
$size = $this->getSize() / $divider;
if ($unit !== FileSizeUnit::B) {
$size = number_format($size, $precision);
}
return $size;
}
}
@@ -0,0 +1,408 @@
<?php
declare(strict_types=1);
/**
* 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\Exceptions\InvalidArgumentException;
use CodeIgniter\Files\Exceptions\FileException;
use CodeIgniter\Files\Exceptions\FileNotFoundException;
use Countable;
use Generator;
use IteratorAggregate;
/**
* File Collection Class
*
* Representation for a group of files, with utilities for locating,
* filtering, and ordering them.
*
* @template-implements IteratorAggregate<int, File>
* @see \CodeIgniter\Files\FileCollectionTest
*/
class FileCollection implements Countable, IteratorAggregate
{
/**
* The current list of file paths.
*
* @var list<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 list<string> $files
*
* @return list<string>
*/
final protected static function filterFiles(array $files, string $directory): array
{
$directory = self::resolveDirectory($directory);
return array_filter($files, static fn (string $value): bool => str_starts_with($value, $directory));
}
/**
* Returns any files whose `basename` matches the given pattern.
*
* @param list<string> $files
* @param string $pattern Regex or pseudo-regex string
*
* @return list<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 = "#\\A{$pattern}\\z#";
}
return array_filter($files, static fn ($value): bool => (bool) preg_match($pattern, basename($value)));
}
// --------------------------------------------------------------------
// Class Core
// --------------------------------------------------------------------
/**
* Loads the Filesystem helper and adds any initial files.
*
* @param list<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 list<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 list<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 list<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) {
$this->addFile($path);
continue;
}
$this->addDirectory($path, $recursive);
}
return $this;
}
// --------------------------------------------------------------------
// File Handling
// --------------------------------------------------------------------
/**
* Verifies and adds files to the list.
*
* @param list<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 list<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 list<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)));
}
/**
* Keeps only the files from the list that match multiple patterns
* (within the optional scope).
*
* @param list<string> $patterns Array of regex or pseudo-regex strings
* @param string|null $scope A directory to limit the scope
*
* @return $this
*/
public function retainMultiplePatterns(array $patterns, ?string $scope = null)
{
if ($patterns === []) {
return $this;
}
if (count($patterns) === 1 && $patterns[0] === '') {
return $this;
}
// Start with all files or those in scope
$files = $scope === null ? $this->files : self::filterFiles($this->files, $scope);
// Add files to retain to array
$filesToRetain = [];
foreach ($patterns as $pattern) {
if ($pattern === '') {
continue;
}
// Matches the pattern within the scoped files
$filesToRetain = array_merge($filesToRetain, self::matchFiles($files, $pattern));
}
// Remove the inverse of files to retain
return $this->removeFiles(array_diff($files, $filesToRetain));
}
// --------------------------------------------------------------------
// 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.
*
* @return Generator<File>
*
* @throws FileNotFoundException
*/
public function getIterator(): Generator
{
foreach ($this->get() as $file) {
yield new File($file, true);
}
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/**
* 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\Exceptions\InvalidArgumentException;
enum FileSizeUnit: int
{
case B = 0;
case KB = 1;
case MB = 2;
case GB = 3;
case TB = 4;
/**
* Allows the creation of a FileSizeUnit from Strings like "kb" or "mb"
*
* @throws InvalidArgumentException
*/
public static function fromString(string $unit): self
{
return match (strtolower($unit)) {
'b' => self::B,
'kb' => self::KB,
'mb' => self::MB,
'gb' => self::GB,
'tb' => self::TB,
default => throw new InvalidArgumentException("Invalid unit: {$unit}"),
};
}
}