Skip to content

Commit

Permalink
Filesystem: Consistent method names, Write streams, WP_Uploaded_Direc…
Browse files Browse the repository at this point in the history
…tory_Tree_Filesystem
  • Loading branch information
adamziel committed Jan 1, 2025
1 parent 8bc9dc3 commit 5840909
Show file tree
Hide file tree
Showing 7 changed files with 470 additions and 66 deletions.
101 changes: 92 additions & 9 deletions src/WordPress/Filesystem/WP_Abstract_Filesystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace WordPress\Filesystem;

use WordPress\ByteReader\WP_Byte_Reader;

/**
* Abstract class for filesystem implementations.
*
Expand Down Expand Up @@ -40,16 +42,16 @@ abstract public function is_file($path);
*
* @example
*
* $fs->open_file_stream($path);
* $fs->open_read_stream($path);
* while($fs->next_file_chunk()) {
* $chunk = $fs->get_file_chunk();
* // process $chunk
* }
* $fs->close_file_stream();
* $fs->close_read_stream();
*
* @param string $path The path to the file.
*/
abstract public function open_file_stream($path);
abstract public function open_read_stream($path);

/**
* Get the next chunk of a file.
Expand Down Expand Up @@ -77,21 +79,102 @@ abstract public function get_streamed_file_length();
*
* @return string|false The error message or false if no error occurred.
*/
abstract public function get_error_message();
abstract public function get_last_error();

/**
* Close the file reader.
*/
abstract public function close_file_stream();
abstract public function close_read_stream();

// @TODO: Support for write methods, perhaps in a separate interface?
// abstract public function append_to($path, $data);
// abstract public function overwrite($path, $data);
// abstract public function open_write_stream($path);
// abstract public function append_bytes($data);
// abstract public function close_write_stream();
// abstract public function rename($old_path, $new_path);
// abstract public function mkdir($path);
// abstract public function rm($path);
// abstract public function rmdir($path, $options = []);

public function put_contents($path, $data, $options = []) {
if(!$this->open_write_stream($path)) {
return false;
}
if(is_string($data)) {
if(!$this->append_bytes($data)) {
return false;
}
} else if(is_object($data) && $data instanceof WP_Byte_Reader) {
while($data->next_chunk()) {
if(!$this->append_bytes($data->get_chunk())) {
return false;
}
}
} else {
_doing_it_wrong(__METHOD__, 'Invalid $data argument provided. Expected a string or a WP_Byte_Reader instance. Received: ' . gettype($data), '1.0.0');
return false;
}
if(!$this->close_write_stream()) {
return false;
}
return true;
}

public function copy($from_path, $to_path, $options = []) {
$to_fs = $options['to_fs'] ?? $this;
$recursive = $options['recursive'] ?? false;
if($this->is_dir($from_path) && !$recursive) {
_doing_it_wrong( __METHOD__, 'Cannot copy a directory without recursive => true option', '1.0.0' );
return false;
}

$stack = [[$from_path, $to_path]];
while(!empty($stack)) {
[$from_path, $to_path] = array_shift($stack);
if($this->is_dir($from_path)) {
if(!$to_fs->is_dir($to_path)) {
$to_fs->mkdir($to_path);
}
foreach($this->ls($from_path) as $child) {
$stack[] = [
wp_join_paths($from_path, $child),
wp_join_paths($to_path, $child)
];
}
} else {
if(false === $this->open_read_stream($from_path)) {
throw new \Exception('Failed to open read stream for ' . $from_path);
return false;
}
if(false === $to_fs->open_write_stream($to_path)) {
throw new \Exception('Failed to open write stream for ' . $to_path);
return false;
}
$chunks_written = 0;
while($this->next_file_chunk()) {
if(false === $to_fs->append_bytes($this->get_file_chunk())) {
throw new \Exception('Failed to append bytes to ' . $to_path);
return false;
}
$chunks_written++;
}
if($chunks_written === 0) {
// Make sure the file receives at least one chunk
// so we can be sure it gets created in case the
// destination filesystem is lazy.
$to_fs->append_bytes('');
}
if(false === $this->close_read_stream()) {
throw new \Exception('Failed to close read stream for ' . $from_path);
return false;
}
if(false === $to_fs->close_write_stream()) {
throw new \Exception('Failed to close write stream for ' . $to_path);
return false;
}
}
}
return true;
}

/**
* Buffers the entire contents of a file into a string
Expand All @@ -101,7 +184,7 @@ abstract public function close_file_stream();
* @return string|false The contents of the file or false if the file does not exist.
*/
public function get_contents($path) {
$this->open_file_stream($path);
$this->open_read_stream($path);
$body = '';
while($this->next_file_chunk()) {
$chunk = $this->get_file_chunk();
Expand All @@ -110,7 +193,7 @@ public function get_contents($path) {
}
$body .= $chunk;
}
$this->close_file_stream();
$this->close_read_stream();
return $body;
}

Expand Down
89 changes: 89 additions & 0 deletions src/WordPress/Filesystem/WP_Filesystem_Chroot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace WordPress\Filesystem;

