aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_external/lib/Lib/Storage/Google.php
diff options
context:
space:
mode:
authorJoas Schilling <nickvergessen@owncloud.com>2016-05-13 11:46:36 +0200
committerJoas Schilling <nickvergessen@owncloud.com>2016-05-24 08:41:22 +0200
commit4576891f10aa41cc146dc6ed421384269eca283a (patch)
tree0b3f8696b880fc0867c885af6ca9c2be14e3c372 /apps/files_external/lib/Lib/Storage/Google.php
parentb9fd7d4cc7834fd0fff01c1c6aa478a62a905bb1 (diff)
downloadnextcloud-server-4576891f10aa41cc146dc6ed421384269eca283a.tar.gz
nextcloud-server-4576891f10aa41cc146dc6ed421384269eca283a.zip
Move Lib\Storage to PSR-4
Diffstat (limited to 'apps/files_external/lib/Lib/Storage/Google.php')
-rw-r--r--apps/files_external/lib/Lib/Storage/Google.php710
1 files changed, 710 insertions, 0 deletions
diff --git a/apps/files_external/lib/Lib/Storage/Google.php b/apps/files_external/lib/Lib/Storage/Google.php
new file mode 100644
index 00000000000..13e89299c22
--- /dev/null
+++ b/apps/files_external/lib/Lib/Storage/Google.php
@@ -0,0 +1,710 @@
+<?php
+/**
+ * @author Adam Williamson <awilliam@redhat.com>
+ * @author Arthur Schiwon <blizzz@owncloud.com>
+ * @author Bart Visscher <bartv@thisnet.nl>
+ * @author Christopher Schäpers <kondou@ts.unde.re>
+ * @author Jörn Friedrich Dreyer <jfd@butonic.de>
+ * @author Lukas Reschke <lukas@owncloud.com>
+ * @author Michael Gapczynski <GapczynskiM@gmail.com>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Philipp Kapfer <philipp.kapfer@gmx.at>
+ * @author Robin Appelman <icewind@owncloud.com>
+ * @author Robin McCorkell <robin@mccorkell.me.uk>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2016, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\Files_External\Lib\Storage;
+
+use GuzzleHttp\Exception\RequestException;
+use Icewind\Streams\IteratorDirectory;
+use Icewind\Streams\RetryWrapper;
+
+set_include_path(get_include_path().PATH_SEPARATOR.
+ \OC_App::getAppPath('files_external').'/3rdparty/google-api-php-client/src');
+require_once 'Google/Client.php';
+require_once 'Google/Service/Drive.php';
+
+class Google extends \OC\Files\Storage\Common {
+
+ private $client;
+ private $id;
+ private $service;
+ private $driveFiles;
+
+ private static $tempFiles = array();
+
+ // Google Doc mimetypes
+ const FOLDER = 'application/vnd.google-apps.folder';
+ const DOCUMENT = 'application/vnd.google-apps.document';
+ const SPREADSHEET = 'application/vnd.google-apps.spreadsheet';
+ const DRAWING = 'application/vnd.google-apps.drawing';
+ const PRESENTATION = 'application/vnd.google-apps.presentation';
+
+ public function __construct($params) {
+ if (isset($params['configured']) && $params['configured'] === 'true'
+ && isset($params['client_id']) && isset($params['client_secret'])
+ && isset($params['token'])
+ ) {
+ $this->client = new \Google_Client();
+ $this->client->setClientId($params['client_id']);
+ $this->client->setClientSecret($params['client_secret']);
+ $this->client->setScopes(array('https://www.googleapis.com/auth/drive'));
+ $this->client->setAccessToken($params['token']);
+ // if curl isn't available we're likely to run into
+ // https://github.com/google/google-api-php-client/issues/59
+ // - disable gzip to avoid it.
+ if (!function_exists('curl_version') || !function_exists('curl_exec')) {
+ $this->client->setClassConfig("Google_Http_Request", "disable_gzip", true);
+ }
+ // note: API connection is lazy
+ $this->service = new \Google_Service_Drive($this->client);
+ $token = json_decode($params['token'], true);
+ $this->id = 'google::'.substr($params['client_id'], 0, 30).$token['created'];
+ } else {
+ throw new \Exception('Creating Google storage failed');
+ }
+ }
+
+ public function getId() {
+ return $this->id;
+ }
+
+ /**
+ * Get the Google_Service_Drive_DriveFile object for the specified path.
+ * Returns false on failure.
+ * @param string $path
+ * @return \Google_Service_Drive_DriveFile|false
+ */
+ private function getDriveFile($path) {
+ // Remove leading and trailing slashes
+ $path = trim($path, '/');
+ if (isset($this->driveFiles[$path])) {
+ return $this->driveFiles[$path];
+ } else if ($path === '') {
+ $root = $this->service->files->get('root');
+ $this->driveFiles[$path] = $root;
+ return $root;
+ } else {
+ // Google Drive SDK does not have methods for retrieving files by path
+ // Instead we must find the id of the parent folder of the file
+ $parentId = $this->getDriveFile('')->getId();
+ $folderNames = explode('/', $path);
+ $path = '';
+ // Loop through each folder of this path to get to the file
+ foreach ($folderNames as $name) {
+ // Reconstruct path from beginning
+ if ($path === '') {
+ $path .= $name;
+ } else {
+ $path .= '/'.$name;
+ }
+ if (isset($this->driveFiles[$path])) {
+ $parentId = $this->driveFiles[$path]->getId();
+ } else {
+ $q = "title='" . str_replace("'","\\'", $name) . "' and '" . str_replace("'","\\'", $parentId) . "' in parents and trashed = false";
+ $result = $this->service->files->listFiles(array('q' => $q))->getItems();
+ if (!empty($result)) {
+ // Google Drive allows files with the same name, ownCloud doesn't
+ if (count($result) > 1) {
+ $this->onDuplicateFileDetected($path);
+ return false;
+ } else {
+ $file = current($result);
+ $this->driveFiles[$path] = $file;
+ $parentId = $file->getId();
+ }
+ } else {
+ // Google Docs have no extension in their title, so try without extension
+ $pos = strrpos($path, '.');
+ if ($pos !== false) {
+ $pathWithoutExt = substr($path, 0, $pos);
+ $file = $this->getDriveFile($pathWithoutExt);
+ if ($file) {
+ // Switch cached Google_Service_Drive_DriveFile to the correct index
+ unset($this->driveFiles[$pathWithoutExt]);
+ $this->driveFiles[$path] = $file;
+ $parentId = $file->getId();
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ }
+ }
+ return $this->driveFiles[$path];
+ }
+ }
+
+ /**
+ * Set the Google_Service_Drive_DriveFile object in the cache
+ * @param string $path
+ * @param Google_Service_Drive_DriveFile|false $file
+ */
+ private function setDriveFile($path, $file) {
+ $path = trim($path, '/');
+ $this->driveFiles[$path] = $file;
+ if ($file === false) {
+ // Set all child paths as false
+ $len = strlen($path);
+ foreach ($this->driveFiles as $key => $file) {
+ if (substr($key, 0, $len) === $path) {
+ $this->driveFiles[$key] = false;
+ }
+ }
+ }
+ }
+
+ /**
+ * Write a log message to inform about duplicate file names
+ * @param string $path
+ */
+ private function onDuplicateFileDetected($path) {
+ $about = $this->service->about->get();
+ $user = $about->getName();
+ \OCP\Util::writeLog('files_external',
+ 'Ignoring duplicate file name: '.$path.' on Google Drive for Google user: '.$user,
+ \OCP\Util::INFO
+ );
+ }
+
+ /**
+ * Generate file extension for a Google Doc, choosing Open Document formats for download
+ * @param string $mimetype
+ * @return string
+ */
+ private function getGoogleDocExtension($mimetype) {
+ if ($mimetype === self::DOCUMENT) {
+ return 'odt';
+ } else if ($mimetype === self::SPREADSHEET) {
+ return 'ods';
+ } else if ($mimetype === self::DRAWING) {
+ return 'jpg';
+ } else if ($mimetype === self::PRESENTATION) {
+ // Download as .odp is not available
+ return 'pdf';
+ } else {
+ return '';
+ }
+ }
+
+ public function mkdir($path) {
+ if (!$this->is_dir($path)) {
+ $parentFolder = $this->getDriveFile(dirname($path));
+ if ($parentFolder) {
+ $folder = new \Google_Service_Drive_DriveFile();
+ $folder->setTitle(basename($path));
+ $folder->setMimeType(self::FOLDER);
+ $parent = new \Google_Service_Drive_ParentReference();
+ $parent->setId($parentFolder->getId());
+ $folder->setParents(array($parent));
+ $result = $this->service->files->insert($folder);
+ if ($result) {
+ $this->setDriveFile($path, $result);
+ }
+ return (bool)$result;
+ }
+ }
+ return false;
+ }
+
+ public function rmdir($path) {
+ if (!$this->isDeletable($path)) {
+ return false;
+ }
+ if (trim($path, '/') === '') {
+ $dir = $this->opendir($path);
+ if(is_resource($dir)) {
+ while (($file = readdir($dir)) !== false) {
+ if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
+ if (!$this->unlink($path.'/'.$file)) {
+ return false;
+ }
+ }
+ }
+ closedir($dir);
+ }
+ $this->driveFiles = array();
+ return true;
+ } else {
+ return $this->unlink($path);
+ }
+ }
+
+ public function opendir($path) {
+ $folder = $this->getDriveFile($path);
+ if ($folder) {
+ $files = array();
+ $duplicates = array();
+ $pageToken = true;
+ while ($pageToken) {
+ $params = array();
+ if ($pageToken !== true) {
+ $params['pageToken'] = $pageToken;
+ }
+ $params['q'] = "'" . str_replace("'","\\'", $folder->getId()) . "' in parents and trashed = false";
+ $children = $this->service->files->listFiles($params);
+ foreach ($children->getItems() as $child) {
+ $name = $child->getTitle();
+ // Check if this is a Google Doc i.e. no extension in name
+ $extension = $child->getFileExtension();
+ if (empty($extension)
+ && $child->getMimeType() !== self::FOLDER
+ ) {
+ $name .= '.'.$this->getGoogleDocExtension($child->getMimeType());
+ }
+ if ($path === '') {
+ $filepath = $name;
+ } else {
+ $filepath = $path.'/'.$name;
+ }
+ // Google Drive allows files with the same name, ownCloud doesn't
+ // Prevent opendir() from returning any duplicate files
+ $key = array_search($name, $files);
+ if ($key !== false || isset($duplicates[$filepath])) {
+ if (!isset($duplicates[$filepath])) {
+ $duplicates[$filepath] = true;
+ $this->setDriveFile($filepath, false);
+ unset($files[$key]);
+ $this->onDuplicateFileDetected($filepath);
+ }
+ } else {
+ // Cache the Google_Service_Drive_DriveFile for future use
+ $this->setDriveFile($filepath, $child);
+ $files[] = $name;
+ }
+ }
+ $pageToken = $children->getNextPageToken();
+ }
+ return IteratorDirectory::wrap($files);
+ } else {
+ return false;
+ }
+ }
+
+ public function stat($path) {
+ $file = $this->getDriveFile($path);
+ if ($file) {
+ $stat = array();
+ if ($this->filetype($path) === 'dir') {
+ $stat['size'] = 0;
+ } else {
+ // Check if this is a Google Doc
+ if ($this->getMimeType($path) !== $file->getMimeType()) {
+ // Return unknown file size
+ $stat['size'] = \OCP\Files\FileInfo::SPACE_UNKNOWN;
+ } else {
+ $stat['size'] = $file->getFileSize();
+ }
+ }
+ $stat['atime'] = strtotime($file->getLastViewedByMeDate());
+ $stat['mtime'] = strtotime($file->getModifiedDate());
+ $stat['ctime'] = strtotime($file->getCreatedDate());
+ return $stat;
+ } else {
+ return false;
+ }
+ }
+
+ public function filetype($path) {
+ if ($path === '') {
+ return 'dir';
+ } else {
+ $file = $this->getDriveFile($path);
+ if ($file) {
+ if ($file->getMimeType() === self::FOLDER) {
+ return 'dir';
+ } else {
+ return 'file';
+ }
+ } else {
+ return false;
+ }
+ }
+ }
+
+ public function isUpdatable($path) {
+ $file = $this->getDriveFile($path);
+ if ($file) {
+ return $file->getEditable();
+ } else {
+ return false;
+ }
+ }
+
+ public function file_exists($path) {
+ return (bool)$this->getDriveFile($path);
+ }
+
+ public function unlink($path) {
+ $file = $this->getDriveFile($path);
+ if ($file) {
+ $result = $this->service->files->trash($file->getId());
+ if ($result) {
+ $this->setDriveFile($path, false);
+ }
+ return (bool)$result;
+ } else {
+ return false;
+ }
+ }
+
+ public function rename($path1, $path2) {
+ $file = $this->getDriveFile($path1);
+ if ($file) {
+ $newFile = $this->getDriveFile($path2);
+ if (dirname($path1) === dirname($path2)) {
+ if ($newFile) {
+ // rename to the name of the target file, could be an office file without extension
+ $file->setTitle($newFile->getTitle());
+ } else {
+ $file->setTitle(basename(($path2)));
+ }
+ } else {
+ // Change file parent
+ $parentFolder2 = $this->getDriveFile(dirname($path2));
+ if ($parentFolder2) {
+ $parent = new \Google_Service_Drive_ParentReference();
+ $parent->setId($parentFolder2->getId());
+ $file->setParents(array($parent));
+ } else {
+ return false;
+ }
+ }
+ // We need to get the object for the existing file with the same
+ // name (if there is one) before we do the patch. If oldfile
+ // exists and is a directory we have to delete it before we
+ // do the rename too.
+ $oldfile = $this->getDriveFile($path2);
+ if ($oldfile && $this->is_dir($path2)) {
+ $this->rmdir($path2);
+ $oldfile = false;
+ }
+ $result = $this->service->files->patch($file->getId(), $file);
+ if ($result) {
+ $this->setDriveFile($path1, false);
+ $this->setDriveFile($path2, $result);
+ if ($oldfile && $newFile) {
+ // only delete if they have a different id (same id can happen for part files)
+ if ($newFile->getId() !== $oldfile->getId()) {
+ $this->service->files->delete($oldfile->getId());
+ }
+ }
+ }
+ return (bool)$result;
+ } else {
+ return false;
+ }
+ }
+
+ public function fopen($path, $mode) {
+ $pos = strrpos($path, '.');
+ if ($pos !== false) {
+ $ext = substr($path, $pos);
+ } else {
+ $ext = '';
+ }
+ switch ($mode) {
+ case 'r':
+ case 'rb':
+ $file = $this->getDriveFile($path);
+ if ($file) {
+ $exportLinks = $file->getExportLinks();
+ $mimetype = $this->getMimeType($path);
+ $downloadUrl = null;
+ if ($exportLinks && isset($exportLinks[$mimetype])) {
+ $downloadUrl = $exportLinks[$mimetype];
+ } else {
+ $downloadUrl = $file->getDownloadUrl();
+ }
+ if (isset($downloadUrl)) {
+ $request = new \Google_Http_Request($downloadUrl, 'GET', null, null);
+ $httpRequest = $this->client->getAuth()->sign($request);
+ // the library's service doesn't support streaming, so we use Guzzle instead
+ $client = \OC::$server->getHTTPClientService()->newClient();
+ try {
+ $response = $client->get($downloadUrl, [
+ 'headers' => $httpRequest->getRequestHeaders(),
+ 'stream' => true,
+ 'verify' => __DIR__ . '/../../3rdparty/google-api-php-client/src/Google/IO/cacerts.pem',
+ ]);
+ } catch (RequestException $e) {
+ if(!is_null($e->getResponse())) {
+ if ($e->getResponse()->getStatusCode() === 404) {
+ return false;
+ } else {
+ throw $e;
+ }
+ } else {
+ throw $e;
+ }
+ }
+
+ $handle = $response->getBody();
+ return RetryWrapper::wrap($handle);
+ }
+ }
+ return false;
+ case 'w':
+ case 'wb':
+ case 'a':
+ case 'ab':
+ case 'r+':
+ case 'w+':
+ case 'wb+':
+ case 'a+':
+ case 'x':
+ case 'x+':
+ case 'c':
+ case 'c+':
+ $tmpFile = \OCP\Files::tmpFile($ext);
+ \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack'));
+ if ($this->file_exists($path)) {
+ $source = $this->fopen($path, 'rb');
+ file_put_contents($tmpFile, $source);
+ }
+ self::$tempFiles[$tmpFile] = $path;
+ return fopen('close://'.$tmpFile, $mode);
+ }
+ }
+
+ public function writeBack($tmpFile) {
+ if (isset(self::$tempFiles[$tmpFile])) {
+ $path = self::$tempFiles[$tmpFile];
+ $parentFolder = $this->getDriveFile(dirname($path));
+ if ($parentFolder) {
+ $mimetype = \OC::$server->getMimeTypeDetector()->detect($tmpFile);
+ $params = array(
+ 'mimeType' => $mimetype,
+ 'uploadType' => 'media'
+ );
+ $result = false;
+
+ $chunkSizeBytes = 10 * 1024 * 1024;
+
+ $useChunking = false;
+ $size = filesize($tmpFile);
+ if ($size > $chunkSizeBytes) {
+ $useChunking = true;
+ } else {
+ $params['data'] = file_get_contents($tmpFile);
+ }
+
+ if ($this->file_exists($path)) {
+ $file = $this->getDriveFile($path);
+ $this->client->setDefer($useChunking);
+ $request = $this->service->files->update($file->getId(), $file, $params);
+ } else {
+ $file = new \Google_Service_Drive_DriveFile();
+ $file->setTitle(basename($path));
+ $file->setMimeType($mimetype);
+ $parent = new \Google_Service_Drive_ParentReference();
+ $parent->setId($parentFolder->getId());
+ $file->setParents(array($parent));
+ $this->client->setDefer($useChunking);
+ $request = $this->service->files->insert($file, $params);
+ }
+
+ if ($useChunking) {
+ // Create a media file upload to represent our upload process.
+ $media = new \Google_Http_MediaFileUpload(
+ $this->client,
+ $request,
+ 'text/plain',
+ null,
+ true,
+ $chunkSizeBytes
+ );
+ $media->setFileSize($size);
+
+ // Upload the various chunks. $status will be false until the process is
+ // complete.
+ $status = false;
+ $handle = fopen($tmpFile, 'rb');
+ while (!$status && !feof($handle)) {
+ $chunk = fread($handle, $chunkSizeBytes);
+ $status = $media->nextChunk($chunk);
+ }
+
+ // The final value of $status will be the data from the API for the object
+ // that has been uploaded.
+ $result = false;
+ if ($status !== false) {
+ $result = $status;
+ }
+
+ fclose($handle);
+ } else {
+ $result = $request;
+ }
+
+ // Reset to the client to execute requests immediately in the future.
+ $this->client->setDefer(false);
+
+ if ($result) {
+ $this->setDriveFile($path, $result);
+ }
+ }
+ unlink($tmpFile);
+ }
+ }
+
+ public function getMimeType($path) {
+ $file = $this->getDriveFile($path);
+ if ($file) {
+ $mimetype = $file->getMimeType();
+ // Convert Google Doc mimetypes, choosing Open Document formats for download
+ if ($mimetype === self::FOLDER) {
+ return 'httpd/unix-directory';
+ } else if ($mimetype === self::DOCUMENT) {
+ return 'application/vnd.oasis.opendocument.text';
+ } else if ($mimetype === self::SPREADSHEET) {
+ return 'application/x-vnd.oasis.opendocument.spreadsheet';
+ } else if ($mimetype === self::DRAWING) {
+ return 'image/jpeg';
+ } else if ($mimetype === self::PRESENTATION) {
+ // Download as .odp is not available
+ return 'application/pdf';
+ } else {
+ // use extension-based detection, could be an encrypted file
+ return parent::getMimeType($path);
+ }
+ } else {
+ return false;
+ }
+ }
+
+ public function free_space($path) {
+ $about = $this->service->about->get();
+ return $about->getQuotaBytesTotal() - $about->getQuotaBytesUsed();
+ }
+
+ public function touch($path, $mtime = null) {
+ $file = $this->getDriveFile($path);
+ $result = false;
+ if ($file) {
+ if (isset($mtime)) {
+ // This is just RFC3339, but frustratingly, GDrive's API *requires*
+ // the fractions portion be present, while no handy PHP constant
+ // for RFC3339 or ISO8601 includes it. So we do it ourselves.
+ $file->setModifiedDate(date('Y-m-d\TH:i:s.uP', $mtime));
+ $result = $this->service->files->patch($file->getId(), $file, array(
+ 'setModifiedDate' => true,
+ ));
+ } else {
+ $result = $this->service->files->touch($file->getId());
+ }
+ } else {
+ $parentFolder = $this->getDriveFile(dirname($path));
+ if ($parentFolder) {
+ $file = new \Google_Service_Drive_DriveFile();
+ $file->setTitle(basename($path));
+ $parent = new \Google_Service_Drive_ParentReference();
+ $parent->setId($parentFolder->getId());
+ $file->setParents(array($parent));
+ $result = $this->service->files->insert($file);
+ }
+ }
+ if ($result) {
+ $this->setDriveFile($path, $result);
+ }
+ return (bool)$result;
+ }
+
+ public function test() {
+ if ($this->free_space('')) {
+ return true;
+ }
+ return false;
+ }
+
+ public function hasUpdated($path, $time) {
+ $appConfig = \OC::$server->getAppConfig();
+ if ($this->is_file($path)) {
+ return parent::hasUpdated($path, $time);
+ } else {
+ // Google Drive doesn't change modified times of folders when files inside are updated
+ // Instead we use the Changes API to see if folders have been updated, and it's a pain
+ $folder = $this->getDriveFile($path);
+ if ($folder) {
+ $result = false;
+ $folderId = $folder->getId();
+ $startChangeId = $appConfig->getValue('files_external', $this->getId().'cId');
+ $params = array(
+ 'includeDeleted' => true,
+ 'includeSubscribed' => true,
+ );
+ if (isset($startChangeId)) {
+ $startChangeId = (int)$startChangeId;
+ $largestChangeId = $startChangeId;
+ $params['startChangeId'] = $startChangeId + 1;
+ } else {
+ $largestChangeId = 0;
+ }
+ $pageToken = true;
+ while ($pageToken) {
+ if ($pageToken !== true) {
+ $params['pageToken'] = $pageToken;
+ }
+ $changes = $this->service->changes->listChanges($params);
+ if ($largestChangeId === 0 || $largestChangeId === $startChangeId) {
+ $largestChangeId = $changes->getLargestChangeId();
+ }
+ if (isset($startChangeId)) {
+ // Check if a file in this folder has been updated
+ // There is no way to filter by folder at the API level...
+ foreach ($changes->getItems() as $change) {
+ $file = $change->getFile();
+ if ($file) {
+ foreach ($file->getParents() as $parent) {
+ if ($parent->getId() === $folderId) {
+ $result = true;
+ // Check if there are changes in different folders
+ } else if ($change->getId() <= $largestChangeId) {
+ // Decrement id so this change is fetched when called again
+ $largestChangeId = $change->getId();
+ $largestChangeId--;
+ }
+ }
+ }
+ }
+ $pageToken = $changes->getNextPageToken();
+ } else {
+ // Assuming the initial scan just occurred and changes are negligible
+ break;
+ }
+ }
+ $appConfig->setValue('files_external', $this->getId().'cId', $largestChangeId);
+ return $result;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * check if curl is installed
+ */
+ public static function checkDependencies() {
+ return true;
+ }
+
+}