<?php
declare(strict_types=1);
namespace League\Flysystem\PhpseclibV3;
use League\Flysystem\Config;
use League\Flysystem\DirectoryAttributes;
use League\Flysystem\FileAttributes;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\FilesystemException;
use League\Flysystem\PathPrefixer;
use League\Flysystem\StorageAttributes;
use League\Flysystem\UnableToCheckDirectoryExistence;
use League\Flysystem\UnableToCheckFileExistence;
use League\Flysystem\UnableToCopyFile;
use League\Flysystem\UnableToCreateDirectory;
use League\Flysystem\UnableToMoveFile;
use League\Flysystem\UnableToReadFile;
use League\Flysystem\UnableToRetrieveMetadata;
use League\Flysystem\UnableToSetVisibility;
use League\Flysystem\UnableToWriteFile;
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
use League\Flysystem\UnixVisibility\VisibilityConverter;
use League\MimeTypeDetection\FinfoMimeTypeDetector;
use League\MimeTypeDetection\MimeTypeDetector;
use phpseclib3\Net\SFTP;
use Throwable;
use function rtrim;
class SftpAdapter implements FilesystemAdapter
{
private VisibilityConverter $visibilityConverter;
private PathPrefixer $prefixer;
private MimeTypeDetector $mimeTypeDetector;
public function __construct(
private ConnectionProvider $connectionProvider,
string $root,
VisibilityConverter $visibilityConverter = null,
MimeTypeDetector $mimeTypeDetector = null
) {
$this->prefixer = new PathPrefixer($root);
$this->visibilityConverter = $visibilityConverter ?: new PortableVisibilityConverter();
$this->mimeTypeDetector = $mimeTypeDetector ?: new FinfoMimeTypeDetector();
}
public function fileExists(string $path): bool
{
$location = $this->prefixer->prefixPath($path);
try {
return $this->connectionProvider->provideConnection()->is_file($location);
} catch (Throwable $exception) {
throw UnableToCheckFileExistence::forLocation($path, $exception);
}
}
public function directoryExists(string $path): bool
{
$location = $this->prefixer->prefixDirectoryPath($path);
try {
return $this->connectionProvider->provideConnection()->is_dir($location);
} catch (Throwable $exception) {
throw UnableToCheckDirectoryExistence::forLocation($path, $exception);
}
}
/**
* @param string $path
* @param string|resource $contents
* @param Config $config
*
* @throws FilesystemException
*/
private function upload(string $path, $contents, Config $config): void
{
$this->ensureParentDirectoryExists($path, $config);
$connection = $this->connectionProvider->provideConnection();
$location = $this->prefixer->prefixPath($path);
if ( ! $connection->put($location, $contents, SFTP::SOURCE_STRING)) {
throw UnableToWriteFile::atLocation($path, 'not able to write the file');
}
if ($visibility = $config->get(Config::OPTION_VISIBILITY)) {
$this->setVisibility($path, $visibility);
}
}
private function ensureParentDirectoryExists(string $path, Config $config): void
{
$parentDirectory = dirname($path);
if ($parentDirectory === '' || $parentDirectory === '.') {
return;
}
/** @var string $visibility */
$visibility = $config->get(Config::OPTION_DIRECTORY_VISIBILITY);
$this->makeDirectory($parentDirectory, $visibility);
}
private function makeDirectory(string $directory, ?string $visibility): void
{
$location = $this->prefixer->prefixPath($directory);
$connection = $this->connectionProvider->provideConnection();
if ($connection->is_dir($location)) {
return;
}
$mode = $visibility ? $this->visibilityConverter->forDirectory(
$visibility
) : $this->visibilityConverter->defaultForDirectories();
if ( ! $connection->mkdir($location, $mode, true) && ! $connection->is_dir($location)) {
throw UnableToCreateDirectory::atLocation($directory);
}
}
public function write(string $path, string $contents, Config $config): void
{
try {
$this->upload($path, $contents, $config);
} catch (UnableToWriteFile $exception) {
throw $exception;
} catch (Throwable $exception) {
throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception);
}
}
public function writeStream(string $path, $contents, Config $config): void
{
try {
$this->upload($path, $contents, $config);
} catch (UnableToWriteFile $exception) {
throw $exception;
} catch (Throwable $exception) {
throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception);
}
}
public function read(string $path): string
{
$location = $this->prefixer->prefixPath($path);
$connection = $this->connectionProvider->provideConnection();
$contents = $connection->get($location);
if ( ! is_string($contents)) {
throw UnableToReadFile::fromLocation($path);
}
return $contents;
}
public function readStream(string $path)
{
$location = $this->prefixer->prefixPath($path);
$connection = $this->connectionProvider->provideConnection();
/** @var resource $readStream */
$readStream = fopen('php://temp', 'w+');
if ( ! $connection->get($location, $readStream)) {
fclose($readStream);
throw UnableToReadFile::fromLocation($path);
}
rewind($readStream);
return $readStream;
}
public function delete(string $path): void
{
$location = $this->prefixer->prefixPath($path);
$connection = $this->connectionProvider->provideConnection();
$connection->delete($location);
}
public function deleteDirectory(string $path): void
{
$location = rtrim($this->prefixer->prefixPath($path), '/') . '/';
$connection = $this->connectionProvider->provideConnection();
$connection->delete($location);
$connection->rmdir($location);
}
public function createDirectory(string $path, Config $config): void
{
$this->makeDirectory($path, $config->get(Config::OPTION_DIRECTORY_VISIBILITY, $config->get(Config::OPTION_VISIBILITY)));
}
public function setVisibility(string $path, string $visibility): void
{
$location = $this->prefixer->prefixPath($path);
$connection = $this->connectionProvider->provideConnection();
$mode = $this->visibilityConverter->forFile($visibility);
if ( ! $connection->chmod($mode, $location, false)) {
throw UnableToSetVisibility::atLocation($path);
}
}
private function fetchFileMetadata(string $path, string $type): FileAttributes
{
$location = $this->prefixer->prefixPath($path);
$connection = $this->connectionProvider->provideConnection();
$stat = $connection->stat($location);
if ( ! is_array($stat)) {
throw UnableToRetrieveMetadata::create($path, $type);
}
$attributes = $this->convertListingToAttributes($path, $stat);
if ( ! $attributes instanceof FileAttributes) {
throw UnableToRetrieveMetadata::create($path, $type, 'path is not a file');
}
return $attributes;
}
public function mimeType(string $path): FileAttributes
{
try {
$contents = $this->read($path);
$mimetype = $this->mimeTypeDetector->detectMimeType($path, $contents);
} catch (Throwable $exception) {
throw UnableToRetrieveMetadata::mimeType($path, $exception->getMessage(), $exception);
}
if ($mimetype === null) {
throw UnableToRetrieveMetadata::mimeType($path, 'Unknown.');
}
return new FileAttributes($path, null, null, null, $mimetype);
}
public function lastModified(string $path): FileAttributes
{
return $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_LAST_MODIFIED);
}
public function fileSize(string $path): FileAttributes
{
return $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_FILE_SIZE);
}
public function visibility(string $path): FileAttributes
{
return $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_VISIBILITY);
}
public function listContents(string $path, bool $deep): iterable
{
$connection = $this->connectionProvider->provideConnection();
$location = $this->prefixer->prefixPath(rtrim($path, '/')) . '/';
$listing = $connection->rawlist($location, false);
if (false === $listing) {
return;
}
foreach ($listing as $filename => $attributes) {
if ($filename === '.' || $filename === '..') {
continue;
}
// Ensure numeric keys are strings.
$filename = (string) $filename;
$path = $this->prefixer->stripPrefix($location . ltrim($filename, '/'));
$attributes = $this->convertListingToAttributes($path, $attributes);
yield $attributes;
if ($deep && $attributes->isDir()) {
foreach ($this->listContents($attributes->path(), true) as $child) {
yield $child;
}
}
}
}
private function convertListingToAttributes(string $path, array $attributes): StorageAttributes
{
$permissions = $attributes['mode'] & 0777;
$lastModified = $attributes['mtime'] ?? null;
if (($attributes['type'] ?? null) === NET_SFTP_TYPE_DIRECTORY) {
return new DirectoryAttributes(
ltrim($path, '/'),
$this->visibilityConverter->inverseForDirectory($permissions),
$lastModified
);
}
return new FileAttributes(
$path,
$attributes['size'],
$this->visibilityConverter->inverseForFile($permissions),
$lastModified
);
}
public function move(string $source, string $destination, Config $config): void
{
$sourceLocation = $this->prefixer->prefixPath($source);
$destinationLocation = $this->prefixer->prefixPath($destination);
$connection = $this->connectionProvider->provideConnection();
try {
$this->ensureParentDirectoryExists($destination, $config);
} catch (Throwable $exception) {
throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);
}
if ( ! $connection->rename($sourceLocation, $destinationLocation)) {
throw UnableToMoveFile::fromLocationTo($source, $destination);
}
}
public function copy(string $source, string $destination, Config $config): void
{
try {
$readStream = $this->readStream($source);
$visibility = $this->visibility($source)->visibility();
$this->writeStream($destination, $readStream, new Config(compact('visibility')));
} catch (Throwable $exception) {
if (isset($readStream) && is_resource($readStream)) {
@fclose($readStream);
}
throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);
}
}
}