gestione documentale avanzata 1 parte

This commit is contained in:
2026-05-28 09:34:28 +02:00
parent 3471befb1a
commit f2b0833b90
34482 changed files with 4312269 additions and 546 deletions
+25
View File
@@ -0,0 +1,25 @@
{
"name": "as247/cloud-storages",
"description": "Storages wrapper with path support for drive services: google drive, onedrive,...",
"keywords": ["php", "storage", "drive","google","google drive","one drive"],
"homepage": "https://github.com/as247",
"license": "MIT",
"authors": [
{
"name": "As247",
"email": "as247@vui360.com",
"homepage": "http://as247.vui360.com"
}
],
"require": {
"php": ">=7.1",
"ext-json": "*"
},
"autoload": {
"psr-4": {
"As247\\CloudStorages\\": "src/"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
+4
View File
@@ -0,0 +1,4 @@
###### This package used to support for google drive and onedrive package
- [as247/flysystem-google-drive](https://packagist.org/packages/as247/flysystem-google-drive)
- [as247/flysystem-onedrive](https://packagist.org/packages/as247/flysystem-onedrive)
- [as247/flysystem-alist](https://packagist.org/packages/as247/flysystem-alist)
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace As247\CloudStorages\Cache;
use As247\CloudStorages\Cache\Stores\ArrayStore;
use As247\CloudStorages\Cache\Stores\GoogleDriveStore;
use As247\CloudStorages\Contracts\Cache\PathStore;
/**
* Class PathCache
* @package As247\CloudStorages\Cache
* @mixin PathStore
* @mixin GoogleDriveStore
* @mixin ArrayStore
*/
class PathCache
{
protected $store;
public function __construct(PathStore $store=null)
{
if($store==null) {
$store=new ArrayStore();
}
$this->store = $store;
}
/**
* @return ArrayStore|GoogleDriveStore|PathStore|null
*/
public function getStore(){
return $this->store;
}
public function __call($name, $arguments)
{
return $this->store->$name(...$arguments);
}
}
@@ -0,0 +1,162 @@
<?php
namespace As247\CloudStorages\Cache\Stores;
use As247\CloudStorages\Contracts\Cache\PathStore;
use As247\CloudStorages\Support\Path;
use RuntimeException;
class ArrayStore implements PathStore
{
protected $files = [];
protected $completed = [];
public function query($path, $deep = 1)
{
$directory = Path::clean($path);
$results = [];
$dirSegCount = Path::countSegments($directory);
foreach ($this->files as $path => $file) {
if (strpos($path, $directory) === 0) {
if ($deep>0) {
if ($path!==$directory && (Path::countSegments($path) - $dirSegCount <= $deep)) {
$results[$path] = $file;
}
}else{
$results[$path] = $file;
}
}
}
return $results;
}
public function get($key)
{
$key = Path::clean($key);
return $this->files[$key] ?? null;
}
public function put($key, $data, $seconds = 3600)
{
$key = Path::clean($key);
if($data===null){
unset($this->files[$key]);
$this->complete($key,false);
}else {
$this->files[$key] = $data;
}
}
public function flush()
{
$root = $this->get('/');
$this->files = [];
$this->put('/', $root);
$this->completed = [];
}
public function forget($path)
{
$this->put($path,null,-1);
}
public function forever($key, $value)
{
$this->put($key,$value,315360000);
}
public function forgetBranch($path)
{
$tmpPath = Path::clean($path);
do {
if(!$this->get($tmpPath)){
$this->forget($tmpPath);//Forget false item only
}
$this->complete($tmpPath,false);
}while(($tmpPath = Path::clean(dirname($tmpPath))) && $tmpPath !== '/');
}
public function delete($path)
{
$this->put($path,false);
}
public function deleteBranch($path)
{
$tmpPath = Path::clean($path);
do{
$this->put($tmpPath, false);
}
while (($tmpPath = Path::clean(dirname($tmpPath))) && $tmpPath !== '/');
}
public function forgetDir($path)
{
$path=Path::clean($path);
foreach ($this->query($path,0) as $key => $file) {
$this->forget($key);
}
$this->complete($path,false);
}
public function deleteDir($path)
{
$path=Path::clean($path);
foreach ($this->query($path,0) as $key => $file) {
$this->put($key, false);
}
$this->complete($path,false);
}
function move($from,$to)
{
if(!$from || !$to){
throw new RuntimeException("Invalid path $from -> $to");
}
$from=Path::clean($from);
$to=Path::clean($to);
//Destination tree changed we should clean up all parent
//This need for onedrive because we not keep track of parents in cache
$this->forgetBranch($to);
foreach ($this->query($from,0) as $path => $file ){
$newPath=Path::replace($from, $to, $path);
if($path!==$newPath){
$this->put($newPath,$file);
$this->put($path,false);
}
}
foreach ($this->getCompleted($from) as $key => $value) {
$newKey = Path::replace($from, $to, $key);
if ($newKey !== $key) {
$this->complete($newKey,$value);
$this->complete($key,false);
}
}
}
public function getCompleted($path){
return $this->completed;
}
public function complete($path, $isCompleted = true)
{
$path = Path::clean($path);
if($isCompleted) {
$this->completed[$path] = $isCompleted;
}else{
$path=Path::clean($path);
foreach ($this->getCompleted($path) as $key => $value) {
if (strpos($key, $path) === 0) {
unset($this->completed[$key]);
}
}
}
}
public function isCompleted($path)
{
$path = Path::clean($path);
return $this->completed[$path] ?? false;
}
}
@@ -0,0 +1,73 @@
<?php
namespace As247\CloudStorages\Cache\Stores;
use As247\CloudStorages\Support\Path;
class GoogleDrivePersistentStore extends GoogleDriveStore
{
protected $sqliteCache;
public function __construct($cacheFile)
{
$this->sqliteCache=new SqliteCache($cacheFile);
}
public function put($key, $data, $seconds = 3600)
{
$key=Path::clean($key);
if(!$data){
//parent::put($key,$data);//Keep false item in object cache
if(is_null($data)){
$this->sqliteCache->forget($key);
$this->complete($key,false);
}else {
$this->sqliteCache->put($key, $data);
}
}else{
//parent::put($key,null);
$this->sqliteCache->forever($key,$data);
}
}
public function get($key)
{
if(null!==($parentValue=parent::get($key))){
return $parentValue;
}
$key=Path::clean($key);
$cache= $this->sqliteCache->get($key);
return $cache;
}
function query($path, $deep = 1)
{
$path=Path::clean($path);
$directory=$path;
if($deep>=1) {
//Like /dirname/% and not like /dirname/%/%
$directory = $path === '/' ? $path : $path . '/';
}//Like /dirname%
$list=$this->sqliteCache->pathQuery($directory,$deep);
if($deep===0){
return $list;
}
unset($list[$path]);
return $list;
}
public function complete($path, $isCompleted = true)
{
$path=Path::clean($path);
return $this->sqliteCache->complete($path,$isCompleted);
}
public function isCompleted($path)
{
$path=Path::clean($path);
return $this->sqliteCache->isCompleted($path); // TODO: Change the autogenerated stub
}
public function getCompleted($path)
{
$path=Path::clean($path);
return $this->sqliteCache->getCompleted($path); // TODO: Change the autogenerated stub
}
}
@@ -0,0 +1,32 @@
<?php
namespace As247\CloudStorages\Cache\Stores;
use As247\CloudStorages\Service\GoogleDrive;
use As247\CloudStorages\Support\Path;
use Google_Service_Drive_DriveFile;
class GoogleDriveStore extends ArrayStore
{
function mapDirectory($path,$id){
return $this->mapFile($path,$id,GoogleDrive::DIR_MIME);
}
function mapFile($path,$id,$mimeType=''){
$path=Path::clean($path);
if(!$id){
$this->forget($path);
return $this;
}
$file=$id;
if(!$file instanceof Google_Service_Drive_DriveFile){
$file = new Google_Service_Drive_DriveFile();
$file->setId($id);
$file->setMimeType($mimeType);
$file->setPermissions([]);
}
$this->forever($path,$file);
return $this;
}
}
@@ -0,0 +1,86 @@
<?php
namespace As247\CloudStorages\Cache\Stores;
use As247\CloudStorages\Contracts\Cache\PathStore;
class NullStore implements PathStore
{
public function forgetBranch($path)
{
// TODO: Implement forgetBranch() method.
}
public function delete($path)
{
// TODO: Implement delete() method.
}
public function deleteBranch($path)
{
// TODO: Implement deleteBranch() method.
}
public function forgetDir($path)
{
// TODO: Implement forgetDir() method.
}
public function deleteDir($path)
{
// TODO: Implement deleteDir() method.
}
public function move($source, $destination)
{
// TODO: Implement move() method.
}
public function query($path, $match = '*')
{
// TODO: Implement query() method.
}
public function complete($path, $isCompleted = true)
{
// TODO: Implement complete() method.
}
public function isCompleted($path)
{
// TODO: Implement isCompleted() method.
}
public function getCompleted($path)
{
// TODO: Implement getCompleted() method.
}
public function put($path, $data, $seconds = 3600)
{
// TODO: Implement put() method.
}
public function forever($path, $value)
{
// TODO: Implement forever() method.
}
public function get($path)
{
// TODO: Implement get() method.
}
public function forget($path)
{
// TODO: Implement forget() method.
}
public function flush()
{
// TODO: Implement flush() method.
}
}
@@ -0,0 +1,240 @@
<?php
namespace As247\CloudStorages\Cache\Stores;
use As247\CloudStorages\Contracts\Cache\Store;
use Exception;
use PDO;
class SqliteCache implements Store
{
protected $pdo;
/**
* SqliteCache constructor.
* @param null $dataFile
* @throws Exception
*/
public function __construct($dataFile=null)
{
if($dataFile===null){
$dataFile=sys_get_temp_dir().'/'.md5(static::class).'';
}
$isNewDB=!file_exists($dataFile);
$this->pdo=new PDO('sqlite:' . $dataFile);
if($isNewDB) {
$this->createTable();
}else{
$this->checkMalformed();
}
}
/**
* @throws Exception
*/
protected function checkMalformed(){
$this->pdo->prepare("select 1 from cache where 0=1");
$error = $this->pdo->errorInfo();
if($error[0] !=='00000'){
throw new Exception(sprintf("SQLSTATE[%s]: Error [%s] %s",$error[0],$error[1],$error[2]));
}
}
protected function createTable(){
$this->pdo->query("
CREATE TABLE IF NOT EXISTS `cache` (
`key` varchar(500) not null,
`value` text not null,
`expiration` integer(11),
PRIMARY KEY (`key`)
)
");
$this->pdo->query("
CREATE TABLE IF NOT EXISTS `completed` (
`path` varchar(500) not null,
`expiration` integer(11),
PRIMARY KEY (`path`)
)
");
}
public function get($key){
$statement=$this->pdo->prepare("SELECT * FROM cache WHERE key=? limit 1");
$statement->bindValue(1,$key);
$statement->execute();
$cache=$statement->fetch(PDO::FETCH_OBJ);
if(!$cache){
return null;
}
if ($this->currentTime() >= $cache->expiration) {
$this->forget($key);
return null;
}
return unserialize($cache->value);
}
public function put($key, $value, $seconds=3600){
$value=serialize($value);
$statement=$this->pdo->prepare(
"insert into cache (`key`,`value`,`expiration`) values (?,?,?)"
);
if($seconds===-1){
$expire=2147483647;
}else{
$expire=$this->currentTime()+$seconds;
}
$statement->bindValue(1,$key);
$statement->bindValue(2,$value);
$statement->bindValue(3,$expire);
if(!$statement->execute()){
$statement=$this->pdo->prepare("UPDATE cache SET value=:value,expiration=:expiration WHERE key=:key");
$statement->bindValue(':key',$key);
$statement->bindValue(':value',$value);
$statement->bindValue(':expiration',$expire);
return $statement->execute();
}else{
return true;
}
}
public function forget($key){
$statement=$this->pdo->prepare("DELETE FROM cache WHERE key=?");
$statement->bindValue(1,$key);
$statement->execute();
return true;
}
/**
* Store an item in the cache indefinitely.
*
* @param string $path
* @param mixed $id
* @return bool
*/
public function forever($path, $value)
{
return $this->put($path, $value, -1);
}
public function getCompleted($key=''){
if($key) {
$statement = $this->pdo->prepare("SELECT * FROM completed WHERE path like ?");
$statement->bindValue(1, $key . '%');
}else{
$statement = $this->pdo->prepare("SELECT * FROM completed");
}
$statement->execute();
$allRecords=$statement->fetchAll(PDO::FETCH_OBJ);
if(!$allRecords){
return [];
}
$results=[];
foreach ($allRecords as $cache) {
if ($this->currentTime() >= $cache->expiration) {
$this->forget($key);
}else{
$results[$cache->path]=true;
}
}
return $results;
}
public function isCompleted($key){
$statement=$this->pdo->prepare("SELECT * FROM completed WHERE path=? limit 1");
$statement->bindValue(1,$key);
$statement->execute();
$cache=$statement->fetch(PDO::FETCH_OBJ);
if(!$cache){
return false;
}
if ($this->currentTime() >= $cache->expiration) {
$this->complete($key,false);
return false;
}
return true;
}
public function complete($key, $completed=true, $seconds=3600){
if($completed){
$statement=$this->pdo->prepare(
"insert into completed (`path`,`expiration`) values (?,?)"
);
if($seconds===-1){
$expire=2147483647;
}else{
$expire=$this->currentTime()+$seconds;
}
$statement->bindValue(1,$key);
$statement->bindValue(2,$expire);
if(!$statement->execute()){
$statement=$this->pdo->prepare("UPDATE completed SET expiration=:expiration WHERE path=:key");
$statement->bindValue(':key',$key);
$statement->bindValue(':expiration',$expire);
return $statement->execute();
}else{
return true;
}
}else{
return $this->forgetComplete($key);
}
}
protected function forgetComplete($key){
$key.='%';
$statement=$this->pdo->prepare("DELETE FROM completed WHERE path like ?");
$statement->bindValue(1,$key);
return $statement->execute();
}
public function flush(){
$statement=$this->pdo->prepare("DELETE FROM cache");
$statement->execute();
return true;
}
public function clearExpires(){
$statement1=$this->pdo->prepare("DELETE FROM cache WHERE expiration < ?");
$statement2=$this->pdo->prepare("DELETE FROM completed WHERE expiration < ?");
$statement1->bindValue(1,$this->currentTime());
$statement2->bindValue(1,$this->currentTime());
$statement1->execute();
$statement2->execute();
}
public function getPdo(){
return $this->pdo;
}
public function pathQuery($key, $deep=1){
$like=$key.'%';
$notLike='';
if($deep>0){
$notLike=$like;
while($deep-->0){
$notLike.='/%';
}
}
if($notLike){
$statement=$this->pdo->prepare("SELECT * FROM cache WHERE key like ? and key not like ?");
$statement->bindValue(2,$notLike);
}else{
$statement=$this->pdo->prepare("SELECT * FROM cache WHERE key like ?");
}
$statement->bindValue(1,$like);
//$statement->bindValue(2,$notLike);
$statement->execute();
$allRecords=$statement->fetchAll(PDO::FETCH_OBJ);
if(!$allRecords){
return [];
}
$results=[];
foreach ($allRecords as $cache) {
if ($this->currentTime() >= $cache->expiration) {
$this->forget($key);
}else{
$results[$cache->key]=unserialize($cache->value);
}
}
return $results;
}
/**
* Get the current system time as a UNIX timestamp.
*
* @return int
*/
protected function currentTime()
{
return time();
}
}
+105
View File
@@ -0,0 +1,105 @@
<?php
namespace As247\CloudStorages\Cache;
use RuntimeException;
class TempCache
{
protected $cacheDir;
function __construct($key)
{
$name=md5(serialize(func_get_args()));
$this->cacheDir=sys_get_temp_dir().'/'.$name;
}
function get($key){
return $this->getPayload($key)['data'] ?? null;
}
function put($key,$value,$expires=3600){
$path=$this->path($key);
if($this->ensureCacheDir()) {
file_put_contents($path,
serialize([
'data' => $value,
'expires' => $expires,
'created'=>time(),
]));
}
}
protected function ensureCacheDir(){
if(is_dir($this->cacheDir)){
return true;
}
@$created=mkdir($this->cacheDir, 0777, true);
if(!$created){
@unlink($this->cacheDir);//It may be a file
@$created=mkdir($this->cacheDir, 0777, true);
}
if(!$created){
throw new RuntimeException('Could not create directory '.$this->cacheDir);
}
return $created;
}
/**
* Retrieve an item and expiry time from the cache by key.
*
* @param string $key
* @return array
*/
protected function getPayload(string $key)
{
$path = $this->path($key);
$payload=[];
if(file_exists($path) && is_file($path)){
$content=file_get_contents($path);
if($content) {
$payload = unserialize($content);
if(!isset($payload['data']) || !isset($payload['expires']) || !isset($payload['created'])){
return [];
}
if($payload['expires']> 0 && $payload['created']+$payload['expires'] < time()){
return [];
}
}
}
return $payload;
}
protected function path($key){
$key=md5($key);
return $this->cacheDir.'/'.$key;
}
public function has($key)
{
return !empty($this->getPayload($key));
}
public function forget($key)
{
$path=$this->path($key);
@unlink($path);
}
public function forever($key, $value)
{
$this->put($key,$value,0);
}
public function flush()
{
if(!is_dir($this->cacheDir)){
return ;
}
if ($dh = opendir($this->cacheDir)) {
while (($file = readdir($dh)) !== false) {
if($file!=='.' && $file !=='..'){
unlink($this->cacheDir.'/'.$file);
}
}
closedir($dh);
}
}
}
@@ -0,0 +1,79 @@
<?php
namespace As247\CloudStorages\Contracts\Cache;
interface PathStore extends Store
{
/**
* Forget path and its parent
* @param $path
* @return mixed
*/
public function forgetBranch($path);
/**
* Set false value for $path
* @param $path
* @return mixed
*/
public function delete($path);
/**
* Delete path and all its parents
* @param $path
* @return mixed
*/
public function deleteBranch($path);
/**
* Forget a path and all its children
* eg if forget /a then /a/b /a/b/c also removed
* @param $path
* @return mixed
*/
public function forgetDir($path);
/**
* Set false value for $path and all existing children
* Eg: If deleteDir('/a') is called and '/a/b','/a/c/e.txt' exists in cache
* Then all of them set to false
* @param $path
* @return mixed
*/
public function deleteDir($path);
/**
* Simulate rename function, we need to move all value from $source tree to $destination
*
* @param $source
* @param $destination
* @return mixed
*/
public function move($source, $destination);
/**
* Query for matching path
* @param $path
* @param string|int $match * content in current directory ** include subdirectory
* @return mixed
*/
public function query($path, $match = '*');
/**
* Mark the path is completed that mean nothing under this path is outside cache, used for listing
* @param $path
* @param bool $isCompleted
* @return mixed
*/
public function complete($path, $isCompleted = true);
/**
* Check if current path is completed
* @param $path
* @return mixed
*/
public function isCompleted($path);
public function getCompleted($path);
}
@@ -0,0 +1,47 @@
<?php
namespace As247\CloudStorages\Contracts\Cache;
interface Store
{
/**
* Put value to cache, update if existing
* @param $path
* @param $data
* @param int $seconds
* @return mixed
*/
public function put($path, $data, $seconds=3600);
/**
* Never expire cache
* @param $path
* @param $value
* @return mixed
*/
public function forever($path, $value);
/**
* Get or return default
* @param $path
* @return mixed
*/
public function get($path);
/**
* For get a path
* @param $path
* @return mixed
*/
public function forget($path);
/**
* Flush cache
* @return mixed
*/
public function flush();
}
@@ -0,0 +1,44 @@
<?php
namespace As247\CloudStorages\Contracts\Storage;
use As247\CloudStorages\Exception\FileNotFoundException;
interface ObjectStorage
{
/**
* @param string $urn the unified resource name used to identify the object
* @return resource stream with the read data
* @throws \Exception when something goes wrong, message will be logged
* @throws FileNotFoundException if file does not exist
* @since 1.0.15
*/
public function readObject($urn);
/**
* @param string $urn the unified resource name used to identify the object
* @param resource $stream stream with the data to write
* @throws \Exception when something goes wrong, message will be logged
* @since 1.0.15
*/
public function writeObject($urn, $stream);
/**
* @param string $urn the unified resource name used to identify the object
* @return void
* @throws \Exception when something goes wrong, message will be logged
* @since 1.0.15
*/
public function deleteObject($urn);
/**
* Check if an object exists in the object store
*
* @param string $urn
* @return bool
* @since 1.0.15
*/
public function objectExists($urn);
}
@@ -0,0 +1,129 @@
<?php
namespace As247\CloudStorages\Contracts\Storage;
use As247\CloudStorages\Exception\FileNotFoundException;
use As247\CloudStorages\Exception\InvalidVisibilityProvided;
use As247\CloudStorages\Exception\UnableToCopyFile;
use As247\CloudStorages\Exception\UnableToCreateDirectory;
use As247\CloudStorages\Exception\UnableToDeleteDirectory;
use As247\CloudStorages\Exception\UnableToDeleteFile;
use As247\CloudStorages\Exception\UnableToMoveFile;
use As247\CloudStorages\Exception\UnableToReadFile;
use As247\CloudStorages\Exception\UnableToRetrieveMetadata;
use As247\CloudStorages\Exception\UnableToWriteFile;
use As247\CloudStorages\Service\GoogleDrive;
use As247\CloudStorages\Service\OneDrive;
use As247\CloudStorages\Support\FileAttributes;
use As247\CloudStorages\Support\StorageAttributes;
use As247\CloudStorages\Support\Config;
use As247\CloudStorages\Exception\FilesystemException;
use Traversable;
interface StorageContract
{
/**
* @const VISIBILITY_PUBLIC public visibility
*/
const VISIBILITY_PUBLIC = 'public';
/**
* @const VISIBILITY_PRIVATE private visibility
*/
const VISIBILITY_PRIVATE = 'private';
/**
* @return mixed | GoogleDrive | OneDrive
*/
public function getService();
/**
* @param string $path
* @param $contents
* @param Config|null $config
* @throws UnableToWriteFile
* @throws FilesystemException
*/
public function writeStream(string $path, $contents, Config $config=null): void;
/**
* @param string $path
* @return resource
* @throws UnableToReadFile
* @throws FileNotFoundException
* @throws FilesystemException
*/
public function readStream(string $path);
/**
* @param string $path
* @throws UnableToDeleteFile
* @throws FilesystemException
* @throws FileNotFoundException
*/
public function delete(string $path): void;
/**
* @param string $path
* @throws UnableToDeleteDirectory
* @throws FilesystemException
* @throws FileNotFoundException
*/
public function deleteDirectory(string $path): void;
/**
* @param string $path
* @param Config|null $config
* @throws UnableToCreateDirectory
* @throws FilesystemException
*/
public function createDirectory(string $path, Config $config=null): void;
/**
* @param string $path
* @param mixed $visibility
* @throws InvalidVisibilityProvided
* @throws FilesystemException
*/
public function setVisibility(string $path, $visibility): void;
/**
* @param string $path
* @param bool $deep
* @return Traversable<StorageAttributes>
* @throws FilesystemException
*/
public function listContents(string $path, bool $deep): Traversable;
/**
* @param string $source
* @param string $destination
* @param Config|null $config
* @throws UnableToMoveFile
* @throws FilesystemException
*/
public function move(string $source, string $destination, Config $config=null): void;
/**
* @param string $source
* @param string $destination
* @param Config|null $config
* @throws UnableToCopyFile
* @throws FilesystemException
*/
public function copy(string $source, string $destination, Config $config=null): void;
/**
* @param $path
* @return FileAttributes
* @throws FileNotFoundException
* @throws UnableToRetrieveMetadata
* @throws FilesystemException
*/
public function getMetadata($path): FileAttributes;
public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, Config $config=null): string;
}
@@ -0,0 +1,57 @@
<?php
namespace As247\CloudStorages\Controllers;
use Exception;
use Google_Client;
use Google_Service_Drive;
class GoogleDriveController
{
protected $client;
public function __construct($clientId,$clientSecret)
{
$this->client=new Google_Client();
$this->client->setClientId($clientId);
$this->client->setClientSecret($clientSecret);
$this->client->addScope(Google_Service_Drive::DRIVE);
$this->client->setAccessType('offline');
$this->client->setApprovalPrompt("force");
$this->client->setRedirectUri($this->getCurrentUrl());
}
public function dispatch(){
if($code=$this->getCode()){
try{
$result=$this->client->fetchAccessTokenWithAuthCode($code);
$refreshToken=$result['refresh_token'];
}catch (Exception $e){
$refreshToken=$e->getMessage();
}
$this->showRefreshToken($refreshToken);
}else{
$this->redirectTo($this->client->createAuthUrl());
}
}
protected function redirectTo($url){
$redirect='<html lang="en">
<head>
<meta http-equiv="refresh" content="1; url=%1$s">
<title>Redirecting....</title>
</head>
<body>Redirecting to %1$s...</body>
</html>';
printf($redirect,$url);
}
protected function showRefreshToken($refreshToken){
echo '<textarea cols="100" rows="20">', htmlspecialchars($refreshToken,ENT_QUOTES) . '</textarea>';
}
protected function getCode(){
return $_REQUEST['code']??null;
}
protected function getCurrentUrl()
{
return 'http://' . $_SERVER['HTTP_HOST'];
}
}
@@ -0,0 +1,52 @@
<?php
namespace As247\CloudStorages\Controllers;
use As247\CloudStorages\Support\OneDriveOauth;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
class OneDriveController
{
protected $oauth;
public function __construct($clientId,$clientSecret,$tenantId='common')
{
$this->oauth=new OneDriveOauth();
$this->oauth->setClientId($clientId);
$this->oauth->setClientSecret($clientSecret);
$this->oauth->setTenantId($tenantId);
}
public function dispatch(){
if($code=$this->getCode()){
try{
$result=$this->oauth->fetchAccessTokenWithAuthCode($code);
$refreshToken=$result['refresh_token'];
}catch (Exception $e){
$refreshToken=$e->getMessage();
} catch (GuzzleException $e) {
$refreshToken=$e->getMessage();
}
$this->showRefreshToken($refreshToken);
}else{
$this->redirectTo($this->oauth->createAuthUrl());
}
}
protected function redirectTo($url){
$redirect='<html lang="en">
<head>
<meta http-equiv="refresh" content="1; url=%1$s">
<title>Redirecting....</title>
</head>
<body>Redirecting to %1$s...</body>
</html>';
printf($redirect,$url);
}
protected function showRefreshToken($refreshToken){
echo '<textarea cols="100" rows="20">', htmlspecialchars($refreshToken,ENT_QUOTES) . '</textarea>';
}
protected function getCode(){
return $_REQUEST['code']??null;
}
}
@@ -0,0 +1,13 @@
<?php
namespace As247\CloudStorages\Exception;
abstract class AbstractException extends \RuntimeException
{
public $type;
public function __construct($message = "", $type='', \Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
$this->type=$type;
}
}
@@ -0,0 +1,40 @@
<?php
namespace As247\CloudStorages\Exception;
use Throwable;
class FileNotFoundException extends AbstractException
{
/**
* @var string
*/
protected $path;
/**
* Constructor.
*
* @param string $path
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(string $path, $code = 0, Throwable $previous = null)
{
$this->path = $path;
parent::__construct('File not found at path: ' . $this->getPath(), $code, $previous);
}
/**
* Get the path which was not found.
*
* @return string
*/
public function getPath()
{
return $this->path;
}
public static function create($path){
return new static($path);
}
}
@@ -0,0 +1,8 @@
<?php
namespace As247\CloudStorages\Exception;
class StorageException extends AbstractException
{
}
File diff suppressed because it is too large Load Diff
+535
View File
@@ -0,0 +1,535 @@
<?php
namespace As247\CloudStorages\Service;
use As247\CloudStorages\Exception\StorageException;
use As247\CloudStorages\Support\StorageAttributes;
use Google\Auth\HttpHandler\Guzzle5HttpHandler;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google_Http_MediaFileUpload;
use Google_Service_Drive;
use Google_Service_Drive_Permission;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Stream;
use As247\CloudStorages\Contracts\Storage\StorageContract;
use Psr\Http\Message\RequestInterface;
use Google_Service_Drive_FileList;
use Google_Service_Drive_DriveFile;
use Psr\Http\Message\StreamInterface;
class GoogleDrive
{
/**
* MIME type of directory
*
* @var string
*/
const DIR_MIME = 'application/vnd.google-apps.folder';
/**
* Default options
*
* @var array
*/
protected static $defaultOptions = [
'additionalFetchField' => '',
'publishPermission' => [
'type' => 'anyone',
'role' => 'reader',
'withLink' => true
],
'appsExportMap' => [
'application/vnd.google-apps.document' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.google-apps.spreadsheet' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.google-apps.drawing' => 'application/pdf',
'application/vnd.google-apps.presentation' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.google-apps.script' => 'application/vnd.google-apps.script+json',
'default' => 'application/pdf'
],
// Default parameters for each command
// see https://developers.google.com/drive/v3/reference/files
// ex. 'defaultParams' => ['files.list' => ['includeTeamDriveItems' => true]]
'defaultParams' => [
'files.list'=>[
'corpora'=>'user'// default is user
],
],
'teamDrive' => false,
'useTrash' => true,
];
protected $options;
protected $publishPermission;
/**
* Fetch fields setting for get
*
* @var string
*/
protected $fetchFieldsGet='id,name,mimeType,modifiedTime,parents,permissions,size,webContentLink,webViewLink';
protected $fetchFieldsList='files({{fieldsGet}}),nextPageToken';
protected $additionalFields;
protected $defaultParams;
protected $service;
//Add prefix / when original root has /
protected $rootPrefix='';
use HasLogger;
public function __construct(Google_Service_Drive $service,$options=[])
{
$this->service=$service;
$this->setupLogger($options);
$this->options = array_replace_recursive(static::$defaultOptions, $options);
$this->publishPermission = $this->options['publishPermission'];
if ($this->options['additionalFetchField']) {
$this->fetchFieldsGet .= ',' . $this->options['additionalFetchField'];
$this->additionalFields = explode(',', $this->options['additionalFetchField']);
}
$this->fetchFieldsList = str_replace('{{fieldsGet}}', $this->fetchFieldsGet, $this->fetchFieldsList);
if (isset($this->options['defaultParams'])) {
$this->defaultParams = $this->options['defaultParams'];
}
if ($this->options['teamDrive']) {
if(is_string($this->options['teamDrive'])){
$this->options['teamDrive']=[
'driveId'=>$this->options['teamDrive'],
'corpora'=>'drive',
'includeItemsFromAllDrives'=>true,
];
}
$this->enableTeamDriveSupport();
}
$root=$options['prefix']??'';
if($root){
$firstChar=substr($root,0,1);
if($firstChar==='/' || $firstChar==='\\'){
$this->rootPrefix=$firstChar;
}
}
}
public function isTeamDrive(){
return $this->options['teamDrive'];
}
public function getTeamDriveId(){
return $this->options['teamDrive']['driveId']??null;
}
public function getClient(){
return $this->service->getClient();
}
public function normalizeMetadata(Google_Service_Drive_DriveFile $object, $path)
{
$id = $object->getId();
$result = [
StorageAttributes::ATTRIBUTE_PATH => is_string($path) ? $this->rootPrefix.ltrim($path,'\/'):null,
StorageAttributes::ATTRIBUTE_TYPE => $object->mimeType === self::DIR_MIME ? StorageAttributes::TYPE_DIRECTORY : StorageAttributes::TYPE_FILE,
StorageAttributes::ATTRIBUTE_LAST_MODIFIED=>strtotime($object->getModifiedTime())
];
$result[StorageAttributes::ATTRIBUTE_MIME_TYPE] = $object->getMimeType();
$result[StorageAttributes::ATTRIBUTE_FILE_SIZE] = (int) $object->getSize();
$result[StorageAttributes::ATTRIBUTE_VISIBILITY]=$this->getVisibility($object);
// attach additional fields
if ($this->additionalFields) {
foreach($this->additionalFields as $field) {
if (property_exists($object, $field)) {
$result['@'.$field] = $object->$field;
}
}
}
$result['@id']=$id;
$result['@shareLink']=$result['@link']=$object->getWebViewLink();
$result['@downloadUrl']=$object->getWebContentLink();
return $result;
}
protected function getVisibility(Google_Service_Drive_DriveFile $object){
$permissions = $object->getPermissions();
$visibility = StorageContract::VISIBILITY_PRIVATE;
foreach ($permissions as $permission) {
if ($permission->type === $this->publishPermission['type'] && $permission->role === $this->publishPermission['role']) {
$visibility = StorageContract::VISIBILITY_PUBLIC;
break;
}
}
return $visibility;
}
/**
* Enables Team Drive support by changing default parameters
*
* @return void
*
* @see https://developers.google.com/drive/v3/reference/files
* @see \Google_Service_Drive_Resource_Files
*/
public function enableTeamDriveSupport()
{
$this->defaultParams = array_merge_recursive(
array_fill_keys([
'files.copy', 'files.create', 'files.delete',
'files.trash', 'files.get', 'files.list', 'files.update',
'files.watch',
'files.permission.create',
'files.permission.delete'
], ['supportsAllDrives' => true]),
$this->defaultParams
);
$this->mergeCommandDefaultParams('files.list',$this->options['teamDrive']);
}
protected function getParams($cmd, ...$params){
$default=$this->getDefaultParams($cmd);
return array_replace($default,...$params);
}
protected function getDefaultParams($cmd){
if(isset($this->defaultParams[$cmd]) && is_array($this->defaultParams[$cmd])){
return $this->defaultParams[$cmd];
}
return [];
}
protected function mergeCommandDefaultParams($cmd,$params){
if(!isset($this->defaultParams[$cmd])){
$this->defaultParams[$cmd]=[];
}
$this->defaultParams[$cmd]=array_replace_recursive($this->defaultParams[$cmd],$params);
return $this;
}
/**
* Create directory
* @param $name
* @param $parentId
* @return bool|Google_Service_Drive_DriveFile|RequestInterface
*/
public function dirCreate($name, $parentId){
$file = new Google_Service_Drive_DriveFile();
$file->setName($name);
$file->setParents([
$parentId
]);
$file->setMimeType(self::DIR_MIME);
return $this->filesCreate($file);
}
/**
* Find files by name in given directory
* @param $name
* @param $parent
* @param $mineType
* @return Google_Service_Drive_FileList|Google_Service_Drive_DriveFile[]
*/
public function filesFindByName($name,$parent, $mineType=null){
if($parent instanceof Google_Service_Drive_DriveFile){
$parent=$parent->getId();
}
$timerStart=microtime(true);
$client=$this->service->getClient();
$collectOthers=false;
$q='trashed = false and "%s" in parents and name = "%s"';
$argsMatchName = [
'pageSize' => 2,
'q' =>sprintf($q,$parent,$name,static::DIR_MIME),
];
if($collectOthers) {
$client->setUseBatch(true);
$batch = $this->service->createBatch();
$filesMatchedName=$this->filesListFiles($argsMatchName);
$q='trashed = false and "%s" in parents';
if($mineType){
$q.=" and mimeType ".$mineType;
}
$argsOthers = [
'pageSize' => 50,
'q' =>sprintf($q,$parent,$name,static::DIR_MIME),
];
$otherFiles=$this->filesListFiles($argsOthers);
$batch->add($filesMatchedName,'matched');
$batch->add($otherFiles,'others');
$results = $batch->execute();
$files=[];
$isFullResult=empty($mineType);//if limited to a mime type so it is not full result
if(!isset($results['response-others'])){
$isFullResult=false;
}
foreach ($results as $key => $result) {
if ($result instanceof Google_Service_Drive_FileList) {
if($key==='response-matched'){
if(count($result)>1){
throw new StorageException("Duplicated file ".$name.' in '.$parent);
}
}
foreach ($result as $file) {
if (!isset($files[$file->id])) {
$files[$file->id] = $file;
}
}
if ($key === 'response-others' && $result->nextPageToken) {
$isFullResult = false;
}
}
}
$client->setUseBatch(false);
}else{
$isFullResult=false;
$list=$this->filesListFiles($argsMatchName);
$files=$list->getFiles();
}
$this->logRequest('files.list.by_name',[
'query'=>'find for '.$name.' in '.$parent,
'duration'=>microtime(true)-$timerStart]);
$list=new Google_Service_Drive_FileList();
$list->setFiles($files);
return [$list,$isFullResult];
}
/**
* @param array $optParams
* @return Google_Service_Drive_FileList | RequestInterface
*/
public function filesListFiles($optParams = array()){
$timerStart=microtime(true);
$optParams=$this->getParams('files.list',['fields' => $this->fetchFieldsList],$optParams);
$result= $this->service->files->listFiles($optParams);
if(!$this->service->getClient()->shouldDefer()) {
$this->logRequest('files.list', [
'query'=>func_get_args(),
'duration'=>microtime(true)-$timerStart,
]);
}
return $result;
}
/**
* @param $fileId
* @param array $optParams
* @return Google_Service_Drive_DriveFile|RequestInterface
*/
public function filesGet($fileId, $optParams = array()){
if($fileId instanceof Google_Service_Drive_DriveFile){
$fileId=$fileId->getId();
}
$timerStart=microtime(true);
$optParams=$this->getParams('files.get',['fields' => $this->fetchFieldsGet],$optParams);
$result= $this->service->files->get($fileId,$optParams);
$this->logRequest('files.get', [
'query'=>func_get_args(),
'duration'=>microtime(true)-$timerStart,
]);
return $result;
}
/**
* @param Google_Service_Drive_DriveFile $postBody
* @param array $optParams
* @return Google_Service_Drive_DriveFile | RequestInterface
*/
public function filesCreate(Google_Service_Drive_DriveFile $postBody, $optParams = array()){
$timerStart=microtime(true);
$optParams=$this->getParams('files.create',['fields' => $this->fetchFieldsGet],$optParams);
$result= $this->service->files->create($postBody,$optParams);
$this->logRequest('files.create', [
'query'=>func_get_args(),
'duration'=>microtime(true)-$timerStart,
]);
return $result;
}
public function filesUpdate($fileId, Google_Service_Drive_DriveFile $postBody, $optParams = array()){
$timerStart=microtime(true);
if($fileId instanceof Google_Service_Drive_DriveFile){
$fileId=$fileId->getId();
}
$optParams=$this->getParams('files.update',['fields' => $this->fetchFieldsGet],$optParams);
$result= $this->service->files->update($fileId,$postBody,$optParams);
$this->logRequest('files.update', [
'query'=>func_get_args(),
'duration'=>microtime(true)-$timerStart,
]);
return $result;
}
public function filesCopy($fileId, Google_Service_Drive_DriveFile $postBody, $optParams = array()){
$timerStart=microtime(true);
if($fileId instanceof Google_Service_Drive_DriveFile){
$fileId=$fileId->getId();
}
$optParams=$this->getParams('files.copy',['fields' => $this->fetchFieldsGet],$optParams);
$result= $this->service->files->copy($fileId,$postBody,$optParams);
$this->logRequest('files.copy', [
'query'=>func_get_args(),
'duration'=>microtime(true)-$timerStart,
]);
return $result;
}
public function filesDelete($fileId, $optParams = array()){
$timerStart=microtime(true);
if($fileId instanceof Google_Service_Drive_DriveFile){
$fileId=$fileId->getId();
}
$optParams=$this->getParams('files.delete',$optParams);
if($this->options['useTrash']){
$fileUpdate=new Google_Service_Drive_DriveFile();
$fileUpdate->setTrashed(true);
$result=$this->service->files->update($fileId,$fileUpdate,$optParams);
}else {
$result = $this->service->files->delete($fileId, $optParams);
}
$this->logRequest('files.delete', [
'query'=>func_get_args(),
'duration'=>microtime(true)-$timerStart,
]);
return $result;
}
/**
* @param $fileId
* @return resource|null
* @throws GuzzleException
*/
public function filesRead($fileId){
$timerStart=microtime(true);
if($fileId instanceof Google_Service_Drive_DriveFile){
$fileId=$fileId->getId();
}
$this->service->getClient()->setUseBatch(true);
$request=$this->filesGet($fileId, ['alt' => 'media']);
$this->service->getClient()->setUseBatch(false);
$stream=null;
$client=$this->service->getClient()->authorize();
$handler=HttpHandlerFactory::build($client);
if($handler instanceof Guzzle5HttpHandler){
//Handler v5 still working but stream read all to buffer
//We use native method to read
$stream=$this->fileReadNative($request);
}else {
$response = $handler($request, ['stream' => true]);
if ($response->getBody() instanceof Stream) {
$stream = $response->getBody()->detach();
}
}
$this->logRequest('files.read', [
'query'=>func_get_args(),
'duration'=>microtime(true)-$timerStart,
]);
return $stream;
}
protected function fileReadNative(RequestInterface $request){
$token=$this->service->getClient()->getAccessToken();
$token=$token['access_token'];
$url=$request->getUri()->__toString();
$auth = "Authorization: Bearer $token";
$opts = array (
'http' => array (
'method' => "GET",
'header' => $auth,
'user_agent' => 'as247/cloud-storages',
)
);
$context = stream_context_create($opts);
$fp = fopen($url, 'rb', false, $context);
return $fp;
}
/**
* Publish file
* @param Google_Service_Drive_DriveFile $file
*/
public function publish(Google_Service_Drive_DriveFile $file)
{
if ($this->getVisibility($file) === StorageContract::VISIBILITY_PUBLIC) {//already published
return;
}
$timerStart=microtime(true);
$permission = new Google_Service_Drive_Permission($this->publishPermission);
$optParams=$this->getParams('files.permission.create');
if ($newPermission=$this->service->permissions->create($file->getId(), $permission, $optParams)) {
$permissions=$file->getPermissions();
$permissions=array_merge($permissions,[$newPermission]);
$file->setPermissions($permissions);
}
$this->logRequest('files.permission.create', [
'query'=>$file->getId(),
'duration'=>microtime(true)-$timerStart,
]);
}
/**
* Un-publish specified path item
* @param Google_Service_Drive_DriveFile $file
*/
public function unPublish(Google_Service_Drive_DriveFile $file)
{
$timerStart=microtime(true);
$permissions = $file->getPermissions();
$optParams=$this->getParams('files.permission.delete');
foreach ($permissions as $index=> $permission) {
if ($permission->type === 'anyone' && $permission->role === 'reader') {
$this->service->permissions->delete($file->getId(), $permission->getId(), $optParams);
unset($permissions[$index]);
}
}
$file->setPermissions($permissions);
$this->logRequest('files.permission.create', [
'query'=>$file->getId(),
'duration'=>microtime(true)-$timerStart,
]);
}
public function filesUploadChunk(Google_Service_Drive_DriveFile $file,StreamInterface $contents,$chunk){
$client = $this->service->getClient();
$client->setDefer(true);
if (!$file->getId()) {
$request = $this->filesCreate($file);
} else {
$update=new Google_Service_Drive_DriveFile();
$update->setMimeType($file->getMimeType());
$request = $this->filesUpdate($file->getId(), $update);
}
$mime=$file->getMimeType();
// Create a media file upload to represent our upload process.
$media = new Google_Http_MediaFileUpload($client, $request, $mime, null, true, $chunk);
$media->setFileSize($contents->getSize());
// Upload the various chunks. $status will be false until the process is
// complete.
$status = false;
$contents->rewind();
while (!$status && !$contents->eof()) {
$status = $media->nextChunk($contents->read($chunk));
}
$client->setDefer(false);
return $status;
}
/**
* @param Google_Service_Drive_DriveFile $file
* @param $contents
* @return Google_Service_Drive_DriveFile|RequestInterface
*/
public function filesUploadSimple(Google_Service_Drive_DriveFile $file,StreamInterface $contents){
$params = [
'data' => $contents->getContents(),
'uploadType' => 'media',
];
if (!$file->getId()) {
$obj = $this->filesCreate($file, $params);
} else {
$update=new Google_Service_Drive_DriveFile();
$update->setMimeType($file->getMimeType());
$obj = $this->filesUpdate($file->getId(), $update, $params);
}
return $obj;
}
protected function logRequest($cmd, $query){
$this->logger->request($cmd,$query);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace As247\CloudStorages\Service;
trait HasLogger
{
protected $logger;
public function getLogger(){
return $this->logger;
}
public function setLogger($logger){
$this->logger=$logger;
return $this;
}
protected function setupLogger($options){
$dir=__DIR__.'/../../';
$log=$options['log']??false;
if(is_string($log)){
$dir=$log;
$log=true;
}
$dir=$options['log.dir']??$dir;
$this->logger=new Logger($dir);
$this->logger->enable($log);
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace As247\CloudStorages\Service;
class Logger
{
protected $logDir;
protected $enabled=false;
protected $num_requests=0;
protected $withTrace=false;
public function __construct($logDir='')
{
$this->logDir=$logDir;
}
public function enable($flag=true){
$previous= $this->enabled;
$this->enabled=$flag;
return $previous;
}
public function withTrace($flag=true){
$this->withTrace=$flag;
return $this;
}
function log($message, $level='debug'){
if(!$this->enabled){
return $this;
}
if(is_array($message) || is_object($message)){
$message=json_encode($message,JSON_PRETTY_PRINT);
}
$this->write("[$level] $message",'debug');
return $this;
}
function request($cmd, $query){
if(!$this->enabled){
return $this;
}
if(is_array($query) || is_object($query)) {
$query = json_encode($query, JSON_PRETTY_PRINT);
}
$this->write("{$cmd}\n$query",'query');
$this->num_requests++;
return $this;
}
public function getNumRequests(){
return $this->num_requests;
}
protected function write($line,$file){
if(!$this->enabled || !$this->logDir){
return ;
}
$time=date('Y-m-d h:i:s');
$trace='';
if($this->withTrace){
$trace=PHP_EOL.
'Trace:'.
$this->debugBacktraceSummary(null,1);
}
file_put_contents($this->logDir."/$file.log",
$time.' '. $line.$trace
.PHP_EOL.PHP_EOL,FILE_APPEND);
}
protected function debugBacktraceSummary( $ignore_class = null, $skip_frames = 0, $pretty = true ) {
static $truncate_paths;
$trace = debug_backtrace( false );
$caller = array();
$check_class = ! is_null( $ignore_class );
$skip_frames++; // Skip this function.
if ( ! isset( $truncate_paths ) ) {
$truncate_paths = array(
);
}
foreach ( $trace as $call ) {
if ( $skip_frames > 0 ) {
$skip_frames--;
} elseif ( isset( $call['class'] ) ) {
if ( $check_class && $ignore_class == $call['class'] ) {
continue; // Filter out calls.
}
$caller[] = "{$call['class']}{$call['type']}{$call['function']}";
} else {
if ( in_array( $call['function'], array( 'do_action', 'apply_filters', 'do_action_ref_array', 'apply_filters_ref_array' ), true ) ) {
$caller[] = "{$call['function']}('{$call['args'][0]}')";
} elseif ( in_array( $call['function'], array( 'include', 'include_once', 'require', 'require_once' ), true ) ) {
$filename = isset( $call['args'][0] ) ? $call['args'][0] : '';
$caller[] = $call['function'] . "('" . str_replace( $truncate_paths, '', ( $filename ) ) . "')";
} else {
$caller[] = $call['function'];
}
}
}
if ( $pretty ) {
return join( ', ', array_reverse( $caller ) );
} else {
return $caller;
}
}
}
+328
View File
@@ -0,0 +1,328 @@
<?php
namespace As247\CloudStorages\Service;
use ArrayObject;
use As247\CloudStorages\Exception\StorageException;
use As247\CloudStorages\Support\Path;
use As247\CloudStorages\Support\StorageAttributes;
use Generator;
use As247\CloudStorages\Contracts\Storage\StorageContract;
use InvalidArgumentException;
use Microsoft\Graph\Exception\GraphException;
use Microsoft\Graph\Graph;
use Microsoft\Graph\Http\GraphRequest;
class OneDrive
{
protected $graph;
const ROOT = '/me/drive/root';
protected $publishPermission = [
'role' => 'read',
'scope' => 'anonymous',
'withLink' => true
];
protected $options;
//Add prefix / when original root has /
protected $rootPrefix='';
use HasLogger;
public function __construct(Graph $graph,$options=[])
{
$this->options=$options;
$root=$options['root']??'';
if($root){
$firstChar=substr($root,0,1);
if($firstChar==='/' || $firstChar==='\\'){
$this->rootPrefix=$firstChar;
}
}
$this->setupLogger($options);
$this->graph=$graph;
}
function normalizeMetadata(array $response, string $path): array
{
$permissions=$response['permissions']??[];
$visibility = StorageContract::VISIBILITY_PRIVATE;
$shareLink=null;
foreach ($permissions as $permission) {
if(!isset($permission['link']['scope']) || !isset($permission['roles'])){
continue;
}
if(in_array($this->publishPermission['role'],$permission['roles'])
&& $permission['link']['scope']==$this->publishPermission['scope']){
$visibility = StorageContract::VISIBILITY_PUBLIC;
$shareLink=$permission['link']['webUrl']??null;
break;
}
}
$meta= [
StorageAttributes::ATTRIBUTE_PATH => $this->rootPrefix.ltrim($path,'\/'),
StorageAttributes::ATTRIBUTE_LAST_MODIFIED => strtotime($response['lastModifiedDateTime']),
StorageAttributes::ATTRIBUTE_FILE_SIZE => $response['size'],
StorageAttributes::ATTRIBUTE_TYPE => isset($response['file']) ? 'file' : 'dir',
StorageAttributes::ATTRIBUTE_MIME_TYPE => $response['file']['mimeType'] ?? null,
StorageAttributes::ATTRIBUTE_VISIBILITY=>$visibility,
'@id'=>$response['id']??null,
'@link' => $response['webUrl'] ?? null,
'@shareLink'=>$shareLink,
'@downloadUrl' => $response['@microsoft.graph.downloadUrl']?? null,
];
return $meta;
}
function getEndpoint($path='',$action='',$params=[]){
$this->validatePath($path);
$path=Path::clean($path);
$path=trim($path,'\\/');
$path=static::ROOT.':/'.$path;
/**
* Path should not end with /
* /me/drive/root:/path/to/file
* /me/drive/root
*/
$path=rtrim($path,':/');
if($action===true){//path reference
if(strpos($path,':')===false) {
$path .= ':';//root path should end with :
}
}
if ($action && is_string($action)) {
/**
* Append action to path
* /me/drive/root:/path:/action
* trim : for root
* /me/drive/root/action
*/
$path= rtrim($path,':');
if(strpos($path,':')!==false) {
$path .=':/' . $action;//root:/path:/action
}else{
$path .= '/' . $action;//root/action
}
}
if($params){
$path.='?'.http_build_query($params);
}
return $path;
}
/**
* @param $path
* @param $newPath
* @return array|null
* @throws GraphException
*/
public function copy($path,$newPath){
$endpoint = $this->getEndpoint($path,'copy');
$name=basename($newPath);
$this->createDirectory(dirname($newPath));
$newPathParent=$this->getEndpoint(dirname($newPath),true);
$body=[
'name' => $name,
'parentReference' => [
'path' => $newPathParent,
],
];
return $this->createRequest('POST', $endpoint)
->attachBody($body)
->execute()->getBody();
}
/**
* @param $path
* @return array|null
* @throws GraphException
*/
public function createDirectory($path){
$path=Path::clean($path);
if($path==='/'){
return $this->getItem('/');
}
$endpoint=$this->getEndpoint($path);
return $this->createRequest('PATCH', $endpoint)
->attachBody([
'folder' => new ArrayObject(),
])->execute()->getBody();
}
/**
* @param $path
* @return array|null
* @throws GraphException
*/
public function delete($path){
$endpoint=$this->getEndpoint($path);
return $this->createRequest('DELETE', $endpoint)->execute()->getBody();
}
/**
* @param $path
* @param null $format
* @return resource|null
* @throws GraphException
*/
public function download($path,$format=null){
$args=[];
if($format){
if(is_string($format)){
$args=['format'=>$format];
}elseif(is_array($format)){
$args=$format;
}
}
$endpoint=$this->getEndpoint($path,'content',$args);
$response=$this->createRequest('GET',$endpoint)->setReturnType('GuzzleHttp\Psr7\Stream')->execute();
/**
* @var StreamWrapper $response
*/
return $response->detach();
}
/**
* @param $path
* @param array $args
* @return array|null
* @throws GraphException
*/
public function getItem($path,$args=[]){
$endpoint=$this->getEndpoint($path,'',$args);
$response = $this->createRequest('GET', $endpoint)->execute();
return $response->getBody();
}
/**
* @param $path
* @return Generator
* @throws GraphException
*/
public function listChildren($path){
$endpoint = $this->getEndpoint($path,'children');
$nextPage=null;
do {
if ($nextPage) {
$endpoint = $nextPage;
}
$response = $this->createRequest('GET', $endpoint)
->execute();
$nextPage = $response->getNextLink();
$items = $response->getBody()['value']??[];
if(!is_array($items)){
$items=[];
}
yield from $items;
}while($nextPage);
}
/**
* @param $path
* @param $newPath
* @return array|null
* @throws GraphException
*/
public function move($path,$newPath){
$endpoint = $this->getEndpoint($path);
$name=basename($newPath);
$this->createDirectory(dirname($newPath));
$newPathParent=$this->getEndpoint(dirname($newPath),true);
$body=[
'name' => $name,
'parentReference' => [
'path' => $newPathParent,
],
];
return $this->createRequest('PATCH', $endpoint)
->attachBody($body)
->execute()->getBody();
}
/**
* @param $path
* @param $contents
* @return array|null
* @throws GraphException
*/
public function upload($path,$contents){
$endpoint = $this->getEndpoint($path,'content');
try {
$stream = StreamWrapper::wrap($contents);
}catch (InvalidArgumentException $e){
throw new StorageException("Invalid contents. ".$e->getMessage());
}
$this->createDirectory(dirname($path));
return $this->createRequest('PUT', $endpoint)
->attachBody($stream)
->execute()->getBody();
}
/**
* @param $path
* @return array|mixed
* @throws GraphException
*/
public function getPermissions($path){
$endpoint=$this->getEndpoint($path,'permissions');
$response = $this->createRequest('GET', $endpoint)->execute();
return $response->getBody()['value']??[];
}
/**
* @param $path
* @return array
* @throws GraphException
*/
function publish($path){
$endpoint=$this->getEndpoint($path,'createLink');
$body=['type'=>'view','scope'=>'anonymous'];
$response = $this->createRequest('POST', $endpoint)
->attachBody($body)->execute();
return $response->getBody();
}
/**
* @param $path
* @throws GraphException
*/
function unPublish($path){
$permissions=$this->getPermissions($path);
$idToRemove='';
foreach ($permissions as $permission){
if(in_array($this->publishPermission['role'],$permission['roles'])
&& $permission['link']['scope']==$this->publishPermission['scope']){
$idToRemove=$permission['id'];
break;
}
}
if(!$idToRemove){
return ;
}
$endpoint=$this->getEndpoint($path,'permissions/'.$idToRemove);
$this->createRequest('DELETE', $endpoint)->execute();
}
/**
* @param $requestType
* @param $endpoint
* @return GraphRequest
* @throws GraphException
*/
protected function createRequest($requestType, $endpoint){
$this->logger->request($requestType,$endpoint);
return $this->graph->createRequest($requestType,$endpoint);
}
protected function validatePath($path){
$invalidChars=['"','*',':','<','>','?', '|'];
foreach ($invalidChars as $char){
if(strpos($path,$char)!==false){
throw new StorageException("Invalid character $char in path $path");
}
}
}
}
@@ -0,0 +1,22 @@
<?php
namespace As247\CloudStorages\Service;
use GuzzleHttp\Psr7\Utils;
/**
* Custom implement Psr7 Stream to fix AssemblyStream
* Google require min chunk size: 262144 but assembly stream may have smaller chunk size
* Eg NextCloud chunk size: 8192 and you will got following error
* Invalid request. The number of bytes uploaded is required to be equal or greater than 262144, except for the final request (it's recommended to be the exact multiple of 262144). The received request contained 8192 bytes, which does not meet this requirement.
* Class Stream
* @package As247\CloudStorages\Service
*/
class StreamWrapper
{
public static function wrap($stream,$options=[]){
return Utils::streamFor($stream,$options);
}
}
+134
View File
@@ -0,0 +1,134 @@
<?php
namespace As247\CloudStorages\Storage;
use As247\CloudStorages\Exception\FileNotFoundException;
use As247\CloudStorages\Exception\StorageException;
use As247\CloudStorages\Service\AListService;
use As247\CloudStorages\Support\Config;
use As247\CloudStorages\Support\FileAttributes;
use As247\CloudStorages\Support\Path;
use Traversable;
class AList extends Storage
{
protected $service;
protected $root = '';
protected $fakeVisibility=[];
public function __construct($url, $options = [])
{
$this->service = new AListService($url, $options);
$this->root=Path::clean($options['root']??'');
}
public function getService()
{
return $this->service;
}
public function writeStream(string $path, $contents, Config $config = null): void
{
$this->service->put($path, $contents);
if($config){
$this->setVisibility($path,$config->get('visibility'));
}
}
public function readStream(string $path)
{
$contents = $this->service->read($path);
if ($contents) {
return $contents;
}
throw new StorageException('Unable to read file: ' . $path,'readStream');
}
public function delete(string $path): void
{
if(Path::clean($path)===$this->root){
return ;
}
$this->service->remove(dirname($path),basename($path));
}
public function deleteDirectory(string $path): void
{
$this->delete($path);
}
public function createDirectory(string $path, Config $config = null): void
{
$created=$this->service->mkdir($path);
if(!$created){
throw new StorageException('Unable to create directory: ' . $path,'createDirectory');
}
}
public function setVisibility(string $path, $visibility): void
{
$this->getMetadata($path);
$this->fakeVisibility[$path]=$visibility;
//throw new StorageException('setVisibility is not supported by AList','setVisibility');
}
public function listContents(string $path, bool $deep): Traversable
{
$results = $this->service->listContents($path);
foreach ($results as $id => $result) {
$result = $this->service->normalizeMetadata($result, rtrim($path,'\/') . '/' . $result['name']);
yield $id => $result;
if ($deep && $result['type'] === 'dir') {
yield from $this->listContents($result['path'], $deep);
}
}
}
public function move(string $source, string $destination, Config $config = null): void
{
if(Path::clean($source)===Path::clean($destination)){
throw new StorageException('Source and destination are the same: ' . $source,'move');
}
$this->getMetadata($source);
$this->copy($source,$destination,$config);
$this->delete($source);
}
public function copy(string $source, string $destination, Config $config = null): void
{
$srcDir=dirname($source);
$dstDir=dirname($destination);
$tmpDir=Path::join($srcDir,'.tmp');
$this->createDirectory($tmpDir);
$this->createDirectory($dstDir);
$this->service->copy($srcDir,$tmpDir,basename($source));
$this->service->rename(Path::join($tmpDir,basename($source)),basename($destination));
$this->service->move($tmpDir,$dstDir,basename($destination));
if($config){
$this->setVisibility($destination,$this->getMetadata($source)->visibility());
}
}
/**
* @param $path
* @return FileAttributes
*/
public function getMetadata($path): FileAttributes
{
$meta=$this->service->get($path);
if(!$meta){
throw FileNotFoundException::create($path);
}
$attributes=$this->service->normalizeMetadata($meta,$path);
if(isset($this->fakeVisibility[$path])){
$attributes['visibility']=$this->fakeVisibility[$path];
}
return new FileAttributes($attributes);
}
public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, Config $config = null): string
{
$expire=$expiresAt->getTimestamp();
return $this->service->getDownloadUrl($path,$expire);
}
}
+631
View File
@@ -0,0 +1,631 @@
<?php
namespace As247\CloudStorages\Storage;
use As247\CloudStorages\Cache\PathCache;
use As247\CloudStorages\Cache\Stores\GoogleDrivePersistentStore;
use As247\CloudStorages\Cache\Stores\GoogleDriveStore;
use As247\CloudStorages\Exception\FileNotFoundException;
use As247\CloudStorages\Exception\StorageException;
use As247\CloudStorages\Service\StreamWrapper;
use As247\CloudStorages\Support\Config;
use As247\CloudStorages\Support\FileAttributes;
use As247\CloudStorages\Support\Path;
use Exception;
use Generator;
use Google_Service_Drive;
use Google_Service_Drive_DriveFile;
use As247\CloudStorages\Service\GoogleDrive as GoogleDriveService;
use Google_Service_Drive_FileList;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
use Traversable;
class GoogleDrive extends Storage
{
/**
* Google_Service_Drive instance
*/
protected $service;
protected $root;//Root id
protected $maxFolderLevel = 128;
public function __construct(Google_Service_Drive $service, $options)
{
if (is_string($options)) {
$options = ['root' => $options];
}
if(isset($options['teamDrive'])){
if($options['teamDrive']===true){
$options['teamDrive']=$options['root'];//Team drive same as root if boolean
}elseif(empty($options['root']) && is_string($options['teamDrive'])){
$options['root']=$options['teamDrive'];
}
}
$this->service = new GoogleDriveService($service, $options);
$this->setLogger($this->service->getLogger());
$this->setRoot($options);
}
/**
* Gets the service (Google_Service_Drive)
*
* @return object Google_Service_Drive
*/
public function getService(){
return $this->service;
}
public function getCache(){
return $this->cache;
}
protected function setRoot($options)
{
$root = $options['root'];
if(empty($root)) {
throw new StorageException("Root folder id is required");
}
$this->root = $root;
if(isset($options['cache']) && is_string($options['cache'])){
$options['cache'] = new PathCache(new GoogleDrivePersistentStore($options['cache']));
}else {
$options['cache'] = new PathCache(new GoogleDriveStore());
}
$this->setupCache($options);
}
public function getRoot(){
return $this->root;
}
protected function initializeCacheRoot(){
$this->cache->getStore()->mapDirectory('/', $this->root);
}
/**
* @param string|array $path create directory structure
* @return string folder id
*/
protected function ensureDirectory($path)
{
$path = Path::clean($path);
if ($this->isFile($path)) {
throw new StorageException("File already exists at $path");
}
if (isset($this->maxFolderLevel)) {
$nestedFolderLevel = count(explode('/', $path)) - 1;
if ($nestedFolderLevel > $this->maxFolderLevel) {// -1 for /
throw new StorageException("Maximum nesting folder exceeded");
}
}
$this->logger->log("mkdir: $path");
list($parent, $paths, $currentPaths) = $this->detectPath($path);
if (count($paths) != 0) {
while (null !== ($name = array_shift($paths))) {
$currentPaths[] = $name;
$currentPathString = join('/', $currentPaths);
if ($this->isFile($currentPaths)) {
throw new StorageException("File already exists at $currentPathString");
}
$created = $this->service->dirCreate($name, $parent);
//echo 'Created: '.print_r($currentPaths);
$this->cache->put($currentPaths, $created);
$this->cache->complete($currentPaths);
$parent = $created->getId();
}
}
return $parent;
}
/**
* @inheritDoc
*/
public function writeStream(string $path, $contents, Config $config = null): void
{
$this->upload($path, $contents, $config);
}
/**
* Delete file only
* @param $path
* @return void
* @throws FileNotFoundException
*/
public function delete(string $path): void
{
if ($this->isDirectory($path)) {
throw new StorageException("$path is directory");
}
$file = $this->find($path);
if (!$file) {//already deleted
throw FileNotFoundException::create($path);
}
if ($file->getId() === $this->root) {
throw new StorageException("Root directory cannot be deleted");
}
$this->service->filesDelete($file);
$this->cache->delete($path);
$this->logger->log("Deleted $path");
}
/**
* @inheritDoc
*/
public function deleteDirectory(string $path): void
{
if ($this->isFile($path)) {
throw new StorageException("$path is file");
}
$file = $this->find($path);
if (!$file) {//already deleted
throw FileNotFoundException::create($path);
}
if ($file->getId() === $this->root) {
throw new StorageException("Root directory cannot be deleted");
}
$this->service->filesDelete($file);
$this->cache->deleteDir($path);
$this->logger->log("Deleted $path");
}
/**
* Find for path
* @param $path
* @return false|Google_Service_Drive_DriveFile
*/
protected function find($path)
{
if ($path instanceof Google_Service_Drive_DriveFile) {
return $path;
}
list(, $paths) = $this->detectPath($path);
if (count($paths) >= 2) {
//remaining 2 segments /A/B/C/file.txt
//C not exists mean file.txt also not exists
return false;
}
if (null!==($cached=$this->cache->get($path))) {
return $cached;
}
return false;
}
public function createDirectory(string $path, Config $config = null): void
{
$this->ensureDirectory(Path::clean($path));
$result = $this->getMetadata($path);
if ($config && $visibility = $config->get('visibility')) {
$this->setVisibility($path, $visibility);
$result['visibility'] = $visibility;
}
}
public function copy(string $fromPath, string $toPath, Config $config = null): void
{
$fromPath = Path::clean($fromPath);
$toPath = Path::clean($toPath);
$from = $this->find($fromPath);
if (!$from) {
throw new StorageException("$fromPath not found");
}
if ($this->isDirectory($fromPath)) {
throw new StorageException("$fromPath is directory");
}
if ($this->isDirectory($toPath)) {
throw new StorageException("$toPath is directory");
}
if ($this->has($toPath)) {
$this->delete($toPath);
}
$paths = $this->parsePath($toPath);
$fileName = array_pop($paths);
$dirName = $paths;
$parents = [$this->ensureDirectory($dirName)];
$file = new Google_Service_Drive_DriveFile();
$file->setName($fileName);
$file->setParents($parents);
$newFile = $this->service->filesCopy($from->id, $file);
$visibilityForNewFile=$this->getMetadata($fromPath)->visibility();
if($config && $visibilityFromConfig = $config->get('visibility')){
$visibilityForNewFile=$visibilityFromConfig;
}
$this->cache->put($toPath, $newFile);
$this->setVisibility($toPath, $visibilityForNewFile);
$this->logger->log("Copied file: $fromPath -> $toPath");
}
public function move(string $fromPath, string $toPath, Config $config = null): void
{
$fromPath = Path::clean($fromPath);
$toPath = Path::clean($toPath);
if ($fromPath === $toPath) {
return;
}
$from = $this->find($fromPath);
if (!$from) {
throw new StorageException("$fromPath not found");
}
$oldParent = $from->getParents()[0];
$newParentId = null;
if ($this->isFile($from)) {//we moving file
if ($this->has($toPath)) {
if ($this->isDirectory($toPath)) {//Destination path is directory
throw new StorageException("$toPath is directory and cannot be overwritten");
} else {
$this->delete($toPath);
}
}
} else {//we moving directory
if ($this->has($toPath)) {
if ($this->isFile($toPath)) {//Destination path is file
throw new StorageException("$toPath is file and cannot be overwritten");
} else {
$this->deleteDirectory($toPath);//overwrite, remove it first
}
}
}
$paths = $this->parsePath($toPath);
$fileName = array_pop($paths);
$dirName = $paths;
$newParentId = $this->ensureDirectory($dirName);
$file = new Google_Service_Drive_DriveFile();
$file->setName($fileName);
$opts=[];
if ($newParentId !== $oldParent) {
$opts['addParents'] = $newParentId;
$opts['removeParents'] = $oldParent;
}
$result=$this->service->filesUpdate($from->getId(), $file, $opts);
if(!in_array($newParentId,$result->getParents())){
throw new StorageException("Service update failure");
}
$this->cache->move($fromPath, $toPath);
$this->logger->log("Moved file: $fromPath -> $toPath");
}
/**
* Upload|Update item
*
* @param string $path
* @param $contents
* @param Config|null $config
* @return FileAttributes
* @throws FileNotFoundException
*/
protected function upload(string $path, $contents, Config $config = null)
{
try {
$contents = StreamWrapper::wrap($contents);
}catch (InvalidArgumentException $e){
throw new StorageException("Invalid contents. ".$e->getMessage());
}
$contents->rewind();
if ($this->isDirectory($path)) {
throw new StorageException("$path is directory");
}
$paths = $this->parsePath($path);
$fileName = array_pop($paths);
$dirName = $paths;
//Try to find file before, because if it was removed before, ensure directory will recreate same directory and it may available again
$parentId = $this->ensureDirectory($dirName);
if (!$parentId) {
throw new StorageException("Not able to create parent directory ".join('/',$dirName));
}
$file = $this->find($path);
if (!$file) {
$file = new Google_Service_Drive_DriveFile();
$file->setName($fileName);
$file->setParents([
$parentId
]);
}
$newMimeType = $config ? $config->get('mimetype', $config->get('mime_type')) : null;
if ($newMimeType) {
$file->setMimeType($newMimeType);
}
$size5MB = 5 * 1024 * 1024;
$chunkSize = $config ? $config->get('chunk_size', $size5MB) : $size5MB;
if ($contents->getSize() <= $size5MB) {
$obj = $this->service->filesUploadSimple($file, $contents);
} else {
$obj = $this->service->filesUploadChunk($file, $contents, $chunkSize);
}
$this->logger->log("Uploaded: $path");
if ($obj instanceof Google_Service_Drive_DriveFile) {
$this->cache->put($path, $obj);//update cache first
if ($config && $visibility = $config->get('visibility')) {
$this->setVisibility($path, $visibility);
}
return $this->getMetadata($path);
}
throw new StorageException("Unable to upload file: $path");
}
/**
* @param string $directory
* @param bool $recursive
* @return Generator
*/
public function listContents(string $directory, bool $recursive = false): Traversable
{
if (!$this->isDirectory($directory)) {
yield from [];
return;
}
$results = $this->fetchDirectory($directory, 1000);
foreach ($results as $id => $result) {
yield $id => $result;
if ($recursive && $result['type'] === 'dir') {
yield from $this->listContents($result['path'], $recursive);
}
}
}
protected function fetchDirectory($directory, $pageSize = 1000)
{
//echo 'Fetching: '.$directory;
if ($this->cache->isCompleted($directory)) {
foreach ($this->cache->query($directory) as $path => $file) {
if ($file instanceof Google_Service_Drive_DriveFile) {
yield $file->getId() => $this->service->normalizeMetadata($file, $path);
}
}
return null;
}
list($itemId) = $this->detectPath($directory);
$pageSize = min($pageSize, 1000);//limit range of page size
$pageSize = max($pageSize, 1);//
$parameters = [
'pageSize' => $pageSize,
'q' => sprintf('trashed = false and "%s" in parents', $itemId)
];
$pageToken = NULL;
do {
try {
if ($pageToken) {
$parameters['pageToken'] = $pageToken;
}
$fileObjs = $this->service->filesListFiles($parameters);
if ($fileObjs instanceof Google_Service_Drive_FileList) {
foreach ($fileObjs as $obj) {
$id = $obj->getId();
$result = $this->service->normalizeMetadata($obj, rtrim($directory,'\/') . '/' . $obj->getName());
yield $id => $result;
$this->cache->put($result['path'], $obj);
}
$pageToken = $fileObjs->getNextPageToken();
} else {
$pageToken = NULL;
}
} catch (Exception $e) {
$pageToken = NULL;
}
} while ($pageToken);
$this->cache->complete($directory);
}
/**
* Publish specified path item
*
* @param string $path
*/
protected function publish(string $path)
{
if (!$file = $this->find($path)) {
throw new StorageException("File $path not found");
}
$this->service->publish($file);
$this->cache->put($path,$file);
}
/**
* Un-publish specified path item
*
* @param string $path
*/
protected function unPublish(string $path)
{
if (!$file = $this->find($path)) {
throw new StorageException("File $path not found");
}
$this->service->unPublish($file);
$this->cache->put($path,$file);
}
/**
* @param string $path
* @param mixed $visibility
*/
public function setVisibility(string $path, $visibility): void
{
if ($visibility === Storage::VISIBILITY_PUBLIC) {
$this->publish($path);
} elseif ($visibility === Storage::VISIBILITY_PRIVATE) {
$this->unPublish($path);
} else {
throw new InvalidArgumentException('Visibility must be either "public" or "private".');
}
}
public function readStream(string $path)
{
$file = $this->find($path);
if (!$this->isFile($path)) {
throw FileNotFoundException::create($path);
}
try {
return $this->service->filesRead($file);
} catch (GuzzleException $e) {
throw new StorageException("Unable to read file: $path", 'readStream', $e);
}
}
protected function parsePath($path)
{
$paths = Path::explode($path);
$directory = [];
$file = [];
$level = 0;
foreach ($paths as $path) {
if ($level++ > $this->maxFolderLevel) {
$file[] = $path;
} else {
$directory[] = $path;
}
}
if (!$file) {
$file[] = array_pop($directory);
}
$file = join('/', $file);
$directory[] = $file;
return $directory;
}
/**
* Travel through the path tree then return folder id, remaining path, current path
* eg: /path/to/the/file/text.txt
* - if we have directory /path/to then it return [path_to_id, ['the','file','text.txt'], ['path','to']
* - if we have /path/to/the/file/text.txt then it return [id_of_path_to_the_file, ['text.txt'], ['path','to','the','file'] ]
* @param $path
* @return array
*/
protected function detectPath($path)
{
$paths = $this->parsePath($path);
$this->logger->log("Path finding: " . join(', ',$paths));
$currentPaths = [];
if($this->cache->get('/')===null) {
$this->initializeCacheRoot();
}
$parent = $this->cache->get('/');
while (null !== ($name = array_shift($paths))) {
$parentPaths = $currentPaths;
$currentPaths[] = $name;
$foundDir = $this->cache->get($currentPaths);
if (!is_null($foundDir)) {
if ($foundDir && $this->isDirectory($foundDir)) {
$parent = $foundDir;
continue;
} else {
//echo 'break at...'.implode($currentPaths);
array_pop($currentPaths);
array_unshift($paths, $name);
break;
}
}
list($files, $isFull) = $this->service->filesFindByName($name, $parent);
if ($isFull) {
$this->cache->complete($parentPaths);
}
$foundDir = false;
//Set current path as not exists, it will be updated again when we got matched file
$this->cache->put($currentPaths, false);
if ($files->count()) {
foreach ($files as $file) {
if ($file instanceof Google_Service_Drive_DriveFile) {
$fileFound = $parentPaths;
$fileFound[]=$file->getName();
$this->cache->put($fileFound, $file);
if ($this->isDirectory($file) && $file->getName() === $name) {
$foundDir = $file;
}
}
}
}
if (!$foundDir) {
array_pop($currentPaths);
array_unshift($paths, $name);
break;
}
$parent = $foundDir;
}
$parent = $parent->getId();
$this->logger->log("Found: " . $parent . '(' . join('/',$currentPaths) . ") " . join('/',$paths));
return [$parent, $paths, $currentPaths];
}
/**
* Check if given path exists
* @param $path
* @return bool
*/
protected function has($path)
{
try {
$this->getMetadata($path);
return true;
}catch (FileNotFoundException $e){
return false;
}
}
protected function isDirectory($path)
{
try {
$meta = $this->getMetadata($path);
return $meta->isDir();
}catch (FileNotFoundException $e){
return false;
}
}
protected function isFile($path)
{
try {
$meta = $this->getMetadata($path);
return $meta->isFile();
}catch (FileNotFoundException $e){
return false;
}
}
/**
* @param $path
* @return FileAttributes
* @throws FileNotFoundException
*/
public function getMetadata($path):FileAttributes
{
if ($obj = $this->find($path)) {
if ($path instanceof Google_Service_Drive_DriveFile) {
$path = null;
}
if ($obj instanceof Google_Service_Drive_DriveFile) {
$attributes = $this->service->normalizeMetadata($obj, $path);
return FileAttributes::fromArray($attributes);
}
throw new StorageException("Unable to get metadata for file at path: $path");
}
throw FileNotFoundException::create(Path::clean($path));
}
public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, Config $config = null): string
{
// TODO: Implement temporaryUrl() method.
}
}
@@ -0,0 +1,100 @@
<?php
namespace As247\CloudStorages\Storage;
use As247\CloudStorages\Cache\Stores\GoogleDrivePersistentStore;
use As247\CloudStorages\Cache\Stores\NullStore;
use As247\CloudStorages\Cache\Stores\SqliteCache;
use As247\CloudStorages\Contracts\Storage\ObjectStorage;
use As247\CloudStorages\Exception\FileNotFoundException;
use As247\CloudStorages\Exception\StorageException;
class GoogleDriveObjectStorage implements ObjectStorage
{
protected $storage;
protected $cache;
protected $storageCache;
public function __construct(GoogleDrive $drive,$cacheFilePath=null)
{
$this->storage=$drive;
if($cacheFilePath===null) {
$cacheFilePath = sys_get_temp_dir() . '/' . $drive->getRoot() . '.sqlite';
}
if($cacheFilePath) {
$this->cache = new SqliteCache($cacheFilePath);
}else{
$this->cache = new NullStore();
}
$this->storageCache=$drive->getCache();
if($this->storageCache->getStore() instanceof GoogleDrivePersistentStore){
throw new StorageException('Persistent cache not work with object storage');
}
}
public function readObject($urn)
{
$urn=$this->sanitizeUrn($urn);
$this->mapCache($urn);
return $this->storage->readStream($urn);
}
public function writeObject($urn, $stream)
{
$urn=$this->sanitizeUrn($urn);
$this->mapCache($urn);
try {
$this->storage->writeStream($urn, $stream);
} catch (StorageException $e) {
$this->cache->forget($urn);
throw $e;
}
//File write success update cache
$this->updateCache($urn);
}
public function deleteObject($urn)
{
$urn=$this->sanitizeUrn($urn);
$this->mapCache($urn);
try {
$this->storage->delete($urn);
} catch (FileNotFoundException $e) {
//Already deleted do nothing
} finally {
$this->cache->forget($urn);
}
}
public function objectExists($urn)
{
$urn=$this->sanitizeUrn($urn);
try {
$this->updateCache($urn);
return true;
} catch (FileNotFoundException $e) {
return false;
}
}
protected function mapCache($urn){
$cached=$this->cache->get($urn);
if($cached){
$this->storageCache->mapFile($urn,$cached);
}
}
protected function updateCache($urn){
try {
$meta=$this->storage->getMetadata($urn);
$this->cache->put($urn,$meta['@id']);
return true;
} catch (FileNotFoundException $e) {
$this->cache->forget($urn);
throw $e;
}
}
protected function sanitizeUrn($urn){
return str_replace(['/','\\'],':',$urn);
}
}
+203
View File
@@ -0,0 +1,203 @@
<?php
namespace As247\CloudStorages\Storage;
use As247\CloudStorages\Exception\FileNotFoundException;
use As247\CloudStorages\Exception\StorageException;
use As247\CloudStorages\Support\Config;
use As247\CloudStorages\Support\FileAttributes;
use Generator;
use GuzzleHttp\Exception\ClientException;
use Microsoft\Graph\Exception\GraphException;
use As247\CloudStorages\Service\OneDrive as OneDriveService;
use Microsoft\Graph\Graph;
use Throwable;
use Traversable;
class OneDrive extends Storage
{
/** @var Graph */
protected $service;
public function __construct(Graph $graph,$options=[])
{
$this->service = new OneDriveService($graph,$options);
$this->setLogger($this->service->getLogger());
$this->setupCache($options);
}
public function getService()
{
return $this->service;
}
public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, Config $config = null): string
{
return $this->getMetadata($path)['@downloadUrl']??'';
}
/**
* @param string $directory
* @param bool $recursive
* @return Generator
* @throws GraphException
*/
public function listContents(string $directory = '', bool $recursive = false): Traversable
{
try {
$results = $this->service->listChildren($directory);
foreach ($results as $id => $result) {
$result = $this->service->normalizeMetadata($result, rtrim($directory,'\/') . '/' . $result['name']);
yield $id => $result;
if ($recursive && $result['type'] === 'dir') {
yield from $this->listContents($result['path'], $recursive);
}
}
} catch (ClientException $e) {
if ($e->getResponse()->getStatusCode() === 404) {
yield from [];
}
}
}
public function writeStream(string $path, $contents, Config $config = null): void
{
try {
$this->service->upload($path, $contents);
$this->cache->forgetBranch($path);
if ($config && $visibility = $config->get('visibility')) {
$this->setVisibility($path, $visibility);
}
} catch (ClientException $e) {
throw new StorageException($e->getMessage(),'writeStream',$e);
} catch (GraphException $e) {
throw new StorageException($e->getMessage(),'writeStream',$e);
}
}
public function readStream(string $path)
{
try {
return $this->service->download($path);
} catch (ClientException $e) {
throw new StorageException($e->getMessage(),'readStream',$e);
} catch (GraphException $e) {
throw new StorageException($e->getMessage(),'readStream',$e);
}
}
public function delete(string $path): void
{
try {
$this->service->delete($path);
$this->cache->delete($path);
} catch (ClientException $e) {
if ($e->getResponse()->getStatusCode() === 404) {
throw FileNotFoundException::create($path);
}
throw new StorageException($e->getMessage(),'delete',$e);
} catch (GraphException $e) {
throw new StorageException($e->getMessage(),'delete',$e);
}
}
public function deleteDirectory(string $path): void
{
$this->delete($path);
$this->cache->deleteDir($path);
}
public function createDirectory(string $path, Config $config = null): void
{
try {
$response = $this->service->createDirectory($path);
$this->cache->forgetBranch($path);
$file = FileAttributes::fromArray($this->service->normalizeMetadata($response, $path));
if (!$file->isDir()) {
throw new StorageException('File already exists at '.$path,'createDirectory');
}
} catch (GraphException $e) {
throw new StorageException($e->getMessage(),'createDirectory');
} catch (ClientException $e) {
throw new StorageException($e->getMessage(),'createDirectory');
}
}
/**
* @param string $path
* @param mixed $visibility
* @throws GraphException
*/
public function setVisibility(string $path, $visibility): void
{
if ($visibility === Storage::VISIBILITY_PUBLIC) {
$this->service->publish($path);
$this->cache->forget($path);
} elseif ($visibility === Storage::VISIBILITY_PRIVATE) {
$this->service->unPublish($path);
$this->cache->forget($path);
} else {
throw new \InvalidArgumentException('Unknown visibility: ' . $visibility);
}
}
/**
* @param string $source
* @param string $destination
* @param Config|null $config
* @throws GraphException
*/
public function move(string $source, string $destination, Config $config = null): void
{
$this->service->move($source, $destination);
$this->cache->move($source,$destination);
}
/**
* @param string $source
* @param string $destination
* @param Config|null $config
* @throws GraphException
*/
public function copy(string $source, string $destination, Config $config = null): void
{
$this->service->copy($source, $destination);
$this->cache->forgetBranch($destination);
$this->setVisibility($destination, $this->getMetadata($source)->visibility());
}
/**
* @param $path
* @return FileAttributes
* @throws FileNotFoundException
*/
public function getMetadata($path): FileAttributes
{
try {
$meta=$this->cache->get($path);
if(!is_null($meta) && !$meta){
throw new FileNotFoundException($path);
}
if(!isset($meta)) {
$meta = $this->service->getItem($path, ['expand' => 'permissions']);
$this->cache->put($path,$meta);
}
$attributes = $this->service->normalizeMetadata($meta, $path);
return FileAttributes::fromArray($attributes);
} catch (ClientException $e) {
if ($e->getResponse()->getStatusCode() === 404) {
$this->cache->put($path,false);
throw new FileNotFoundException($path, 0, $e);
}
throw new StorageException($e->getMessage(),'getMetadata',$e);
} catch (FileNotFoundException $e){
throw $e;
}catch (Throwable $e) {
throw new StorageException($e->getMessage(),'getMetadata',$e);
}
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace As247\CloudStorages\Storage;
use As247\CloudStorages\Cache\PathCache;
use As247\CloudStorages\Contracts\Storage\StorageContract;
use As247\CloudStorages\Service\HasLogger;
use Closure;
abstract class Storage implements StorageContract
{
/**
* @var PathCache
*/
protected $cache;
use HasLogger;
protected function setupCache($options){
$cache=$options['cache']??null;
if($cache instanceof Closure){
$cache=$cache();
}
if(!$cache instanceof PathCache){
$cache=new PathCache();
}
$this->setCache($cache);
}
public function setCache(PathCache $cache){
$this->cache=$cache;
return $this;
}
public function getCache(){
return $this->cache;
}
}
@@ -0,0 +1,64 @@
<?php
namespace As247\CloudStorages\Support;
use RuntimeException;
trait AttributesAccess
{
protected $attributes=[];
/**
* @param mixed $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return array_key_exists($offset,$this->attributes);
}
/**
* @param mixed $offset
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->attributes[$offset];
}
/**
* @param mixed $offset
* @param mixed $value
*/
public function offsetSet($offset, $value): void
{
throw new RuntimeException('Properties can not be manipulated');
}
/**
* @param mixed $offset
*/
public function offsetUnset($offset): void
{
throw new RuntimeException('Properties can not be manipulated');
}
public function __get($name)
{
return $this->offsetGet($name);
}
public function __set($name, $value)
{
$this->offsetSet($name,$value);
}
public function __isset($name)
{
return $this->offsetExists($name);
}
public function __unset($name)
{
$this->offsetUnset($name);
}
}
+107
View File
@@ -0,0 +1,107 @@
<?php
namespace As247\CloudStorages\Support;
class Config
{
/**
* @var array
*/
protected $settings = [];
/**
* @var Config|null
*/
protected $fallback;
/**
* Constructor.
*
* @param array $settings
*/
public function __construct(array $settings = [])
{
$this->settings = $settings;
}
/**
* Get a setting.
*
* @param string $key
* @param mixed $default
*
* @return mixed config setting or default when not found
*/
public function get(string $key, $default = null)
{
if ( ! array_key_exists($key, $this->settings)) {
return $this->getDefault($key, $default);
}
return $this->settings[$key];
}
/**
* Check if an item exists by key.
*
* @param string $key
*
* @return bool
*/
public function has(string $key)
{
if (array_key_exists($key, $this->settings)) {
return true;
}
return $this->fallback instanceof Config
? $this->fallback->has($key)
: false;
}
/**
* Try to retrieve a default setting from a config fallback.
*
* @param string $key
* @param mixed $default
*
* @return mixed config setting or default when not found
*/
protected function getDefault(string $key, $default)
{
if ( ! $this->fallback) {
return $default;
}
return $this->fallback->get($key, $default);
}
/**
* Set a setting.
*
* @param string $key
* @param mixed $value
*
* @return $this
*/
public function set(string $key, $value)
{
$this->settings[$key] = $value;
return $this;
}
/**
* Set the fallback.
*
* @param Config $fallback
*
* @return $this
*/
public function setFallback(Config $fallback)
{
$this->fallback = $fallback;
return $this;
}
}
@@ -0,0 +1,26 @@
<?php
namespace As247\CloudStorages\Support;
trait ConfigConverter
{
protected function convertConfig($config=null)
{
if($config){
if(is_array($config)){
return new \As247\CloudStorages\Support\Config($config);
}
if(method_exists($config,'toArray')) {
return new \As247\CloudStorages\Support\Config($config->toArray());
}else{
return new \As247\CloudStorages\Support\Config([
'copy_destination_same_as_source'=>$config->get('copy_destination_same_as_source'),
'move_destination_same_as_source'=>$config->get('move_destination_same_as_source'),
'visibility'=>$config->get('visibility'),
'directory_visibility'=>$config->get('directory_visibility'),
]);
}
}
return new \As247\CloudStorages\Support\Config();
}
}
@@ -0,0 +1,89 @@
<?php
namespace As247\CloudStorages\Support;
/**
* Class FileAttributes
* @package As247\CloudStorages\Support
* @property $path
* @property $type
* @property $file_size
* @property $visibility
* @property $last_modified
* @property $mime_type
*/
class FileAttributes implements StorageAttributes
{
use AttributesAccess;
public function __construct(array $attributes) {
$this->attributes=$attributes;
if(!isset($this->attributes[static::ATTRIBUTE_TYPE])) {
$this->attributes[static::ATTRIBUTE_TYPE] = StorageAttributes::TYPE_FILE;
}
}
public function type(): string
{
return $this->type;
}
public function path(): string
{
return $this->path;
}
public function fileSize(): ?int
{
return $this->file_size;
}
public function visibility(): ?string
{
return $this->visibility;
}
public function lastModified(): ?int
{
return $this->last_modified;
}
public function mimeType(): ?string
{
return $this->mime_type;
}
public function isFile(): bool
{
return $this->type===StorageAttributes::TYPE_FILE;
}
public function isDir(): bool
{
return $this->type===StorageAttributes::TYPE_DIRECTORY;
}
public static function fromArray(array $attributes): FileAttributes
{
return new FileAttributes($attributes);
}
public function toArray(){
return $this->attributes;
}
public function toArrayV1(){
return array_merge($this->attributes,
[
'size'=>$this->file_size??0,
'mimetype'=>$this->mime_type??0,
'timestamp'=>$this->last_modified??null,
]
);
}
public function jsonSerialize(): array
{
return $this->toArray();
}
}
@@ -0,0 +1,22 @@
<?php
namespace As247\CloudStorages\Support;
trait GetTemporaryUrl
{
public function getTemporaryUrl($path, $expiration=null, $options=[])
{
if($expiration===null){
$expiration=time()+3600;
}
if(is_int($expiration)){
$expiration=(new \DateTime())->setTimestamp($expiration);
}
if(isset($this->prefixer)){
$path=$this->prefixer->prefixPath($path);
}else{
$path=$this->applyPathPrefix($path);
}
return $this->storage->temporaryUrl($path,$expiration,$this->convertConfig($options));
}
}
@@ -0,0 +1,317 @@
<?php
/**
* Created by PhpStorm.
* User: alt
* Date: 17-Oct-18
* Time: 9:41 PM
*/
namespace As247\CloudStorages\Support;
use As247\CloudStorages\Cache\TempCache;
use As247\CloudStorages\Contracts\Cache\Store;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
class OneDriveOauth
{
protected $clientId;
protected $clientSecret;
protected $accessToken;
protected $refreshToken;
protected $tenantId='common';
protected $httpClient;
/**
* @var Store
*/
protected $cache;
protected $tokenEndpoint = 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token';
protected $oauthUrl = 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize';
protected $oauthParams = [
'access_type' => 'offline',
'redirect_uri' => 'http://localhost:53682',
'response_type' => 'code',
'scope' => 'files.readwrite.all offline_access',
];
public function __construct(Store $cache = null)
{
$this->cache = $cache;
}
public function createAuthUrl()
{
$this->oauthParams['client_id'] = $this->clientId;
$this->oauthParams['redirect_uri'] = $this->getCurrentUrl();
$this->oauthParams['state'] = time();
return str_replace('{tenant}', $this->tenantId, $this->oauthUrl) . '?' . http_build_query($this->oauthParams);
}
protected function getCurrentUrl()
{
return 'http://' . $_SERVER['HTTP_HOST'];
}
/**
* @param $code
* @return mixed
* @throws GuzzleException
*/
public function fetchAccessTokenWithAuthCode($code)
{
$postKey = 'form_params';
$response = $this->getHttpClient()->post(
str_replace('{tenant}', $this->tenantId, $this->tokenEndpoint),
[
'headers' => ['Accept' => 'application/json'],
$postKey => [
'client_id' => $this->clientId, 'client_secret' => $this->clientSecret,
'grant_type' => 'authorization_code',
'redirect_uri' => $this->getCurrentUrl(),
'code' => $code,
],
]);
return json_decode($response->getBody(), true);
}
/**
* Fetch new access token or return existing one
* @param int $minLifetime If token life time smaller this value then it will fetch new token
* eg. current token expires in 50 sec, but we need 60, then it fetch new token
* @return mixed
* @throws GuzzleException
*/
function getAccessToken($minLifetime = 600)
{
$key = $this->clientId . $this->clientSecret . $this->refreshToken;
$token = $this->getCache()->get($key);
if (!$token) {
$token = [];
$token['refresh_token'] = $this->refreshToken;
}
$token_created_at = isset($token['created_at']) ? $token['created_at'] : 0;
$token_expired_in = isset($token['expires_in']) ? $token['expires_in'] : 0;
$willBeExpireIn = $token_expired_in + $token_created_at - time();
if (empty($token['access_token']) || $willBeExpireIn <= $minLifetime) {
$token = $this->fetchAccessTokenWithRefreshToken($this->refreshToken);
$token['created_at'] = time();
if (!empty($token['access_token'])) {
$this->getCache()->put($key, $token, 0);
}
}
return $token['access_token'];
}
/**
* @param string $refresh_token
* @return mixed
* @throws GuzzleException
*/
function fetchAccessTokenWithRefreshToken($refresh_token = '')
{
$refresh_token = $refresh_token ?: $this->refreshToken;
$postKey = 'form_params';
$response = $this->getHttpClient()->post(
str_replace('{tenant}', $this->tenantId, $this->tokenEndpoint),
[
'headers' => ['Accept' => 'application/json'],
$postKey => [
'client_id' => $this->clientId, 'client_secret' => $this->clientSecret,
'grant_type' => 'refresh_token',
'refresh_token' => $refresh_token,
],
]);
return json_decode($response->getBody(), true);
}
/**
* Get a instance of the Guzzle HTTP client.
*
* @return Client
*/
protected function getHttpClient()
{
if (is_null($this->httpClient)) {
$this->httpClient = new Client();
}
return $this->httpClient;
}
/**
* @return TempCache|Store
*/
public function getCache()
{
if (!$this->cache) {
$this->cache = new TempCache(static::class);
}
return $this->cache;
}
/**
* @param string $clientId
* @return OneDriveOauth
*/
public function setClientId(string $clientId): OneDriveOauth
{
$this->clientId = $clientId;
return $this;
}
/**
* @param string $clientSecret
* @return OneDriveOauth
*/
public function setClientSecret(string $clientSecret): OneDriveOauth
{
$this->clientSecret = $clientSecret;
return $this;
}
/**
* @param mixed $accessToken
* @return OneDriveOauth
*/
public function setAccessToken($accessToken)
{
$this->accessToken = $accessToken;
return $this;
}
/**
* @param string $refreshToken
* @return OneDriveOauth
*/
public function setRefreshToken(string $refreshToken): OneDriveOauth
{
$this->refreshToken = $refreshToken;
return $this;
}
/**
* @param mixed $httpClient
* @return OneDriveOauth
*/
public function setHttpClient($httpClient)
{
$this->httpClient = $httpClient;
return $this;
}
/**
* @param $cache
* @return OneDriveOauth
*/
public function setCache(Store $cache): OneDriveOauth
{
$this->cache = $cache;
return $this;
}
/**
* @param string $tokenEndpoint
* @return OneDriveOauth
*/
public function setTokenEndpoint(string $tokenEndpoint): OneDriveOauth
{
$this->tokenEndpoint = $tokenEndpoint;
return $this;
}
/**
* @return mixed
*/
public function getTenantId()
{
return $this->tenantId;
}
/**
* @param mixed $tenantId
* @return OneDriveOauth
*/
public function setTenantId($tenantId)
{
$this->tenantId = $tenantId;
return $this;
}
/**
* @return string
*/
public function getOauthUrl(): string
{
return $this->oauthUrl;
}
/**
* @param string $oauthUrl
* @return OneDriveOauth
*/
public function setOauthUrl(string $oauthUrl): OneDriveOauth
{
$this->oauthUrl = $oauthUrl;
return $this;
}
/**
* @return string
*/
public function getClientId(): string
{
return $this->clientId;
}
/**
* @return string
*/
public function getClientSecret(): string
{
return $this->clientSecret;
}
/**
* @return string
*/
public function getRefreshToken(): string
{
return $this->refreshToken;
}
/**
* @return string
*/
public function getTokenEndpoint(): string
{
return $this->tokenEndpoint;
}
/**
* @return string[]
*/
public function getOauthParams(): array
{
return $this->oauthParams;
}
/**
* @param string[] $oauthParams
* @return OneDriveOauth
*/
public function setOauthParams(array $oauthParams): OneDriveOauth
{
$this->oauthParams = $oauthParams;
return $this;
}
}
+116
View File
@@ -0,0 +1,116 @@
<?php
namespace As247\CloudStorages\Support;
use As247\CloudStorages\Exception\PathOutsideRootException;
use LogicException;
class Path
{
/**
* Clean the path
* ./a/b/d/..//c.txt will return /a/b/c.txt
* @param $path
* @return string
*/
public static function clean($path){
return static::cleanPath($path);
}
public static function countSegments($path){
return static::cleanPath($path,'count');
}
/**
* Explode path
* /a/b/c.txt will return ['/','a','b','c.txt']
* @param $path
* @return array
*/
public static function explode($path){
return static::cleanPath($path,'array');
}
public static function join(...$paths){
return static::cleanPath($paths);
}
protected static function cleanPath($path,$return='string'){
if(is_array($path)){
$path=join('/',$path);
}
if(!is_string($path)){
$path=(string)$path;
}
$path=static::normalizeRelativePath($path);
if($return==='string'){
$path='/'.join('/',$path);
}elseif($return==='count'){
return count($path);
}else{
array_unshift($path,'/');
}
return $path;
}
public static function replace($search, $replace, $subject){
$pos = strpos($subject, $search);
if ($pos === 0) {
$subject = substr_replace($subject, $replace, $pos, strlen($search));
}
return $subject;
}
/**
* Normalize relative directories in a path.
*
* @param string $path
*
* @throws LogicException
*
* @return array
*/
protected static function normalizeRelativePath(string $path)
{
$path = str_replace('\\', '/', $path);
$path = static::removeFunkyWhiteSpace($path);
$parts = [];
foreach (explode('/', $path) as $part) {
switch ($part) {
case '':
case '.':
break;
case '..':
if (empty($parts)) {
throw PathOutsideRootException::atLocation($path);
}
array_pop($parts);
break;
default:
$parts[] = $part;
break;
}
}
return $parts;
}
/**
* Removes unprintable characters and invalid unicode characters.
*
* @param string $path
*
* @return string $path
*/
protected static function removeFunkyWhiteSpace(string $path)
{
// We do this check in a loop, since removing invalid unicode characters
// can lead to new characters being created.
while (preg_match('#\p{C}+|^\./#u', $path)) {
$path = preg_replace('#\p{C}+|^\./#u', '', $path);
}
return $path;
}
}
@@ -0,0 +1,30 @@
<?php
namespace As247\CloudStorages\Support;
use ArrayAccess;
use JsonSerializable;
interface StorageAttributes extends JsonSerializable, ArrayAccess
{
public const TYPE_FILE = 'file';
public const TYPE_DIRECTORY = 'dir';
public const ATTRIBUTE_PATH = 'path';
public const ATTRIBUTE_TYPE = 'type';
public const ATTRIBUTE_VISIBILITY = 'visibility';
public const ATTRIBUTE_LAST_MODIFIED = 'last_modified';
public const ATTRIBUTE_FILE_SIZE = 'file_size';
public const ATTRIBUTE_MIME_TYPE = 'mime_type';
public function path(): string;
public function type(): string;
public function visibility(): ?string;
public static function fromArray(array $attributes);
public function isFile(): bool;
public function isDir(): bool;
}
@@ -0,0 +1,243 @@
<?php
namespace As247\CloudStorages\Support;
use As247\CloudStorages\Contracts\Storage\StorageContract;
use As247\CloudStorages\Exception\FileNotFoundException;
use As247\CloudStorages\Storage\AList;
use As247\CloudStorages\Storage\GoogleDrive;
use As247\CloudStorages\Storage\OneDrive;
use As247\CloudStorages\Support\StorageAttributes;
use GuzzleHttp\Psr7\Utils;
use League\Flysystem\Config;
use League\Flysystem\DirectoryAttributes;
use League\Flysystem\FileAttributes;
use League\Flysystem\UnableToRetrieveMetadata;
use League\Flysystem\UnableToWriteFile;
trait StorageToAdapter
{
use ConfigConverter;
/**
* @var StorageContract
*/
protected $storage;
/**
* @return StorageContract|OneDrive|GoogleDrive|AList
*/
public function getStorage()
{
return $this->storage;
}
public function fileExists(string $path): bool
{
try {
return $this->getMetadata($path) instanceof FileAttributes;
} catch (\Exception $e) {
return false;
}
}
public function directoryExists(string $path): bool
{
try {
return $this->getMetadata($path) instanceof DirectoryAttributes;
} catch (\Exception $e) {
return false;
}
}
public function write(string $path, string $contents, Config $config): void
{
$this->writeStream($path, Utils::streamFor($contents), $config);
}
public function writeStream(string $path, $contents, Config $config): void
{
try {
$config = $this->convertConfig($config);
$this->storage->writeStream($this->prefixer->prefixPath($path), $contents, $config);
}catch (\Exception $e){
throw UnableToWriteFile::atLocation($path, $e->getMessage(), $e);
}
}
public function read(string $path): string
{
return stream_get_contents($this->readStream($path));
}
public function readStream(string $path)
{
try {
return $this->storage->readStream($this->prefixer->prefixPath($path));
} catch (\Exception $e) {
throw \League\Flysystem\UnableToReadFile::fromLocation($path, $e->getMessage(), $e);
}
}
public function delete(string $path): void
{
if ($this->isRootPath($path)) {
return ;
}
try {
$this->storage->delete($this->prefixer->prefixPath($path));
}catch (FileNotFoundException $e){
return ;
}
catch (\Exception $e) {
throw \League\Flysystem\UnableToDeleteFile::atLocation($path, $e->getMessage(), $e);
}
}
public function deleteDirectory(string $path): void
{
if ($this->isRootPath($path)) {
return ;
}
try {
$this->storage->deleteDirectory($this->prefixer->prefixPath($path));
} catch(\Exception $e){
throw \League\Flysystem\UnableToDeleteDirectory::atLocation($path, $e->getMessage(), $e);
}
}
public function createDirectory(string $path, Config $config): void
{
try {
$config = $this->convertConfig($config);
$this->storage->createDirectory($this->prefixer->prefixPath($path), $config);
} catch (\Exception $e) {
throw \League\Flysystem\UnableToCreateDirectory::atLocation($path, $e->getMessage(), $e);
}
}
public function setVisibility(string $path, string $visibility): void
{
try {
$this->storage->setVisibility($this->prefixer->prefixPath($path), $visibility);
}catch (\Exception $e){
throw \League\Flysystem\UnableToSetVisibility::atLocation($path, $e->getMessage(), $e);
}
}
public function visibility(string $path): FileAttributes
{
return $this->getMetadata($path);
}
public function mimeType(string $path): FileAttributes
{
$meta=$this->getMetadata($path);
if($meta instanceof FileAttributes){
if($meta->mimeType()!=='application/octet-stream') {
return $meta;
}
}
throw UnableToRetrieveMetadata::create($path, 'mimeType');
}
public function lastModified(string $path): FileAttributes
{
return $this->getMetadata($path);
}
public function fileSize(string $path): FileAttributes
{
$meta= $this->getMetadata($path);
if($meta instanceof FileAttributes){
return $meta;
}
throw UnableToRetrieveMetadata::create($path, 'fileSize');
}
public function listContents(string $path, bool $deep): iterable
{
$contents=$this->storage->listContents($this->prefixer->prefixPath($path), $deep);
foreach ($contents as $key=>$content){
$path=$this->prefixer->stripPrefix($content['path']);
$visibility=$content[StorageAttributes::ATTRIBUTE_VISIBILITY];
$lastModified=$content[StorageAttributes::ATTRIBUTE_LAST_MODIFIED];
$fileSize=$content[StorageAttributes::ATTRIBUTE_FILE_SIZE];
$isDirectory=$content[StorageAttributes::ATTRIBUTE_TYPE]===StorageAttributes::TYPE_DIRECTORY;
yield $isDirectory ?
new DirectoryAttributes($path, $visibility, $lastModified) :
new FileAttributes(
str_replace('\\', '/', $path),
$fileSize,
$visibility,
$lastModified
);
}
}
public function move(string $source, string $destination, Config $config): void
{
try {
$source = $this->prefixer->prefixPath($source);
$destination = $this->prefixer->prefixPath($destination);
$this->storage->move($source, $destination, $this->convertConfig($config));
} catch (\Exception $e) {
throw \League\Flysystem\UnableToMoveFile::fromLocationTo($source, $destination, $e);
}
}
public function copy(string $source, string $destination, Config $config): void
{
try {
$config = $this->convertConfig($config);
$source = $this->prefixer->prefixPath($source);
//Flysystem expect to overwrite the file then we try to delete the destination file first
$this->delete($destination);
$destination = $this->prefixer->prefixPath($destination);
$this->storage->copy($source, $destination, $config);
} catch (\Exception $e) {
throw \League\Flysystem\UnableToCopyFile::fromLocationTo($source, $destination, $e);
}
}
/**
* @param $path
* @return FileAttributes|DirectoryAttributes|bool
*/
public function getMetadata($path)
{
try {
$meta = $this->storage->getMetadata($this->prefixer->prefixPath($path));
return $meta->type()===StorageAttributes::TYPE_DIRECTORY ?
new DirectoryAttributes($path, $meta->visibility(), $meta->lastModified()) :
new FileAttributes(
str_replace('\\', '/', $path),
$meta->fileSize(),
$meta->visibility(),
$meta->lastModified(),
$meta->mimeType()
);
} catch (\Exception $e) {
throw UnableToRetrieveMetadata::create($path, 'metadata', $e);
}
}
public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, Config $config): string
{
try {
return $this->storage->temporaryUrl($this->prefixer->prefixPath($path), $expiresAt, $this->convertConfig($config));
} catch (\Exception $e) {
throw UnableToRetrieveMetadata::create($path, 'temporaryUrl', $e);
}
}
protected function isRootPath($path)
{
if ($this->prefixer->prefixPath($path) === $this->prefixer->prefixPath('')) {
return true;
}
return false;
}
}
@@ -0,0 +1,321 @@
<?php
namespace As247\CloudStorages\Support;
use As247\CloudStorages\Contracts\Storage\StorageContract;
use As247\CloudStorages\Exception\FileNotFoundException;
use As247\CloudStorages\Exception\StorageException;
use As247\CloudStorages\Storage\GoogleDrive;
use As247\CloudStorages\Storage\OneDrive;
use GuzzleHttp\Psr7\Utils;
use League\Flysystem\Config;
use League\Flysystem\Util;
trait StorageToAdapterV1
{
use ConfigConverter;
/**
* @var StorageContract
*/
protected $storage;
protected $throwException = false;
protected $exceptExceptions = [
FileNotFoundException::class,
];
/**
* @return StorageContract|OneDrive|GoogleDrive
*/
public function getStorage()
{
return $this->storage;
}
/**
* @inheritDoc
*/
public function write($path, $contents, Config $config = null)
{
return $this->writeStream($path, Utils::streamFor($contents), $config);
}
/**
* @inheritDoc
*/
public function writeStream($path, $resource, Config $config)
{
try {
$config = $this->convertConfig($config);
$this->storage->writeStream($this->applyPathPrefix($path), $resource, $config);
return $this->getMetadata($path);
} catch (StorageException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function update($path, $contents, Config $config)
{
return $this->write($path, $contents, $config);
}
/**
* @inheritDoc
*/
public function updateStream($path, $resource, Config $config)
{
return $this->writeStream($path, $resource, $config);
}
/**
* @inheritDoc
*/
public function rename($path, $newpath)
{
try {
$path = $this->applyPathPrefix($path);
$newpath = $this->applyPathPrefix($newpath);
$this->storage->move($path, $newpath, $this->convertConfig(new Config()));
return true;
} catch (StorageException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function copy($path, $newpath)
{
try {
$config = $this->convertConfig(new Config());
$path = $this->applyPathPrefix($path);
$newpath = $this->applyPathPrefix($newpath);
$this->storage->copy($path, $newpath, $config);
return true;
} catch (StorageException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function delete($path)
{
if ($this->isRootPath($path)) {
return false;
}
try {
$this->storage->delete($this->applyPathPrefix($path));
return true;
} catch (StorageException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
} catch (FileNotFoundException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function deleteDir($dirname)
{
if ($this->isRootPath($dirname)) {
return false;
}
try {
$this->storage->deleteDirectory($this->applyPathPrefix($dirname));
return true;
} catch (StorageException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
} catch (FileNotFoundException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function createDir($dirname, Config $config)
{
try {
$config = $this->convertConfig($config);
$this->storage->createDirectory($this->applyPathPrefix($dirname), $config);
return $this->getMetadata($dirname);
} catch (StorageException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function setVisibility($path, $visibility)
{
$this->storage->setVisibility($this->applyPathPrefix($path), $visibility);
return $this->getMetadata($path);
}
/**
* @inheritDoc
*/
public function has($path)
{
return (bool)$this->getMetadata($path);
}
/**
* @inheritDoc
*/
public function read($path)
{
$stream = $this->readStream($path);
return ['contents' => stream_get_contents($stream['stream'])];
}
/**
* @inheritDoc
*/
public function readStream($path)
{
try {
return ['stream' => $this->storage->readStream($this->applyPathPrefix($path))];
} catch (StorageException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function listContents($directory = '', $recursive = false)
{
$contents = array_values(iterator_to_array($this->storage->listContents($this->applyPathPrefix($directory), $recursive), false));
$contents = array_map(function ($v) {
$v['path'] = $this->removePathPrefix($v['path']);
return $v;
}, $contents);
return $contents;
}
/**
* @inheritDoc
*/
public function getMetadata($path)
{
try {
$meta = $this->storage->getMetadata($this->applyPathPrefix($path));
return $meta->toArrayV1();
} catch (FileNotFoundException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function getSize($path)
{
return $this->getMetadata($path);
}
/**
* @inheritDoc
*/
public function getMimetype($path)
{
return $this->getMetadata($path);
}
/**
* @inheritDoc
*/
public function getTimestamp($path)
{
return $this->getMetadata($path);
}
/**
* @inheritDoc
*/
public function getVisibility($path)
{
return $this->getMetadata($path);
}
public function setPathPrefix($path)
{
parent::setPathPrefix(Util::normalizePath($path));
}
public function applyPathPrefix($path)
{
return Util::normalizePath(parent::applyPathPrefix($path));
}
protected function isRootPath($path)
{
if ($this->applyPathPrefix($path) === $this->applyPathPrefix('')) {
return true;
}
return false;
}
protected function shouldThrowException($e)
{
if(!$this->throwException){
return false;
}
if (empty($this->exceptExceptions)) {
return $this->throwException;
}
return !in_array(get_class($e), $this->exceptExceptions);
}
public function setExcerptExceptions($exceptions)
{
$this->exceptExceptions = $exceptions;
return $this;
}
public function getExcerptExceptions()
{
return $this->exceptExceptions;
}
}
+33
View File
@@ -0,0 +1,33 @@
{
"name": "as247/flysystem-google-drive",
"description": "Google Drive Adapter for Flysystem",
"keywords": ["php", "storage", "drive","google","google-drive"],
"homepage": "https://github.com/as247",
"license": "MIT",
"authors": [
{
"name": "As247",
"email": "as247@vui360.com",
"homepage": "http://as247.vui360.com"
}
],
"require": {
"php": ">=7.1",
"ext-json": "*",
"as247/cloud-storages": "^1.2.4",
"league/flysystem": "^3.23",
"google/apiclient": "^2.0"
},
"autoload": {
"psr-4": {
"As247\\Flysystem\\GoogleDrive\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"As247\\Flysystem\\GoogleDrive\\Tests\\": "tests/"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
+38
View File
@@ -0,0 +1,38 @@
# Flysystem Adapter for Google Drive
[![Author](https://img.shields.io/badge/author-as247-orange)](http://as247.vui360.com/)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
## Installation
```bash
composer require as247/flysystem-google-drive
```
## Usage
#### Follow [Google Docs](https://developers.google.com/drive/v3/web/enable-sdk) to obtain your `ClientId, ClientSecret & refreshToken`
#### In addition, you can also check these easy-to-follow tutorial by [@ivanvermeyen](https://github.com/ivanvermeyen/laravel-google-drive-demo)
- [Getting your Client ID and Secret](https://github.com/ivanvermeyen/laravel-google-drive-demo/blob/master/README/1-getting-your-dlient-id-and-secret.md)
- [Getting your Refresh Token](https://github.com/ivanvermeyen/laravel-google-drive-demo/blob/master/README/2-getting-your-refresh-token.md)
```php
$client = new \Google_Client();
$client->setClientId('[app client id].apps.googleusercontent.com');
$client->setClientSecret('[app client secret]');
$client->fetchAccessTokenWithRefreshToken('[your refresh token]');
$service = new \Google_Service_Drive($client);
$options=[
'root'=>'[Root folder id]',
'teamDrive'=>'[Team drive id]'//If your root folder inside team drive
'google_drive_adapter_prefix'=>'[Path prefix]',//Path prefix inside root folder
];
$adapter = new \As247\Flysystem\GoogleDrive\GoogleDriveAdapter($service, $options);
$filesystem = new \League\Flysystem\Filesystem($adapter);
```
@@ -0,0 +1,29 @@
<?php
/**
* Created by PhpStorm.
* User: alt
* Date: 04-Oct-18
* Time: 10:46 PM
*/
namespace As247\Flysystem\GoogleDrive;
use As247\CloudStorages\Storage\GoogleDrive;
use As247\CloudStorages\Support\StorageToAdapter;
use Google_Service_Drive;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\PathPrefixer;
class GoogleDriveAdapter implements FilesystemAdapter
{
use StorageToAdapter;
protected $prefixer;
public function __construct(Google_Service_Drive $service, $options = [])
{
if(!is_array($options)){
$options=['root'=>$options];
}
$this->storage = new GoogleDrive($service,$options);
$this->prefixer = new PathPrefixer($options['google_drive_adapter_prefix']??'', DIRECTORY_SEPARATOR);
}
}