Files
filegator/backend/Services/Storage/Filesystem.php
Andrei Telteu 27310f9d48 Added chmod perm, modal to change, api with local/ftp/sftp (#399)
Read a fill current permissions for local driver

Finished permissions for ftp driver, read and change

Read permissions for sftp adapter

Created FilegatorFtp for a cleaner permissions integration

Implemented recursive chmod options for files/folders

Modified tests to cover permissions

Lint frontend permissions component
2024-04-17 17:23:21 +02:00

403 lines
13 KiB
PHP

<?php
/*
* This file is part of the FileGator package.
*
* (c) Milos Stojanovic <alcalbg@gmail.com>
*
* For the full copyright and license information, please view the LICENSE file
*/
namespace Filegator\Services\Storage;
use Filegator\Services\Service;
use League\Flysystem\Filesystem as Flysystem;
use League\Flysystem\Util;
class Filesystem implements Service
{
protected $separator;
protected $storage;
protected $path_prefix;
public function init(array $config = [])
{
$this->separator = $config['separator'];
$this->path_prefix = $this->separator;
$adapter = $config['adapter'];
$config = isset($config['config']) ? $config['config'] : [];
$this->storage = new Flysystem($adapter(), $config);
}
public function createDir(string $path, string $name)
{
$destination = $this->joinPaths($this->applyPathPrefix($path), $name);
while (! empty($this->storage->listContents($destination, true))) {
$destination = $this->upcountName($destination);
}
return $this->storage->createDir($destination);
}
public function createFile(string $path, string $name)
{
$destination = $this->joinPaths($this->applyPathPrefix($path), $name);
while ($this->storage->has($destination)) {
$destination = $this->upcountName($destination);
}
$this->storage->put($destination, '');
}
public function fileExists(string $path)
{
$path = $this->applyPathPrefix($path);
return $this->storage->has($path);
}
public function isDir(string $path)
{
$path = $this->applyPathPrefix($path);
return $this->storage->getSize($path) === false;
}
public function copyFile(string $source, string $destination)
{
$source = $this->applyPathPrefix($source);
$destination = $this->joinPaths($this->applyPathPrefix($destination), $this->getBaseName($source));
while ($this->storage->has($destination)) {
$destination = $this->upcountName($destination);
}
return $this->storage->copy($source, $destination);
}
public function copyDir(string $source, string $destination)
{
$source = $this->applyPathPrefix($this->addSeparators($source));
$destination = $this->applyPathPrefix($this->addSeparators($destination));
$source_dir = $this->getBaseName($source);
$real_destination = $this->joinPaths($destination, $source_dir);
while (! empty($this->storage->listContents($real_destination, true))) {
$real_destination = $this->upcountName($real_destination);
}
$contents = $this->storage->listContents($source, true);
if (empty($contents)) {
$this->storage->createDir($real_destination);
}
foreach ($contents as $file) {
$source_path = $this->separator.ltrim($file['path'], $this->separator);
$path = substr($source_path, strlen($source), strlen($source_path));
if ($file['type'] == 'dir') {
$this->storage->createDir($this->joinPaths($real_destination, $path));
continue;
}
if ($file['type'] == 'file') {
$this->storage->copy($file['path'], $this->joinPaths($real_destination, $path));
}
}
}
public function deleteDir(string $path)
{
return $this->storage->deleteDir($this->applyPathPrefix($path));
}
public function deleteFile(string $path)
{
return $this->storage->delete($this->applyPathPrefix($path));
}
public function readStream(string $path): array
{
if ($this->isDir($path)) {
throw new \Exception('Cannot stream directory');
}
$path = $this->applyPathPrefix($path);
return [
'filename' => $this->getBaseName($path),
'stream' => $this->storage->readStream($path),
'filesize' => $this->storage->getSize($path),
];
}
public function move(string $from, string $to): bool
{
$from = $this->applyPathPrefix($from);
$to = $this->applyPathPrefix($to);
while ($this->storage->has($to)) {
$to = $this->upcountName($to);
}
return $this->storage->rename($from, $to);
}
public function rename(string $destination, string $from, string $to): bool
{
$from = $this->joinPaths($this->applyPathPrefix($destination), $from);
$to = $this->joinPaths($this->applyPathPrefix($destination), $to);
while ($this->storage->has($to)) {
$to = $this->upcountName($to);
}
return $this->storage->rename($from, $to);
}
public function store(string $path, string $name, $resource, bool $overwrite = false): bool
{
$destination = $this->joinPaths($this->applyPathPrefix($path), $name);
while ($this->storage->has($destination)) {
if ($overwrite) {
$this->storage->delete($destination);
} else {
$destination = $this->upcountName($destination);
}
}
return $this->storage->putStream($destination, $resource);
}
/**
* Change file permissions one item, with optional recursion
*
* @param string $path
* @param int $permissions
* @param null|'all'|'folders'|'files' $recursive
* @return bool
* @throws \Exception
*/
public function chmod(string $path, int $permissions, string $recursive = null)
{
$path = $this->applyPathPrefix($path);
$path = Util::normalizePath($path);
$adapter = $this->storage->getAdapter();
$mainResult = $this->chmodItem($path, $permissions);
if ($recursive !== null) {
if (method_exists($adapter, 'setRecurseManually')) {
$adapter->setRecurseManually(true); // this is needed for ftp driver
}
$contents = $this->storage->listContents($path, true);
foreach ($contents as $item) {
try {
if ($item['type'] == 'dir' && ($recursive == 'all' || $recursive == 'folders')) {
$this->chmodItem($item['path'], $permissions);
}
if ($item['type'] == 'file' && ($recursive == 'all' || $recursive == 'files')) {
$this->chmodItem($item['path'], $permissions);
}
} catch (\Exception $e) {
continue;
}
}
}
return $mainResult;
}
/**
* Change file permissions for a single item
*
* @param string $path
* @param int $permissions
* @return bool
* @throws \Exception
*/
public function chmodItem(string $path, int $permissions)
{
$adapter = $this->storage->getAdapter();
switch (get_class($adapter)) {
case 'League\Flysystem\Adapter\Local':
$absolutePath = $adapter->applyPathPrefix($path);
return chmod($absolutePath, octdec($permissions));
break;
case 'League\Flysystem\Sftp\SftpAdapter':
return $adapter->getConnection()->chmod($path, octdec($permissions));
break;
case 'Filegator\Services\Storage\Adapters\FilegatorFtp':
return ftp_chmod($adapter->getConnection(), octdec($permissions), $path) !== false;
break;
default:
throw new \Exception('Selected adapter does not support unix permissions');
break;
}
}
public function setPathPrefix(string $path_prefix)
{
$this->path_prefix = $this->addSeparators($path_prefix);
}
public function getSeparator()
{
return $this->separator;
}
public function getPathPrefix(): string
{
return $this->path_prefix;
}
public function getDirectoryCollection(string $path, bool $recursive = false): DirectoryCollection
{
$collection = new DirectoryCollection($path);
foreach ($this->storage->listContents($this->applyPathPrefix($path), $recursive) as $entry) {
// By default only 'path' and 'type' is present
$name = $this->getBaseName($entry['path']);
$userpath = $this->stripPathPrefix($entry['path']);
$dirname = isset($entry['dirname']) ? $entry['dirname'] : $path;
$size = isset($entry['size']) ? $entry['size'] : 0;
$timestamp = isset($entry['timestamp']) ? $entry['timestamp'] : 0;
$permissions = $this->getPermissions($entry);
$collection->addFile($entry['type'], $userpath, $name, $size, $timestamp, $permissions);
}
if (! $recursive && $this->addSeparators($path) !== $this->separator) {
$collection->addFile('back', $this->getParent($path), '..', 0, 0, -1);
}
return $collection;
}
protected function getPermissions(array $entry): int
{
$adapter = $this->storage->getAdapter();
$path = $entry['path'];
switch (get_class($adapter)) {
case 'League\Flysystem\Adapter\Local':
$path = $adapter->applyPathPrefix($path); // get the full path
$permissions = substr(sprintf('%o', fileperms($path)), -3);
return $permissions;
break;
case 'League\Flysystem\Sftp\SftpAdapter':
$stat = $adapter->getConnection()->stat($path);
return $stat && isset($stat['permissions']) ? substr(decoct($stat['permissions']), -3) : -1;
break;
case 'Filegator\Services\Storage\Adapters\FilegatorFtp':
return isset($entry['permissions']) ? $entry['permissions'] : -1;
break;
}
return -1;
}
protected function upcountCallback($matches)
{
$index = isset($matches[1]) ? intval($matches[1]) + 1 : 1;
$ext = isset($matches[2]) ? $matches[2] : '';
return ' ('.$index.')'.$ext;
}
protected function upcountName($name)
{
return preg_replace_callback(
'/(?:(?: \(([\d]+)\))?(\.[^.]+))?$/',
[$this, 'upcountCallback'],
$name,
1
);
}
private function applyPathPrefix(string $path): string
{
if ($path == '..'
|| strpos($path, '..'.$this->separator) !== false
|| strpos($path, $this->separator.'..') !== false
) {
$path = $this->separator;
}
return $this->joinPaths($this->getPathPrefix(), $path);
}
private function stripPathPrefix(string $path): string
{
$path = $this->separator.ltrim($path, $this->separator);
if (substr($path, 0, strlen($this->getPathPrefix())) == $this->getPathPrefix()) {
$path = $this->separator.substr($path, strlen($this->getPathPrefix()));
}
return $path;
}
private function addSeparators(string $dir): string
{
if (! $dir || $dir == $this->separator || ! trim($dir, $this->separator)) {
return $this->separator;
}
return $this->separator.trim($dir, $this->separator).$this->separator;
}
private function joinPaths(string $path1, string $path2): string
{
$path1 = $this->escapeDots($path1);
$path2 = $this->escapeDots($path2);
if (! $path2 || ! trim($path2, $this->separator)) {
return $this->addSeparators($path1);
}
return $this->addSeparators($path1).ltrim($path2, $this->separator);
}
private function getParent(string $dir): string
{
if (! $dir || $dir == $this->separator || ! trim($dir, $this->separator)) {
return $this->separator;
}
$tmp = explode($this->separator, trim($dir, $this->separator));
array_pop($tmp);
return $this->separator.trim(implode($this->separator, $tmp), $this->separator);
}
private function getBaseName(string $path): string
{
if (! $path || $path == $this->separator || ! trim($path, $this->separator)) {
return $this->separator;
}
$tmp = explode($this->separator, trim($path, $this->separator));
return (string) array_pop($tmp);
}
private function escapeDots(string $path): string
{
$path = preg_replace('/\\\+\.{2,}/', '', $path);
$path = preg_replace('/\.{2,}\\\+/', '', $path);
$path = preg_replace('/\/+\.{2,}/', '', $path);
$path = preg_replace('/\.{2,}\/+/', '', $path);
return $path;
}
}