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
+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.
}
}