diff --git a/src/File.php b/src/File.php index 7b7d5b1..b0c0650 100644 --- a/src/File.php +++ b/src/File.php @@ -4,36 +4,49 @@ namespace Ambimax\File; -use RuntimeException; +use Symfony\Component\Filesystem\Path; class File implements FileInterface { + public const MODE_READ = 'r'; + public const MODE_READ_PLUS = 'r+'; + public const MODE_WRITE = 'w'; + public const MODE_WRITE_PLUS = 'w+'; /** * @var resource */ protected $fileHandle; protected string $filePath; + protected string $mode; + /** + * @param self::MODE_* $mode + */ public function __construct(string $filePath, string $mode) { $this->openStream($filePath, $mode); - $this->filePath = $filePath; } protected function openStream(string $filePath, string $mode): void { - if (!file_exists($filePath)) { - throw new RuntimeException("File '$filePath' does not exist."); + if ( + !in_array($mode, [ + self::MODE_WRITE, + self::MODE_WRITE_PLUS, + ]) && !file_exists($filePath) + ) { + throw new \RuntimeException("File '$filePath' does not exist."); } $tmpFileHandle = fopen($filePath, $mode); if (false === $tmpFileHandle) { - throw new RuntimeException("Could not open file '$filePath'."); + throw new \RuntimeException("Could not open file '$filePath'."); } $this->fileHandle = $tmpFileHandle; $this->filePath = $filePath; + $this->mode = $mode; } public function __destruct() @@ -53,19 +66,94 @@ public function getBasename(): string return basename($this->filePath); } + /** + * @return resource + */ + public function getFileHandle() + { + return $this->fileHandle; + } + /** * @return false|string */ public function getContent() { - return stream_get_contents($this->fileHandle); + $pointerLocation = ftell($this->fileHandle); + if (false === $pointerLocation) { + throw new \RuntimeException('unable to get current location of the file pointer. exception thrown to prevent unpredictable pointer jumping'); + } + rewind($this->fileHandle); + + $content = stream_get_contents($this->fileHandle); + fseek($this->fileHandle, $pointerLocation); + + return $content; } /** - * @return resource + * Changes relative path to an absolut path relative to the current file location. + * This is to prevent confusion of e.g. rename() where the path would be relative to the php working directory. + * If the path is already absolute or has a protocol defined the path won't be affected by this. + * + * If the path ends with the directory separator ("/") the current file name will get appended */ - public function getFileHandle() + protected function ensureAbsolutePath(string $path): string { - return $this->fileHandle; + if ( + true === Path::isLocal($path) && + false === Path::isAbsolute($path) + ) { + $path = Path::join(dirname($this->filePath), $path); + } + + if (DIRECTORY_SEPARATOR === $path[-1]) { + $path = Path::join($path, $this->getBasename()); + } + + return $path; + } + + public function rename(string $newPath): bool + { + $newPath = $this->ensureAbsolutePath($newPath); + + fclose($this->fileHandle); + + $success = rename($this->filePath, $newPath); + $this->openStream($newPath, $this->mode); + + return $success; + } + + public function move(string $newPath): bool + { + return $this->rename($newPath); + } + + /** + * @param int<0, max>|null $length + */ + public function fwrite(string $data, ?int $length = null): int|false + { + return fwrite($this->fileHandle, $data, $length); + } + + /** + * @param int<0, max>|null $length + */ + public function fread(?int $length = null): string|false + { + return fread($this->fileHandle, $length); + } + + public function ftell(): int|false + { + return ftell($this->fileHandle); + } + + public function fseek(int $offset, int $whence = SEEK_SET): int + { + return fseek($this->fileHandle, $offset, $whence); } } diff --git a/src/FileInterface.php b/src/FileInterface.php index 3d7e97a..924157b 100644 --- a/src/FileInterface.php +++ b/src/FileInterface.php @@ -6,11 +6,6 @@ interface FileInterface { - /** - * @return false|string - */ - public function getContent(); - public function getPath(): string; public function getBasename(): string; @@ -19,4 +14,21 @@ public function getBasename(): string; * @return resource */ public function getFileHandle(); + + /** + * @return false|string + */ + public function getContent(); + + public function rename(string $newPath): bool; + + public function move(string $newPath): bool; + + public function fwrite(string $data, ?int $length = null): int|false; + + public function fread(?int $length = null): string|false; + + public function ftell(): int|false; + + public function fseek(int $offset, int $whence = SEEK_SET): int; } diff --git a/src/Remote/SftpFile.php b/src/Remote/SftpFile.php index 784714e..0e28d36 100644 --- a/src/Remote/SftpFile.php +++ b/src/Remote/SftpFile.php @@ -5,20 +5,26 @@ namespace Ambimax\File\Remote; use Ambimax\File\File; +use phpseclib3\Net\SFTP; use phpseclib3\Net\SFTP\Stream; -use RuntimeException; class SftpFile extends File { protected string $hostname; protected string $username; protected string $password; + protected SFTP $sftp; + /** + * @param File::MODE_* $mode + */ public function __construct(string $hostname, string $username, string $password, string $filePath, string $mode) { $this->hostname = $hostname; $this->username = $username; $this->password = $password; + $this->sftp = new SFTP($hostname); + $this->sftp->login($username, $password); parent::__construct($filePath, $mode); } @@ -34,9 +40,23 @@ protected function openStream(string $filePath, string $mode): void $mode); if (false === $tmpFileHandle) { - throw new RuntimeException(sprintf('Could not open file \'%s\'.', $filePath)); + throw new \RuntimeException(sprintf('Could not open file \'%s\'.', $filePath)); } $this->fileHandle = $tmpFileHandle; + $this->filePath = $filePath; + $this->mode = $mode; + } + + public function rename(string $newPath): bool + { + $newPath = $this->ensureAbsolutePath($newPath); + + fclose($this->fileHandle); + + $success = $this->sftp->rename($this->filePath, $newPath); + $this->openStream($newPath, $this->mode); + + return $success; } } diff --git a/src/Test/FileTest.php b/src/Test/FileTest.php index ba40d8e..e73ae7e 100644 --- a/src/Test/FileTest.php +++ b/src/Test/FileTest.php @@ -56,4 +56,36 @@ public function testGetFileHandle(): void $this->assertIsResource($this->readableFile->getFileHandle()); $this->assertIsNotClosedResource($this->readableFile->getFileHandle()); } + + public function testRename(): void + { + $oldFileHandle = $this->readableFile->getFileHandle(); + + $this->readableFile->rename('newtest'); + $this->assertSame($this->root->url().'/newtest', $this->readableFile->getPath()); + + $newFileHandle = $this->readableFile->getFileHandle(); + $this->assertSame('testContent', stream_get_contents($newFileHandle)); + + $this->assertIsClosedResource($oldFileHandle); + } + + public function testMoveToDifferentFolderAbsolutePath(): void + { + $oldFileHandle = $this->readableFile->getFileHandle(); + + mkdir($this->root->url().'/testFolder'); + $this->readableFile->rename($this->root->url().'/testFolder/'); + $this->assertSame($this->root->url().'/testFolder/test', $this->readableFile->getPath()); + $this->assertFileExists($this->root->url().'/testFolder/test'); + + $this->readableFile->rename('../testFile'); + $this->assertSame($this->root->url().'/testFile', $this->readableFile->getPath()); + $this->assertFileExists($this->root->url().'/testFile'); + + $newFileHandle = $this->readableFile->getFileHandle(); + $this->assertSame('testContent', stream_get_contents($newFileHandle)); + + $this->assertIsClosedResource($oldFileHandle); + } }