File: /volume1/@appstore/HyperBackup/addon/google_drive/python/drive_agent.py
#!/usr/bin/env python
#-*-coding: utf-8 -*-
import os, sys, json
from struct import pack, unpack
from time import strptime, mktime
from syslog import syslog, LOG_ERR
debug = False
def log_debug(*args):
if debug:
print >> sys.stderr, ' '.join(args)
def _is_scoket_exception(e):
import socket
if type(e) == socket.gaierror or type(e) == socket.error or type(e) == socket.herror or type(e) == socket.timeout:
return True
return False
def _convert_socket_error_code(message):
error_code = -1
if -1 != message.find("[Errno -2] Name or service not known"):
error_code = -2
elif -1 != message.find("[Errno -3] Temporary failure in name resolution"):
error_code = -2
elif -1 != message.find("[Errno -5] No address associated with hostname"):
error_code = -2
elif -1 != message.find("[Errno 110] Connection timed out"):
error_code = 408
elif -1 != message.find("[Errno 101] Network is unreachable"):
error_code = -4
elif -1 != message.find("[Errno 111] Connection refused"):
error_code = -4
elif -1 != message.find("[Errno 113] No route to host"):
error_code = -4
elif -1 != message.find("[Errno 104] Connection reset by peer"):
error_code = -4
elif -1 != message.find("[Errno 32] Broken pipe"):
error_code = 408
return error_code
def _is_ssl_timeout(message):
#ssl.SSLErr_timeouteption log
if -1 != message.find("timed out"):
# The read operation timed out
# The write operation timed out
# The handshake operation timed out
return True
return False
def _convert_socket_exception(e):
import socket
error_code = -1
error_msg = 'Unknown'
error_reason = 'Unknown'
error_cls = 'Unknown'
try:
error_cls = type(e).__name__
if hasattr(e, '__module__') and e.__module__:
error_cls = e.__module__ + '.' + error_cls
if type(e) == socket.gaierror:
if hasattr(e, 'strerror') and e.strerror:
error_msg = e.strerror
if e.errno == -2:
# socket.gaierror: [Errno -2] Name or service not known
# 1. dns not set
# 2. set to a wrong dns
error_code = -2
elif e.errno == -3:
# socket.gaierror: [Errno -3] Temporary failure in name resolution
# man gai_strerror, and find EAI_AGAIN:
# The name server returned a temporary failure indication. Try again later.
error_code = -2
elif e.errno == -5:
# socket.gaierror: [Errno -5] No address associated with hostname
# man gai_strerror, and find EAI_NODATA:
# The specified network host exists, but does not have any network addresses defined.
error_code = -2
elif e.errno < 100 and e.errno > 0:
error_code = e.errno
elif type(e) == socket.error or type(e) == socket.herror:
if hasattr(e, 'strerror') and e.strerror:
error_msg = e.strerror
elif hasattr(e, 'args'):
error_msg = str(e.args)
if -1 != error_msg.find('Tunnel connection failed'):
error_code = -4
if e.errno == socket.errno.ETIMEDOUT:
# socket.error: [Errno 110] Connection timed out
error_code = 408
elif e.errno == socket.errno.ENETUNREACH:
# socket.error: [Errno 101] Network is unreachable
error_code = -4
elif e.errno == socket.errno.ECONNREFUSED:
# socket.error: [Errno 111] Connection refused
error_code = -4
elif e.errno == socket.errno.EHOSTUNREACH:
# socket.error: [Errno 113] No route to host
error_code = -4
elif e.errno == socket.errno.ECONNRESET:
# socket.error: [Errno 104] Connection reset by peer
error_code = -4
elif e.errno == socket.errno.EPIPE:
# socket.error: [Errno 32] Broken pipe
error_code = 408
elif e.errno < 100 and e.errno > 0:
error_code = e.errno
#syslog(LOG_ERR, "get network error %d:%s" % (e.errno, error_msg))
elif type(e) == socket.timeout:
error_msg = 'timed out'
error_code = 408
else:
syslog(LOG_ERR, "BUG: exception [%s], type [%s]" % (str(e), type(e)))
#syslog(LOG_ERR, "error %s:%d:%s" % (error_cls, error_code, str(error_msg)))
if -1 == error_code:
if hasattr(e, "args"):
syslog(LOG_ERR, "error args: [%s]" % (str(e.args)))
if hasattr(e, "errno"):
syslog(LOG_ERR, "error errno: [%s]" % (str(e.errno)))
if hasattr(e, "strerror"):
syslog(LOG_ERR, "error strerror: [%s]" % (str(e.strerror)))
except Exception as ee:
syslog(LOG_ERR, "parse socket exception failed: [%s]" % (str(ee)))
pass
return {
'success': False,
'error_class': error_cls,
'error_message': error_msg,
'error_reason': error_reason,
'error_code': error_code
}
def _convert_exception(e):
#syslog(LOG_ERR, "convert_exception: %s" % str(e))
import googleapiclient
import oauth2client
import httplib
import httplib2
import ssl
error_code = -1
error_msg = 'Unknown'
error_reason = 'Unknown'
error_cls = 'Unknown'
try:
error_cls = type(e).__name__
if hasattr(e, '__module__') and e.__module__:
error_cls = e.__module__ + '.' + error_cls
try:
if type(e.message) is unicode:
error_msg = repr(e.message).split('\\n')[0]
else:
error_msg = str(e.message).split('\n')[0]
log_debug("\033[31mexception: \033[0m", error_msg)
except:
log_debug("\033[31mexception: \033[0m", "could not parse error msg")
log_debug("type(e): " + str(type(e)))
if type(e) == googleapiclient.errors.HttpError:
error_code = -4
if hasattr(e, 'content') and e.content:
err_content = json.loads(e.content.decode('utf-8')).get('error')
error_code = err_content.get('code') # = e.resp.status
if err_content.get('errors') and type(err_content.get('errors')) is list:
error_reason = err_content.get('errors')[0].get('reason')
error_msg = err_content.get('message')
elif type(e) == googleapiclient.errors.ResumableUploadError:
# request upload resumable URI failed
error_code = 500
elif type(e) == oauth2client.client.HttpAccessTokenRefreshError:
error_code = 401
elif type(e) == TypeError:
# this exception will raise when base64 decode for auth failed
if error_msg == "Incorrect padding":
error_code = 401
elif type(e) == UnicodeDecodeError:
error_code = -5
elif type(e) == httplib.ResponseNotReady:
error_code = -4
elif type(e) == httplib.BadStatusLine:
error_code = -4
elif type(e) == httplib.IncompleteRead:
error_code = -4
elif type(e) == httplib2.ServerNotFoundError:
error_code = -2
elif type(e) == httplib2.HttpLib2Error:
# transport error
error_code = -4
elif type(e) == ssl.SSLError:
error_code = -4
if hasattr(e, "strerror") and e.strerror:
# SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC
# SSL: SSLV3_ALERT_BAD_RECORD_MAC
# SSL: CERTIFICATE_VERIFY_FAILED
error_msg = e.strerror
if _is_ssl_timeout(str(error_msg)):
# The read operation timed out
# The write operation timed out
# The handshake operation timed out
error_code = 408
elif type(e) == ssl.SSLEOFError:
# ssl.SSLEOFError: [Errno 8] EOF occurred in violation of protocol
if hasattr(e, 'strerror') and e.strerror:
error_msg = e.strerror
error_code = -4
elif type(e) == IOError:
if hasattr(e, 'strerror') and e.strerror:
error_msg = e.strerror
error_code = _convert_socket_error_code(str(e))
elif _is_scoket_exception(e):
return _convert_socket_exception(e)
else:
syslog(LOG_ERR, "exception [%s]" % str(e))
syslog(LOG_ERR, "type [%s]" % type(e))
#syslog(LOG_ERR, "error %s:%d:%s:%s" % (error_cls, error_code, str(error_reason), str(error_msg)))
if -1 == error_code:
if hasattr(e, "args"):
syslog(LOG_ERR, "error args: [%s]" % (str(e.args)))
if hasattr(e, "errno"):
syslog(LOG_ERR, "error errno: [%s]" % (str(e.errno)))
if hasattr(e, "strerror"):
syslog(LOG_ERR, "error strerror: [%s]" % (str(e.strerror)))
except Exception as ee:
syslog(LOG_ERR, "parse exception failed: [%s]" % (str(ee)))
pass
return {
'success': False,
'error_class': error_cls,
'error_message': error_msg,
'error_reason': error_reason,
'error_code': error_code
}
def _convert_time_str(val):
# convert time string to unix time (epoch time)
# 2016-03-04T12:34:56.789Z / 1457066096 . 789 -> 1457066097
origin_tz = None
if os.environ.get('TZ'):
origin_tz = os.environ.get('TZ')
os.environ['TZ'] = 'UTC'
time_val, sec_xtime = str(val).split('.')
timestamp = int(mktime(strptime(time_val, '%Y-%m-%dT%H:%M:%S')))
sec_xtime = sec_xtime.rstrip('Z')
if int(sec_xtime) > 0:
timestamp += 1
# for time before 1970-01-01T08:00:00.000Z
if 0 > timestamp:
timestamp = 0
if origin_tz:
os.environ['TZ'] = origin_tz
return timestamp
def _convert_properties(properties):
out_json = {
'isDir': (properties.get('mimeType') == 'application/vnd.google-apps.folder'),
'size': properties.get('size'),
'lastModified': _convert_time_str(properties.get('modifiedTime')),
'md5Checksum': properties.get('md5Checksum'),
'isTrash': properties.get('trashed')
}
return out_json
class SimpleIO(object):
def read_int(self):
data = ''
n = 4
while n > 0:
read_data = sys.stdin.read(n)
if 0 == len(read_data):
raise StopIteration
data += read_data
n -= len(read_data)
if data:
return unpack('i', data)[0]
else:
# check eof?
raise StopIteration
def read_string(self):
n = self.read_int()
if 0 == n:
return ''
data = ''
while n > 0:
read_data = sys.stdin.read(n)
if 0 == len(read_data):
raise StopIteration
data += read_data
n -= len(read_data)
if data:
return data
else:
raise SystemError
def read_json(self):
json_str = self.read_string()
if json_str:
return json.loads(json_str)
else:
return None
def write_int(self, val):
data = pack('i', val)
sys.stdout.write(data)
sys.stdout.flush()
def write_string(self, val):
self.write_int(len(val))
sys.stdout.write(val)
sys.stdout.flush()
# log
def write_json(self, val):
s = json.dumps(val)
self.write_string(s)
def write_exception(self, e):
self.write_json(_convert_exception(e))
class FileProgress(object):
def __init__(self, io):
self._io = io
def set(self, progress): # progress in [0, 1]
res = {
'success': True,
'complete': False,
'progress': progress
}
if self._io:
self._io.write_json(res)
else:
print res
class GoogleDriveApi(object):
def __init__(self, io=None):
import httplib2
self._file_progress = FileProgress(io)
self._credentials = self.getCredentials()
self._http = self._credentials.authorize(httplib2.Http(timeout=600))
self._drive = None
log_debug('agent created')
def getCredentials(self):
import oauth2client
from oauth2client import client
access_token = os.environ.get('GOOGLEDRIVE_ACCESS_TOKEN')
refresh_token = os.environ.get('GOOGLEDRIVE_REFRESH_TOKEN')
client_id = os.environ.get('GOOGLEDRIVE_CLIENT_ID')
client_secret = os.environ.get('GOOGLEDRIVE_CLIENT_SECRET')
user_agent = os.environ.get('SYNO_USER_AGENT')
token_expiry = ''
token_uri = 'https://www.googleapis.com/oauth2/v4/token'
del os.environ['GOOGLEDRIVE_ACCESS_TOKEN']
del os.environ['GOOGLEDRIVE_REFRESH_TOKEN']
del os.environ['SYNO_USER_AGENT']
del os.environ['GOOGLEDRIVE_CLIENT_ID']
del os.environ['GOOGLEDRIVE_CLIENT_SECRET']
#syslog(LOG_ERR, "access_token: %s" % str(access_token))
#syslog(LOG_ERR, "refresh_token: %s" % str(refresh_token))
#syslog(LOG_ERR, "user_agent: %s" % str(user_agent))
credentials = client.OAuth2Credentials(
access_token, client_id, client_secret,
refresh_token, token_expiry, token_uri,
user_agent)
# TODO: cache access tokeni to reuse it
#syslog(LOG_ERR, "access token info: %s" % str(credentials.get_access_token()))
return credentials
def getDrive(self):
from apiclient import discovery
if self._drive is None:
self._drive = discovery.build('drive', 'v3', http=self._http)
return self._drive
def getAccountInfo(self, in_json):
log_debug('@getAccountInfo()')
res = self.getDrive().about().get(
fields='user, storageQuota').execute()
return {
'success': True,
'account': res.get('user').get('emailAddress'),
'userName': res.get('user').get('displayName'),
'quota': res.get('storageQuota').get('limit'),
'usedSize': res.get('storageQuota').get('usage')
}
def getObjectMeta(self, in_json):
log_debug('@getObjectMeta()')
res = self.getDrive().files().get(
fileId=in_json['id'],
fields='id, parents, name, modifiedTime, size, mimeType, md5Checksum, trashed').execute()
return {
'success': True,
'id': res.get('id'),
'name': res.get('name'),
'parents': res.get('parents'),
'properties': _convert_properties(res)
}
def listObjects(self, in_json):
def _doListObjects(in_json, page_token, id_list, objects):
page_size = 1000
query_filter = in_json.get('queryFilter', None) + ' and (trashed = false)'
res = self.getDrive().files().list(
orderBy='folder, name',
pageSize=page_size,
pageToken=page_token,
q=query_filter,
fields='nextPageToken, files(id, parents, name, modifiedTime, size, mimeType, md5Checksum, trashed)').execute()
res_objects = res.get('files')
for o in res_objects:
if o.get('id') in id_list:
syslog(LOG_ERR, "File: {} is repeated".format(o.get('name')))
continue
id_list.append(o.get('id'))
objects.append({
'id': o.get('id'),
'name': o.get('name'),
'parents': o.get('parents'),
'properties': _convert_properties(o)
})
return res.get('nextPageToken')
log_debug('@listObjects()')
page_token = in_json.get('pageToken', None)
objects = []
id_list = []
while True:
nextToken = _doListObjects(in_json, page_token, id_list, objects)
if nextToken:
page_token = nextToken
else:
break
return {
'success': True,
'count': len(objects),
'nextPageToken': nextToken,
'objects': objects
}
def deleteObject(self, in_json):
log_debug('@deleteObjects()')
res = self.getDrive().files().delete(
fileId=in_json['id']).execute()
if res == '':
# 204 No Content
return {'success': True}
else:
return {'success': False}
def createFolder(self, in_json):
log_debug('@createFolder()')
file_metadata = {
'name' : in_json['name'],
'mimeType' : 'application/vnd.google-apps.folder',
'parents' : [ in_json['parentId'] ]
}
res = self.getDrive().files().create(
body=file_metadata,
fields='id, parents, name, modifiedTime, size, mimeType, md5Checksum, trashed').execute()
return {
'success': True,
'id': res.get('id'),
'name': res.get('name'),
'parents': res.get('parents'),
'properties': _convert_properties(res)
}
def downloadFile(self, in_json):
from apiclient.http import MediaIoBaseDownload
log_debug('@downloadFile()')
acknowledgeAbuse = in_json.get('acknowledgeAbuse', False)
fd = open(in_json['outputPath'], 'wb')
request = self.getDrive().files().get_media(
acknowledgeAbuse=acknowledgeAbuse,
fileId=in_json['id'])
downloader = MediaIoBaseDownload(
fd,
request,
chunksize = 100*1024*1024)
done = False
while done is False:
# status: googleapiclient.http.MediaDownloadProgress object
status, done = downloader.next_chunk(num_retries=10)
if status:
log_debug('Download [' + str(status.progress() * 100) + '%], done=[' + str(done) + ']')
self._file_progress.set(status.progress())
return {
'success': True
}
def generateIds(self, in_json):
log_debug('@generateIds()')
res = self.getDrive().files().generateIds(
space='drive',
count=in_json['count'],
fields='ids').execute()
return {
'success': True,
'ids': res.get('ids')
}
# Maximum file size: 5120GB
def uploadFile(self, in_json):
from apiclient.http import MediaFileUpload
log_debug('@uploadFile()')
file_metadata = {
'id' : in_json['id'],
'name' : in_json['name'],
'parents' : [ in_json['parentId'] ]
}
# chunksize = DEFAULT_CHUNK_SIZE = 512*1024 (512KB) -> 5MB
# Google App Engine has a 5MB limit on request size,
# so you should never set your chunksize larger than 5MB
media = MediaFileUpload(
in_json['inputPath'],
chunksize = 5*1024*1024,
resumable=True)
request = self.getDrive().files().create(
body=file_metadata,
media_body=media,
fields='id, parents, name, modifiedTime, size, mimeType, md5Checksum, trashed')
res = None
while res is None:
# retry >=500 with randomized exponential backoff.
# If all retries fail, the raised HttpError represents the last request
status, res = request.next_chunk(num_retries=10) # (default) num_retries = 0
if status:
log_debug('Upload [' + str(status.progress() * 100) + '%]')
self._file_progress.set(status.progress())
if res:
log_debug('Upload res=[' + str(res) + ']')
self._file_progress.set(1)
return {
'success': True,
'id': res.get('id'),
'name': res.get('name'),
'parents': res.get('parents'),
'properties': _convert_properties(res)
}
def uploadEmptyFile(self, in_json):
from apiclient.http import MediaFileUpload
log_debug('@uploadEmptyFile()')
file_metadata = {
'id' : in_json['id'],
'name' : in_json['name'],
'parents' : [ in_json['parentId'] ]
}
media = MediaFileUpload(in_json['inputPath'])
res = self.getDrive().files().create(
body=file_metadata,
media_body=media,
fields='id, parents, name, modifiedTime, size, mimeType, md5Checksum, trashed').execute()
return {
'success': True,
'id': res.get('id'),
'name': res.get('name'),
'parents': res.get('parents'),
'properties': _convert_properties(res)
}
def updateFile(self, in_json):
from apiclient.http import MediaFileUpload
log_debug('@updateFile()')
# if not provide mimeType -> auto detect from uploaded content
# if originaal mimeType is folder -> media_body useless, mimeType no change
media = MediaFileUpload(
in_json['inputPath'],
chunksize = 5*1024*1024,
resumable=True)
request = self.getDrive().files().update(
fileId=in_json['id'],
media_body=media,
fields='id, parents, name, modifiedTime, size, mimeType, md5Checksum, trashed')
res = None
while res is None:
status, res = request.next_chunk(num_retries=10)
if status:
log_debug('Update [' + str(status.progress() * 100) + '%]')
self._file_progress.set(status.progress())
if res:
log_debug('Update res=[' + str(res) + ']')
self._file_progress.set(1)
return {
'success': True,
'id': res.get('id'),
'name': res.get('name'),
'parents': res.get('parents'),
'properties': _convert_properties(res)
}
def updateEmptyFile(self, in_json):
from apiclient.http import MediaFileUpload
log_debug('@updateEmptyFile()')
media = MediaFileUpload(in_json['inputPath'])
res = self.getDrive().files().update(
fileId=in_json['id'],
media_body=media,
fields='id, parents, name, modifiedTime, size, mimeType, md5Checksum, trashed').execute()
return {
'success': True,
'id': res.get('id'),
'name': res.get('name'),
'parents': res.get('parents'),
'properties': _convert_properties(res)
}
def start_server():
io = SimpleIO()
try:
api = GoogleDriveApi(io)
except Exception as e:
io.write_exception(e)
return False
io.write_string('start')
while True:
try:
in_json = io.read_json()
fn_name = in_json['fn']
fn = getattr(api, fn_name)
del in_json['fn']
if fn is None:
raise SystemError('no such fn: ' + fn_name)
#log_debug('\033[34mexecute: ' + fn_name + ' ' + json.dumps(in_json) + '\033[0m');
res = fn(in_json)
io.write_json(res)
except StopIteration:
break
except Exception as e:
io.write_exception(e)
continue
if __name__ == '__main__':
# add include path
script_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
sys.path.insert(0, script_dir + '/apiclient')
sys.path.insert(1, script_dir + '/googleapiclient')
sys.path.insert(2, script_dir + '/module')
res = start_server()
sys.exit(0 if res else 1)