google.php 19 KB

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