Google.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Adam Williamson <awilliam@redhat.com>
  6. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  7. * @author Bart Visscher <bartv@thisnet.nl>
  8. * @author Christopher Schäpers <kondou@ts.unde.re>
  9. * @author Francesco Rovelli <francesco.rovelli@gmail.com>
  10. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  11. * @author Lukas Reschke <lukas@statuscode.ch>
  12. * @author Michael Gapczynski <GapczynskiM@gmail.com>
  13. * @author Morris Jobke <hey@morrisjobke.de>
  14. * @author Philipp Kapfer <philipp.kapfer@gmx.at>
  15. * @author Robin Appelman <robin@icewind.nl>
  16. * @author Robin McCorkell <robin@mccorkell.me.uk>
  17. * @author Thomas Müller <thomas.mueller@tmit.eu>
  18. * @author Vincent Petry <pvince81@owncloud.com>
  19. *
  20. * @license AGPL-3.0
  21. *
  22. * This code is free software: you can redistribute it and/or modify
  23. * it under the terms of the GNU Affero General Public License, version 3,
  24. * as published by the Free Software Foundation.
  25. *
  26. * This program is distributed in the hope that it will be useful,
  27. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  28. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  29. * GNU Affero General Public License for more details.
  30. *
  31. * You should have received a copy of the GNU Affero General Public License, version 3,
  32. * along with this program. If not, see <http://www.gnu.org/licenses/>
  33. *
  34. */
  35. namespace OCA\Files_External\Lib\Storage;
  36. use GuzzleHttp\Exception\RequestException;
  37. use Icewind\Streams\CallbackWrapper;
  38. use Icewind\Streams\IteratorDirectory;
  39. use Icewind\Streams\RetryWrapper;
  40. set_include_path(get_include_path().PATH_SEPARATOR.
  41. \OC_App::getAppPath('files_external').'/3rdparty/google-api-php-client/src');
  42. require_once 'Google/autoload.php';
  43. class Google extends \OC\Files\Storage\Common {
  44. private $client;
  45. private $id;
  46. private $service;
  47. private $driveFiles;
  48. // Google Doc mimetypes
  49. const FOLDER = 'application/vnd.google-apps.folder';
  50. const DOCUMENT = 'application/vnd.google-apps.document';
  51. const SPREADSHEET = 'application/vnd.google-apps.spreadsheet';
  52. const DRAWING = 'application/vnd.google-apps.drawing';
  53. const PRESENTATION = 'application/vnd.google-apps.presentation';
  54. const MAP = 'application/vnd.google-apps.map';
  55. public function __construct($params) {
  56. if (isset($params['configured']) && $params['configured'] === 'true'
  57. && isset($params['client_id']) && isset($params['client_secret'])
  58. && isset($params['token'])
  59. ) {
  60. $this->client = new \Google_Client();
  61. $this->client->setClientId($params['client_id']);
  62. $this->client->setClientSecret($params['client_secret']);
  63. $this->client->setScopes(array('https://www.googleapis.com/auth/drive'));
  64. $this->client->setAccessToken($params['token']);
  65. // if curl isn't available we're likely to run into
  66. // https://github.com/google/google-api-php-client/issues/59
  67. // - disable gzip to avoid it.
  68. if (!function_exists('curl_version') || !function_exists('curl_exec')) {
  69. $this->client->setClassConfig("Google_Http_Request", "disable_gzip", true);
  70. }
  71. // note: API connection is lazy
  72. $this->service = new \Google_Service_Drive($this->client);
  73. $token = json_decode($params['token'], true);
  74. $this->id = 'google::'.substr($params['client_id'], 0, 30).$token['created'];
  75. } else {
  76. throw new \Exception('Creating Google storage failed');
  77. }
  78. }
  79. public function getId() {
  80. return $this->id;
  81. }
  82. /**
  83. * Get the Google_Service_Drive_DriveFile object for the specified path.
  84. * Returns false on failure.
  85. * @param string $path
  86. * @return \Google_Service_Drive_DriveFile|false
  87. */
  88. private function getDriveFile($path) {
  89. // Remove leading and trailing slashes
  90. $path = trim($path, '/');
  91. if ($path === '.') {
  92. $path = '';
  93. }
  94. if (isset($this->driveFiles[$path])) {
  95. return $this->driveFiles[$path];
  96. } else if ($path === '') {
  97. $root = $this->service->files->get('root');
  98. $this->driveFiles[$path] = $root;
  99. return $root;
  100. } else {
  101. // Google Drive SDK does not have methods for retrieving files by path
  102. // Instead we must find the id of the parent folder of the file
  103. $parentId = $this->getDriveFile('')->getId();
  104. $folderNames = explode('/', $path);
  105. $path = '';
  106. // Loop through each folder of this path to get to the file
  107. foreach ($folderNames as $name) {
  108. // Reconstruct path from beginning
  109. if ($path === '') {
  110. $path .= $name;
  111. } else {
  112. $path .= '/'.$name;
  113. }
  114. if (isset($this->driveFiles[$path])) {
  115. $parentId = $this->driveFiles[$path]->getId();
  116. } else {
  117. $q = "title='" . str_replace("'","\\'", $name) . "' and '" . str_replace("'","\\'", $parentId) . "' in parents and trashed = false";
  118. $result = $this->service->files->listFiles(array('q' => $q))->getItems();
  119. if (!empty($result)) {
  120. // Google Drive allows files with the same name, ownCloud doesn't
  121. if (count($result) > 1) {
  122. $this->onDuplicateFileDetected($path);
  123. return false;
  124. } else {
  125. $file = current($result);
  126. $this->driveFiles[$path] = $file;
  127. $parentId = $file->getId();
  128. }
  129. } else {
  130. // Google Docs have no extension in their title, so try without extension
  131. $pos = strrpos($path, '.');
  132. if ($pos !== false) {
  133. $pathWithoutExt = substr($path, 0, $pos);
  134. $file = $this->getDriveFile($pathWithoutExt);
  135. if ($file && $this->isGoogleDocFile($file)) {
  136. // Switch cached Google_Service_Drive_DriveFile to the correct index
  137. unset($this->driveFiles[$pathWithoutExt]);
  138. $this->driveFiles[$path] = $file;
  139. $parentId = $file->getId();
  140. } else {
  141. return false;
  142. }
  143. } else {
  144. return false;
  145. }
  146. }
  147. }
  148. }
  149. return $this->driveFiles[$path];
  150. }
  151. }
  152. /**
  153. * Set the Google_Service_Drive_DriveFile object in the cache
  154. * @param string $path
  155. * @param \Google_Service_Drive_DriveFile|false $file
  156. */
  157. private function setDriveFile($path, $file) {
  158. $path = trim($path, '/');
  159. $this->driveFiles[$path] = $file;
  160. if ($file === false) {
  161. // Remove all children
  162. $len = strlen($path);
  163. foreach ($this->driveFiles as $key => $file) {
  164. if (substr($key, 0, $len) === $path) {
  165. unset($this->driveFiles[$key]);
  166. }
  167. }
  168. }
  169. }
  170. /**
  171. * Write a log message to inform about duplicate file names
  172. * @param string $path
  173. */
  174. private function onDuplicateFileDetected($path) {
  175. $about = $this->service->about->get();
  176. $user = $about->getName();
  177. \OCP\Util::writeLog('files_external',
  178. 'Ignoring duplicate file name: '.$path.' on Google Drive for Google user: '.$user,
  179. \OCP\Util::INFO
  180. );
  181. }
  182. /**
  183. * Generate file extension for a Google Doc, choosing Open Document formats for download
  184. * @param string $mimetype
  185. * @return string
  186. */
  187. private function getGoogleDocExtension($mimetype) {
  188. if ($mimetype === self::DOCUMENT) {
  189. return 'odt';
  190. } else if ($mimetype === self::SPREADSHEET) {
  191. return 'ods';
  192. } else if ($mimetype === self::DRAWING) {
  193. return 'jpg';
  194. } else if ($mimetype === self::PRESENTATION) {
  195. // Download as .odp is not available
  196. return 'pdf';
  197. } else {
  198. return '';
  199. }
  200. }
  201. /**
  202. * Returns whether the given drive file is a Google Doc file
  203. *
  204. * @param \Google_Service_Drive_DriveFile
  205. *
  206. * @return true if the file is a Google Doc file, false otherwise
  207. */
  208. private function isGoogleDocFile($file) {
  209. return $this->getGoogleDocExtension($file->getMimeType()) !== '';
  210. }
  211. public function mkdir($path) {
  212. if (!$this->is_dir($path)) {
  213. $parentFolder = $this->getDriveFile(dirname($path));
  214. if ($parentFolder) {
  215. $folder = new \Google_Service_Drive_DriveFile();
  216. $folder->setTitle(basename($path));
  217. $folder->setMimeType(self::FOLDER);
  218. $parent = new \Google_Service_Drive_ParentReference();
  219. $parent->setId($parentFolder->getId());
  220. $folder->setParents(array($parent));
  221. $result = $this->service->files->insert($folder);
  222. if ($result) {
  223. $this->setDriveFile($path, $result);
  224. }
  225. return (bool)$result;
  226. }
  227. }
  228. return false;
  229. }
  230. public function rmdir($path) {
  231. if (!$this->isDeletable($path)) {
  232. return false;
  233. }
  234. if (trim($path, '/') === '') {
  235. $dir = $this->opendir($path);
  236. if(is_resource($dir)) {
  237. while (($file = readdir($dir)) !== false) {
  238. if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
  239. if (!$this->unlink($path.'/'.$file)) {
  240. return false;
  241. }
  242. }
  243. }
  244. closedir($dir);
  245. }
  246. $this->driveFiles = array();
  247. return true;
  248. } else {
  249. return $this->unlink($path);
  250. }
  251. }
  252. public function opendir($path) {
  253. $folder = $this->getDriveFile($path);
  254. if ($folder) {
  255. $files = array();
  256. $duplicates = array();
  257. $pageToken = true;
  258. while ($pageToken) {
  259. $params = array();
  260. if ($pageToken !== true) {
  261. $params['pageToken'] = $pageToken;
  262. }
  263. $params['q'] = "'" . str_replace("'","\\'", $folder->getId()) . "' in parents and trashed = false";
  264. $children = $this->service->files->listFiles($params);
  265. foreach ($children->getItems() as $child) {
  266. $name = $child->getTitle();
  267. // Check if this is a Google Doc i.e. no extension in name
  268. $extension = $child->getFileExtension();
  269. if (empty($extension)) {
  270. if ($child->getMimeType() === self::MAP) {
  271. continue; // No method known to transfer map files, ignore it
  272. } else if ($child->getMimeType() !== self::FOLDER) {
  273. $name .= '.'.$this->getGoogleDocExtension($child->getMimeType());
  274. }
  275. }
  276. if ($path === '') {
  277. $filepath = $name;
  278. } else {
  279. $filepath = $path.'/'.$name;
  280. }
  281. // Google Drive allows files with the same name, ownCloud doesn't
  282. // Prevent opendir() from returning any duplicate files
  283. $key = array_search($name, $files);
  284. if ($key !== false || isset($duplicates[$filepath])) {
  285. if (!isset($duplicates[$filepath])) {
  286. $duplicates[$filepath] = true;
  287. $this->setDriveFile($filepath, false);
  288. unset($files[$key]);
  289. $this->onDuplicateFileDetected($filepath);
  290. }
  291. } else {
  292. // Cache the Google_Service_Drive_DriveFile for future use
  293. $this->setDriveFile($filepath, $child);
  294. $files[] = $name;
  295. }
  296. }
  297. $pageToken = $children->getNextPageToken();
  298. }
  299. return IteratorDirectory::wrap($files);
  300. } else {
  301. return false;
  302. }
  303. }
  304. public function stat($path) {
  305. $file = $this->getDriveFile($path);
  306. if ($file) {
  307. $stat = array();
  308. if ($this->filetype($path) === 'dir') {
  309. $stat['size'] = 0;
  310. } else {
  311. // Check if this is a Google Doc
  312. if ($this->isGoogleDocFile($file)) {
  313. // Return unknown file size
  314. $stat['size'] = \OCP\Files\FileInfo::SPACE_UNKNOWN;
  315. } else {
  316. $stat['size'] = $file->getFileSize();
  317. }
  318. }
  319. $stat['atime'] = strtotime($file->getLastViewedByMeDate());
  320. $stat['mtime'] = strtotime($file->getModifiedDate());
  321. $stat['ctime'] = strtotime($file->getCreatedDate());
  322. return $stat;
  323. } else {
  324. return false;
  325. }
  326. }
  327. public function filetype($path) {
  328. if ($path === '') {
  329. return 'dir';
  330. } else {
  331. $file = $this->getDriveFile($path);
  332. if ($file) {
  333. if ($file->getMimeType() === self::FOLDER) {
  334. return 'dir';
  335. } else {
  336. return 'file';
  337. }
  338. } else {
  339. return false;
  340. }
  341. }
  342. }
  343. public function isUpdatable($path) {
  344. $file = $this->getDriveFile($path);
  345. if ($file) {
  346. return $file->getEditable();
  347. } else {
  348. return false;
  349. }
  350. }
  351. public function file_exists($path) {
  352. return (bool)$this->getDriveFile($path);
  353. }
  354. public function unlink($path) {
  355. $file = $this->getDriveFile($path);
  356. if ($file) {
  357. $result = $this->service->files->trash($file->getId());
  358. if ($result) {
  359. $this->setDriveFile($path, false);
  360. }
  361. return (bool)$result;
  362. } else {
  363. return false;
  364. }
  365. }
  366. public function rename($path1, $path2) {
  367. $file = $this->getDriveFile($path1);
  368. if ($file) {
  369. $newFile = $this->getDriveFile($path2);
  370. if (dirname($path1) === dirname($path2)) {
  371. if ($newFile) {
  372. // rename to the name of the target file, could be an office file without extension
  373. $file->setTitle($newFile->getTitle());
  374. } else {
  375. $file->setTitle(basename(($path2)));
  376. }
  377. } else {
  378. // Change file parent
  379. $parentFolder2 = $this->getDriveFile(dirname($path2));
  380. if ($parentFolder2) {
  381. $parent = new \Google_Service_Drive_ParentReference();
  382. $parent->setId($parentFolder2->getId());
  383. $file->setParents(array($parent));
  384. } else {
  385. return false;
  386. }
  387. }
  388. // We need to get the object for the existing file with the same
  389. // name (if there is one) before we do the patch. If oldfile
  390. // exists and is a directory we have to delete it before we
  391. // do the rename too.
  392. $oldfile = $this->getDriveFile($path2);
  393. if ($oldfile && $this->is_dir($path2)) {
  394. $this->rmdir($path2);
  395. $oldfile = false;
  396. }
  397. $result = $this->service->files->patch($file->getId(), $file);
  398. if ($result) {
  399. $this->setDriveFile($path1, false);
  400. $this->setDriveFile($path2, $result);
  401. if ($oldfile && $newFile) {
  402. // only delete if they have a different id (same id can happen for part files)
  403. if ($newFile->getId() !== $oldfile->getId()) {
  404. $this->service->files->delete($oldfile->getId());
  405. }
  406. }
  407. }
  408. return (bool)$result;
  409. } else {
  410. return false;
  411. }
  412. }
  413. public function fopen($path, $mode) {
  414. $pos = strrpos($path, '.');
  415. if ($pos !== false) {
  416. $ext = substr($path, $pos);
  417. } else {
  418. $ext = '';
  419. }
  420. switch ($mode) {
  421. case 'r':
  422. case 'rb':
  423. $file = $this->getDriveFile($path);
  424. if ($file) {
  425. $exportLinks = $file->getExportLinks();
  426. $mimetype = $this->getMimeType($path);
  427. $downloadUrl = null;
  428. if ($exportLinks && isset($exportLinks[$mimetype])) {
  429. $downloadUrl = $exportLinks[$mimetype];
  430. } else {
  431. $downloadUrl = $file->getDownloadUrl();
  432. }
  433. if (isset($downloadUrl)) {
  434. $request = new \Google_Http_Request($downloadUrl, 'GET', null, null);
  435. $httpRequest = $this->client->getAuth()->sign($request);
  436. // the library's service doesn't support streaming, so we use Guzzle instead
  437. $client = \OC::$server->getHTTPClientService()->newClient();
  438. try {
  439. $response = $client->get($downloadUrl, [
  440. 'headers' => $httpRequest->getRequestHeaders(),
  441. 'stream' => true,
  442. 'verify' => realpath(__DIR__ . '/../../../3rdparty/google-api-php-client/src/Google/IO/cacerts.pem'),
  443. ]);
  444. } catch (RequestException $e) {
  445. if(!is_null($e->getResponse())) {
  446. if ($e->getResponse()->getStatusCode() === 404) {
  447. return false;
  448. } else {
  449. throw $e;
  450. }
  451. } else {
  452. throw $e;
  453. }
  454. }
  455. $handle = $response->getBody();
  456. return RetryWrapper::wrap($handle);
  457. }
  458. }
  459. return false;
  460. case 'w':
  461. case 'wb':
  462. case 'a':
  463. case 'ab':
  464. case 'r+':
  465. case 'w+':
  466. case 'wb+':
  467. case 'a+':
  468. case 'x':
  469. case 'x+':
  470. case 'c':
  471. case 'c+':
  472. $tmpFile = \OCP\Files::tmpFile($ext);
  473. if ($this->file_exists($path)) {
  474. $source = $this->fopen($path, 'rb');
  475. file_put_contents($tmpFile, $source);
  476. }
  477. $handle = fopen($tmpFile, $mode);
  478. return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
  479. $this->writeBack($tmpFile, $path);
  480. });
  481. }
  482. }
  483. public function writeBack($tmpFile, $path) {
  484. $parentFolder = $this->getDriveFile(dirname($path));
  485. if ($parentFolder) {
  486. $mimetype = \OC::$server->getMimeTypeDetector()->detect($tmpFile);
  487. $params = array(
  488. 'mimeType' => $mimetype,
  489. 'uploadType' => 'media'
  490. );
  491. $result = false;
  492. $chunkSizeBytes = 10 * 1024 * 1024;
  493. $useChunking = false;
  494. $size = filesize($tmpFile);
  495. if ($size > $chunkSizeBytes) {
  496. $useChunking = true;
  497. } else {
  498. $params['data'] = file_get_contents($tmpFile);
  499. }
  500. if ($this->file_exists($path)) {
  501. $file = $this->getDriveFile($path);
  502. $this->client->setDefer($useChunking);
  503. $request = $this->service->files->update($file->getId(), $file, $params);
  504. } else {
  505. $file = new \Google_Service_Drive_DriveFile();
  506. $file->setTitle(basename($path));
  507. $file->setMimeType($mimetype);
  508. $parent = new \Google_Service_Drive_ParentReference();
  509. $parent->setId($parentFolder->getId());
  510. $file->setParents(array($parent));
  511. $this->client->setDefer($useChunking);
  512. $request = $this->service->files->insert($file, $params);
  513. }
  514. if ($useChunking) {
  515. // Create a media file upload to represent our upload process.
  516. $media = new \Google_Http_MediaFileUpload(
  517. $this->client,
  518. $request,
  519. 'text/plain',
  520. null,
  521. true,
  522. $chunkSizeBytes
  523. );
  524. $media->setFileSize($size);
  525. // Upload the various chunks. $status will be false until the process is
  526. // complete.
  527. $status = false;
  528. $handle = fopen($tmpFile, 'rb');
  529. while (!$status && !feof($handle)) {
  530. $chunk = fread($handle, $chunkSizeBytes);
  531. $status = $media->nextChunk($chunk);
  532. }
  533. // The final value of $status will be the data from the API for the object
  534. // that has been uploaded.
  535. $result = false;
  536. if ($status !== false) {
  537. $result = $status;
  538. }
  539. fclose($handle);
  540. } else {
  541. $result = $request;
  542. }
  543. // Reset to the client to execute requests immediately in the future.
  544. $this->client->setDefer(false);
  545. if ($result) {
  546. $this->setDriveFile($path, $result);
  547. }
  548. }
  549. }
  550. public function getMimeType($path) {
  551. $file = $this->getDriveFile($path);
  552. if ($file) {
  553. $mimetype = $file->getMimeType();
  554. // Convert Google Doc mimetypes, choosing Open Document formats for download
  555. if ($mimetype === self::FOLDER) {
  556. return 'httpd/unix-directory';
  557. } else if ($mimetype === self::DOCUMENT) {
  558. return 'application/vnd.oasis.opendocument.text';
  559. } else if ($mimetype === self::SPREADSHEET) {
  560. return 'application/x-vnd.oasis.opendocument.spreadsheet';
  561. } else if ($mimetype === self::DRAWING) {
  562. return 'image/jpeg';
  563. } else if ($mimetype === self::PRESENTATION) {
  564. // Download as .odp is not available
  565. return 'application/pdf';
  566. } else {
  567. // use extension-based detection, could be an encrypted file
  568. return parent::getMimeType($path);
  569. }
  570. } else {
  571. return false;
  572. }
  573. }
  574. public function free_space($path) {
  575. $about = $this->service->about->get();
  576. return $about->getQuotaBytesTotal() - $about->getQuotaBytesUsed();
  577. }
  578. public function touch($path, $mtime = null) {
  579. $file = $this->getDriveFile($path);
  580. $result = false;
  581. if ($file) {
  582. if (isset($mtime)) {
  583. // This is just RFC3339, but frustratingly, GDrive's API *requires*
  584. // the fractions portion be present, while no handy PHP constant
  585. // for RFC3339 or ISO8601 includes it. So we do it ourselves.
  586. $file->setModifiedDate(date('Y-m-d\TH:i:s.uP', $mtime));
  587. $result = $this->service->files->patch($file->getId(), $file, array(
  588. 'setModifiedDate' => true,
  589. ));
  590. } else {
  591. $result = $this->service->files->touch($file->getId());
  592. }
  593. } else {
  594. $parentFolder = $this->getDriveFile(dirname($path));
  595. if ($parentFolder) {
  596. $file = new \Google_Service_Drive_DriveFile();
  597. $file->setTitle(basename($path));
  598. $parent = new \Google_Service_Drive_ParentReference();
  599. $parent->setId($parentFolder->getId());
  600. $file->setParents(array($parent));
  601. $result = $this->service->files->insert($file);
  602. }
  603. }
  604. if ($result) {
  605. $this->setDriveFile($path, $result);
  606. }
  607. return (bool)$result;
  608. }
  609. public function test() {
  610. if ($this->free_space('')) {
  611. return true;
  612. }
  613. return false;
  614. }
  615. public function hasUpdated($path, $time) {
  616. $appConfig = \OC::$server->getAppConfig();
  617. if ($this->is_file($path)) {
  618. return parent::hasUpdated($path, $time);
  619. } else {
  620. // Google Drive doesn't change modified times of folders when files inside are updated
  621. // Instead we use the Changes API to see if folders have been updated, and it's a pain
  622. $folder = $this->getDriveFile($path);
  623. if ($folder) {
  624. $result = false;
  625. $folderId = $folder->getId();
  626. $startChangeId = $appConfig->getValue('files_external', $this->getId().'cId');
  627. $params = array(
  628. 'includeDeleted' => true,
  629. 'includeSubscribed' => true,
  630. );
  631. if (isset($startChangeId)) {
  632. $startChangeId = (int)$startChangeId;
  633. $largestChangeId = $startChangeId;
  634. $params['startChangeId'] = $startChangeId + 1;
  635. } else {
  636. $largestChangeId = 0;
  637. }
  638. $pageToken = true;
  639. while ($pageToken) {
  640. if ($pageToken !== true) {
  641. $params['pageToken'] = $pageToken;
  642. }
  643. $changes = $this->service->changes->listChanges($params);
  644. if ($largestChangeId === 0 || $largestChangeId === $startChangeId) {
  645. $largestChangeId = $changes->getLargestChangeId();
  646. }
  647. if (isset($startChangeId)) {
  648. // Check if a file in this folder has been updated
  649. // There is no way to filter by folder at the API level...
  650. foreach ($changes->getItems() as $change) {
  651. $file = $change->getFile();
  652. if ($file) {
  653. foreach ($file->getParents() as $parent) {
  654. if ($parent->getId() === $folderId) {
  655. $result = true;
  656. // Check if there are changes in different folders
  657. } else if ($change->getId() <= $largestChangeId) {
  658. // Decrement id so this change is fetched when called again
  659. $largestChangeId = $change->getId();
  660. $largestChangeId--;
  661. }
  662. }
  663. }
  664. }
  665. $pageToken = $changes->getNextPageToken();
  666. } else {
  667. // Assuming the initial scan just occurred and changes are negligible
  668. break;
  669. }
  670. }
  671. $appConfig->setValue('files_external', $this->getId().'cId', $largestChangeId);
  672. return $result;
  673. }
  674. }
  675. return false;
  676. }
  677. /**
  678. * check if curl is installed
  679. */
  680. public static function checkDependencies() {
  681. return true;
  682. }
  683. }