2014-10-09 18:31:38 +02:00
|
|
|
import io
|
|
|
|
import mimetypes
|
|
|
|
|
2017-11-22 22:38:06 +01:00
|
|
|
from googleapiclient import errors
|
|
|
|
from googleapiclient.http import MediaIoBaseUpload
|
2014-10-09 18:31:38 +02:00
|
|
|
from functools import wraps
|
|
|
|
|
|
|
|
from .apiattr import ApiAttribute
|
|
|
|
from .apiattr import ApiAttributeMixin
|
|
|
|
from .apiattr import ApiResource
|
|
|
|
from .apiattr import ApiResourceList
|
|
|
|
from .auth import LoadAuth
|
|
|
|
|
|
|
|
|
|
|
|
class FileNotUploadedError(RuntimeError):
|
|
|
|
"""Error trying to access metadata of file that is not uploaded."""
|
|
|
|
|
|
|
|
|
|
|
|
class ApiRequestError(IOError):
|
|
|
|
"""Error while making any API requests."""
|
|
|
|
|
|
|
|
|
|
|
|
class FileNotDownloadableError(RuntimeError):
|
|
|
|
"""Error trying to download file that is not downloadable."""
|
|
|
|
|
|
|
|
|
|
|
|
def LoadMetadata(decoratee):
|
|
|
|
"""Decorator to check if the file has metadata and fetches it if not.
|
|
|
|
|
|
|
|
:raises: ApiRequestError, FileNotUploadedError
|
|
|
|
"""
|
|
|
|
@wraps(decoratee)
|
|
|
|
def _decorated(self, *args, **kwargs):
|
|
|
|
if not self.uploaded:
|
|
|
|
self.FetchMetadata()
|
|
|
|
return decoratee(self, *args, **kwargs)
|
|
|
|
return _decorated
|
|
|
|
|
|
|
|
|
|
|
|
class GoogleDriveFileList(ApiResourceList):
|
|
|
|
"""Google Drive FileList instance.
|
|
|
|
|
|
|
|
Equivalent to Files.list() in Drive APIs.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, auth=None, param=None):
|
|
|
|
"""Create an instance of GoogleDriveFileList."""
|
|
|
|
super(GoogleDriveFileList, self).__init__(auth=auth, metadata=param)
|
|
|
|
|
|
|
|
@LoadAuth
|
|
|
|
def _GetList(self):
|
|
|
|
"""Overwritten method which actually makes API call to list files.
|
|
|
|
|
|
|
|
:returns: list -- list of pydrive.files.GoogleDriveFile.
|
|
|
|
"""
|
|
|
|
self.metadata = self.auth.service.files().list(**dict(self)).execute()
|
|
|
|
result = []
|
|
|
|
for file_metadata in self.metadata['items']:
|
|
|
|
tmp_file = GoogleDriveFile(
|
|
|
|
auth=self.auth,
|
|
|
|
metadata=file_metadata,
|
|
|
|
uploaded=True)
|
|
|
|
result.append(tmp_file)
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
class GoogleDriveFile(ApiAttributeMixin, ApiResource):
|
|
|
|
"""Google Drive File instance.
|
|
|
|
|
|
|
|
Inherits ApiResource which inherits dict.
|
|
|
|
Can access and modify metadata like dictionary.
|
|
|
|
"""
|
|
|
|
content = ApiAttribute('content')
|
|
|
|
uploaded = ApiAttribute('uploaded')
|
|
|
|
metadata = ApiAttribute('metadata')
|
|
|
|
|
|
|
|
def __init__(self, auth=None, metadata=None, uploaded=False):
|
|
|
|
"""Create an instance of GoogleDriveFile.
|
|
|
|
|
|
|
|
:param auth: authorized GoogleAuth instance.
|
|
|
|
:type auth: pydrive.auth.GoogleAuth
|
|
|
|
:param metadata: file resource to initialize GoogleDirveFile with.
|
|
|
|
:type metadata: dict.
|
|
|
|
:param uploaded: True if this file is confirmed to be uploaded.
|
|
|
|
:type uploaded: bool.
|
|
|
|
"""
|
|
|
|
ApiAttributeMixin.__init__(self)
|
|
|
|
ApiResource.__init__(self)
|
|
|
|
self.metadata = {}
|
|
|
|
self.dirty = {'content': False}
|
|
|
|
self.auth = auth
|
|
|
|
self.uploaded = uploaded
|
|
|
|
if uploaded:
|
|
|
|
self.UpdateMetadata(metadata)
|
|
|
|
elif metadata:
|
|
|
|
self.update(metadata)
|
|
|
|
|
|
|
|
def __getitem__(self, key):
|
|
|
|
"""Overwrites manner of accessing Files resource.
|
|
|
|
|
|
|
|
If this file instance is not uploaded and id is specified,
|
|
|
|
it will try to look for metadata with Files.get().
|
|
|
|
|
|
|
|
:param key: key of dictionary query.
|
|
|
|
:type key: str.
|
|
|
|
:returns: value of Files resource
|
|
|
|
:raises: KeyError, FileNotUploadedError
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
return dict.__getitem__(self, key)
|
|
|
|
except KeyError, e:
|
|
|
|
if self.uploaded:
|
|
|
|
raise KeyError(e)
|
|
|
|
if self.get('id'):
|
|
|
|
self.FetchMetadata()
|
|
|
|
return dict.__getitem__(self, key)
|
|
|
|
else:
|
|
|
|
raise FileNotUploadedError()
|
|
|
|
|
|
|
|
def SetContentString(self, content):
|
|
|
|
"""Set content of this file to be a string.
|
|
|
|
|
|
|
|
Creates io.BytesIO instance of utf-8 encoded string.
|
|
|
|
Sets mimeType to be 'text/plain' if not specified.
|
|
|
|
|
|
|
|
:param content: content of the file in string.
|
|
|
|
:type content: str.
|
|
|
|
"""
|
|
|
|
self.content = io.BytesIO(content.encode('utf-8'))
|
|
|
|
if self.get('mimeType') is None:
|
|
|
|
self['mimeType'] = 'text/plain'
|
|
|
|
|
|
|
|
def SetContentFile(self, filename):
|
|
|
|
"""Set content of this file from a file.
|
|
|
|
|
|
|
|
Opens the file specified by this method.
|
|
|
|
Will be read, uploaded, and closed by Upload() method.
|
|
|
|
Sets metadata 'title' and 'mimeType' automatically if not specified.
|
|
|
|
|
|
|
|
:param filename: name of the file to be uploaded.
|
|
|
|
:type filename: str.
|
|
|
|
"""
|
|
|
|
self.content = open(filename, 'rb')
|
|
|
|
|
|
|
|
if self.get('title') is None:
|
|
|
|
self['title'] = filename
|
|
|
|
if self.get('mimeType') is None:
|
|
|
|
self['mimeType'] = mimetypes.guess_type(filename)[0]
|
|
|
|
|
|
|
|
def GetContentString(self):
|
|
|
|
"""Get content of this file as a string.
|
|
|
|
|
|
|
|
:returns: str -- utf-8 decoded content of the file
|
|
|
|
:raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError
|
|
|
|
"""
|
|
|
|
if self.content is None or type(self.content) is not io.BytesIO:
|
|
|
|
self.FetchContent()
|
|
|
|
return self.content.getvalue().decode('utf-8')
|
|
|
|
|
|
|
|
def GetContentFile(self, filename, mimetype=None):
|
|
|
|
"""Save content of this file as a local file.
|
|
|
|
|
|
|
|
:param filename: name of the file to write to.
|
|
|
|
:type filename: str.
|
|
|
|
:raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError
|
|
|
|
"""
|
|
|
|
if self.content is None or type(self.content) is not io.BytesIO:
|
|
|
|
self.FetchContent(mimetype)
|
|
|
|
f = open(filename, 'wb')
|
|
|
|
f.write(self.content.getvalue())
|
|
|
|
f.close()
|
|
|
|
|
|
|
|
@LoadAuth
|
|
|
|
def FetchMetadata(self):
|
|
|
|
"""Download file's metadata from id using Files.get().
|
|
|
|
|
|
|
|
:raises: ApiRequestError, FileNotUploadedError
|
|
|
|
"""
|
|
|
|
file_id = self.metadata.get('id') or self.get('id')
|
|
|
|
if file_id:
|
|
|
|
try:
|
|
|
|
metadata = self.auth.service.files().get(fileId=file_id).execute()
|
|
|
|
except errors.HttpError, error:
|
|
|
|
raise ApiRequestError(error)
|
|
|
|
else:
|
|
|
|
self.uploaded = True
|
|
|
|
self.UpdateMetadata(metadata)
|
|
|
|
else:
|
|
|
|
raise FileNotUploadedError()
|
|
|
|
|
|
|
|
@LoadMetadata
|
|
|
|
def FetchContent(self, mimetype=None):
|
|
|
|
"""Download file's content from download_url.
|
|
|
|
|
|
|
|
:raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError
|
|
|
|
"""
|
|
|
|
download_url = self.metadata.get('downloadUrl')
|
|
|
|
if download_url:
|
|
|
|
self.content = io.BytesIO(self._DownloadFromUrl(download_url))
|
|
|
|
self.dirty['content'] = False
|
|
|
|
return
|
|
|
|
|
|
|
|
export_links = self.metadata.get('exportLinks')
|
|
|
|
if export_links and export_links.get(mimetype):
|
|
|
|
self.content = io.BytesIO(
|
|
|
|
self._DownloadFromUrl(export_links.get(mimetype)))
|
|
|
|
self.dirty['content'] = False
|
|
|
|
return
|
|
|
|
|
|
|
|
raise FileNotDownloadableError(
|
|
|
|
'No downloadLink/exportLinks for mimetype found in metadata')
|
|
|
|
|
|
|
|
def Upload(self, param=None):
|
|
|
|
"""Upload/update file by choosing the most efficient method.
|
|
|
|
|
|
|
|
:param param: additional parameter to upload file.
|
|
|
|
:type param: dict.
|
|
|
|
:raises: ApiRequestError
|
|
|
|
"""
|
|
|
|
if self.uploaded or self.get('id') is not None:
|
|
|
|
if self.dirty['content']:
|
|
|
|
self._FilesUpdate(param=param)
|
|
|
|
else:
|
|
|
|
self._FilesPatch(param=param)
|
|
|
|
else:
|
|
|
|
self._FilesInsert(param=param)
|
|
|
|
|
|
|
|
def Delete(self):
|
|
|
|
if self.get('id') is not None:
|
|
|
|
self.auth.service.files().delete(fileId=self.get('id')).execute()
|
|
|
|
|
|
|
|
@LoadAuth
|
|
|
|
def _FilesInsert(self, param=None):
|
|
|
|
"""Upload a new file using Files.insert().
|
|
|
|
|
|
|
|
:param param: additional parameter to upload file.
|
|
|
|
:type param: dict.
|
|
|
|
:raises: ApiRequestError
|
|
|
|
"""
|
|
|
|
if param is None:
|
|
|
|
param = {}
|
|
|
|
param['body'] = self.GetChanges()
|
|
|
|
try:
|
|
|
|
if self.dirty['content']:
|
|
|
|
param['media_body'] = self._BuildMediaBody()
|
|
|
|
metadata = self.auth.service.files().insert(**param).execute()
|
|
|
|
except errors.HttpError, error:
|
|
|
|
raise ApiRequestError(error)
|
|
|
|
else:
|
|
|
|
self.uploaded = True
|
|
|
|
self.dirty['content'] = False
|
|
|
|
self.UpdateMetadata(metadata)
|
|
|
|
|
|
|
|
@LoadAuth
|
|
|
|
@LoadMetadata
|
|
|
|
def _FilesUpdate(self, param=None):
|
|
|
|
"""Update metadata and/or content using Files.Update().
|
|
|
|
|
|
|
|
:param param: additional parameter to upload file.
|
|
|
|
:type param: dict.
|
|
|
|
:raises: ApiRequestError, FileNotUploadedError
|
|
|
|
"""
|
|
|
|
if param is None:
|
|
|
|
param = {}
|
|
|
|
param['body'] = self.GetChanges()
|
|
|
|
param['fileId'] = self.metadata.get('id')
|
|
|
|
try:
|
|
|
|
if self.dirty['content']:
|
|
|
|
param['media_body'] = self._BuildMediaBody()
|
|
|
|
metadata = self.auth.service.files().update(**param).execute()
|
|
|
|
except errors.HttpError, error:
|
|
|
|
raise ApiRequestError(error)
|
|
|
|
else:
|
|
|
|
self.uploaded = True
|
|
|
|
self.dirty['content'] = False
|
|
|
|
self.UpdateMetadata(metadata)
|
|
|
|
|
|
|
|
@LoadAuth
|
|
|
|
@LoadMetadata
|
|
|
|
def _FilesPatch(self, param=None):
|
|
|
|
"""Update metadata using Files.Patch().
|
|
|
|
|
|
|
|
:param param: additional parameter to upload file.
|
|
|
|
:type param: dict.
|
|
|
|
:raises: ApiRequestError, FileNotUploadedError
|
|
|
|
"""
|
|
|
|
if param is None:
|
|
|
|
param = {}
|
|
|
|
param['body'] = self.GetChanges()
|
|
|
|
param['fileId'] = self.metadata.get('id')
|
|
|
|
try:
|
|
|
|
metadata = self.auth.service.files().patch(**param).execute()
|
|
|
|
except errors.HttpError, error:
|
|
|
|
raise ApiRequestError(error)
|
|
|
|
else:
|
|
|
|
self.UpdateMetadata(metadata)
|
|
|
|
|
|
|
|
def _BuildMediaBody(self):
|
|
|
|
"""Build MediaIoBaseUpload to get prepared to upload content of the file.
|
|
|
|
|
|
|
|
Sets mimeType as 'application/octet-stream' if not specified.
|
|
|
|
|
|
|
|
:returns: MediaIoBaseUpload -- instance that will be used to upload content.
|
|
|
|
"""
|
|
|
|
if self.get('mimeType') is None:
|
|
|
|
self['mimeType'] = 'application/octet-stream'
|
|
|
|
|
|
|
|
return MediaIoBaseUpload(self.content, self['mimeType'])
|
|
|
|
|
|
|
|
@LoadAuth
|
|
|
|
def _DownloadFromUrl(self, url):
|
|
|
|
"""Download file from url using provided credential.
|
|
|
|
|
|
|
|
:param url: link of the file to download.
|
|
|
|
:type url: str.
|
|
|
|
:returns: str -- content of downloaded file in string.
|
|
|
|
:raises: ApiRequestError
|
|
|
|
"""
|
|
|
|
resp, content = self.auth.service._http.request(url)
|
|
|
|
if resp.status != 200:
|
|
|
|
raise ApiRequestError('Cannot download file: %s' % resp)
|
|
|
|
return content
|