494 lines
15 KiB
PHP
494 lines
15 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace FlysystemOffload\StreamWrapper;
|
|
|
|
use FlysystemOffload\Helpers\PathHelper;
|
|
use League\Flysystem\FilesystemException;
|
|
use League\Flysystem\FilesystemOperator;
|
|
use League\Flysystem\StorageAttributes;
|
|
use League\Flysystem\UnableToDeleteDirectory;
|
|
use League\Flysystem\UnableToDeleteFile;
|
|
use League\Flysystem\UnableToMoveFile;
|
|
use League\Flysystem\UnableToReadFile;
|
|
use League\Flysystem\UnableToWriteFile;
|
|
use League\Flysystem\Visibility;
|
|
|
|
final class FlysystemStreamWrapper {
|
|
private static ?FilesystemOperator $filesystem = null;
|
|
private static string $protocol = 'flysystem';
|
|
private static string $rootPrefix = '';
|
|
private static string $defaultVisibility = Visibility::PUBLIC;
|
|
|
|
/** @var resource|null */
|
|
private $resource = null;
|
|
private string $mode = 'r';
|
|
private string $path = '';
|
|
private string $uri = '';
|
|
private bool $dirty = false;
|
|
|
|
/** @var list<string> */
|
|
private array $directoryListing = [];
|
|
private int $directoryPosition = 0;
|
|
|
|
public static function register(
|
|
FilesystemOperator $filesystem,
|
|
string $protocol = 'flysystem',
|
|
string $rootPrefix = '',
|
|
string $defaultVisibility = Visibility::PUBLIC,
|
|
bool $force = true
|
|
): void {
|
|
self::$filesystem = $filesystem;
|
|
self::$protocol = $protocol;
|
|
self::$rootPrefix = PathHelper::normalize($rootPrefix);
|
|
self::$defaultVisibility = $defaultVisibility;
|
|
|
|
$wrappers = stream_get_wrappers();
|
|
if (in_array($protocol, $wrappers, true)) {
|
|
if (! $force) {
|
|
return;
|
|
}
|
|
stream_wrapper_unregister($protocol);
|
|
}
|
|
|
|
if (! stream_wrapper_register($protocol, static::class, STREAM_IS_URL)) {
|
|
throw new \RuntimeException(sprintf('Unable to register stream wrapper for protocol "%s".', $protocol));
|
|
}
|
|
}
|
|
|
|
public static function unregister(): void {
|
|
if (self::$protocol !== '' && in_array(self::$protocol, stream_get_wrappers(), true)) {
|
|
stream_wrapper_unregister(self::$protocol);
|
|
}
|
|
self::$filesystem = null;
|
|
}
|
|
|
|
public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool {
|
|
$this->ensureFilesystem();
|
|
|
|
$this->uri = $path;
|
|
$this->path = self::uriToKey($path);
|
|
$this->mode = $mode;
|
|
$this->resource = fopen('php://temp', 'w+b');
|
|
$this->dirty = false;
|
|
|
|
if ($this->resource === false) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->requiresExistingFile($mode) && ! $this->fileExists($this->path)) {
|
|
if ($options & STREAM_REPORT_ERRORS) {
|
|
trigger_error(sprintf('Flysystem path "%s" does not exist.', $this->path), E_USER_WARNING);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if ($this->isExclusiveCreateMode($mode) && $this->fileExists($this->path)) {
|
|
return false;
|
|
}
|
|
|
|
if (($this->requiresExistingFile($mode) || $this->isAppendMode($mode)) && $this->fileExists($this->path)) {
|
|
try {
|
|
$contents = self::$filesystem->read($this->path);
|
|
fwrite($this->resource, $contents);
|
|
unset($contents);
|
|
|
|
if ($this->isAppendMode($mode)) {
|
|
fseek($this->resource, 0, SEEK_END);
|
|
} else {
|
|
rewind($this->resource);
|
|
}
|
|
} catch (UnableToReadFile|FilesystemException $exception) {
|
|
if ($options & STREAM_REPORT_ERRORS) {
|
|
trigger_error($exception->getMessage(), E_USER_WARNING);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function stream_read(int $count): string|false {
|
|
if (! $this->resource) {
|
|
return false;
|
|
}
|
|
|
|
return fread($this->resource, $count);
|
|
}
|
|
|
|
public function stream_write(string $data): int|false {
|
|
if (! $this->resource) {
|
|
return false;
|
|
}
|
|
|
|
$written = fwrite($this->resource, $data);
|
|
if ($written !== false) {
|
|
$this->dirty = true;
|
|
}
|
|
|
|
return $written;
|
|
}
|
|
|
|
public function stream_tell(): int|false {
|
|
if (! $this->resource) {
|
|
return false;
|
|
}
|
|
|
|
return ftell($this->resource);
|
|
}
|
|
|
|
public function stream_eof(): bool {
|
|
if (! $this->resource) {
|
|
return true;
|
|
}
|
|
|
|
return feof($this->resource);
|
|
}
|
|
|
|
public function stream_seek(int $offset, int $whence = SEEK_SET): bool {
|
|
if (! $this->resource) {
|
|
return false;
|
|
}
|
|
|
|
return fseek($this->resource, $offset, $whence) === 0;
|
|
}
|
|
|
|
public function stream_flush(): bool {
|
|
if (! $this->resource || ! $this->dirty || ! $this->isWriteMode($this->mode)) {
|
|
return true;
|
|
}
|
|
|
|
$position = ftell($this->resource);
|
|
rewind($this->resource);
|
|
$contents = stream_get_contents($this->resource);
|
|
fseek($this->resource, $position);
|
|
|
|
try {
|
|
if ($this->fileExists($this->path)) {
|
|
try {
|
|
self::$filesystem->delete($this->path);
|
|
} catch (UnableToDeleteFile|FilesystemException) {
|
|
// Intentamos sobrescribir igualmente.
|
|
}
|
|
}
|
|
|
|
self::$filesystem->write(
|
|
$this->path,
|
|
(string) $contents,
|
|
['visibility' => self::$defaultVisibility]
|
|
);
|
|
$this->dirty = false;
|
|
|
|
return true;
|
|
} catch (UnableToWriteFile|FilesystemException $exception) {
|
|
trigger_error($exception->getMessage(), E_USER_WARNING);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function stream_close(): void {
|
|
$this->stream_flush();
|
|
|
|
if ($this->resource) {
|
|
fclose($this->resource);
|
|
}
|
|
|
|
$this->resource = null;
|
|
$this->mode = 'r';
|
|
$this->path = '';
|
|
$this->uri = '';
|
|
$this->dirty = false;
|
|
}
|
|
|
|
public function stream_truncate(int $new_size): bool {
|
|
if (! $this->resource) {
|
|
return false;
|
|
}
|
|
|
|
$result = ftruncate($this->resource, $new_size);
|
|
if ($result) {
|
|
$this->dirty = true;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function stream_stat(): array|false {
|
|
if ($this->path === '') {
|
|
return false;
|
|
}
|
|
|
|
return $this->statKey($this->path);
|
|
}
|
|
|
|
public function url_stat(string $path, int $flags): array|false {
|
|
$key = self::uriToKey($path);
|
|
$stat = $this->statKey($key);
|
|
|
|
if ($stat === false && ($flags & STREAM_URL_STAT_QUIET) === 0) {
|
|
trigger_error(sprintf('Flysystem path "%s" not found.', $key), E_USER_WARNING);
|
|
}
|
|
|
|
return $stat;
|
|
}
|
|
|
|
public function unlink(string $path): bool {
|
|
$this->ensureFilesystem();
|
|
|
|
$key = self::uriToKey($path);
|
|
|
|
try {
|
|
if ($this->fileExists($key)) {
|
|
self::$filesystem->delete($key);
|
|
}
|
|
|
|
return true;
|
|
} catch (UnableToDeleteFile|FilesystemException $exception) {
|
|
trigger_error($exception->getMessage(), E_USER_WARNING);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function rename(string $path_from, string $path_to): bool {
|
|
$this->ensureFilesystem();
|
|
|
|
$from = self::uriToKey($path_from);
|
|
$to = self::uriToKey($path_to);
|
|
|
|
try {
|
|
self::$filesystem->move($from, $to);
|
|
return true;
|
|
} catch (UnableToMoveFile|FilesystemException $exception) {
|
|
trigger_error($exception->getMessage(), E_USER_WARNING);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function mkdir(string $path, int $mode, int $options): bool {
|
|
$this->ensureFilesystem();
|
|
|
|
$key = self::uriToKey($path);
|
|
|
|
try {
|
|
self::$filesystem->createDirectory($key, ['visibility' => self::$defaultVisibility]);
|
|
return true;
|
|
} catch (FilesystemException $exception) {
|
|
if ($options & STREAM_REPORT_ERRORS) {
|
|
trigger_error($exception->getMessage(), E_USER_WARNING);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function rmdir(string $path, int $options): bool {
|
|
$this->ensureFilesystem();
|
|
|
|
$key = self::uriToKey($path);
|
|
|
|
try {
|
|
self::$filesystem->deleteDirectory($key);
|
|
return true;
|
|
} catch (UnableToDeleteDirectory|FilesystemException $exception) {
|
|
if ($options & STREAM_REPORT_ERRORS) {
|
|
trigger_error($exception->getMessage(), E_USER_WARNING);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function dir_opendir(string $path, int $options): bool {
|
|
$this->ensureFilesystem();
|
|
|
|
$key = self::uriToKey($path);
|
|
$this->directoryListing = ['.', '..'];
|
|
|
|
try {
|
|
foreach (self::$filesystem->listContents($key, false) as $attributes) {
|
|
if ($attributes instanceof StorageAttributes) {
|
|
$this->directoryListing[] = basename($attributes->path());
|
|
}
|
|
}
|
|
} catch (FilesystemException $exception) {
|
|
if ($options & STREAM_REPORT_ERRORS) {
|
|
trigger_error($exception->getMessage(), E_USER_WARNING);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
$this->directoryPosition = 0;
|
|
|
|
return true;
|
|
}
|
|
|
|
public function dir_readdir(): string|false {
|
|
if (! isset($this->directoryListing[$this->directoryPosition])) {
|
|
return false;
|
|
}
|
|
|
|
return $this->directoryListing[$this->directoryPosition++];
|
|
}
|
|
|
|
public function dir_rewinddir(): bool {
|
|
$this->directoryPosition = 0;
|
|
return true;
|
|
}
|
|
|
|
public function dir_closedir(): bool {
|
|
$this->directoryListing = [];
|
|
$this->directoryPosition = 0;
|
|
return true;
|
|
}
|
|
|
|
public function stream_metadata(string $path, int $option, mixed $value): bool {
|
|
$this->ensureFilesystem();
|
|
|
|
if ($option === STREAM_META_TOUCH) {
|
|
$key = self::uriToKey($path);
|
|
|
|
try {
|
|
if (! $this->fileExists($key)) {
|
|
self::$filesystem->write($key, '', ['visibility' => self::$defaultVisibility]);
|
|
}
|
|
|
|
return true;
|
|
} catch (FilesystemException $exception) {
|
|
trigger_error($exception->getMessage(), E_USER_WARNING);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function stream_set_option(int $option, int $arg1, int $arg2): bool {
|
|
return false;
|
|
}
|
|
|
|
public function stream_lock(int $operation): bool {
|
|
return true;
|
|
}
|
|
|
|
private function requiresExistingFile(string $mode): bool {
|
|
return str_starts_with($mode, 'r');
|
|
}
|
|
|
|
private function isExclusiveCreateMode(string $mode): bool {
|
|
return str_starts_with($mode, 'x');
|
|
}
|
|
|
|
private function isAppendMode(string $mode): bool {
|
|
return str_contains($mode, 'a');
|
|
}
|
|
|
|
private function isWriteMode(string $mode): bool {
|
|
return strpbrk($mode, 'waxc+') !== false;
|
|
}
|
|
|
|
private static function uriToKey(string $uri): string {
|
|
$components = parse_url($uri);
|
|
|
|
$host = $components['host'] ?? '';
|
|
$path = $components['path'] ?? '';
|
|
|
|
$relative = PathHelper::join($host, $path);
|
|
|
|
if (self::$rootPrefix !== '') {
|
|
$relative = PathHelper::join(self::$rootPrefix, $relative);
|
|
}
|
|
|
|
return $relative;
|
|
}
|
|
|
|
private function fileExists(string $key): bool {
|
|
try {
|
|
return self::$filesystem->fileExists($key);
|
|
} catch (FilesystemException) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private function statKey(string $key): array|false {
|
|
$this->ensureFilesystem();
|
|
|
|
try {
|
|
if (self::$filesystem->fileExists($key)) {
|
|
$size = 0;
|
|
$mtime = time();
|
|
|
|
try {
|
|
$size = self::$filesystem->fileSize($key);
|
|
} catch (FilesystemException) {
|
|
$size = 0;
|
|
}
|
|
|
|
try {
|
|
$mtime = self::$filesystem->lastModified($key);
|
|
} catch (FilesystemException) {
|
|
$mtime = time();
|
|
}
|
|
|
|
return $this->formatStat(0100644, $size, $mtime, 1);
|
|
}
|
|
|
|
if (self::$filesystem->directoryExists($key)) {
|
|
$mtime = time();
|
|
try {
|
|
$mtime = self::$filesystem->lastModified($key);
|
|
} catch (FilesystemException) {
|
|
$mtime = time();
|
|
}
|
|
|
|
return $this->formatStat(0040755, 0, $mtime, 2);
|
|
}
|
|
} catch (FilesystemException) {
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* 0:int,1:int,2:int,3:int,4:int,5:int,6:int,7:int,8:int,9:int,10:int,11:int,12:int,
|
|
* dev:int,ino:int,mode:int,nlink:int,uid:int,gid:int,rdev:int,size:int,atime:int,mtime:int,ctime:int,blksize:int,blocks:int
|
|
* }
|
|
*/
|
|
private function formatStat(int $mode, int $size, int $timestamp, int $nlink = 1): array {
|
|
return [
|
|
0 => 0,
|
|
'dev' => 0,
|
|
1 => 0,
|
|
'ino' => 0,
|
|
2 => $mode,
|
|
'mode' => $mode,
|
|
3 => $nlink,
|
|
'nlink' => $nlink,
|
|
4 => 0,
|
|
'uid' => 0,
|
|
5 => 0,
|
|
'gid' => 0,
|
|
6 => 0,
|
|
'rdev' => 0,
|
|
7 => $size,
|
|
'size' => $size,
|
|
8 => $timestamp,
|
|
'atime' => $timestamp,
|
|
9 => $timestamp,
|
|
'mtime' => $timestamp,
|
|
10 => $timestamp,
|
|
'ctime' => $timestamp,
|
|
11 => -1,
|
|
'blksize' => -1,
|
|
12 => -1,
|
|
'blocks' => -1,
|
|
];
|
|
}
|
|
|
|
private function ensureFilesystem(): void {
|
|
if (! self::$filesystem) {
|
|
throw new \RuntimeException('Flysystem filesystem has not been registered.');
|
|
}
|
|
}
|
|
}
|