· 6 years ago · Oct 07, 2019, 10:02 AM
1<?php
2namespace Hypweb\Flysystem\GoogleDrive;
3
4use Google_Service_Drive;
5use Google_Service_Drive_DriveFile;
6use Google_Service_Drive_FileList;
7use Google_Service_Drive_Permission;
8use Google_Http_MediaFileUpload;
9use League\Flysystem\Adapter\AbstractAdapter;
10use League\Flysystem\AdapterInterface;
11use League\Flysystem\Config;
12use League\Flysystem\Util;
13
14class GoogleDriveAdapter extends AbstractAdapter
15{
16
17 /**
18 * Fetch fields setting for get
19 *
20 * @var string
21 */
22 const FETCHFIELDS_GET = 'id,name,mimeType,modifiedTime,parents,permissions,size,webContentLink,webViewLink';
23 /**
24 * Fetch fields setting for list
25 *
26 * @var string
27 */
28 const FETCHFIELDS_LIST = 'files(FETCHFIELDS_GET),nextPageToken';
29
30
31 /**
32 * MIME tyoe of directory
33 *
34 * @var string
35 */
36 const DIRMIME = 'application/vnd.google-apps.folder';
37
38 /**
39 * Google_Service_Drive instance
40 *
41 * @var Google_Service_Drive
42 */
43 protected $service;
44
45 /**
46 * Default options
47 *
48 * @var array
49 */
50 protected static $defaultOptions = [
51 'spaces' => 'drive',
52 'useHasDir' => false,
53 'additionalFetchField' => 'webViewLink,id',
54 'publishPermission' => [
55 'type' => 'anyone',
56 'role' => 'reader',
57 'withLink' => true
58 ],
59 'appsExportMap' => [
60 'application/vnd.google-apps.document' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
61 'application/vnd.google-apps.spreadsheet' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
62 'application/vnd.google-apps.drawing' => 'application/pdf',
63 'application/vnd.google-apps.presentation' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
64 'application/vnd.google-apps.script' => 'application/vnd.google-apps.script+json',
65 'default' => 'application/pdf'
66 ],
67 // Default parameters for each command
68 // see https://developers.google.com/drive/v3/reference/files
69 // ex. 'defaultParams' => ['files.list' => ['includeTeamDriveItems' => true]]
70 'defaultParams' => [],
71 // Team Drive Id
72 'teamDriveId' => null,
73 // Corpora value for files.list with the Team Drive
74 'corpora' => 'teamDrive',
75 // Delete action 'trash' (Into trash) or 'delete' (Permanently delete)
76 'deleteAction' => 'trash'
77 ];
78
79 /**
80 * A comma-separated list of spaces to query
81 * Supported values are 'drive', 'appDataFolder' and 'photos'
82 *
83 * @var string
84 */
85 protected $spaces;
86
87 /**
88 * Permission array as published item
89 *
90 * @var array
91 */
92 protected $publishPermission;
93
94 /**
95 * Cache of file objects
96 *
97 * @var array
98 */
99 private $cacheFileObjects = [];
100
101 /**
102 * Cache of file objects by ParentId/Name based
103 *
104 * @var array
105 */
106 private $cacheFileObjectsByName = [];
107
108 /**
109 * Cache of hasDir
110 *
111 * @var array
112 */
113 private $cacheHasDirs = [];
114
115 /**
116 * Use hasDir function
117 *
118 * @var bool
119 */
120 private $useHasDir = false;
121
122 /**
123 * List of fetch field for get
124 *
125 * @var string
126 */
127 private $fetchfieldsGet = '';
128
129 /**
130 * List of fetch field for lest
131 *
132 * @var string
133 */
134 private $fetchfieldsList = '';
135
136 /**
137 * Additional fetch fields array
138 *
139 * @var array
140 */
141 private $additionalFields = [];
142
143 /**
144 * Options array
145 *
146 * @var array
147 */
148 private $options = [];
149
150 /**
151 * Default parameters of each commands
152 *
153 * @var array
154 */
155 private $defaultParams = [];
156
157 public function __construct(Google_Service_Drive $service, $root = null, $options = [])
158 {
159 if (! $root) {
160 $root = 'root';
161 }
162 $this->service = $service;
163 $this->setPathPrefix($root);
164 $this->root = $root;
165
166 $this->options = array_replace_recursive(static::$defaultOptions, $options);
167
168 $this->spaces = $this->options['spaces'];
169 $this->useHasDir = $this->options['useHasDir'];
170 $this->publishPermission = $this->options['publishPermission'];
171
172 $this->fetchfieldsGet = self::FETCHFIELDS_GET;
173 if ($this->options['additionalFetchField']) {
174 $this->fetchfieldsGet .= ',' . $this->options['additionalFetchField'];
175 $this->additionalFields = explode(',', $this->options['additionalFetchField']);
176 }
177 $this->fetchfieldsList = str_replace('FETCHFIELDS_GET', $this->fetchfieldsGet, self::FETCHFIELDS_LIST);
178 if (isset($this->options['defaultParams']) && is_array($this->options['defaultParams'])) {
179 $this->defaultParams = $this->options['defaultParams'];
180 }
181
182 if ($this->options['teamDriveId']) {
183 $this->setTeamDriveId($this->options['teamDriveId'], $this->options['corpora']);
184 }
185 }
186
187 /**
188 * Gets the service (Google_Service_Drive)
189 *
190 * @return object Google_Service_Drive
191 */
192 public function getService()
193 {
194 return $this->service;
195 }
196
197 /**
198 * Write a new file.
199 *
200 * @param string $path
201 * @param string $contents
202 * @param Config $config
203 * Config object
204 *
205 * @return array|false false on failure file meta data on success
206 */
207 public function write($path, $contents, Config $config)
208 {
209 return $this->upload($path, $contents, $config);
210 }
211
212 /**
213 * Write a new file using a stream.
214 *
215 * @param string $path
216 * @param resource $resource
217 * @param Config $config
218 * Config object
219 *
220 * @return array|false false on failure file meta data on success
221 */
222 public function writeStream($path, $resource, Config $config)
223 {
224 return $this->write($path, $resource, $config);
225 }
226
227 /**
228 * Update a file.
229 *
230 * @param string $path
231 * @param string $contents
232 * @param Config $config
233 * Config object
234 *
235 * @return array|false false on failure file meta data on success
236 */
237 public function update($path, $contents, Config $config)
238 {
239 return $this->write($path, $contents, $config);
240 }
241
242 /**
243 * Update a file using a stream.
244 *
245 * @param string $path
246 * @param resource $resource
247 * @param Config $config
248 * Config object
249 *
250 * @return array|false false on failure file meta data on success
251 */
252 public function updateStream($path, $resource, Config $config)
253 {
254 return $this->write($path, $resource, $config);
255 }
256
257 /**
258 * Rename a file.
259 *
260 * @param string $path
261 * @param string $newpath
262 *
263 * @return bool
264 */
265 public function rename($path, $newpath)
266 {
267 list ($oldParent, $fileId) = $this->splitPath($path);
268 list ($newParent, $newName) = $this->splitPath($newpath);
269
270 $file = new Google_Service_Drive_DriveFile();
271 $file->setName($newName);
272 $opts = [
273 'fields' => $this->fetchfieldsGet
274 ];
275 if ($newParent !== $oldParent) {
276 $opts['addParents'] = $newParent;
277 $opts['removeParents'] = $oldParent;
278 }
279
280 $updatedFile = $this->service->files->update($fileId, $file, $this->applyDefaultParams($opts, 'files.update'));
281
282 if ($updatedFile) {
283 $this->cacheFileObjects[$updatedFile->getId()] = $updatedFile;
284 $this->cacheFileObjectsByName[$newParent . '/' . $newName] = $updatedFile;
285 return true;
286 }
287
288 return false;
289 }
290
291 /**
292 * Copy a file.
293 *
294 * @param string $path
295 * @param string $newpath
296 *
297 * @return bool
298 */
299 public function copy($path, $newpath)
300 {
301 list (, $srcId) = $this->splitPath($path);
302
303 list ($newParentId, $fileName) = $this->splitPath($newpath);
304
305 $file = new Google_Service_Drive_DriveFile();
306 $file->setName($fileName);
307 $file->setParents([
308 $newParentId
309 ]);
310
311 $newFile = $this->service->files->copy($srcId, $file, $this->applyDefaultParams([
312 'fields' => $this->fetchfieldsGet
313 ], 'files.copy'));
314
315 if ($newFile instanceof Google_Service_Drive_DriveFile) {
316 $this->cacheFileObjects[$newFile->getId()] = $newFile;
317 $this->cacheFileObjectsByName[$newParentId . '/' . $fileName] = $newFile;
318 list ($newDir) = $this->splitPath($newpath);
319 $newpath = (($newDir === $this->root) ? '' : ($newDir . '/')) . $newFile->getId();
320 if ($this->getRawVisibility($path) === AdapterInterface::VISIBILITY_PUBLIC) {
321 $this->publish($newpath);
322 } else {
323 $this->unPublish($newpath);
324 }
325 return true;
326 }
327
328 return false;
329 }
330
331 /**
332 * Delete a file.
333 *
334 * @param string $path
335 *
336 * @return bool
337 */
338 public function delete($path)
339 {
340 if ($file = $this->getFileObject($path)) {
341 $name = $file->getName();
342 list ($parentId, $id) = $this->splitPath($path);
343 if ($parents = $file->getParents()) {
344 $file = new Google_Service_Drive_DriveFile();
345 $opts = [];
346 $res = false;
347 if (count($parents) > 1) {
348 $opts['removeParents'] = $parentId;
349 } else {
350 if ($this->options['deleteAction'] === 'delete') {
351 try {
352 $this->service->files->delete($id);
353 } catch (Google_Exception $e) {
354 return false;
355 }
356 $res = true;
357 } else {
358 $file->setTrashed(true);
359 }
360 }
361 if (!$res) {
362 try {
363 $this->service->files->update($id, $file, $this->applyDefaultParams($opts, 'files.update'));
364 } catch (Google_Exception $e) {
365 return false;
366 }
367 }
368 unset($this->cacheFileObjects[$id], $this->cacheHasDirs[$id], $this->cacheFileObjectsByName[$parentId . '/' . $name]);
369 return true;
370 }
371 }
372 return false;
373 }
374
375 /**
376 * Delete a directory.
377 *
378 * @param string $dirname
379 *
380 * @return bool
381 */
382 public function deleteDir($dirname)
383 {
384 return $this->delete($dirname);
385 }
386
387 /**
388 * Create a directory.
389 *
390 * @param string $dirname
391 * directory name
392 * @param Config $config
393 *
394 * @return array|false
395 */
396 public function createDir($dirname, Config $config)
397 {
398 list ($pdirId, $name) = $this->splitPath($dirname);
399
400 $folder = $this->createDirectory($name, $pdirId);
401 if ($folder) {
402 $itemId = $folder->getId();
403 $this->cacheFileObjectsByName[$pdirId . '/' . $name] = $folder; // for confirmation by getMetaData() oe has() while in this connection
404 $this->cacheFileObjects[$itemId] = $folder;
405 $this->cacheHasDirs[$itemId] = false;
406 $path_parts = $this->splitFileExtension($name);
407 $result = [
408 'path' => Util::dirname($dirname) . '/' . $itemId,
409 'filename' => $path_parts['filename'],
410 'extension' => $path_parts['extension']
411 ];
412 return $result;
413 }
414
415 return false;
416 }
417
418 /**
419 * Check whether a file exists.
420 *
421 * @param string $path
422 *
423 * @return array|bool|null
424 */
425 public function has($path)
426 {
427 return ($this->getFileObject($path, true) instanceof Google_Service_Drive_DriveFile);
428 }
429
430 /**
431 * Read a file.
432 *
433 * @param string $path
434 *
435 * @return array|false
436 */
437 public function read($path)
438 {
439 list (, $fileId) = $this->splitPath($path);
440 if ($response = $this->service->files->get($fileId, $this->applyDefaultParams([
441 'alt' => 'media'
442 ], 'files.get'))) {
443 return [
444 'contents' => (string) $response->getBody()
445 ];
446 }
447
448 return false;
449 }
450
451 /**
452 * Read a file as a stream.
453 *
454 * @param string $path
455 *
456 * @return array|false
457 */
458 public function readStream($path)
459 {
460 $redirect = [];
461 if (func_num_args() > 1) {
462 $redirect = func_get_arg(1);
463 }
464 if (! $redirect) {
465 $redirect = [
466 'cnt' => 0,
467 'url' => '',
468 'token' => '',
469 'cookies' => []
470 ];
471 if ($file = $this->getFileObject($path)) {
472 $dlurl = $this->getDownloadUrl($file);
473 $client = $this->service->getClient();
474 if ($client->isUsingApplicationDefaultCredentials()) {
475 $token = $client->fetchAccessTokenWithAssertion();
476 } else {
477 $token = $client->getAccessToken();
478 }
479 $access_token = '';
480 if (is_array($token)) {
481 if (empty($token['access_token']) && !empty($token['refresh_token'])) {
482 $token = $client->fetchAccessTokenWithRefreshToken();
483 }
484 $access_token = $token['access_token'];
485 } else {
486 if ($token = @json_decode($client->getAccessToken())) {
487 $access_token = $token->access_token;
488 }
489 }
490 $redirect = [
491 'cnt' => 0,
492 'url' => '',
493 'token' => $access_token,
494 'cookies' => []
495 ];
496 }
497 } else {
498 if ($redirect['cnt'] > 5) {
499 return false;
500 }
501 $dlurl = $redirect['url'];
502 $redirect['url'] = '';
503 $access_token = $redirect['token'];
504 }
505
506 if ($dlurl) {
507 $url = parse_url($dlurl);
508 $cookies = [];
509 if ($redirect['cookies']) {
510 foreach ($redirect['cookies'] as $d => $c) {
511 if (strpos($url['host'], $d) !== false) {
512 $cookies[] = $c;
513 }
514 }
515 }
516 if ($access_token) {
517 $query = isset($url['query']) ? '?' . $url['query'] : '';
518 $stream = stream_socket_client('ssl://' . $url['host'] . ':443');
519 stream_set_timeout($stream, 300);
520 fputs($stream, "GET {$url['path']}{$query} HTTP/1.1\r\n");
521 fputs($stream, "Host: {$url['host']}\r\n");
522 fputs($stream, "Authorization: Bearer {$access_token}\r\n");
523 fputs($stream, "Connection: Close\r\n");
524 if ($cookies) {
525 fputs($stream, "Cookie: " . join('; ', $cookies) . "\r\n");
526 }
527 fputs($stream, "\r\n");
528 while (($res = trim(fgets($stream))) !== '') {
529 // find redirect
530 if (preg_match('/^Location: (.+)$/', $res, $m)) {
531 $redirect['url'] = $m[1];
532 }
533 // fetch cookie
534 if (strpos($res, 'Set-Cookie:') === 0) {
535 $domain = $url['host'];
536 if (preg_match('/^Set-Cookie:(.+)(?:domain=\s*([^ ;]+))?/i', $res, $c1)) {
537 if (! empty($c1[2])) {
538 $domain = trim($c1[2]);
539 }
540 if (preg_match('/([^ ]+=[^;]+)/', $c1[1], $c2)) {
541 $redirect['cookies'][$domain] = $c2[1];
542 }
543 }
544 }
545 }
546 if ($redirect['url']) {
547 $redirect['cnt'] ++;
548 fclose($stream);
549 return $this->readStream($path, $redirect);
550 }
551 return compact('stream');
552 }
553 }
554 return false;
555 }
556
557 /**
558 * List contents of a directory.
559 *
560 * @param string $dirname
561 * @param bool $recursive
562 *
563 * @return array
564 */
565 public function listContents($dirname = '', $recursive = false)
566 {
567 return $this->getItems($dirname, $recursive);
568 }
569
570 public function listFolderContents($id)
571 {
572 return $service->files->list(array('q' => "'$id' in parents"));
573 }
574
575 /**
576 * Get all the meta data of a file or directory.
577 *
578 * @param string $path
579 *
580 * @return array|false
581 */
582 public function getMetadata($path)
583 {
584 if ($obj = $this->getFileObject($path, true)) {
585 if ($obj instanceof Google_Service_Drive_DriveFile) {
586 return $this->normaliseObject($obj, Util::dirname($path));
587 }
588 }
589 return false;
590 }
591
592 /**
593 * Get all the meta data of a file or directory.
594 *
595 * @param string $path
596 *
597 * @return array|false
598 */
599 public function getSize($path)
600 {
601 $meta = $this->getMetadata($path);
602 return ($meta && isset($meta['size'])) ? $meta : false;
603 }
604
605 /**
606 * Get the mimetype of a file.
607 *
608 * @param string $path
609 *
610 * @return array|false
611 */
612 public function getMimetype($path)
613 {
614 $meta = $this->getMetadata($path);
615 return ($meta && isset($meta['mimetype'])) ? $meta : false;
616 }
617
618 /**
619 * Get the timestamp of a file.
620 *
621 * @param string $path
622 *
623 * @return array|false
624 */
625 public function getTimestamp($path)
626 {
627 $meta = $this->getMetadata($path);
628 return ($meta && isset($meta['timestamp'])) ? $meta : false;
629 }
630
631 /**
632 * Set the visibility for a file.
633 *
634 * @param string $path
635 * @param string $visibility
636 *
637 * @return array|false file meta data
638 */
639 public function setVisibility($path, $visibility)
640 {
641 $result = ($visibility === AdapterInterface::VISIBILITY_PUBLIC) ? $this->publish($path) : $this->unPublish($path);
642
643 if ($result) {
644 return compact('path', 'visibility');
645 }
646
647 return false;
648 }
649
650 /**
651 * Get the visibility of a file.
652 *
653 * @param string $path
654 *
655 * @return array|false
656 */
657 public function getVisibility($path)
658 {
659 return [
660 'visibility' => $this->getRawVisibility($path)
661 ];
662 }
663
664 // /////////////////- ORIGINAL METHODS -///////////////////
665
666 /**
667 * Get contents parmanent URL
668 *
669 * @param string $path
670 * itemId path
671 *
672 * @return string|false
673 */
674 public function getUrl($path)
675 {
676 if ($this->publish($path)) {
677 $obj = $this->getFileObject($path);
678 if ($url = $obj->getWebContentLink()) {
679 return str_replace('export=download', 'export=media', $url);
680 }
681 if ($url = $obj->getWebViewLink()) {
682 return $url;
683 }
684 }
685 return false;
686 }
687
688 /**
689 * Has child directory
690 *
691 * @param string $path
692 * itemId path
693 *
694 * @return array
695 */
696 public function hasDir($path)
697 {
698 $meta = $this->getMetadata($path);
699 return ($meta && isset($meta['hasdir'])) ? $meta : [
700 'hasdir' => true
701 ];
702 }
703
704 /**
705 * Do cache cacheHasDirs with batch request
706 *
707 * @param array $targets
708 * [[path => id],...]
709 *
710 * @return void
711 */
712 protected function setHasDir($targets, $object)
713 {
714 $service = $this->service;
715 $client = $service->getClient();
716 $gFiles = $service->files;
717 $opts = [
718 'pageSize' => 1
719 ];
720 $paths = [];
721 $client->setUseBatch(true);
722 $batch = $service->createBatch();
723 $i = 0;
724 foreach ($targets as $id) {
725 $opts['q'] = sprintf('trashed = false and "%s" in parents and mimeType = "%s"', $id, self::DIRMIME);
726 $request = $gFiles->listFiles($this->applyDefaultParams($opts, 'files.list'));
727 $key = ++ $i;
728 $batch->add($request, (string) $key);
729 $paths['response-' . $key] = $id;
730 }
731 $results = $batch->execute();
732 foreach ($results as $key => $result) {
733 if ($result instanceof Google_Service_Drive_FileList) {
734 $object[$paths[$key]]['hasdir'] = $this->cacheHasDirs[$paths[$key]] = (bool) $result->getFiles();
735 }
736 }
737 $client->setUseBatch(false);
738 return $object;
739 }
740
741 /**
742 * Get the object permissions presented as a visibility.
743 *
744 * @param string $path
745 * itemId path
746 *
747 * @return string
748 */
749 protected function getRawVisibility($path)
750 {
751 $file = $this->getFileObject($path);
752 $permissions = $file->getPermissions();
753 $visibility = AdapterInterface::VISIBILITY_PRIVATE;
754 foreach ($permissions as $permission) {
755 if ($permission->type === $this->publishPermission['type'] && $permission->role === $this->publishPermission['role']) {
756 $visibility = AdapterInterface::VISIBILITY_PUBLIC;
757 break;
758 }
759 }
760 return $visibility;
761 }
762
763 /**
764 * Publish specified path item
765 *
766 * @param string $path
767 * itemId path
768 *
769 * @return bool
770 */
771 protected function publish($path)
772 {
773 if (($file = $this->getFileObject($path))) {
774 if ($this->getRawVisibility($path) === AdapterInterface::VISIBILITY_PUBLIC) {
775 return true;
776 }
777 try {
778 $permission = new Google_Service_Drive_Permission($this->publishPermission);
779 if ($this->service->permissions->create($file->getId(), $permission)) {
780 return true;
781 }
782 } catch (Exception $e) {
783 return false;
784 }
785 }
786
787 return false;
788 }
789
790 /**
791 * Un-publish specified path item
792 *
793 * @param string $path
794 * itemId path
795 *
796 * @return bool
797 */
798 protected function unPublish($path)
799 {
800 if (($file = $this->getFileObject($path))) {
801 $permissions = $file->getPermissions();
802 try {
803 foreach ($permissions as $permission) {
804 if ($permission->type === 'anyone' && $permission->role === 'reader') {
805 $this->service->permissions->delete($file->getId(), $permission->getId());
806 }
807 }
808 return true;
809 } catch (Exception $e) {
810 return false;
811 }
812 }
813
814 return false;
815 }
816
817 /**
818 * Path splits to dirId, fileId or newName
819 *
820 * @param string $path
821 *
822 * @return array [ $dirId , $fileId|newName ]
823 */
824 protected function splitPath($path, $getParentId = true)
825 {
826 if ($path === '' || $path === '/') {
827 $fileName = $this->root;
828 $dirName = '';
829 } else {
830 $paths = explode('/', $path);
831 $fileName = array_pop($paths);
832 if ($getParentId) {
833 $dirName = $paths ? array_pop($paths) : '';
834 } else {
835 $dirName = join('/', $paths);
836 }
837 if ($dirName === '') {
838 $dirName = $this->root;
839 }
840 }
841 return [
842 $dirName,
843 $fileName
844 ];
845 }
846
847 /**
848 * Item name splits to filename and extension
849 * This function supported include '/' in item name
850 *
851 * @param string $name
852 *
853 * @return array [ 'filename' => $filename , 'extension' => $extension ]
854 */
855 protected function splitFileExtension($name)
856 {
857 $extension = '';
858 $name_parts = explode('.', $name);
859 if (isset($name_parts[1])) {
860 $extension = array_pop($name_parts);
861 }
862 $filename = join('.', $name_parts);
863 return compact('filename', 'extension');
864 }
865
866 /**
867 * Get normalised files array from Google_Service_Drive_DriveFile
868 *
869 * @param Google_Service_Drive_DriveFile $object
870 * @param String $dirname
871 * Parent directory itemId path
872 *
873 * @return array Normalised files array
874 */
875 protected function normaliseObject(Google_Service_Drive_DriveFile $object, $dirname)
876 {
877 $id = $object->getId();
878 $path_parts = $this->splitFileExtension($object->getName());
879 //$result['idGoogle'] = $object['id'];
880 //$result['webViewLink'] = $object['webViewLink'];
881 $result['size'] = $object['size'];
882 $result['modifiedTime'] = $object['modifiedTime'];
883 $result = ['name' => $object->getName()];
884 $result['type'] = $object->mimeType === self::DIRMIME ? 'dir' : 'file';
885 $result['path'] = ($dirname ? ($dirname . '/') : '') . $id;
886 $result['filename'] = $path_parts['filename'];
887 $result['extension'] = $path_parts['extension'];
888 $result['timestamp'] = strtotime($object->getModifiedTime());
889 if ($result['type'] === 'file') {
890 $result['mimetype'] = $object->mimeType;
891 $result['size'] = (int) $object->getSize();
892 }
893 if ($result['type'] === 'dir') {
894 $result['size'] = 0;
895 if ($this->useHasDir) {
896 $result['hasdir'] = isset($this->cacheHasDirs[$id]) ? $this->cacheHasDirs[$id] : false;
897 }
898 }
899 // attach additional fields
900 if ($this->additionalFields) {
901 foreach($this->additionalFields as $field) {
902 if (property_exists($object, $field)) {
903 $result[$field] = $object->$field;
904 }
905 }
906 }
907 return $result;
908 }
909
910 /**
911 * Get items array of target dirctory
912 *
913 * @param string $dirname
914 * itemId path
915 * @param bool $recursive
916 * @param number $maxResults
917 * @param string $query
918 *
919 * @return array Items array
920 */
921 protected function getItems($dirname, $recursive = false, $maxResults = 0, $query = '')
922 {
923 list (, $itemId) = $this->splitPath($dirname);
924
925 $maxResults = min($maxResults, 1000);
926 $results = [];
927 $parameters = [
928 'pageSize' => $maxResults ?: 1000,
929 'fields' => $this->fetchfieldsList,
930 'spaces' => $this->spaces,
931 'q' => sprintf('trashed = false and "%s" in parents', $itemId)
932 ];
933 if ($query) {
934 $parameters['q'] .= ' and (' . $query . ')';
935 }
936 $parameters = $this->applyDefaultParams($parameters, 'files.list');
937 $pageToken = NULL;
938 $gFiles = $this->service->files;
939 $this->cacheHasDirs[$itemId] = false;
940 $setHasDir = [];
941
942 do {
943 try {
944 if ($pageToken) {
945 $parameters['pageToken'] = $pageToken;
946 }
947 $fileObjs = $gFiles->listFiles($parameters);
948 if ($fileObjs instanceof Google_Service_Drive_FileList) {
949 foreach ($fileObjs as $obj) {
950 $id = $obj->getId();
951 $this->cacheFileObjects[$id] = $obj;
952 $result = $this->normaliseObject($obj, $dirname);
953 $results[$id] = $result;
954 if ($result['type'] === 'dir') {
955 if ($this->useHasDir) {
956 $setHasDir[$id] = $id;
957 }
958 if ($this->cacheHasDirs[$itemId] === false) {
959 $this->cacheHasDirs[$itemId] = true;
960 unset($setHasDir[$itemId]);
961 }
962 if ($recursive) {
963 $results = array_merge($results, $this->getItems($result['path'], true, $maxResults, $query));
964 }
965 }
966 }
967 $pageToken = $fileObjs->getNextPageToken();
968 } else {
969 $pageToken = NULL;
970 }
971 } catch (Exception $e) {
972 $pageToken = NULL;
973 }
974 } while ($pageToken && $maxResults === 0);
975
976 if ($setHasDir) {
977 $results = $this->setHasDir($setHasDir, $results);
978 }
979 return array_values($results);
980 }
981
982 /**
983 * Get file oblect Google_Service_Drive_DriveFile
984 *
985 * @param string $path
986 * itemId path
987 * @param string $checkDir
988 * do check hasdir
989 *
990 * @return Google_Service_Drive_DriveFile|null
991 */
992 protected function getFileObject($path, $checkDir = false)
993 {
994 list ($parentId, $itemId) = $this->splitPath($path, true);
995 if (isset($this->cacheFileObjects[$itemId])) {
996 return $this->cacheFileObjects[$itemId];
997 } else if (isset($this->cacheFileObjectsByName[$parentId . '/' . $itemId])) {
998 return $this->cacheFileObjectsByName[$parentId . '/' . $itemId];
999 }
1000
1001 $service = $this->service;
1002 $client = $service->getClient();
1003
1004 $client->setUseBatch(true);
1005 $batch = $service->createBatch();
1006
1007 $opts = [
1008 'fields' => $this->fetchfieldsGet
1009 ];
1010
1011 $batch->add($this->service->files->get($itemId, $this->applyDefaultParams($opts, 'files.get')), 'obj');
1012 if ($checkDir && $this->useHasDir) {
1013 $batch->add($service->files->listFiles($this->applyDefaultParams([
1014 'pageSize' => 1,
1015 'q' => sprintf('trashed = false and "%s" in parents and mimeType = "%s"', $itemId, self::DIRMIME)
1016 ], 'files.list')), 'hasdir');
1017 }
1018 $results = array_values($batch->execute());
1019
1020 list ($fileObj, $hasdir) = array_pad($results, 2, null);
1021 $client->setUseBatch(false);
1022
1023 if ($fileObj instanceof Google_Service_Drive_DriveFile) {
1024 if ($hasdir && $fileObj->mimeType === self::DIRMIME) {
1025 if ($hasdir instanceof Google_Service_Drive_FileList) {
1026 $this->cacheHasDirs[$fileObj->getId()] = (bool) $hasdir->getFiles();
1027 }
1028 }
1029 } else {
1030 $fileObj = NULL;
1031 }
1032 $this->cacheFileObjects[$itemId] = $fileObj;
1033
1034 return $fileObj;
1035 }
1036
1037 /**
1038 * Get download url
1039 *
1040 * @param Google_Service_Drive_DriveFile $file
1041 *
1042 * @return string|false
1043 */
1044 protected function getDownloadUrl($file)
1045 {
1046 if (strpos($file->mimeType, 'application/vnd.google-apps') !== 0) {
1047 return 'https://www.googleapis.com/drive/v3/files/' . $file->getId() . '?alt=media';
1048 } else {
1049 $mimeMap = $this->options['appsExportMap'];
1050 if (isset($mimeMap[$file->getMimeType()])) {
1051 $mime = $mimeMap[$file->getMimeType()];
1052 } else {
1053 $mime = $mimeMap['default'];
1054 }
1055 $mime = rawurlencode($mime);
1056
1057 return 'https://www.googleapis.com/drive/v3/files/' . $file->getId() . '/export?mimeType=' . $mime;
1058 }
1059
1060 return false;
1061 }
1062
1063 /**
1064 * Create dirctory
1065 *
1066 * @param string $name
1067 * @param string $parentId
1068 *
1069 * @return Google_Service_Drive_DriveFile|NULL
1070 */
1071 protected function createDirectory($name, $parentId)
1072 {
1073 $file = new Google_Service_Drive_DriveFile();
1074 $file->setName($name);
1075 $file->setParents([
1076 $parentId
1077 ]);
1078 $file->setMimeType(self::DIRMIME);
1079
1080 $obj = $this->service->files->create($file, $this->applyDefaultParams([
1081 'fields' => $this->fetchfieldsGet
1082 ], 'files.create'));
1083
1084 return ($obj instanceof Google_Service_Drive_DriveFile) ? $obj : false;
1085 }
1086
1087 /**
1088 * Upload|Update item
1089 *
1090 * @param string $path
1091 * @param string|resource $contents
1092 * @param Config $config
1093 *
1094 * @return array|false item info array
1095 */
1096 protected function upload($path, $contents, Config $config)
1097 {
1098 list ($parentId, $fileName) = $this->splitPath($path);
1099 $srcDriveFile = $this->getFileObject($path);
1100 if (is_resource($contents)) {
1101 $uploadedDriveFile = $this->uploadResourceToGoogleDrive($contents, $parentId, $fileName, $srcDriveFile, $config->get('mimetype'));
1102 } else {
1103 $uploadedDriveFile = $this->uploadStringToGoogleDrive($contents, $parentId, $fileName, $srcDriveFile, $config->get('mimetype'));
1104 }
1105
1106 return $this->normaliseUploadedFile($uploadedDriveFile, $path, $config->get('visibility'));
1107 }
1108
1109 /**
1110 * Detect the largest chunk size that can be used for uploading a file
1111 *
1112 * @return int
1113 */
1114 protected function detectChunkSizeBytes()
1115 {
1116 // Max and default chunk size of 100MB
1117 $chunkSizeBytes = 100 * 1024 * 1024;
1118 $memoryLimit = $this->getIniBytes('memory_limit');
1119 if ($memoryLimit > 0) {
1120 $availableMemory = $memoryLimit - $this->getMemoryUsedBytes();
1121 /*
1122 * We need some breathing room, so we only take 1/4th of the available memory for use in chunking (the divide by 4 does this).
1123 * The chunk size must be a multiple of 256KB(262144).
1124 * An example of why we need the breathing room is detecting the mime type for a file that is just small enough to fit into one chunk.
1125 * In this scenario, we send the entire file off as a string to have the mime type detected. Unfortunately, this leads to the entire
1126 * file being loaded into memory again, separately from the copy we're holding.
1127 */
1128 $chunkSizeBytes = max(262144, min($chunkSizeBytes, floor($availableMemory / 4 / 262144) * 262144));
1129 }
1130
1131 return (int)$chunkSizeBytes;
1132 }
1133
1134 /**
1135 * Normalise a Drive File that has been created
1136 *
1137 * @param Google_Service_Drive_DriveFile $uploadedFile
1138 * @param string $localPath
1139 * @param string $visibility
1140 * @return array|bool
1141 */
1142 protected function normaliseUploadedFile($uploadedFile, $localPath, $visibility)
1143 {
1144 list ($parentId, $fileName) = $this->splitPath($localPath);
1145
1146 if (!($uploadedFile instanceof Google_Service_Drive_DriveFile)) {
1147 return false;
1148 }
1149
1150 $this->cacheFileObjects[$uploadedFile->getId()] = $uploadedFile;
1151 if (! $this->getFileObject($localPath)) {
1152 $this->cacheFileObjectsByName[$parentId . '/' . $fileName] = $uploadedFile;
1153 }
1154 $result = $this->normaliseObject($uploadedFile, Util::dirname($localPath));
1155
1156 if ($visibility && $this->setVisibility($localPath, $visibility)) {
1157 $result['visibility'] = $visibility;
1158 }
1159
1160 return $result;
1161 }
1162
1163 /**
1164 * Upload a PHP resource stream to Google Drive
1165 *
1166 * @param resource $resource
1167 * @param string $parentId
1168 * @param string $fileName
1169 * @param string $mime
1170 * @return bool|Google_Service_Drive_DriveFile
1171 */
1172 protected function uploadResourceToGoogleDrive($resource, $parentId, $fileName, $srcDriveFile, $mime)
1173 {
1174 $chunkSizeBytes = $this->detectChunkSizeBytes();
1175 $fileSize = $this->getFileSizeBytes($resource);
1176
1177 if ($fileSize <= $chunkSizeBytes) {
1178 // If the resource fits in a single chunk, we'll just upload it in a single request
1179 return $this->uploadStringToGoogleDrive(stream_get_contents($resource), $parentId, $fileName, $srcDriveFile, $mime);
1180 }
1181
1182 $client = $this->service->getClient();
1183 // Call the API with the media upload, defer so it doesn't immediately return.
1184 $client->setDefer(true);
1185 $request = $this->ensureDriveFileExists('', $parentId, $fileName, $srcDriveFile, $mime);
1186 $client->setDefer(false);
1187 $media = $this->getMediaFileUpload($client, $request, $mime, $chunkSizeBytes);
1188 $media->setFileSize($fileSize);
1189
1190 // Upload chunks until we run out of file to upload; $status will be false until the process is complete.
1191 $status = false;
1192 while (! $status && ! feof($resource)) {
1193 $chunk = $this->readFileChunk($resource, $chunkSizeBytes);
1194 $status = $media->nextChunk($chunk);
1195 }
1196
1197 // The final value of $status will be the data from the API for the object that has been uploaded.
1198 return $status;
1199 }
1200
1201 /**
1202 * Upload a string to Google Drive
1203 *
1204 * @param string $contents
1205 * @param string $parentId
1206 * @param string $fileName
1207 * @param string $mime
1208 * @return Google_Service_Drive_DriveFile
1209 */
1210 protected function uploadStringToGoogleDrive($contents, $parentId, $fileName, $srcDriveFile, $mime)
1211 {
1212 return $this->ensureDriveFileExists($contents, $parentId, $fileName, $srcDriveFile, $mime);
1213 }
1214
1215 /**
1216 * Ensure that a file exists on Google Drive by creating it if it doesn't exist or updating it if it does
1217 *
1218 * @param string $contents
1219 * @param string $parentId
1220 * @param string $fileName
1221 * @param string $mime
1222 * @return Google_Service_Drive_DriveFile
1223 */
1224 protected function ensureDriveFileExists($contents, $parentId, $fileName, $srcDriveFile, $mime)
1225 {
1226 if (! $mime) {
1227 $mime = Util::guessMimeType($fileName, $contents);
1228 }
1229
1230 $driveFile = new Google_Service_Drive_DriveFile();
1231
1232 $mode = 'update';
1233 if (! $srcDriveFile) {
1234 $mode = 'insert';
1235 $driveFile->setName($fileName);
1236 $driveFile->setParents([$parentId]);
1237 }
1238
1239 $driveFile->setMimeType($mime);
1240
1241 $params = ['fields' => $this->fetchfieldsGet];
1242 if ($contents) {
1243 $params['data'] = $contents;
1244 $params['uploadType'] = 'media';
1245 }
1246 if ($mode === 'insert') {
1247 $retrievedDriveFile = $this->service->files->create($driveFile, $this->applyDefaultParams($params, 'files.create'));
1248 } else {
1249 $retrievedDriveFile = $this->service->files->update(
1250 $srcDriveFile->getId(),
1251 $driveFile,
1252 $this->applyDefaultParams($params, 'files.update')
1253 );
1254 }
1255
1256 return $retrievedDriveFile;
1257 }
1258
1259 /**
1260 * Read file chunk
1261 *
1262 * @param resource $handle
1263 * @param int $chunkSize
1264 *
1265 * @return string
1266 */
1267 protected function readFileChunk($handle, $chunkSize)
1268 {
1269 $byteCount = 0;
1270 $giantChunk = '';
1271 while (! feof($handle)) {
1272 // fread will never return more than 8192 bytes if the stream is read buffered and it does not represent a plain file
1273 // An example of a read buffered file is when reading from a URL
1274 $chunk = fread($handle, 8192);
1275 $byteCount += strlen($chunk);
1276 $giantChunk .= $chunk;
1277 if ($byteCount >= $chunkSize) {
1278 return $giantChunk;
1279 }
1280 }
1281 return $giantChunk;
1282 }
1283
1284 /**
1285 * Return bytes from php.ini value
1286 *
1287 * @param string $iniName
1288 * @param string $val
1289 * @return number
1290 */
1291 protected function getIniBytes($iniName = '', $val = '')
1292 {
1293 if ($iniName !== '') {
1294 $val = ini_get($iniName);
1295 if ($val === false) {
1296 return 0;
1297 }
1298 }
1299 $val = trim($val, "bB \t\n\r\0\x0B");
1300 $last = strtolower($val[strlen($val) - 1]);
1301 $val = (int)$val;
1302 switch ($last) {
1303 case 't':
1304 $val *= 1024;
1305 case 'g':
1306 $val *= 1024;
1307 case 'm':
1308 $val *= 1024;
1309 case 'k':
1310 $val *= 1024;
1311 }
1312 return $val;
1313 }
1314
1315 /**
1316 * Return the number of memory bytes allocated to PHP
1317 *
1318 * @return int
1319 */
1320 protected function getMemoryUsedBytes()
1321 {
1322 return memory_get_usage(true);
1323 }
1324
1325 /**
1326 * Get the size of a file resource
1327 *
1328 * @param $resource
1329 *
1330 * @return int
1331 */
1332 protected function getFileSizeBytes($resource)
1333 {
1334 return fstat($resource)['size'];
1335 }
1336
1337 /**
1338 * Get a MediaFileUpload
1339 *
1340 * @param $client
1341 * @param $request
1342 * @param $mime
1343 * @param $chunkSizeBytes
1344 *
1345 * @return Google_Http_MediaFileUpload
1346 */
1347 protected function getMediaFileUpload($client, $request, $mime, $chunkSizeBytes)
1348 {
1349 return new Google_Http_MediaFileUpload($client, $request, $mime, null, true, $chunkSizeBytes);
1350 }
1351
1352 /**
1353 * Apply optional parameters for each command
1354 *
1355 * @param array $params The parameters
1356 * @param string $cmdName The command name
1357 *
1358 * @return array
1359 *
1360 * @see https://developers.google.com/drive/v3/reference/files
1361 * @see \Google_Service_Drive_Resource_Files
1362 */
1363 protected function applyDefaultParams($params, $cmdName)
1364 {
1365 if (isset($this->defaultParams[$cmdName]) && is_array($this->defaultParams[$cmdName])) {
1366 return array_replace($this->defaultParams[$cmdName], $params);
1367 } else {
1368 return $params;
1369 }
1370 }
1371
1372 /**
1373 * Enables Team Drive support by changing default parameters
1374 *
1375 * @return void
1376 *
1377 * @see https://developers.google.com/drive/v3/reference/files
1378 * @see \Google_Service_Drive_Resource_Files
1379 */
1380 public function enableTeamDriveSupport()
1381 {
1382 $this->defaultParams = array_merge_recursive(
1383 array_fill_keys([
1384 'files.copy', 'files.create', 'files.delete',
1385 'files.trash', 'files.get', 'files.list', 'files.update',
1386 'files.watch'
1387 ], ['supportsTeamDrives' => true]),
1388 $this->defaultParams
1389 );
1390 }
1391
1392 /**
1393 * Selects Team Drive to operate by changing default parameters
1394 *
1395 * @return void
1396 *
1397 * @param string $teamDriveId Team Drive id
1398 * @param string $corpora Corpora value for files.list
1399 *
1400 * @see https://developers.google.com/drive/v3/reference/files
1401 * @see https://developers.google.com/drive/v3/reference/files/list
1402 * @see \Google_Service_Drive_Resource_Files
1403 */
1404 public function setTeamDriveId($teamDriveId, $corpora = 'teamDrive')
1405 {
1406 $this->enableTeamDriveSupport();
1407 $this->defaultParams = array_merge_recursive($this->defaultParams, [
1408 'files.list' => [
1409 'corpora' => $corpora,
1410 'includeTeamDriveItems' => true,
1411 'teamDriveId' => $teamDriveId
1412 ]
1413 ]);
1414
1415 if ($this->root === 'root') {
1416 $this->setPathPrefix($teamDriveId);
1417 $this->root = $teamDriveId;
1418 }
1419 }
1420}