/**
* A filesystem wrapper that chroot's the filesystem to a specific path.
*/
class WP_Filesystem_Chroot extends WP_Abstract_Filesystem {

/**
* @var WP_Abstract_Filesystem
*/
private $fs;

/**
* @var string
*/
private $root;

/**
* @param WP_Abstract_Filesystem $fs The filesystem to chroot.
* @param string $root The root path to chroot to.
*/
public function __construct(WP_Abstract_Filesystem $fs, $root) {
$this->fs = $fs;
$this->root = rtrim($root, '/');
}

public function exists($path) {
return $this->fs->exists($this->to_chrooted_path($path));
}

public function is_file($path) {
return $this->fs->is_file($this->to_chrooted_path($path));
}

public function is_dir($path) {
return $this->fs->is_dir($this->to_chrooted_path($path));
}

public function mkdir($path, $options = []) {
return $this->fs->mkdir($this->to_chrooted_path($path), $options);
}

public function rmdir($path, $options = []) {
return $this->fs->rmdir($this->to_chrooted_path($path), $options);
}

public function ls($path = '/') {
return $this->fs->ls($this->to_chrooted_path($path));
}

public function open_read_stream($path) {
return $this->fs->open_read_stream($this->to_chrooted_path($path));
}

public function next_file_chunk() {
return $this->fs->next_file_chunk();
}

public function get_file_chunk() {
return $this->fs->get_file_chunk();
}

public function get_streamed_file_length() {
return $this->fs->get_streamed_file_length();
}

public function get_last_error() {
return $this->fs->get_last_error();
}

public function close_read_stream() {
return $this->fs->close_read_stream();
}

public function get_contents($path) {
return $this->fs->get_contents($this->to_chrooted_path($path));
}

public function put_contents($path, $contents, $options = []) {
return $this->fs->put_contents($this->to_chrooted_path($path), $contents, $options);
}

private function to_chrooted_path($path) {
return wp_join_paths($this->root, $path);
}

}
57 changes: 51 additions & 6 deletions src/WordPress/Filesystem/WP_In_Memory_Filesystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class WP_In_Memory_Filesystem extends WP_Abstract_Filesystem {

private $files = [];
private $last_file_reader = null;
private $write_stream = null;

public function __construct() {
$this->files['/'] = [
Expand All @@ -18,35 +19,39 @@ public function __construct() {
}

public function ls($parent = '/') {
$parent = rtrim($parent, '/');
$parent = wp_canonicalize_path($parent);
if (!isset($this->files[$parent]) || $this->files[$parent]['type'] !== 'dir') {
return false;
}
return array_keys($this->files[$parent]['contents']);
}

public function is_dir($path) {
$path = wp_canonicalize_path($path);
return isset($this->files[$path]) && $this->files[$path]['type'] === 'dir';
}

public function is_file($path) {
$path = wp_canonicalize_path($path);
return isset($this->files[$path]) && $this->files[$path]['type'] === 'file';
}

public function exists($path) {
$path = wp_canonicalize_path($path);
return isset($this->files[$path]);
}

private function get_parent_dir($path) {
$path = rtrim($path, '/');
$path = wp_canonicalize_path($path);
$parent = dirname($path);
if($parent === '.') {
return '/';
}
return $parent;
}

public function open_file_stream($path) {
public function open_read_stream($path) {
$path = wp_canonicalize_path($path);
if($this->last_file_reader) {
$this->last_file_reader->close();
}
Expand Down Expand Up @@ -78,14 +83,14 @@ public function get_streamed_file_length() {
return $this->last_file_reader->length();
}

public function get_error_message() {
public function get_last_error() {
if(!$this->last_file_reader) {
return false;
}
return $this->last_file_reader->get_last_error();
}

public function close_file_stream() {
public function close_read_stream() {
if(!$this->last_file_reader) {
return false;
}
Expand All @@ -95,6 +100,8 @@ public function close_file_stream() {
}

public function rename($old_path, $new_path) {
$old_path = wp_canonicalize_path($old_path);
$new_path = wp_canonicalize_path($new_path);
if (!$this->exists($old_path)) {
return false;
}
Expand All @@ -110,6 +117,7 @@ public function rename($old_path, $new_path) {
}

public function mkdir($path) {
$path = wp_canonicalize_path($path);
if ($this->exists($path)) {
return false;
}
Expand All @@ -128,6 +136,7 @@ public function mkdir($path) {
}

public function rm($path) {
$path = wp_canonicalize_path($path);
if (!$this->is_file($path)) {
return false;
}
Expand All @@ -139,6 +148,7 @@ public function rm($path) {
}

public function rmdir($path, $options = []) {
$path = wp_canonicalize_path($path);
$recursive = $options['recursive'] ?? false;
if (!$this->is_dir($path)) {
return false;
Expand All @@ -161,7 +171,8 @@ public function rmdir($path, $options = []) {
return true;
}

public function put_contents($path, $data) {
public function put_contents($path, $data, $options = []) {
$path = wp_canonicalize_path($path);
$parent = $this->get_parent_dir($path);
if (!$this->is_dir($parent)) {
return false;
Expand All @@ -175,4 +186,38 @@ public function put_contents($path, $data) {
return true;
}

public function open_write_stream($path) {
if($this->write_stream) {
_doing_it_wrong(__METHOD__, 'Cannot open a new write stream while another write stream is open.', '1.0.0');
return false;
}
$this->write_stream = [
'path' => $path,
'contents' => '',
];
return true;
}

public function append_bytes($data) {
if(!$this->write_stream) {
_doing_it_wrong(__METHOD__, 'Cannot append bytes to a write stream that is not open.', '1.0.0');
return false;
}
$path = $this->write_stream['path'];
if(!isset($this->files[$path])) {
$this->put_contents($path, '');
}
$this->files[$path]['contents'] .= $data;
return true;
}

public function close_write_stream() {
if(!$this->write_stream) {
_doing_it_wrong(__METHOD__, 'Cannot close a write stream that is not open.', '1.0.0');
return false;
}
$this->write_stream = null;
return true;
}

}
Loading

0 comments on commit 5840909

Please sign in to comment.