HEX
Server: Apache/2.2.34 (Unix) mod_fastcgi/mod_fastcgi-SNAP-0910052141
System: Linux Kou-Etsu-Dou 4.4.59+ #25556 SMP PREEMPT Thu Mar 4 18:03:46 CST 2021 x86_64
User: hosam (1026)
PHP: 7.2.29
Disabled: NONE
Upload Files
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)