diff --git a/apps/files_external/ajax/google.php b/apps/files_external/ajax/google.php index 70adcb2c2a..bd5a6099bb 100644 --- a/apps/files_external/ajax/google.php +++ b/apps/files_external/ajax/google.php @@ -1,64 +1,41 @@ $scope, 'oauth_callback' => $callback); - $request = OAuthRequest::from_consumer_and_token($consumer, null, 'GET', $url, $params); - $request->sign_request($sigMethod, $consumer, null); - $response = send_signed_request('GET', $url, array($request->to_header()), null, false); - $token = array(); - parse_str($response, $token); - if (isset($token['oauth_token']) && isset($token['oauth_token_secret'])) { - $authUrl = 'https://www.google.com/accounts/OAuthAuthorizeToken?oauth_token='.$token['oauth_token']; - OCP\JSON::success(array('data' => array('url' => $authUrl, - 'request_token' => $token['oauth_token'], - 'request_token_secret' => $token['oauth_token_secret']))); - } else { +if (isset($_POST['client_id']) && isset($_POST['client_secret']) && isset($_POST['redirect'])) { + $client = new Google_Client(); + $client->setClientId($_POST['client_id']); + $client->setClientSecret($_POST['client_secret']); + $client->setRedirectUri($_POST['redirect']); + $client->setScopes(array('https://www.googleapis.com/auth/drive')); + if (isset($_POST['step'])) { + $step = $_POST['step']; + if ($step == 1) { + try { + $authUrl = $client->createAuthUrl(); + OCP\JSON::success(array('data' => array( + 'url' => $authUrl + ))); + } catch (Exception $exception) { OCP\JSON::error(array('data' => array( - 'message' => 'Fetching request tokens failed. Error: '.$response - ))); + 'message' => 'Step 1 failed. Exception: '.$exception->getMessage() + ))); } - break; - case 2: - if (isset($_POST['oauth_verifier']) - && isset($_POST['request_token']) - && isset($_POST['request_token_secret']) - ) { - $token = new OAuthToken($_POST['request_token'], $_POST['request_token_secret']); - $url = 'https://www.google.com/accounts/OAuthGetAccessToken'; - $request = OAuthRequest::from_consumer_and_token($consumer, $token, 'GET', $url, - array('oauth_verifier' => $_POST['oauth_verifier'])); - $request->sign_request($sigMethod, $consumer, $token); - $response = send_signed_request('GET', $url, array($request->to_header()), null, false); - $token = array(); - parse_str($response, $token); - if (isset($token['oauth_token']) && isset($token['oauth_token_secret'])) { - OCP\JSON::success(array('access_token' => $token['oauth_token'], - 'access_token_secret' => $token['oauth_token_secret'])); - } else { - OCP\JSON::error(array('data' => array( - 'message' => 'Fetching access tokens failed. Error: '.$response - ))); - } + } else if ($step == 2 && isset($_POST['code'])) { + try { + $token = $client->authenticate($_POST['code']); + OCP\JSON::success(array('data' => array( + 'token' => $token + ))); + } catch (Exception $exception) { + OCP\JSON::error(array('data' => array( + 'message' => 'Step 2 failed. Exception: '.$exception->getMessage() + ))); } - break; + } } -} +} \ No newline at end of file diff --git a/apps/files_external/js/google.js b/apps/files_external/js/google.js index 7be1b338e9..7e111a95d9 100644 --- a/apps/files_external/js/google.js +++ b/apps/files_external/js/google.js @@ -1,69 +1,89 @@ $(document).ready(function() { - $('#externalStorage tbody tr.\\\\OC\\\\Files\\\\Storage\\\\Google').each(function(index, tr) { - setupGoogleRow(tr); - }); - - $('#externalStorage').on('change', '#selectBackend', function() { - if ($(this).val() == '\\OC\\Files\\Storage\\Google') { - setupGoogleRow($('#externalStorage tbody>tr:last').prev('tr')); - } - }); - - function setupGoogleRow(tr) { - var configured = $(tr).find('[data-parameter="configured"]'); + $('#externalStorage tbody tr.\\\\OC\\\\Files\\\\Storage\\\\Google').each(function() { + var configured = $(this).find('[data-parameter="configured"]'); if ($(configured).val() == 'true') { - $(tr).find('.configuration').append(''+t('files_external', 'Access granted')+''); + $(this).find('.configuration input').attr('disabled', 'disabled'); + $(this).find('.configuration').append($('').attr('id', 'access') + .text(t('files_external', 'Access granted'))); } else { - var token = $(tr).find('[data-parameter="token"]'); - var token_secret = $(tr).find('[data-parameter="token_secret"]'); - var params = {}; - window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m, key, value) { - params[key] = value; - }); - if (params['oauth_token'] !== undefined && params['oauth_verifier'] !== undefined && decodeURIComponent(params['oauth_token']) == $(token).val()) { - var statusSpan = $(tr).find('.status span'); - statusSpan.removeClass(); - statusSpan.addClass('waiting'); - $.post(OC.filePath('files_external', 'ajax', 'google.php'), { step: 2, oauth_verifier: params['oauth_verifier'], request_token: $(token).val(), request_token_secret: $(token_secret).val() }, function(result) { - if (result && result.status == 'success') { - $(token).val(result.access_token); - $(token_secret).val(result.access_token_secret); - $(configured).val('true'); - OC.MountConfig.saveStorage(tr); - $(tr).find('.configuration').append(''+t('files_external', 'Access granted')+''); - } else { - OC.dialogs.alert(result.data.message, t('files_external', 'Error configuring Google Drive storage')); - onGoogleInputsChange(tr); - } + var client_id = $(this).find('.configuration [data-parameter="client_id"]').val(); + var client_secret = $(this).find('.configuration [data-parameter="client_secret"]') + .val(); + if (client_id != '' && client_secret != '') { + var params = {}; + window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m, key, value) { + params[key] = value; }); + if (params['code'] !== undefined) { + var tr = $(this); + var token = $(this).find('.configuration [data-parameter="token"]'); + var statusSpan = $(tr).find('.status span'); + statusSpan.removeClass(); + statusSpan.addClass('waiting'); + $.post(OC.filePath('files_external', 'ajax', 'google.php'), + { + step: 2, + client_id: client_id, + client_secret: client_secret, + redirect: location.protocol + '//' + location.host + location.pathname, + code: params['code'], + }, function(result) { + if (result && result.status == 'success') { + $(token).val(result.data.token); + $(configured).val('true'); + OC.MountConfig.saveStorage(tr); + $(tr).find('.configuration input').attr('disabled', 'disabled'); + $(tr).find('.configuration').append($('') + .attr('id', 'access') + .text(t('files_external', 'Access granted'))); + } else { + OC.dialogs.alert(result.data.message, + t('files_external', 'Error configuring Google Drive storage') + ); + } + } + ); + } } else { - onGoogleInputsChange(tr); + onGoogleInputsChange($(this)); } } - } - - $('#externalStorage').on('paste', 'tbody tr.\\\\OC\\\\Files\\\\Storage\\\\Google td', function() { - var tr = $(this).parent(); - setTimeout(function() { - onGoogleInputsChange(tr); - }, 20); }); - $('#externalStorage').on('keyup', 'tbody tr.\\\\OC\\\\Files\\\\Storage\\\\Google td', function() { - onGoogleInputsChange($(this).parent()); - }); + $('#externalStorage').on('paste', 'tbody tr.\\\\OC\\\\Files\\\\Storage\\\\Google td', + function() { + var tr = $(this).parent(); + setTimeout(function() { + onGoogleInputsChange(tr); + }, 20); + } + ); - $('#externalStorage').on('change', 'tbody tr.\\\\OC\\\\Files\\\\Storage\\\\Google .chzn-select', function() { - onGoogleInputsChange($(this).parent().parent()); - }); + $('#externalStorage').on('keyup', 'tbody tr.\\\\OC\\\\Files\\\\Storage\\\\Google td', + function() { + onGoogleInputsChange($(this).parent()); + } + ); + + $('#externalStorage').on('change', 'tbody tr.\\\\OC\\\\Files\\\\Storage\\\\Google .chzn-select' + , function() { + onGoogleInputsChange($(this).parent().parent()); + } + ); function onGoogleInputsChange(tr) { if ($(tr).find('[data-parameter="configured"]').val() != 'true') { var config = $(tr).find('.configuration'); - if ($(tr).find('.mountPoint input').val() != '' && ($(tr).find('.chzn-select').length == 0 || $(tr).find('.chzn-select').val() != null)) { + if ($(tr).find('.mountPoint input').val() != '' + && $(config).find('[data-parameter="client_id"]').val() != '' + && $(config).find('[data-parameter="client_secret"]').val() != '' + && ($(tr).find('.chzn-select').length == 0 + || $(tr).find('.chzn-select').val() != null)) + { if ($(tr).find('.google').length == 0) { - $(config).append(''+t('files_external', 'Grant access')+''); + $(config).append($('').addClass('button google') + .text(t('files_external', 'Grant access'))); } else { $(tr).find('.google').show(); } @@ -77,22 +97,33 @@ $(document).ready(function() { event.preventDefault(); var tr = $(this).parent().parent(); var configured = $(this).parent().find('[data-parameter="configured"]'); - var token = $(this).parent().find('[data-parameter="token"]'); - var token_secret = $(this).parent().find('[data-parameter="token_secret"]'); + var client_id = $(this).parent().find('[data-parameter="client_id"]').val(); + var client_secret = $(this).parent().find('[data-parameter="client_secret"]').val(); var statusSpan = $(tr).find('.status span'); - $.post(OC.filePath('files_external', 'ajax', 'google.php'), { step: 1, callback: location.protocol + '//' + location.host + location.pathname }, function(result) { - if (result && result.status == 'success') { - $(configured).val('false'); - $(token).val(result.data.request_token); - $(token_secret).val(result.data.request_token_secret); - OC.MountConfig.saveStorage(tr); - statusSpan.removeClass(); - statusSpan.addClass('waiting'); - window.location = result.data.url; - } else { - OC.dialogs.alert(result.data.message, t('files_external', 'Error configuring Google Drive storage')); - } - }); + if (client_id != '' && client_secret != '') { + var token = $(this).parent().find('[data-parameter="token"]'); + $.post(OC.filePath('files_external', 'ajax', 'google.php'), + { + step: 1, + client_id: client_id, + client_secret: client_secret, + redirect: location.protocol + '//' + location.host + location.pathname, + }, function(result) { + if (result && result.status == 'success') { + $(configured).val('false'); + $(token).val('false'); + OC.MountConfig.saveStorage(tr); + statusSpan.removeClass(); + statusSpan.addClass('waiting'); + window.location = result.data.url; + } else { + OC.dialogs.alert(result.data.message, + t('files_external', 'Error configuring Google Drive storage') + ); + } + } + ); + } }); -}); +}); \ No newline at end of file diff --git a/apps/files_external/lib/config.php b/apps/files_external/lib/config.php index 4cb9b7c8ec..9356fab9ab 100755 --- a/apps/files_external/lib/config.php +++ b/apps/files_external/lib/config.php @@ -74,8 +74,9 @@ class OC_Mount_Config { 'backend' => 'Google Drive', 'configuration' => array( 'configured' => '#configured', - 'token' => '#token', - 'token_secret' => '#token secret'), + 'client_id' => 'Client ID', + 'client_secret' => 'Client secret', + 'token' => '#token'), 'custom' => 'google'); $backends['\OC\Files\Storage\SWIFT']=array( diff --git a/apps/files_external/lib/google.php b/apps/files_external/lib/google.php index ec7de3f357..259a776142 100644 --- a/apps/files_external/lib/google.php +++ b/apps/files_external/lib/google.php @@ -1,5 +1,4 @@ id = 'google::' . $params['token']; - $this->consumer = new \OAuthConsumer($consumer_key, $consumer_secret); - $this->oauth_token = new \OAuthToken($params['token'], $params['token_secret']); - $this->sig_method = new \OAuthSignatureMethod_HMAC_SHA1(); - $this->entries = array(); + $client = new \Google_Client(); + $client->setClientId($params['client_id']); + $client->setClientSecret($params['client_secret']); + $client->setRedirectUri('http://localhost/workspace/core'); + $client->setScopes(array('https://www.googleapis.com/auth/drive')); + $client->setUseObjects(true); + $client->setAccessToken($params['token']); + $this->service = new \Google_DriveService($client); + $this->root = isset($params['root']) ? $params['root'] : ''; + $token = json_decode($params['token'], true); + $this->id = 'google::'.$params['client_id'].$token['created']; } else { throw new \Exception('Creating \OC\Files\Storage\Google storage failed'); } } - private function sendRequest($uri, - $httpMethod, - $postData = null, - $extraHeaders = null, - $isDownload = false, - $returnHeaders = false, - $isContentXML = true, - $returnHTTPCode = false) { - $uri = trim($uri); - // create an associative array from each key/value url query param pair. - $params = array(); - $pieces = explode('?', $uri); - if (isset($pieces[1])) { - $params = explode_assoc('=', '&', $pieces[1]); - } - // urlencode each url parameter key/value pair - $tempStr = $pieces[0]; - foreach ($params as $key => $value) { - $tempStr .= '&' . urlencode($key) . '=' . urlencode($value); - } - $uri = preg_replace('/&/', '?', $tempStr, 1); - $request = \OAuthRequest::from_consumer_and_token($this->consumer, - $this->oauth_token, - $httpMethod, - $uri, - $params); - $request->sign_request($this->sig_method, $this->consumer, $this->oauth_token); - $auth_header = $request->to_header(); - $headers = array($auth_header, 'GData-Version: 3.0'); - if ($isContentXML) { - $headers = array_merge($headers, array('Content-Type: application/atom+xml')); - } - if (is_array($extraHeaders)) { - $headers = array_merge($headers, $extraHeaders); - } - $curl = curl_init($uri); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - curl_setopt($curl, CURLOPT_FAILONERROR, false); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); - switch ($httpMethod) { - case 'GET': - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - break; - case 'POST': - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - curl_setopt($curl, CURLOPT_POST, 1); - curl_setopt($curl, CURLOPT_POSTFIELDS, $postData); - break; - case 'PUT': - $headers[] = 'If-Match: *'; - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $httpMethod); - curl_setopt($curl, CURLOPT_POSTFIELDS, $postData); - break; - case 'DELETE': - $headers[] = 'If-Match: *'; - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $httpMethod); - break; - default: - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - } - if ($isDownload) { - $tmpFile = \OC_Helper::tmpFile(); - $handle = fopen($tmpFile, 'w'); - curl_setopt($curl, CURLOPT_FILE, $handle); - } - if ($returnHeaders) { - curl_setopt($curl, CURLOPT_HEADER, true); - } - $result = curl_exec($curl); - $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); - curl_close($curl); - if ($result) { - // TODO https://developers.google.com/google-apps/documents-list/#handling_api_errors - // TODO Log error messages - if ($httpCode <= 308) { - if ($isDownload) { - return $tmpFile; - } else if ($returnHTTPCode) { - return array('result' => $result, 'code' => $httpCode); - } else { - return $result; - } - } - } - return false; - } - - private function getFeed($feedUri, $httpMethod, $postData = null) { - $result = $this->sendRequest($feedUri, $httpMethod, $postData); - if ($result) { - $dom = new \DOMDocument(); - $dom->loadXML($result); - return $dom; - } - return false; - } - - /** - * Base url for google docs feeds - */ - const BASE_URI='https://docs.google.com/feeds'; - - private function getResource($path) { - $file = basename($path); - if (array_key_exists($file, $this->entries)) { - return $this->entries[$file]; - } else { - // Strip the file extension; file could be a native Google Docs resource - if ($pos = strpos($file, '.')) { - $title = substr($file, 0, $pos); - $dom = $this->getFeed(self::BASE_URI.'/default/private/full?showfolders=true&title='.$title, 'GET'); - // Check if request was successful and entry exists - if ($dom && $entry = $dom->getElementsByTagName('entry')->item(0)) { - $this->entries[$file] = $entry; - return $entry; - } - } - $dom = $this->getFeed(self::BASE_URI.'/default/private/full?showfolders=true&title='.$file, 'GET'); - // Check if request was successful and entry exists - if ($dom && $entry = $dom->getElementsByTagName('entry')->item(0)) { - $this->entries[$file] = $entry; - return $entry; - } - return false; - } - } - - private function getExtension($entry) { - $mimetype = $this->getMimeType('', $entry); - switch ($mimetype) { - case 'httpd/unix-directory': - return ''; - case 'application/vnd.oasis.opendocument.text': - return 'odt'; - case 'application/vnd.oasis.opendocument.spreadsheet': - return 'ods'; - case 'application/vnd.oasis.opendocument.presentation': - return 'pptx'; - case 'text/html': - return 'html'; - default: - return 'html'; - } - } - - public function getId(){ + public function getId() { return $this->id; } - public function mkdir($path) { - $collection = dirname($path); - // Check if path parent is root directory - if ($collection == '/' || $collection == '\.' || $collection == '.') { - $uri = self::BASE_URI.'/default/private/full'; + /** + * Get the Google_DriveFile object for the specified path + * @param string $path + * @return Google_DriveFile + */ + private function getDriveFile($path) { + // Remove leading and trailing slashes + $path = trim($this->root.$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 { - // Get parent content link - $dom = $this->getResource(basename($collection)); - if ($dom) { - $uri = $dom->getElementsByTagName('content')->item(0)->getAttribute('src'); + // 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='".$name."' and '".$parentId."' in parents"; + $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_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]; } - if (isset($uri)) { - $title = basename($path); - // Construct post data - $postData = ''; - $postData .= ''; - $postData .= ''; - $postData .= ''; - $dom = $this->sendRequest($uri, 'POST', $postData); - if ($dom) { - return true; - } + } + + /** + * 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) { + $parentFolder = $this->getDriveFile(dirname($path)); + if ($parentFolder) { + $folder = new \Google_DriveFile(); + $folder->setTitle(basename($path)); + $folder->setMimeType(self::FOLDER); + $parent = new \Google_ParentReference(); + $parent->setId($parentFolder->getId()); + $folder->setParents(array($parent)); + return (bool)$this->service->files->insert($folder); + } else { + return false; } - return false; } public function rmdir($path) { @@ -236,92 +183,98 @@ class Google extends \OC\Files\Storage\Common { } public function opendir($path) { - if ($path == '' || $path == '/') { - $next = self::BASE_URI.'/default/private/full/folder%3Aroot/contents'; + // Remove leading and trailing slashes + $path = trim($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'] = "'".$folder->getId()."' in parents"; + $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 + if ($child->getFileExtension() == '' + && $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 + if (isset($this->driveFiles[$filepath]) && !isset($duplicates[$filepath])) { + // Save this key to unset later in case there are more than 2 duplicates + $duplicates[$filepath] = $name; + } else { + // Cache the Google_DriveFile for future use + $this->driveFiles[$filepath] = $child; + $files[] = $name; + } + } + $pageToken = $children->getNextPageToken(); + } + // Remove all duplicate files + foreach ($duplicates as $filepath => $name) { + unset($this->driveFiles[$filepath]); + $key = array_search($name, $files); + unset($files[$key]); + $this->onDuplicateFileDetected($filepath); + } + // Reindex $files array if duplicates were removed + // This is necessary for \OC\Files\Stream\Dir + if (!empty($duplicates)) { + $files = array_values($files); + } + \OC\Files\Stream\Dir::register('google'.$path, $files); + return opendir('fakedir://google'.$path); } else { - $entry = $this->getResource($path); - if ($entry) { - $next = $entry->getElementsByTagName('content')->item(0)->getAttribute('src'); + return false; + } + } + + public function stat($path) { + $file = $this->getDriveFile($path); + if ($file) { + $stat = array(); + if ($this->filetype($path) === 'dir') { + $stat['size'] = 0; + } 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; } } - $files = array(); - while ($next) { - $dom = $this->getFeed($next, 'GET'); - $links = $dom->getElementsByTagName('link'); - foreach ($links as $link) { - if ($link->getAttribute('rel') == 'next') { - $next = $link->getAttribute('src'); - break; - } else { - $next = false; - } - } - $entries = $dom->getElementsByTagName('entry'); - foreach ($entries as $entry) { - $name = $entry->getElementsByTagName('title')->item(0)->nodeValue; - // Google Docs resources don't always include extensions in title - if ( ! strpos($name, '.')) { - $extension = $this->getExtension($entry); - if ($extension != '') { - $name .= '.'.$extension; - } - } - $files[] = basename($name); - // Cache entry for future use - $this->entries[$name] = $entry; - } - } - \OC\Files\Stream\Dir::register('google'.$path, $files); - return opendir('fakedir://google'.$path); - } - - public function stat($path) { - if ($path == '' || $path == '/') { - $stat['size'] = $this->free_space($path); - $stat['atime'] = time(); - $stat['mtime'] = time(); - $stat['ctime'] = time(); - } else { - $entry = $this->getResource($path); - if ($entry) { - // NOTE: Native resources don't have a file size - $stat['size'] = $entry->getElementsByTagNameNS('http://schemas.google.com/g/2005', - 'quotaBytesUsed')->item(0)->nodeValue; - //if (isset($atime = $entry->getElementsByTagNameNS('http://schemas.google.com/g/2005', - // 'lastViewed')->item(0)->nodeValue)) - //$stat['atime'] = strtotime($entry->getElementsByTagNameNS('http://schemas.google.com/g/2005', - // 'lastViewed')->item(0)->nodeValue); - $stat['mtime'] = strtotime($entry->getElementsByTagName('updated')->item(0)->nodeValue); - } - } - if (isset($stat)) { - return $stat; - } - return false; - } - - public function filetype($path) { - if ($path == '' || $path == '/') { - return 'dir'; - } else { - $entry = $this->getResource($path); - if ($entry) { - $categories = $entry->getElementsByTagName('category'); - foreach ($categories as $category) { - if ($category->getAttribute('scheme') == 'http://schemas.google.com/g/2005#kind') { - $type = $category->getAttribute('label'); - if (strlen(strstr($type, 'folder')) > 0) { - return 'dir'; - } else { - return 'file'; - } - } - } - } - } - return false; } public function isReadable($path) { @@ -329,109 +282,79 @@ class Google extends \OC\Files\Storage\Common { } public function isUpdatable($path) { - if ($path == '' || $path == '/') { - return true; + $file = $this->getDriveFile($path); + if ($file) { + return $file->getEditable(); } else { - $entry = $this->getResource($path); - if ($entry) { - // Check if edit or edit-media links exist - $links = $entry->getElementsByTagName('link'); - foreach ($links as $link) { - if ($link->getAttribute('rel') == 'edit') { - return true; - } else if ($link->getAttribute('rel') == 'edit-media') { - return true; - } - } - } + return false; } - return false; } public function file_exists($path) { - if ($path == '' || $path == '/') { - return true; - } else if ($this->getResource($path)) { - return true; - } - return false; + return (bool)$this->getDriveFile($path); } public function unlink($path) { - // Get resource self link to trash resource - $entry = $this->getResource($path); - if ($entry) { - $links = $entry->getElementsByTagName('link'); - foreach ($links as $link) { - if ($link->getAttribute('rel') == 'self') { - $uri = $link->getAttribute('href'); - break; - } - } + $file = $this->getDriveFile($path); + if ($file) { + return (bool)$this->service->files->trash($file->getId()); + } else { + return false; } - if (isset($uri)) { - $this->sendRequest($uri, 'DELETE'); - return true; - } - return false; } public function rename($path1, $path2) { - $entry = $this->getResource($path1); - if ($entry) { - $collection = dirname($path2); - if (dirname($path1) == $collection) { - // Get resource edit link to rename resource - $etag = $entry->getAttribute('gd:etag'); - $links = $entry->getElementsByTagName('link'); - foreach ($links as $link) { - if ($link->getAttribute('rel') == 'edit') { - $uri = $link->getAttribute('href'); - break; - } - } - $title = basename($path2); - // Construct post data - $postData = ''; - $postData .= ''; - $postData .= ''.$title.''; - $postData .= ''; - $this->sendRequest($uri, 'PUT', $postData); - return true; + $file = $this->getDriveFile($path1); + if ($file) { + if (dirname($path1) === dirname($path2)) { + $file->setTitle(basename(($path2))); } else { - // Move to different collection - $collectionEntry = $this->getResource($collection); - if ($collectionEntry) { - $feedUri = $collectionEntry->getElementsByTagName('content')->item(0)->getAttribute('src'); - // Construct post data - $postData = ''; - $postData .= ''; - $postData .= ''.$entry->getElementsByTagName('id')->item(0).''; - $postData .= ''; - $this->sendRequest($feedUri, 'POST', $postData); - return true; + // Change file parent + $parentFolder2 = $this->getDriveFile(dirname($path2)); + if ($parentFolder2) { + $parent = new \Google_ParentReference(); + $parent->setId($parentFolder2->getId()); + $file->setParents(array($parent)); } } + return (bool)$this->service->files->patch($file->getId(), $file); + } else { + return false; } - 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': - $entry = $this->getResource($path); - if ($entry) { - $extension = $this->getExtension($entry); - $downloadUri = $entry->getElementsByTagName('content')->item(0)->getAttribute('src'); - // TODO Non-native documents don't need these additional parameters - $downloadUri .= '&exportFormat='.$extension.'&format='.$extension; - $tmpFile = $this->sendRequest($downloadUri, 'GET', null, null, true); - return fopen($tmpFile, 'r'); + $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_HttpRequest($downloadUrl, 'GET', null, null); + $httpRequest = \Google_Client::$io->authenticatedRequest($request); + if ($httpRequest->getResponseHttpCode() == 200) { + $tmpFile = \OC_Helper::tmpFile($ext); + $data = $httpRequest->getResponseBody(); + file_put_contents($tmpFile, $data); + return fopen($tmpFile, $mode); + } + } } + return null; case 'w': case 'wb': case 'a': @@ -444,156 +367,88 @@ class Google extends \OC\Files\Storage\Common { case 'x+': case 'c': case 'c+': - if (strrpos($path, '.') !== false) { - $ext = substr($path, strrpos($path, '.')); - } else { - $ext = ''; - } $tmpFile = \OC_Helper::tmpFile($ext); \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack')); if ($this->file_exists($path)) { - $source = $this->fopen($path, 'r'); + $source = $this->fopen($path, 'rb'); file_put_contents($tmpFile, $source); } self::$tempFiles[$tmpFile] = $path; return fopen('close://'.$tmpFile, $mode); } - return false; } public function writeBack($tmpFile) { if (isset(self::$tempFiles[$tmpFile])) { - $this->uploadFile($tmpFile, self::$tempFiles[$tmpFile]); + $path = self::$tempFiles[$tmpFile]; + $parentFolder = $this->getDriveFile(dirname($path)); + if ($parentFolder) { + $file = new \Google_DriveFile(); + $file->setTitle(basename($path)); + $mimetype = \OC_Helper::getMimeType($tmpFile); + $file->setMimeType($mimetype); + $parent = new \Google_ParentReference(); + $parent->setId($parentFolder->getId()); + $file->setParents(array($parent)); + // TODO Research resumable upload + $data = file_get_contents($tmpFile); + $params = array( + 'data' => $data, + 'mimeType' => $mimetype, + ); + if ($this->file_exists($path)) { + $this->service->files->update($file->getId(), $file, $params); + } else { + $this->service->files->insert($file, $params); + } + } unlink($tmpFile); } } - private function uploadFile($path, $target) { - $entry = $this->getResource($target); - if ( ! $entry) { - if (dirname($target) == '.' || dirname($target) == '/') { - $uploadUri = self::BASE_URI.'/upload/create-session/default/private/full/folder%3Aroot/contents'; - } else { - $entry = $this->getResource(dirname($target)); - } - } - if ( ! isset($uploadUri) && $entry) { - $links = $entry->getElementsByTagName('link'); - foreach ($links as $link) { - if ($link->getAttribute('rel') == 'http://schemas.google.com/g/2005#resumable-create-media') { - $uploadUri = $link->getAttribute('href'); - break; - } - } - } - if (isset($uploadUri) && $handle = fopen($path, 'r')) { - $uploadUri .= '?convert=false'; - $mimetype = \OC_Helper::getMimeType($path); - $size = filesize($path); - $headers = array('X-Upload-Content-Type: ' => $mimetype, 'X-Upload-Content-Length: ' => $size); - $postData = ''; - $postData .= ''; - $postData .= ''.basename($target).''; - $postData .= ''; - $result = $this->sendRequest($uploadUri, 'POST', $postData, $headers, false, true); - if ($result) { - // Get location to upload file - if (preg_match('@^Location: (.*)$@m', $result, $matches)) { - $uploadUri = trim($matches[1]); - } - } else { - return false; - } - // 512 kB chunks - $chunkSize = 524288; - $i = 0; - while (!feof($handle)) { - if ($i + $chunkSize > $size) { - if ($i == 0) { - $chunkSize = $size; - } else { - $chunkSize = $size % $i; - } - } - $end = $i + $chunkSize - 1; - $headers = array('Content-Length: '.$chunkSize, - 'Content-Type: '.$mimetype, - 'Content-Range: bytes '.$i.'-'.$end.'/'.$size); - $postData = fread($handle, $chunkSize); - $result = $this->sendRequest($uploadUri, 'PUT', $postData, $headers, false, true, false, true); - if ($result['code'] == '308') { - if (preg_match('@^Location: (.*)$@m', $result['result'], $matches)) { - // Get next location to upload file chunk - $uploadUri = trim($matches[1]); - } - $i += $chunkSize; - } else { - return false; - } - } - // TODO Wait for resource entry - } - } - - public function getMimeType($path, $entry = null) { - // Entry can be passed, because extension is required for opendir - // and the entry can't be cached without the extension - if ($entry == null) { - if ($path == '' || $path == '/') { + 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 { - $entry = $this->getResource($path); + return $mimetype; } + } else { + return false; } - if ($entry) { - $mimetype = $entry->getElementsByTagName('content')->item(0)->getAttribute('type'); - // Native Google Docs resources often default to text/html, - // but it may be more useful to default to a corresponding ODF mimetype - // Collections get reported as application/atom+xml, - // make sure it actually is a folder and fix the mimetype - if ($mimetype == 'text/html' || $mimetype == 'application/atom+xml;type=feed') { - $categories = $entry->getElementsByTagName('category'); - foreach ($categories as $category) { - if ($category->getAttribute('scheme') == 'http://schemas.google.com/g/2005#kind') { - $type = $category->getAttribute('label'); - if (strlen(strstr($type, 'folder')) > 0) { - return 'httpd/unix-directory'; - } else if (strlen(strstr($type, 'document')) > 0) { - return 'application/vnd.oasis.opendocument.text'; - } else if (strlen(strstr($type, 'spreadsheet')) > 0) { - return 'application/vnd.oasis.opendocument.spreadsheet'; - } else if (strlen(strstr($type, 'presentation')) > 0) { - return 'application/vnd.oasis.opendocument.presentation'; - } else if (strlen(strstr($type, 'drawing')) > 0) { - return 'application/vnd.oasis.opendocument.graphics'; - } else { - // If nothing matches return text/html, - // all native Google Docs resources can be exported as text/html - return 'text/html'; - } - } - } - } - return $mimetype; - } - return false; } public function free_space($path) { - $dom = $this->getFeed(self::BASE_URI.'/metadata/default', 'GET'); - if ($dom) { - // NOTE: Native Google Docs resources don't count towards quota - $total = $dom->getElementsByTagNameNS('http://schemas.google.com/g/2005', - 'quotaBytesTotal')->item(0)->nodeValue; - $used = $dom->getElementsByTagNameNS('http://schemas.google.com/g/2005', - 'quotaBytesUsed')->item(0)->nodeValue; - return $total - $used; - } - return false; + $about = $this->service->about->get(); + return $about->getQuotaBytesTotal() - $about->getQuotaBytesUsed(); } public function touch($path, $mtime = null) { - + $file = $this->getDriveFile($path); + if ($file) { + if (isset($mtime)) { + $file->setModifiedDate($mtime); + $this->service->files->patch($file->getId(), $file, array( + 'setModifiedDate' => true, + )); + } else { + return (bool)$this->service->files->touch($file->getId()); + } + } else { + return false; + } } public function test() { @@ -603,4 +458,4 @@ class Google extends \OC\Files\Storage\Common { return false; } -} +} \ No newline at end of file