3.0.0
This commit is contained in:
parent
b974fb340a
commit
04784ee337
|
|
@ -26,7 +26,7 @@ class ConfigLoader
|
||||||
|
|
||||||
// Merge con prioridad a la base de datos sobre el archivo
|
// Merge con prioridad a la base de datos sobre el archivo
|
||||||
// Si hay configuración en BD, usar esa; si no, usar archivo
|
// Si hay configuración en BD, usar esa; si no, usar archivo
|
||||||
$config = !empty($dbConfig['provider'])
|
$config = !empty($dbConfig['provider'])
|
||||||
? array_merge($fileConfig, $dbConfig)
|
? array_merge($fileConfig, $dbConfig)
|
||||||
: $fileConfig;
|
: $fileConfig;
|
||||||
|
|
||||||
|
|
@ -114,7 +114,7 @@ class ConfigLoader
|
||||||
$config['bucket'] = $s3Config['bucket'] ?? '';
|
$config['bucket'] = $s3Config['bucket'] ?? '';
|
||||||
$config['endpoint'] = $s3Config['endpoint'] ?? '';
|
$config['endpoint'] = $s3Config['endpoint'] ?? '';
|
||||||
$config['use_path_style_endpoint'] = $s3Config['use_path_style_endpoint'] ?? false;
|
$config['use_path_style_endpoint'] = $s3Config['use_path_style_endpoint'] ?? false;
|
||||||
|
|
||||||
// Usar prefix específico de S3 si existe, sino el global
|
// Usar prefix específico de S3 si existe, sino el global
|
||||||
if (isset($s3Config['prefix'])) {
|
if (isset($s3Config['prefix'])) {
|
||||||
$config['prefix'] = $s3Config['prefix'];
|
$config['prefix'] = $s3Config['prefix'];
|
||||||
|
|
@ -133,10 +133,10 @@ class ConfigLoader
|
||||||
case 'webdav':
|
case 'webdav':
|
||||||
if (isset($rawConfig['webdav'])) {
|
if (isset($rawConfig['webdav'])) {
|
||||||
$webdavConfig = $rawConfig['webdav'];
|
$webdavConfig = $rawConfig['webdav'];
|
||||||
|
|
||||||
// Determinar base_uri desde endpoint o base_url
|
// Determinar base_uri desde endpoint o base_url
|
||||||
$config['base_uri'] = $webdavConfig['endpoint']
|
$config['base_uri'] = $webdavConfig['endpoint']
|
||||||
?? $webdavConfig['base_url']
|
?? $webdavConfig['base_url']
|
||||||
?? '';
|
?? '';
|
||||||
|
|
||||||
// Credenciales
|
// Credenciales
|
||||||
|
|
@ -157,10 +157,10 @@ class ConfigLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permisos (usar valores por defecto si no están definidos)
|
// Permisos (usar valores por defecto si no están definidos)
|
||||||
$config['file_public'] = '0644';
|
$config['file_public'] = $webdavConfig['file_public'] ?? '0644';
|
||||||
$config['file_private'] = '0600';
|
$config['file_private'] = $webdavConfig['file_private'] ?? '0600';
|
||||||
$config['dir_public'] = '0755';
|
$config['dir_public'] = $webdavConfig['dir_public'] ?? '0755';
|
||||||
$config['dir_private'] = '0700';
|
$config['dir_private'] = $webdavConfig['dir_private'] ?? '0700';
|
||||||
|
|
||||||
// Preservar configuración completa de WebDAV
|
// Preservar configuración completa de WebDAV
|
||||||
$config['webdav'] = $webdavConfig;
|
$config['webdav'] = $webdavConfig;
|
||||||
|
|
@ -417,12 +417,12 @@ class ConfigLoader
|
||||||
|
|
||||||
if (is_string($permission)) {
|
if (is_string($permission)) {
|
||||||
$permission = trim($permission);
|
$permission = trim($permission);
|
||||||
|
|
||||||
// Si ya tiene el formato correcto, retornar
|
// Si ya tiene el formato correcto, retornar
|
||||||
if (preg_match('/^0[0-7]{3}$/', $permission)) {
|
if (preg_match('/^0[0-7]{3}$/', $permission)) {
|
||||||
return $permission;
|
return $permission;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si es solo dígitos sin el 0 inicial, añadirlo
|
// Si es solo dígitos sin el 0 inicial, añadirlo
|
||||||
if (preg_match('/^[0-7]{3}$/', $permission)) {
|
if (preg_match('/^[0-7]{3}$/', $permission)) {
|
||||||
return '0' . $permission;
|
return '0' . $permission;
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class WebdavAdapter implements FilesystemAdapter
|
||||||
private Client $client;
|
private Client $client;
|
||||||
private string $prefix;
|
private string $prefix;
|
||||||
private PortableVisibilityConverter $visibilityConverter;
|
private PortableVisibilityConverter $visibilityConverter;
|
||||||
|
private bool $baseDirectoryEnsured = false;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
Client $client,
|
Client $client,
|
||||||
|
|
@ -39,6 +40,70 @@ class WebdavAdapter implements FilesystemAdapter
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asegurar que el directorio base (prefix) existe
|
||||||
|
* Se ejecuta lazy (solo cuando se necesita)
|
||||||
|
*/
|
||||||
|
private function ensureBaseDirectoryExists(): void
|
||||||
|
{
|
||||||
|
if ($this->baseDirectoryEnsured || $this->prefix === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log('[WebdavAdapter] Ensuring base directory exists...');
|
||||||
|
|
||||||
|
$parts = array_filter(explode('/', $this->prefix));
|
||||||
|
$path = '';
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$path .= '/' . $part;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Intentar verificar si existe
|
||||||
|
$this->client->propFind($path, ['{DAV:}resourcetype'], 0);
|
||||||
|
error_log(sprintf('[WebdavAdapter] Base directory exists: "%s"', $path));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// No existe, crear
|
||||||
|
error_log(sprintf('[WebdavAdapter] Base directory does not exist, creating: "%s"', $path));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $this->client->request('MKCOL', $path);
|
||||||
|
|
||||||
|
// Verificar el código de estado
|
||||||
|
$statusCode = $response['statusCode'] ?? 0;
|
||||||
|
|
||||||
|
if ($statusCode >= 200 && $statusCode < 300) {
|
||||||
|
// Éxito (201 Created)
|
||||||
|
error_log(sprintf('[WebdavAdapter] Created base directory: "%s", status: %d', $path, $statusCode));
|
||||||
|
} elseif ($statusCode === 405) {
|
||||||
|
// 405 Method Not Allowed = ya existe
|
||||||
|
error_log(sprintf('[WebdavAdapter] Base directory already exists: "%s"', $path));
|
||||||
|
} else {
|
||||||
|
// Error
|
||||||
|
$errorMsg = sprintf('Failed to create directory "%s", status: %d', $path, $statusCode);
|
||||||
|
error_log(sprintf('[WebdavAdapter] %s', $errorMsg));
|
||||||
|
throw new \RuntimeException($errorMsg);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e2) {
|
||||||
|
// Verificar si el error es porque ya existe (405)
|
||||||
|
if (strpos($e2->getMessage(), '405') !== false) {
|
||||||
|
error_log(sprintf('[WebdavAdapter] Base directory already exists (405): "%s"', $path));
|
||||||
|
} else {
|
||||||
|
error_log(sprintf(
|
||||||
|
'[WebdavAdapter] Failed to create base directory: "%s", error: %s',
|
||||||
|
$path,
|
||||||
|
$e2->getMessage()
|
||||||
|
));
|
||||||
|
throw $e2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->baseDirectoryEnsured = true;
|
||||||
|
error_log('[WebdavAdapter] Base directory ensured successfully');
|
||||||
|
}
|
||||||
|
|
||||||
private function prefixPath(string $path): string
|
private function prefixPath(string $path): string
|
||||||
{
|
{
|
||||||
if ($this->prefix === '') {
|
if ($this->prefix === '') {
|
||||||
|
|
@ -86,6 +151,9 @@ class WebdavAdapter implements FilesystemAdapter
|
||||||
|
|
||||||
public function write(string $path, string $contents, Config $config): void
|
public function write(string $path, string $contents, Config $config): void
|
||||||
{
|
{
|
||||||
|
// ✅ CRÍTICO: Asegurar que el directorio base existe ANTES de escribir
|
||||||
|
$this->ensureBaseDirectoryExists();
|
||||||
|
|
||||||
$prefixedPath = $this->prefixPath($path);
|
$prefixedPath = $this->prefixPath($path);
|
||||||
|
|
||||||
error_log(sprintf(
|
error_log(sprintf(
|
||||||
|
|
@ -95,7 +163,11 @@ class WebdavAdapter implements FilesystemAdapter
|
||||||
strlen($contents)
|
strlen($contents)
|
||||||
));
|
));
|
||||||
|
|
||||||
$this->ensureDirectoryExists(dirname($path), $config);
|
// Asegurar que el directorio padre del archivo existe
|
||||||
|
$dirname = dirname($path);
|
||||||
|
if ($dirname !== '.' && $dirname !== '') {
|
||||||
|
$this->ensureDirectoryExists($dirname, $config);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = $this->client->request('PUT', $prefixedPath, $contents);
|
$response = $this->client->request('PUT', $prefixedPath, $contents);
|
||||||
|
|
@ -124,7 +196,11 @@ class WebdavAdapter implements FilesystemAdapter
|
||||||
|
|
||||||
public function writeStream(string $path, $contents, Config $config): void
|
public function writeStream(string $path, $contents, Config $config): void
|
||||||
{
|
{
|
||||||
$this->write($path, stream_get_contents($contents), $config);
|
$streamContents = stream_get_contents($contents);
|
||||||
|
if ($streamContents === false) {
|
||||||
|
throw UnableToWriteFile::atLocation($path, 'Unable to read from stream');
|
||||||
|
}
|
||||||
|
$this->write($path, $streamContents, $config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function read(string $path): string
|
public function read(string $path): string
|
||||||
|
|
@ -191,6 +267,9 @@ class WebdavAdapter implements FilesystemAdapter
|
||||||
|
|
||||||
public function createDirectory(string $path, Config $config): void
|
public function createDirectory(string $path, Config $config): void
|
||||||
{
|
{
|
||||||
|
// ✅ Asegurar que el directorio base existe
|
||||||
|
$this->ensureBaseDirectoryExists();
|
||||||
|
|
||||||
$prefixedPath = $this->prefixPath($path);
|
$prefixedPath = $this->prefixPath($path);
|
||||||
|
|
||||||
error_log(sprintf(
|
error_log(sprintf(
|
||||||
|
|
@ -202,6 +281,7 @@ class WebdavAdapter implements FilesystemAdapter
|
||||||
try {
|
try {
|
||||||
$response = $this->client->request('MKCOL', $prefixedPath);
|
$response = $this->client->request('MKCOL', $prefixedPath);
|
||||||
|
|
||||||
|
// 405 significa que el directorio ya existe
|
||||||
if ($response['statusCode'] >= 400 && $response['statusCode'] !== 405) {
|
if ($response['statusCode'] >= 400 && $response['statusCode'] !== 405) {
|
||||||
throw UnableToCreateDirectory::atLocation(
|
throw UnableToCreateDirectory::atLocation(
|
||||||
$path,
|
$path,
|
||||||
|
|
@ -221,8 +301,11 @@ class WebdavAdapter implements FilesystemAdapter
|
||||||
$e->getMessage()
|
$e->getMessage()
|
||||||
));
|
));
|
||||||
|
|
||||||
if ($e->getCode() !== 405) {
|
// 405 significa que ya existe, no es un error
|
||||||
|
if (strpos($e->getMessage(), '405') === false) {
|
||||||
throw UnableToCreateDirectory::atLocation($path, $e->getMessage(), $e);
|
throw UnableToCreateDirectory::atLocation($path, $e->getMessage(), $e);
|
||||||
|
} else {
|
||||||
|
error_log(sprintf('[WebdavAdapter] Directory already exists: "%s"', $path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -233,18 +316,29 @@ class WebdavAdapter implements FilesystemAdapter
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$parts = explode('/', trim($dirname, '/'));
|
error_log(sprintf('[WebdavAdapter] ensureDirectoryExists - dirname: "%s"', $dirname));
|
||||||
|
|
||||||
|
$parts = array_filter(explode('/', trim($dirname, '/')));
|
||||||
$path = '';
|
$path = '';
|
||||||
|
|
||||||
foreach ($parts as $part) {
|
foreach ($parts as $part) {
|
||||||
if ($part === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$path .= ($path !== '' ? '/' : '') . $part;
|
$path .= ($path !== '' ? '/' : '') . $part;
|
||||||
|
|
||||||
if (!$this->directoryExists($path)) {
|
error_log(sprintf('[WebdavAdapter] Checking/creating directory: "%s"', $path));
|
||||||
|
|
||||||
|
// Intentar crear directamente
|
||||||
|
try {
|
||||||
$this->createDirectory($path, $config);
|
$this->createDirectory($path, $config);
|
||||||
|
} catch (UnableToCreateDirectory $e) {
|
||||||
|
// Si falla y no es porque ya existe, propagar el error
|
||||||
|
if (strpos($e->getMessage(), '405') === false && !$this->directoryExists($path)) {
|
||||||
|
error_log(sprintf(
|
||||||
|
'[WebdavAdapter] Failed to ensure directory exists: "%s", error: %s',
|
||||||
|
$path,
|
||||||
|
$e->getMessage()
|
||||||
|
));
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -252,7 +346,6 @@ class WebdavAdapter implements FilesystemAdapter
|
||||||
public function setVisibility(string $path, string $visibility): void
|
public function setVisibility(string $path, string $visibility): void
|
||||||
{
|
{
|
||||||
// WebDAV no soporta visibilidad de forma nativa
|
// WebDAV no soporta visibilidad de forma nativa
|
||||||
// Se podría implementar con propiedades personalizadas si es necesario
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function visibility(string $path): FileAttributes
|
public function visibility(string $path): FileAttributes
|
||||||
|
|
@ -383,6 +476,8 @@ class WebdavAdapter implements FilesystemAdapter
|
||||||
|
|
||||||
public function move(string $source, string $destination, Config $config): void
|
public function move(string $source, string $destination, Config $config): void
|
||||||
{
|
{
|
||||||
|
$this->ensureBaseDirectoryExists();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->client->request(
|
$this->client->request(
|
||||||
'MOVE',
|
'MOVE',
|
||||||
|
|
@ -397,6 +492,8 @@ class WebdavAdapter implements FilesystemAdapter
|
||||||
|
|
||||||
public function copy(string $source, string $destination, Config $config): void
|
public function copy(string $source, string $destination, Config $config): void
|
||||||
{
|
{
|
||||||
|
$this->ensureBaseDirectoryExists();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->client->request(
|
$this->client->request(
|
||||||
'COPY',
|
'COPY',
|
||||||
|
|
|
||||||
|
|
@ -136,14 +136,21 @@ class FilesystemFactory
|
||||||
|
|
||||||
// Configurar cliente Sabre
|
// Configurar cliente Sabre
|
||||||
$settings = [
|
$settings = [
|
||||||
'baseUri' => $baseUri,
|
'baseUri' => rtrim($baseUri, '/') . '/',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Agregar autenticación si está configurada
|
// Agregar autenticación si está configurada
|
||||||
if (!empty($username)) {
|
if (!empty($username)) {
|
||||||
$settings['userName'] = $username;
|
$settings['userName'] = $username;
|
||||||
$settings['password'] = $password;
|
$settings['password'] = $password;
|
||||||
$settings['authType'] = constant('Sabre\DAV\Client::AUTH_' . strtoupper($authType));
|
|
||||||
|
// Mapear auth_type a constante de Sabre
|
||||||
|
$authTypeConstant = match(strtolower($authType)) {
|
||||||
|
'digest' => \Sabre\DAV\Client::AUTH_DIGEST,
|
||||||
|
'ntlm' => \Sabre\DAV\Client::AUTH_NTLM,
|
||||||
|
default => \Sabre\DAV\Client::AUTH_BASIC,
|
||||||
|
};
|
||||||
|
$settings['authType'] = $authTypeConstant;
|
||||||
}
|
}
|
||||||
|
|
||||||
$client = new SabreClient($settings);
|
$client = new SabreClient($settings);
|
||||||
|
|
@ -155,21 +162,20 @@ class FilesystemFactory
|
||||||
$dirPrivate = self::normalizePermission($config['dir_private'] ?? 0700);
|
$dirPrivate = self::normalizePermission($config['dir_private'] ?? 0700);
|
||||||
|
|
||||||
error_log(sprintf(
|
error_log(sprintf(
|
||||||
'[Flysystem Offload] WebDAV permissions - file_public: %o (%d), file_private: %o (%d), dir_public: %o (%d), dir_private: %o (%d)',
|
'[Flysystem Offload] WebDAV permissions - file_public: %o, file_private: %o, dir_public: %o, dir_private: %o',
|
||||||
$filePublic, $filePublic,
|
$filePublic,
|
||||||
$filePrivate, $filePrivate,
|
$filePrivate,
|
||||||
$dirPublic, $dirPublic,
|
$dirPublic,
|
||||||
$dirPrivate, $dirPrivate
|
$dirPrivate
|
||||||
));
|
));
|
||||||
|
|
||||||
// Crear converter de visibilidad
|
// Crear converter de visibilidad
|
||||||
// Los primeros 4 parámetros son int, el 5to es string (visibility: 'public' o 'private')
|
|
||||||
$visibilityConverter = new PortableVisibilityConverter(
|
$visibilityConverter = new PortableVisibilityConverter(
|
||||||
filePublic: $filePublic,
|
filePublic: $filePublic,
|
||||||
filePrivate: $filePrivate,
|
filePrivate: $filePrivate,
|
||||||
directoryPublic: $dirPublic,
|
directoryPublic: $dirPublic,
|
||||||
directoryPrivate: $dirPrivate,
|
directoryPrivate: $dirPrivate,
|
||||||
defaultForDirectories: 'public' // String: 'public' o 'private'
|
defaultForDirectories: 'public'
|
||||||
);
|
);
|
||||||
|
|
||||||
error_log(sprintf(
|
error_log(sprintf(
|
||||||
|
|
@ -262,12 +268,12 @@ class FilesystemFactory
|
||||||
// Si es string, convertir
|
// Si es string, convertir
|
||||||
if (is_string($permission)) {
|
if (is_string($permission)) {
|
||||||
$permission = trim($permission);
|
$permission = trim($permission);
|
||||||
|
|
||||||
// Si tiene el formato 0xxx, convertir desde octal
|
// Si tiene el formato 0xxx, convertir desde octal
|
||||||
if (preg_match('/^0[0-7]{3}$/', $permission)) {
|
if (preg_match('/^0[0-7]{3}$/', $permission)) {
|
||||||
return intval($permission, 8);
|
return intval($permission, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si es solo dígitos sin el 0 inicial, añadirlo y convertir
|
// Si es solo dígitos sin el 0 inicial, añadirlo y convertir
|
||||||
if (preg_match('/^[0-7]{3}$/', $permission)) {
|
if (preg_match('/^[0-7]{3}$/', $permission)) {
|
||||||
return intval('0' . $permission, 8);
|
return intval('0' . $permission, 8);
|
||||||
|
|
|
||||||
|
|
@ -17,29 +17,10 @@ use Throwable;
|
||||||
*/
|
*/
|
||||||
class Plugin
|
class Plugin
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Instancia del Filesystem
|
|
||||||
*/
|
|
||||||
private static ?Filesystem $filesystem = null;
|
private static ?Filesystem $filesystem = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuración del plugin
|
|
||||||
*/
|
|
||||||
private static array $config = [];
|
private static array $config = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* Indica si el plugin está inicializado
|
|
||||||
*/
|
|
||||||
private static bool $initialized = false;
|
private static bool $initialized = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* Instancia de MediaHooks
|
|
||||||
*/
|
|
||||||
private static ?MediaHooks $mediaHooks = null;
|
private static ?MediaHooks $mediaHooks = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Instancia de SettingsPage
|
|
||||||
*/
|
|
||||||
private static ?SettingsPage $settingsPage = null;
|
private static ?SettingsPage $settingsPage = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -70,14 +51,17 @@ class Plugin
|
||||||
// Crear filesystem
|
// Crear filesystem
|
||||||
self::$filesystem = FilesystemFactory::create(self::$config);
|
self::$filesystem = FilesystemFactory::create(self::$config);
|
||||||
|
|
||||||
// Registrar stream wrapper
|
// Registrar stream wrapper con protocolo desde config
|
||||||
FlysystemStreamWrapper::register(self::$filesystem);
|
$protocol = (string)(self::$config['stream']['protocol'] ?? 'flysystem');
|
||||||
|
FlysystemStreamWrapper::register(self::$filesystem, $protocol);
|
||||||
|
|
||||||
// Registrar hooks de medios (instanciar la clase)
|
error_log('[Flysystem Offload] Stream wrappers: ' . implode(', ', stream_get_wrappers()));
|
||||||
|
|
||||||
|
// Registrar hooks de medios
|
||||||
self::$mediaHooks = new MediaHooks(self::$filesystem, self::$config);
|
self::$mediaHooks = new MediaHooks(self::$filesystem, self::$config);
|
||||||
self::$mediaHooks->register();
|
self::$mediaHooks->register();
|
||||||
|
|
||||||
// Registrar página de ajustes (instanciar la clase)
|
// Registrar página de ajustes
|
||||||
if (is_admin()) {
|
if (is_admin()) {
|
||||||
self::$settingsPage = new SettingsPage(self::$filesystem, self::$config);
|
self::$settingsPage = new SettingsPage(self::$filesystem, self::$config);
|
||||||
self::$settingsPage->register();
|
self::$settingsPage->register();
|
||||||
|
|
@ -99,42 +83,21 @@ class Plugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtiene la instancia del Filesystem
|
|
||||||
*
|
|
||||||
* @return Filesystem|null
|
|
||||||
*/
|
|
||||||
public static function getFilesystem(): ?Filesystem
|
public static function getFilesystem(): ?Filesystem
|
||||||
{
|
{
|
||||||
return self::$filesystem;
|
return self::$filesystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtiene la configuración del plugin
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function getConfig(): array
|
public static function getConfig(): array
|
||||||
{
|
{
|
||||||
return self::$config;
|
return self::$config;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifica si el plugin está inicializado
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function isInitialized(): bool
|
public static function isInitialized(): bool
|
||||||
{
|
{
|
||||||
return self::$initialized;
|
return self::$initialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Registra un aviso de administración
|
|
||||||
*
|
|
||||||
* @param string $message Mensaje a mostrar
|
|
||||||
* @param string $type Tipo de aviso (error, warning, info, success)
|
|
||||||
*/
|
|
||||||
private static function registerAdminNotice(string $message, string $type = 'error'): void
|
private static function registerAdminNotice(string $message, string $type = 'error'): void
|
||||||
{
|
{
|
||||||
add_action('admin_notices', static function () use ($message, $type): void {
|
add_action('admin_notices', static function () use ($message, $type): void {
|
||||||
|
|
@ -150,22 +113,19 @@ class Plugin
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reconstruye el filesystem (útil después de cambiar configuración)
|
|
||||||
*
|
|
||||||
* @throws Throwable
|
|
||||||
*/
|
|
||||||
public static function rebuild(): void
|
public static function rebuild(): void
|
||||||
{
|
{
|
||||||
self::$initialized = false;
|
self::$initialized = false;
|
||||||
self::$filesystem = null;
|
self::$filesystem = null;
|
||||||
self::$mediaHooks = null;
|
self::$mediaHooks = null;
|
||||||
self::$settingsPage = null;
|
self::$settingsPage = null;
|
||||||
|
|
||||||
|
$protocol = (string)(self::$config['stream']['protocol'] ?? 'flysystem');
|
||||||
self::$config = [];
|
self::$config = [];
|
||||||
|
|
||||||
// Desregistrar stream wrapper si existe
|
// Desregistrar stream wrapper si existe
|
||||||
if (in_array('fly', stream_get_wrappers(), true)) {
|
if (in_array($protocol, stream_get_wrappers(), true)) {
|
||||||
stream_wrapper_unregister('fly');
|
@stream_wrapper_unregister($protocol);
|
||||||
}
|
}
|
||||||
|
|
||||||
self::bootstrap();
|
self::bootstrap();
|
||||||
|
|
|
||||||
|
|
@ -1,527 +1,296 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace FlysystemOffload\StreamWrapper;
|
namespace FlysystemOffload\StreamWrapper;
|
||||||
|
|
||||||
use FlysystemOffload\Helpers\PathHelper;
|
|
||||||
use League\Flysystem\FilesystemException;
|
|
||||||
use League\Flysystem\FilesystemOperator;
|
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\UnableToWriteFile;
|
||||||
use League\Flysystem\Visibility;
|
|
||||||
|
|
||||||
final class FlysystemStreamWrapper {
|
/**
|
||||||
|
* Stream wrapper para Flysystem
|
||||||
|
* Soporta protocolo configurable (por defecto 'flysystem')
|
||||||
|
*/
|
||||||
|
class FlysystemStreamWrapper
|
||||||
|
{
|
||||||
private static ?FilesystemOperator $filesystem = null;
|
private static ?FilesystemOperator $filesystem = null;
|
||||||
private static string $protocol = 'flysystem';
|
private static string $protocol = 'flysystem';
|
||||||
private static string $rootPrefix = '';
|
|
||||||
private static string $defaultVisibility = Visibility::PUBLIC;
|
|
||||||
|
|
||||||
/** @var resource|null */
|
/** @var resource|null */
|
||||||
private $resource = null;
|
private $stream;
|
||||||
private string $mode = 'r';
|
|
||||||
|
/** @var string Ruta remota normalizada (sin protocolo ni host) */
|
||||||
private string $path = '';
|
private string $path = '';
|
||||||
private string $uri = '';
|
|
||||||
private bool $dirty = false;
|
|
||||||
|
|
||||||
/** @var list<string> */
|
/** @var string Buffer en memoria para modo write */
|
||||||
private array $directoryListing = [];
|
private string $buffer = '';
|
||||||
private int $directoryPosition = 0;
|
|
||||||
|
|
||||||
public static function register(
|
/** @var int Posición del puntero */
|
||||||
FilesystemOperator $filesystem,
|
private int $position = 0;
|
||||||
string $protocol = 'flysystem',
|
|
||||||
string $rootPrefix = '',
|
/** @var string Modo de apertura */
|
||||||
string $defaultVisibility = Visibility::PUBLIC,
|
private string $mode = '';
|
||||||
bool $force = true
|
|
||||||
): void {
|
/** @var array Opciones de contexto */
|
||||||
|
public $context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra el stream wrapper
|
||||||
|
*/
|
||||||
|
public static function register(FilesystemOperator $filesystem, string $protocol = 'flysystem'): void
|
||||||
|
{
|
||||||
self::$filesystem = $filesystem;
|
self::$filesystem = $filesystem;
|
||||||
self::$protocol = $protocol;
|
self::$protocol = $protocol;
|
||||||
self::$rootPrefix = PathHelper::normalize($rootPrefix);
|
|
||||||
self::$defaultVisibility = $defaultVisibility;
|
|
||||||
|
|
||||||
$wrappers = stream_get_wrappers();
|
// Desregistrar si ya existe
|
||||||
if (in_array($protocol, $wrappers, true)) {
|
if (in_array(self::$protocol, stream_get_wrappers(), true)) {
|
||||||
if (! $force) {
|
@stream_wrapper_unregister(self::$protocol);
|
||||||
return;
|
|
||||||
}
|
|
||||||
stream_wrapper_unregister($protocol);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! stream_wrapper_register($protocol, static::class, STREAM_IS_URL)) {
|
$ok = @stream_wrapper_register(self::$protocol, self::class);
|
||||||
throw new \RuntimeException(sprintf('Unable to register stream wrapper for protocol "%s".', $protocol));
|
if ($ok) {
|
||||||
|
error_log('[FlysystemStreamWrapper] Registered protocol: "' . self::$protocol . '"');
|
||||||
|
} else {
|
||||||
|
error_log('[FlysystemStreamWrapper] ERROR registering protocol "' . self::$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;
|
|
||||||
|
|
||||||
error_log(sprintf(
|
|
||||||
'[FlysystemStreamWrapper] stream_open - uri: "%s", path: "%s", mode: "%s"',
|
|
||||||
$this->uri,
|
|
||||||
$this->path,
|
|
||||||
$this->mode
|
|
||||||
));
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
error_log(sprintf(
|
|
||||||
'[FlysystemStreamWrapper] stream_write - path: "%s", bytes: %d, dirty: %s',
|
|
||||||
$this->path,
|
|
||||||
$written !== false ? $written : 0,
|
|
||||||
$this->dirty ? 'true' : 'false'
|
|
||||||
));
|
|
||||||
|
|
||||||
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 {
|
|
||||||
error_log(sprintf(
|
|
||||||
'[FlysystemStreamWrapper] stream_flush - path: "%s", dirty: %s, write_mode: %s',
|
|
||||||
$this->path,
|
|
||||||
$this->dirty ? 'true' : 'false',
|
|
||||||
$this->isWriteMode($this->mode) ? 'true' : 'false'
|
|
||||||
));
|
|
||||||
|
|
||||||
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]
|
|
||||||
);
|
|
||||||
|
|
||||||
error_log(sprintf(
|
|
||||||
'[FlysystemStreamWrapper] stream_flush SUCCESS - path: "%s", size: %d bytes',
|
|
||||||
$this->path,
|
|
||||||
strlen((string) $contents)
|
|
||||||
));
|
|
||||||
|
|
||||||
$this->dirty = false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (UnableToWriteFile|FilesystemException $exception) {
|
|
||||||
error_log(sprintf(
|
|
||||||
'[FlysystemStreamWrapper] stream_flush ERROR - path: "%s", error: %s',
|
|
||||||
$this->path,
|
|
||||||
$exception->getMessage()
|
|
||||||
));
|
|
||||||
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{
|
* Normaliza ruta removiendo protocolo y host
|
||||||
* 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,
|
* Ejemplo: flysystem://uploads/2025/11/file.jpg -> 2025/11/file.jpg
|
||||||
* 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 {
|
private static function normalizePath(string $path): string
|
||||||
return [
|
{
|
||||||
0 => 0,
|
// Remover protocolo
|
||||||
'dev' => 0,
|
$p = preg_replace('#^' . preg_quote(self::$protocol, '#') . '://#', '', $path) ?? '';
|
||||||
1 => 0,
|
|
||||||
'ino' => 0,
|
// Remover host si existe (primer segmento después de //)
|
||||||
2 => $mode,
|
// Ejemplo: flysystem://uploads/2025/11/file.jpg -> 2025/11/file.jpg
|
||||||
'mode' => $mode,
|
$parts = explode('/', ltrim($p, '/'), 2);
|
||||||
3 => $nlink,
|
if (count($parts) === 2) {
|
||||||
'nlink' => $nlink,
|
// Si hay host, devolver solo la parte después del host
|
||||||
4 => 0,
|
return $parts[1];
|
||||||
'uid' => 0,
|
}
|
||||||
5 => 0,
|
|
||||||
'gid' => 0,
|
return ltrim($p, '/');
|
||||||
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 {
|
public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
|
||||||
if (! self::$filesystem) {
|
{
|
||||||
throw new \RuntimeException('Flysystem filesystem has not been registered.');
|
if (!self::$filesystem) {
|
||||||
|
error_log('[FlysystemStreamWrapper] stream_open failed: filesystem not set');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->path = self::normalizePath($path);
|
||||||
|
$this->mode = $mode;
|
||||||
|
$this->buffer = '';
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
error_log(sprintf('[FlysystemStreamWrapper] stream_open path="%s" normalized="%s" mode="%s"', $path, $this->path, $mode));
|
||||||
|
|
||||||
|
// Para lectura, intentar cargar contenido existente
|
||||||
|
if (str_contains($mode, 'r') || str_contains($mode, '+')) {
|
||||||
|
try {
|
||||||
|
$this->buffer = self::$filesystem->read($this->path);
|
||||||
|
error_log('[FlysystemStreamWrapper] Loaded existing file (' . strlen($this->buffer) . ' bytes)');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Si no existe y es modo 'r' puro, fallar
|
||||||
|
if ($mode === 'r') {
|
||||||
|
error_log('[FlysystemStreamWrapper] stream_open read error: ' . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Para otros modos (w, a, etc.) continuar con buffer vacío
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// En modo append, posicionar al final
|
||||||
|
if (str_starts_with($mode, 'a')) {
|
||||||
|
$this->position = strlen($this->buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_write(string $data): int
|
||||||
|
{
|
||||||
|
$len = strlen($data);
|
||||||
|
|
||||||
|
// Insertar data en la posición actual
|
||||||
|
$before = substr($this->buffer, 0, $this->position);
|
||||||
|
$after = substr($this->buffer, $this->position);
|
||||||
|
|
||||||
|
$this->buffer = $before . $data . $after;
|
||||||
|
$this->position += $len;
|
||||||
|
|
||||||
|
error_log(sprintf('[FlysystemStreamWrapper] stream_write %d bytes (buffer now %d bytes)', $len, strlen($this->buffer)));
|
||||||
|
|
||||||
|
return $len;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_read(int $count): string
|
||||||
|
{
|
||||||
|
$chunk = substr($this->buffer, $this->position, $count);
|
||||||
|
$this->position += strlen($chunk);
|
||||||
|
return $chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_tell(): int
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_eof(): bool
|
||||||
|
{
|
||||||
|
return $this->position >= strlen($this->buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_seek(int $offset, int $whence = SEEK_SET): bool
|
||||||
|
{
|
||||||
|
$newPos = $this->position;
|
||||||
|
switch ($whence) {
|
||||||
|
case SEEK_SET:
|
||||||
|
$newPos = $offset;
|
||||||
|
break;
|
||||||
|
case SEEK_CUR:
|
||||||
|
$newPos += $offset;
|
||||||
|
break;
|
||||||
|
case SEEK_END:
|
||||||
|
$newPos = strlen($this->buffer) + $offset;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newPos < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$this->position = $newPos;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_flush(): bool
|
||||||
|
{
|
||||||
|
// Persistir buffer en remoto
|
||||||
|
if ($this->buffer === '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
error_log('[FlysystemStreamWrapper] stream_flush -> write to "' . $this->path . '" (' . strlen($this->buffer) . ' bytes)');
|
||||||
|
self::$filesystem->write($this->path, $this->buffer);
|
||||||
|
return true;
|
||||||
|
} catch (UnableToWriteFile $e) {
|
||||||
|
error_log('[FlysystemStreamWrapper] stream_flush UnableToWriteFile: ' . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log('[FlysystemStreamWrapper] stream_flush error: ' . $e->getMessage());
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function stream_close(): void
|
||||||
|
{
|
||||||
|
// Flush final en close
|
||||||
|
if ($this->buffer !== '' && str_contains($this->mode, 'w') || str_contains($this->mode, 'a') || str_contains($this->mode, '+')) {
|
||||||
|
try {
|
||||||
|
error_log('[FlysystemStreamWrapper] stream_close -> persisting "' . $this->path . '"');
|
||||||
|
self::$filesystem->write($this->path, $this->buffer);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log('[FlysystemStreamWrapper] stream_close error: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->buffer = '';
|
||||||
|
$this->position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_stat(): array
|
||||||
|
{
|
||||||
|
return $this->getStatArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url_stat(string $path, int $flags)
|
||||||
|
{
|
||||||
|
$p = self::normalizePath($path);
|
||||||
|
try {
|
||||||
|
$size = self::$filesystem->fileSize($p);
|
||||||
|
return $this->getStatArray($size);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
if ($flags & STREAM_URL_STAT_QUIET) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unlink(string $path): bool
|
||||||
|
{
|
||||||
|
$p = self::normalizePath($path);
|
||||||
|
try {
|
||||||
|
error_log('[FlysystemStreamWrapper] unlink "' . $p . '"');
|
||||||
|
self::$filesystem->delete($p);
|
||||||
|
return true;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log('[FlysystemStreamWrapper] unlink error: ' . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mkdir(string $path, int $mode, int $options): bool
|
||||||
|
{
|
||||||
|
$p = self::normalizePath($path);
|
||||||
|
try {
|
||||||
|
error_log('[FlysystemStreamWrapper] mkdir "' . $p . '"');
|
||||||
|
self::$filesystem->createDirectory($p);
|
||||||
|
return true;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log('[FlysystemStreamWrapper] mkdir error: ' . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rmdir(string $path, int $options): bool
|
||||||
|
{
|
||||||
|
$p = self::normalizePath($path);
|
||||||
|
try {
|
||||||
|
error_log('[FlysystemStreamWrapper] rmdir "' . $p . '"');
|
||||||
|
self::$filesystem->deleteDirectory($p);
|
||||||
|
return true;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log('[FlysystemStreamWrapper] rmdir error: ' . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rename(string $path_from, string $path_to): bool
|
||||||
|
{
|
||||||
|
$from = self::normalizePath($path_from);
|
||||||
|
$to = self::normalizePath($path_to);
|
||||||
|
try {
|
||||||
|
error_log('[FlysystemStreamWrapper] rename "' . $from . '" -> "' . $to . '"');
|
||||||
|
self::$filesystem->move($from, $to);
|
||||||
|
return true;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log('[FlysystemStreamWrapper] rename error: ' . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getStatArray(int $size = 0): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
0 => 0, 'dev' => 0,
|
||||||
|
1 => 0, 'ino' => 0,
|
||||||
|
2 => 0100666, 'mode' => 0100666,
|
||||||
|
3 => 0, 'nlink' => 0,
|
||||||
|
4 => 0, 'uid' => 0,
|
||||||
|
5 => 0, 'gid' => 0,
|
||||||
|
6 => -1, 'rdev' => -1,
|
||||||
|
7 => $size, 'size' => $size,
|
||||||
|
8 => 0, 'atime' => 0,
|
||||||
|
9 => 0, 'mtime' => 0,
|
||||||
|
10 => 0, 'ctime' => 0,
|
||||||
|
11 => -1, 'blksize' => -1,
|
||||||
|
12 => -1, 'blocks' => -1,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue