gcs-8.x-1.0-alpha3/src/GcsFileSystem/GcsFileSystem.php
src/GcsFileSystem/GcsFileSystem.php
<?php
namespace Drupal\gcs\GcsFileSystem;
use Drupal\Component\FileSystem\FileSystem as FileSystemComponent;
use Drupal\Core\File\FileSystem;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\File\Exception\DirectoryNotReadyException;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\Exception\FileExistsException;
use Drupal\Core\File\Exception\FileNotExistsException;
use Drupal\Core\File\Exception\FileWriteException;
use Drupal\Core\File\Exception\NotRegularFileException;
use Drupal\Core\Site\Settings;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Psr\Log\LoggerInterface;
use Google\Cloud\Storage\StorageClient;
/**
* Defines GcsFileSystem class.
*/
class GcsFileSystem extends FileSystem implements FileSystemInterface {
/**
* Default mode for new directories. See self::chmod().
*/
const CHMOD_DIRECTORY = 0775;
/**
* Default mode for new files. See self::chmod().
*/
const CHMOD_FILE = 0664;
/**
* The site settings.
*
* @var \Drupal\Core\Site\Settings
*/
protected $settings;
/**
* The file logger channel.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The stream wrapper manager.
*
* @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
*/
protected $streamWrapperManager;
/**
* Constructs a new FileSystem.
*
* @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
* The stream wrapper manager.
* @param \Drupal\Core\Site\Settings $settings
* The site settings.
* @param \Psr\Log\LoggerInterface $logger
* The file logger channel.
*/
public function __construct(StreamWrapperManagerInterface $stream_wrapper_manager, Settings $settings, LoggerInterface $logger) {
$this->streamWrapperManager = $stream_wrapper_manager;
$this->settings = $settings;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public function moveUploadedFile($filename, $uri) {
error_log(__LINE__);
$result = @move_uploaded_file($filename, $uri);
// PHP's move_uploaded_file() does not properly support streams if
// open_basedir is enabled so if the move failed, try finding a real path
// and retry the move operation.
if (!$result) {
if ($realpath = $this
->realpath($uri)) {
$result = move_uploaded_file($filename, $realpath);
}
else {
$result = move_uploaded_file($filename, $uri);
}
}
return $result;
}
/**
* {@inheritdoc}
*/
public function chmod($uri, $mode = NULL) {
error_log(__LINE__);
if (!isset($mode)) {
if (is_dir($uri)) {
$mode = $this->settings
->get('file_chmod_directory', static::CHMOD_DIRECTORY);
}
else {
$mode = $this->settings
->get('file_chmod_file', static::CHMOD_FILE);
}
}
if (@chmod($uri, $mode)) {
return TRUE;
}
$this->logger
->error('The file permissions could not be set on %uri.', [
'%uri' => $uri,
]);
return FALSE;
}
/**
* {@inheritdoc}
*/
public function unlink($uri, $context = NULL) {
$this->gcs_delete_object($uri);
return TRUE;
}
/**
* {@inheritdoc}
*/
public function realpath($uri) {
error_log(__LINE__);
// If this URI is a stream, pass it off to the appropriate stream wrapper.
// Otherwise, attempt PHP's realpath. This allows use of this method even
// for unmanaged files outside of the stream wrapper interface.
if ($wrapper = $this->streamWrapperManager
->getViaUri($uri)) {
return $wrapper
->realpath();
}
return realpath($uri);
}
/**
* {@inheritdoc}
*/
public function dirname($uri) {
error_log(__LINE__);
$scheme = StreamWrapperManager::getScheme($uri);
if ($this->streamWrapperManager
->isValidScheme($scheme)) {
return $this->streamWrapperManager
->getViaScheme($scheme)
->dirname($uri);
}
else {
return dirname($uri);
}
}
/**
* {@inheritdoc}
* @todo annnnnnd this is where I stopped.
*/
public function mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
error_log(__LINE__);
if (!isset($mode)) {
$mode = $this->settings
->get('file_chmod_directory', static::CHMOD_DIRECTORY);
}
// If the URI has a scheme, don't override the umask - schemes can handle
// this issue in their own implementation.
if (StreamWrapperManager::getScheme($uri)) {
return $this
->mkdirCall($uri, $mode, $recursive, $context);
}
// If recursive, create each missing component of the parent directory
// individually and set the mode explicitly to override the umask.
if ($recursive) {
// Ensure the path is using DIRECTORY_SEPARATOR, and trim off any trailing
// slashes because they can throw off the loop when creating the parent
// directories.
$uri = rtrim(str_replace('/', DIRECTORY_SEPARATOR, $uri), DIRECTORY_SEPARATOR);
// Determine the components of the path.
$components = explode(DIRECTORY_SEPARATOR, $uri);
// If the filepath is absolute the first component will be empty as there
// will be nothing before the first slash.
if ($components[0] == '') {
$recursive_path = DIRECTORY_SEPARATOR;
// Get rid of the empty first component.
array_shift($components);
}
else {
$recursive_path = '';
}
// Don't handle the top-level directory in this loop.
array_pop($components);
// Create each component if necessary.
foreach ($components as $component) {
$recursive_path .= $component;
if (!file_exists($recursive_path)) {
if (!$this
->mkdirCall($recursive_path, $mode, FALSE, $context)) {
return FALSE;
}
// Not necessary to use self::chmod() as there is no scheme.
if (!chmod($recursive_path, $mode)) {
return FALSE;
}
}
$recursive_path .= DIRECTORY_SEPARATOR;
}
}
// Do not check if the top-level directory already exists, as this condition
// must cause this function to fail.
if (!$this
->mkdirCall($uri, $mode, FALSE, $context)) {
return FALSE;
}
// Not necessary to use self::chmod() as there is no scheme.
return chmod($uri, $mode);
}
/**
* Helper function. Ensures we don't pass a NULL as a context resource to
* mkdir().
*
* @see self::mkdir()
*/
protected function mkdirCall($uri, $mode, $recursive, $context) {
error_log(__LINE__);
if (is_null($context)) {
return mkdir($uri, $mode, $recursive);
}
else {
return mkdir($uri, $mode, $recursive, $context);
}
}
/**
* {@inheritdoc}
*/
public function rmdir($uri, $context = NULL) {
$this->gcs_delete_object($uri);
return TRUE;
}
/**
* {@inheritdoc}
*/
public function tempnam($directory, $prefix) {
error_log(__LINE__);
$scheme = StreamWrapperManager::getScheme($directory);
if ($this->streamWrapperManager
->isValidScheme($scheme)) {
$wrapper = $this->streamWrapperManager
->getViaScheme($scheme);
if ($filename = tempnam($wrapper
->getDirectoryPath(), $prefix)) {
return $scheme . '://' . static::basename($filename);
}
else {
return FALSE;
}
}
else {
// Handle as a normal tempnam() call.
return tempnam($directory, $prefix);
}
}
/**
* {@inheritdoc}
*/
public function uriScheme($uri) {
error_log(__LINE__);
@trigger_error('FileSystem::uriScheme() is deprecated in drupal:8.8.0. It will be removed from drupal:9.0.0. Use \\Drupal\\Core\\StreamWrapper\\StreamWrapperManagerInterface::getScheme() instead. See https://www.drupal.org/node/3035273', E_USER_DEPRECATED);
return StreamWrapperManager::getScheme($uri);
}
/**
* {@inheritdoc}
*/
public function validScheme($scheme) {
error_log(__LINE__);
@trigger_error('FileSystem::validScheme() is deprecated in drupal:8.8.0 and will be removed before drupal:9.0.0. Use \\Drupal\\Core\\StreamWrapper\\StreamWrapperManagerInterface::isValidScheme() instead. See https://www.drupal.org/node/3035273', E_USER_DEPRECATED);
return $this->streamWrapperManager
->isValidScheme($scheme);
}
/**
* {@inheritdoc}
*/
public function copy($source, $destination, $replace = self::EXISTS_RENAME) {
error_log(__LINE__);
$this
->prepareDestination($source, $destination, $replace);
if (!@copy($source, $destination)) {
// If the copy failed and realpaths exist, retry the operation using them
// instead.
$real_source = $this
->realpath($source) ?: $source;
$real_destination = $this
->realpath($destination) ?: $destination;
if ($real_source === FALSE || $real_destination === FALSE || !@copy($real_source, $real_destination)) {
$this->logger
->error("The specified file '%source' could not be copied to '%destination'.", [
'%source' => $source,
'%destination' => $destination,
]);
throw new FileWriteException("The specified file '{$source}' could not be copied to '{$destination}'.");
}
}
// Set the permissions on the new file.
$this
->chmod($destination);
return $destination;
}
/**
* {@inheritdoc}
*/
public function deleteRecursive($path, callable $callback = NULL) {
return parent::deleteRecursive($path, $callback);
if ($callback) {
call_user_func($callback, $path);
}
if (is_dir($path)) {
$dir = dir($path);
while (($entry = $dir
->read()) !== FALSE) {
if ($entry == '.' || $entry == '..') {
continue;
}
$entry_path = $path . '/' . $entry;
$this
->deleteRecursive($entry_path, $callback);
}
$dir
->close();
return $this
->rmdir($path);
}
return $this
->delete($path);
}
/**
* {@inheritdoc}
*/
public function move($source, $destination, $replace = self::EXISTS_RENAME) {
error_log(__LINE__);
$this
->prepareDestination($source, $destination, $replace);
// Ensure compatibility with Windows.
// @see \Drupal\Core\File\FileSystemInterface::unlink().
if (!$this->streamWrapperManager
->isValidUri($source) && substr(PHP_OS, 0, 3) == 'WIN') {
chmod($source, 0600);
}
// Attempt to resolve the URIs. This is necessary in certain
// configurations (see above) and can also permit fast moves across local
// schemes.
$real_source = $this
->realpath($source) ?: $source;
$real_destination = $this
->realpath($destination) ?: $destination;
// Perform the move operation.
if (!@rename($real_source, $real_destination)) {
// Fall back to slow copy and unlink procedure. This is necessary for
// renames across schemes that are not local, or where rename() has not
// been implemented. It's not necessary to use FileSystem::unlink() as the
// Windows issue has already been resolved above.
if (!@copy($real_source, $real_destination)) {
$this->logger
->error("The specified file '%source' could not be moved to '%destination'.", [
'%source' => $source,
'%destination' => $destination,
]);
throw new FileWriteException("The specified file '{$source}' could not be moved to '{$destination}'.");
}
if (!@unlink($real_source)) {
$this->logger
->error("The source file '%source' could not be unlinked after copying to '%destination'.", [
'%source' => $source,
'%destination' => $destination,
]);
throw new FileException("The source file '{$source}' could not be unlinked after copying to '{$destination}'.");
}
}
// Set the permissions on the new file.
$this
->chmod($destination);
return $destination;
}
/**
* Prepares the destination for a file copy or move operation.
*
* - Checks if $source and $destination are valid and readable/writable.
* - Checks that $source is not equal to $destination; if they are an error
* is reported.
* - If file already exists in $destination either the call will error out,
* replace the file or rename the file based on the $replace parameter.
*
* @param string $source
* A string specifying the filepath or URI of the source file.
* @param string|null $destination
* A URI containing the destination that $source should be moved/copied to.
* The URI may be a bare filepath (without a scheme) and in that case the
* default scheme (file://) will be used.
* @param int $replace
* Replace behavior when the destination file already exists:
* - FileSystemInterface::EXISTS_REPLACE - Replace the existing file.
* - FileSystemInterface::EXISTS_RENAME - Append _{incrementing number}
* until the filename is unique.
* - FileSystemInterface::EXISTS_ERROR - Do nothing and return FALSE.
*
* @see \Drupal\Core\File\FileSystemInterface::copy()
* @see \Drupal\Core\File\FileSystemInterface::move()
*/
protected function prepareDestination($source, &$destination, $replace) {
error_log(__LINE__);
$original_source = $source;
if (!file_exists($source)) {
if (($realpath = $this
->realpath($original_source)) !== FALSE) {
$this->logger
->error("File '%original_source' ('%realpath') could not be copied because it does not exist.", [
'%original_source' => $original_source,
'%realpath' => $realpath,
]);
throw new FileNotExistsException("File '{$original_source}' ('{$realpath}') could not be copied because it does not exist.");
}
else {
$this->logger
->error("File '%original_source' could not be copied because it does not exist.", [
'%original_source' => $original_source,
]);
throw new FileNotExistsException("File '{$original_source}' could not be copied because it does not exist.");
}
}
// Prepare the destination directory.
if ($this
->prepareDirectory($destination)) {
// The destination is already a directory, so append the source basename.
$destination = $this->streamWrapperManager
->normalizeUri($destination . '/' . $this
->basename($source));
}
else {
// Perhaps $destination is a dir/file?
$dirname = $this
->dirname($destination);
if (!$this
->prepareDirectory($dirname)) {
$this->logger
->error("The specified file '%original_source' could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions.", [
'%original_source' => $original_source,
]);
throw new DirectoryNotReadyException("The specified file '{$original_source}' could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions.");
}
}
// Determine whether we can perform this operation based on overwrite rules.
$destination = $this
->getDestinationFilename($destination, $replace);
if ($destination === FALSE) {
$this->logger
->error("File '%original_source' could not be copied because a file by that name already exists in the destination directory ('%destination').", [
'%original_source' => $original_source,
'%destination' => $destination,
]);
throw new FileExistsException("File '{$original_source}' could not be copied because a file by that name already exists in the destination directory ('{$destination}').");
}
// Assert that the source and destination filenames are not the same.
$real_source = $this
->realpath($source);
$real_destination = $this
->realpath($destination);
if ($source == $destination || $real_source !== FALSE && $real_source == $real_destination) {
$this->logger
->error("File '%source' could not be copied because it would overwrite itself.", [
'%source' => $source,
]);
throw new FileException("File '{$source}' could not be copied because it would overwrite itself.");
}
}
/**
* {@inheritdoc}
*/
public function saveData($data, $destination, $replace = self::EXISTS_RENAME) {
error_log(__LINE__);
// Write the data to a temporary file.
$temp_name = $this
->tempnam('temporary://', 'file');
if (file_put_contents($temp_name, $data) === FALSE) {
$this->logger
->error("Temporary file '%temp_name' could not be created.", [
'%temp_name' => $temp_name,
]);
throw new FileWriteException("Temporary file '{$temp_name}' could not be created.");
}
// Move the file to its final destination.
return $this
->move($temp_name, $destination, $replace);
}
/**
* {@inheritdoc}
*/
public function prepareDirectory(&$directory, $options = self::MODIFY_PERMISSIONS) {
if (!$this->streamWrapperManager
->isValidUri($directory)) {
// Only trim if we're not dealing with a stream.
$directory = rtrim($directory, '/\\');
}
if (!$this->gcs_is_dir($directory)) {
// Let mkdir() recursively create directories and use the default
// directory permissions.
if ($options & static::CREATE_DIRECTORY) {
return @$this
->mkdir($directory, NULL, TRUE);
}
return FALSE;
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getDestinationFilename($destination, $replace) {
error_log(__LINE__);
$basename = $this
->basename($destination);
if (!Unicode::validateUtf8($basename)) {
throw new FileException(sprintf("Invalid filename '%s'", $basename));
}
if (file_exists($destination)) {
switch ($replace) {
case FileSystemInterface::EXISTS_REPLACE:
// Do nothing here, we want to overwrite the existing file.
break;
case FileSystemInterface::EXISTS_RENAME:
$directory = $this
->dirname($destination);
$destination = $this
->createFilename($basename, $directory);
break;
case FileSystemInterface::EXISTS_ERROR:
// Error reporting handled by calling function.
return FALSE;
}
}
return $destination;
}
/**
* {@inheritdoc}
*/
public function createFilename($basename, $directory) {
error_log(__LINE__);
$original = $basename;
// Strip control characters (ASCII value < 32). Though these are allowed in
// some filesystems, not many applications handle them well.
$basename = preg_replace('/[\\x00-\\x1F]/u', '_', $basename);
if (preg_last_error() !== PREG_NO_ERROR) {
throw new FileException(sprintf("Invalid filename '%s'", $original));
}
if (substr(PHP_OS, 0, 3) == 'WIN') {
// These characters are not allowed in Windows filenames.
$basename = str_replace([
':',
'*',
'?',
'"',
'<',
'>',
'|',
], '_', $basename);
}
// A URI or path may already have a trailing slash or look like "public://".
if (substr($directory, -1) == '/') {
$separator = '';
}
else {
$separator = '/';
}
$destination = $directory . $separator . $basename;
if (file_exists($destination)) {
// Destination file already exists, generate an alternative.
$pos = strrpos($basename, '.');
if ($pos !== FALSE) {
$name = substr($basename, 0, $pos);
$ext = substr($basename, $pos);
}
else {
$name = $basename;
$ext = '';
}
$counter = 0;
do {
$destination = $directory . $separator . $name . '_' . $counter++ . $ext;
} while (file_exists($destination));
}
return $destination;
}
/**
* Internal function to handle directory scanning with recursion.
*
* @param string $dir
* The base directory or URI to scan, without trailing slash.
* @param string $mask
* The preg_match() regular expression for files to be included.
* @param array $options
* The options as per ::scanDirectory().
* @param int $depth
* The current depth of recursion.
*
* @return array
* An associative array as per ::scanDirectory().
*
* @throws \Drupal\Core\File\Exception\NotRegularDirectoryException
* If the directory does not exist.
*
* @see \Drupal\Core\File\FileSystemInterface::scanDirectory()
*/
protected function doScanDirectory($dir, $mask, array $options = [], $depth = 0) {
$public_prefix = 'public://';
if(substr($dir, 0, strlen($public_prefix)) != $public_prefix) {
return parent::doScanDirectory($dir, $mask, $options, $depth);
}
$config = \Drupal::config('gcs.settings');
$storage = new StorageClient([
'projectId' => $config->get('gcs.project_id'),
]);
$prefix = substr($dir, strlen($public_prefix), strlen($dir));
$bucket = $storage->bucket($config->get('gcs.bucket_name'));
$files = [];
$gcs_options = [];
if($prefix) {
$gcs_options = ['prefix' => $prefix];
}
foreach ($bucket->objects($gcs_options) as $object) {
// Skip this file if it matches the nomask or starts with a dot.
if ($object->name()[0] != '.' && !preg_match($options['nomask'], $object->name())) {
$uri = "{$public_prefix}{$object->name()}";
if ($options['recurse'] && $prefix == $object->name()) {
continue;
}
elseif ($options['recurse'] && $prefix . '/' == $object->name()) {
// Give priority to files in this folder by merging them in after
// any subdirectory files.
$files = array_merge($this
->doScanDirectory($uri, $mask, $options, $depth + 1), $files);
}
elseif ($depth >= $options['min_depth'] && preg_match($mask, $object->name())) {
// Always use this match over anything already set in $files with
// the same $options['key'].
$file_pathinfo = pathinfo($object->name());
$filename = $file_pathinfo['basename'];
$file = new \stdClass();
$file->uri = $uri;
$file->filename = $filename;
$file->name = $filename;
$key = $options['key'];
$files[$file->{$key}] = $file;
if ($options['callback']) {
$options['callback']($uri);
}
}
}
}
return $files;
}
/**
* GCS Delete object
*/
function gcs_delete_object($uri) {
$parsed_url = parse_url($uri);
$path_to_delete = $parsed_url['host'] . $parsed_url['path'];
$config = \Drupal::config('gcs.settings');
try{
$storage = new StorageClient([
'projectId' => $config->get('gcs.project_id'),
]);
$bucket = $storage->bucket($config->get('gcs.bucket_name'));
$object = $bucket->object($path_to_delete);
$object->delete();
}
catch(\Exception $e) {
// error_log($e);
return FALSE;
}
return TRUE;
}
/**
* GCS Delete object
*/
function gcs_is_dir($uri) {
$parsed_url = parse_url($uri);
$path = $parsed_url['host'] . $parsed_url['path'];
$config = \Drupal::config('gcs.settings');
try{
$storage = new StorageClient([
'projectId' => $config->get('gcs.project_id'),
]);
$bucket = $storage->bucket($config->get('gcs.bucket_name'));
$object = $bucket->object($path);
if($object) {
return TRUE;
}
else {
return FALSE;
}
}
catch(\Exception $e) {
return FALSE;
}
}
}
