403Webshell
Server IP : 193.86.120.172  /  Your IP : 216.73.216.67
Web Server : Apache/2.4.63 (Unix)
System : Linux JServices 3.10.108 #86003 SMP Wed Oct 22 13:20:46 CST 2025 x86_64
User : kubec ( 1026)
PHP Version : 8.2.28
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /volume1/@appstore/SynologyDrive/scripts/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /volume1/@appstore/SynologyDrive/scripts/drive-debug.py
#!/bin/python
########################################################
# Copyright (c) 2020 Synology Inc. All rights reserved.
########################################################
from __future__ import print_function

import json
import sys
import os
import argparse
import subprocess
import stat
import sqlite3
import datetime
import re
from abc import ABCMeta, abstractmethod

# force flusing
def print(*args, **kw):
    __builtins__.print(*args, **kw)
    sys.stdout.flush()

class GlobalInfo:
    def __init__(self):
        self.vol_path = None
        self.repo_path = self.getRepoPath()
        self.version = self.getPackageVersion()
        self.general_db_names = "syncfolder-db,view-route-db,user-db,notification-db,job-db,log-db".split(
            ",")
        self.databases = [self.repo_path + dbname +
            ".sqlite" for dbname in self.general_db_names]
        self.fnull = open(os.devnull, 'w')
        self.path_description = """
path format:
0. link:permanent_link
1. id:file_id
2. id:file_id/basename
3. /mydrive/{relative-path}
4. /team-folders/{team-folder-name}/{relative-path}
5. /views/{view_id}/{relative-path}
6. /volumes/{absolute-path}
7. /views/{view_id}:{node_id}
"""

    def __del__(self):
        self.fnull.close()

    def getRepoPath(self):
        repo_after_2_0 = "/var/packages/SynologyDrive/etc/repo/"
        if os.path.isdir(repo_after_2_0):
            return "/var/packages/SynologyDrive/etc/repo/"
        else:
            return self.getVolPath() + "/@synologydrive/@sync/"

    def getVolPath(self):
        if self.vol_path == None:
            cat_process = subprocess.Popen(["cat", "/var/packages/SynologyDrive/etc/db-path.conf"], stdout=subprocess.PIPE)
            grep_process = subprocess.Popen(["grep", "db-vol"], stdin=cat_process.stdout, stdout=subprocess.PIPE)
            awk_process = subprocess.Popen(["awk", "-F", "=", "{print $2}"], stdin=grep_process.stdout, stdout=subprocess.PIPE)
            self.vol_path = (awk_process.communicate()[0]).decode("utf8").rstrip()
            cat_process.stdout.close()
            grep_process.stdout.close()
            if self.vol_path[0] == '"':
                self.vol_path = self.vol_path[1:-1]
        return self.vol_path

    def getVolTmp(self):
        return self.getVolPath() + '/@tmp/'

    def getPackageVersion(self):
        cat_process = subprocess.Popen(["cat", "/var/packages/SynologyDrive/INFO"], stdout=subprocess.PIPE)
        grep_process = subprocess.Popen(["grep", "^version"], stdin=cat_process.stdout, stdout=subprocess.PIPE)
        awk_process = subprocess.Popen(["awk", "-F", "-", "{print $2}"], stdin=grep_process.stdout, stdout=subprocess.PIPE)
        sed_process = subprocess.Popen(["sed", "-e", "s/\"//"], stdin=awk_process.stdout, stdout=subprocess.PIPE)
        version = int((sed_process.communicate()[0].decode("utf8")).rstrip())
        cat_process.stdout.close()
        grep_process.stdout.close()
        awk_process.stdout.close()
        return version

GLOBALS = GlobalInfo()

def main():
    parsers = [Info(), PathConvert(), ACLGet(), DaemonStatusChecker(), LogParser(), Database()]
    parser_map = {}
    main_parser = argparse.ArgumentParser(description="Synology Drive Debug Tool")
    subparsers = main_parser.add_subparsers(dest="parser_name")

    for parser in parsers:
        parser.addSubparser(subparsers)
        parser_map[parser.getSubparserName()] = parser

    args = main_parser.parse_args(sys.argv[1:])

    if args.parser_name == None:
        main_parser.print_help()
    else:
        parser_map[args.parser_name].execute(args)

class ToolInterface:
    @abstractmethod
    def getSubparserName(self):
        pass

    @abstractmethod
    def addSubparser(self, subparsers):
        pass

    @abstractmethod
    def execute(self, args):
        pass

class Info(ToolInterface):
    def getSubparserName(self):
        return "info"

    def addSubparser(self, subparsers):
        parser = subparsers.add_parser(self.getSubparserName(), help="print info about drive")
        group = parser.add_mutually_exclusive_group(required=True)
        group.add_argument('--general', action='store_true', help="print general information")
        group.add_argument('--view', metavar=('view_id'), type=int, help="print view information")
        group.add_argument('--user', metavar=('username'), type=str, help="print user information")
        group.add_argument('--uid', metavar=('uid'), type=int, help="print user information")

    def execute(self, args):
        if args.uid != None:
            self.doUid(args.uid)
        elif args.user != None:
            self.doUser(args.user)
        elif args.view != None:
            self.doView(args.view)
        elif args.general != None:
            self.doGeneral()

    def doUid(self, uid):
        print("=== System ===")
        subprocess.call(["synouser", "--getuid", str(uid)])

        print("=== DB ===")
        sqlite(GLOBALS.repo_path + 'user-db.sqlite', 'SELECT * FROM user_table WHERE uid = ' + str(uid), output=True, line=True)

    def doUser(self, user):
        print("=== System ===")
        subprocess.call(["synouser", "--get", user])

        print("=== DB ===")
        sqlite(GLOBALS.repo_path + 'user-db.sqlite', 'SELECT * FROM user_table WHERE name = "{}"'.format(user), output=True, line=True)

    def doView(self, view_id):
        if view_id == 0:
            user_database = GLOBALS.repo_path + 'user-db.sqlite'
            views = sqlite(user_database, "SELECT view_id FROM user_table").split('\n')
            for view in views:
                self.doView(int(view))
        else:
            user_database = GLOBALS.repo_path + 'user-db.sqlite'
            print("=== view info for {} (view_id: {}) ===".format(sqlite(user_database, "SELECT name FROM user_table WHERE view_id = {}".format(view_id)), view_id))
            view_database = GLOBALS.repo_path + "view/{}/view-db.sqlite".format(view_id)
            subprocess.call(["ls", "-lh", view_database])
            print("Node Count(approx): ", sqlite(view_database, "SELECT MAX(node_id) - MIN(node_id) FROM node_table"))
            print("Version Count(approx): ", sqlite(view_database, "SELECT MAX(ver_id) - MIN(ver_id) FROM version_table"))
            print("Max Version Count: ", sqlite(view_database, "SELECT MAX(ver_cnt) FROM node_table"), " (the max version count of a file in this view)")
            print("Rotate Policy: ")
            sqlite(view_database, "SELECT key,value FROM config_table WHERE key LIKE 'rotate%'", output=True)
            print("")


    def doGeneral(self):
        print("=== System ===")
        subprocess.call(["uname", "-a"])

        print("\n=== DSM Version ===")
        subprocess.call(["cat", "/etc.defaults/VERSION"])

        print("\n=== Package Version ===")
        cat_process = subprocess.Popen(["cat", "/var/packages/SynologyDrive/INFO"], stdout=subprocess.PIPE)
        grep_process = subprocess.Popen(["grep", "version"], stdin=cat_process.stdout, stdout=subprocess.PIPE)
        subprocess.call(["grep", "-v", "description"], stdin=grep_process.stdout)
        cat_process.stdout.close()
        grep_process.stdout.close()

        print("\n=== Package Info ===")
        self.printPackageInfo()

        print("\n=== Inproper shutdown ===")
        sqlite('/var/log/synolog/.SYNOSYSDB', "SELECT datetime(time, 'unixepoch', 'localtime'),msg FROM logs WHERE level='warning' AND msg LIKE '%improper%'", output = True)

        print("\n=== Disk Info ===")
        subprocess.call(["df", "-h"])

        print("\n=== Upgrade History ===")
        cat_process = subprocess.Popen(["cat", "/var/log/synopkg.log"], stdout=subprocess.PIPE)
        grep_process = subprocess.Popen(["grep", "upgrade"], stdin=cat_process.stdout, stdout=subprocess.PIPE)
        grep_process_2 = subprocess.Popen(["grep", "SynologyDrive"], stdin=grep_process.stdout, stdout=subprocess.PIPE)
        subprocess.call(["grep", "from"], stdin=grep_process_2.stdout)
        cat_process.stdout.close()
        grep_process.stdout.close()
        grep_process_2.stdout.close()

        print("\n=== Restored ===")
        subprocess.call(["ls", "-alh", GLOBALS.repo_path + "/../log/restore.log"])

        print("\n=== DB Versions ===")
        self.printDBVersions()

        print("\n=== DB Sizes ===")
        ls_process = subprocess.Popen(["ls", "-alh", GLOBALS.repo_path], stdout=subprocess.PIPE)
        subprocess.call(["grep", ".sqlite"], stdin=ls_process.stdout)
        ls_process.stdout.close()

        print("\n=== User Stat ===")
        self.printUserStat()

        print("\n=== Job Stat ===")
        self.printJobStat()

        print("\n=== Daemon Stat ===")
        status_checker = DaemonStatusChecker()
        status_checker.execute()

    def printDBVersions(self):
        for database in GLOBALS.databases:
            print(sqlite(database, "SELECT value FROM config_table WHERE key = 'version'") + "\t" + database)

    def printPackageInfo(self):
        database = GLOBALS.repo_path + 'syncfolder-db.sqlite'
        print("Serial:", sqlite(database, "SELECT value FROM config_table WHERE key='serial'"))
        print("Restore ID:", sqlite(database, "SELECT value FROM config_table WHERE key='restore_id'"))

    def printUserStat(self):
        database = GLOBALS.repo_path + 'user-db.sqlite'
        print("Users", sqlite(database, "SELECT COUNT(*) FROM user_table WHERE name NOT LIKE '@%'"))
        print("Shares", sqlite(database, "SELECT COUNT(*) FROM user_table WHERE name LIKE '@%'"))
        print("Sessions", sqlite(database, "SELECT COUNT(*) FROM session_table"))
        print("Domain: ", webapi('SYNO.Core.Directory.Domain', 'get', 1)["data"]["enable_domain"])
        print("LDAP: ", webapi('SYNO.Core.Directory.LDAP', 'get', 1)["data"]["enable_client"])

    def printJobStat(self):
        database = GLOBALS.repo_path + 'job-db.sqlite'
        if not os.path.isfile(database):
            return

        print("Jobs", sqlite(database, "SELECT COUNT(*) FROM job_table"))
        print("Waiting Jobs", sqlite(database, "SELECT COUNT(*) FROM job_table WHERE state = 1"))
        print("Ready Jobs", sqlite(database, "SELECT COUNT(*) FROM job_table WHERE state = 2"))
        print("Running Jobs", sqlite(database, "SELECT COUNT(*) FROM job_table WHERE state = 3"))
        print("Cancelled Jobs", sqlite(database, "SELECT COUNT(*) FROM job_table WHERE state = 4"))
        print("==== Group By Topic ====")
        sqlite(database, "SELECT COUNT(*), topic FROM job_table GROUP BY topic", output=True)

class PathConvert(ToolInterface):
    def getSubparserName(self):
        return "path-convert"

    def addSubparser(self, subparsers):
        parser = subparsers.add_parser(self.getSubparserName(), help="show file path", description = GLOBALS.path_description, formatter_class=argparse.RawDescriptionHelpFormatter)
        parser.add_argument('--user', metavar=('username'), nargs='?', default="root", type=str, help="user information")
        parser.add_argument('--full', action='store_true', help="print full information")
        parser.add_argument('path', type=str, help="path")

    def execute(self, args):
        result = webapi('SYNO.SynologyDrive.Files', 'get', 1, args.user, ['path="{}"'.format(args.path)])
        if result['success'] == False:
            print(result)
        else:
            if not args.full:
                data = getFileBasicInfo(result['data'])
            else:
                data = result['data']
            print(json.dumps(data, indent=2, ensure_ascii=False))
            print('Full Path: ' + getFileFullPath(data['file_id']))

class ACLGet(ToolInterface):
    def getSubparserName(self):
        return 'acl-get'

    def addSubparser(self, subparsers):
        parser = subparsers.add_parser(self.getSubparserName(), help="show the whole ACL chain for permission debugging", description = GLOBALS.path_description, formatter_class=argparse.RawDescriptionHelpFormatter)
        parser.add_argument('--recursive', action='store_true', help="whether or not recursive query to root")
        parser.add_argument('user', type=str, help="username")
        parser.add_argument('path', type=str, help="path")

    def execute(self, args):
        print("User: {}, Path: {}".format(args.user, args.path))
        self.traverse(1, args.user, args.path, args.recursive)

    def traverse(self, depth, user, path, recursive):
        result = webapi('SYNO.SynologyDrive.Files', 'get', 1, user, ['path="{}"'.format(path)])
        if result['success'] == False:
            print(result)
            return

        file_info = result['data']
        full_path = getFileFullPath(file_info['file_id'])
        print(u'=== Depth: {}, Path:  ==='.format(full_path))

        print('==== Drive Permissions ====')
        print(json.dumps(getFileBasicInfo(file_info, 'display_path,name,capabilities,owner,shared_with'), indent=2, ensure_ascii=False))

        print('==== DSM ACL Permissions ====')
        self.printACLResult(user, full_path)

        # if we still has parent entry points
        if recursive and len(file_info['display_path'].split('/')) > 3:
            self.traverse(depth + 1, user, 'id:' + str(file_info['parent_id']), recursive)

    def printACLResult(self, user, full_path):
        subprocess.call(['synoacltool', '-get-perm', full_path, user], stderr=subprocess.STDOUT)

class DaemonStatusChecker(ToolInterface):
    def getSubparserName(self):
        return 'daemon-stat'

    def addSubparser(self, subparsers):
        parser = subparsers.add_parser(self.getSubparserName(), help="check if our daemon is still alive")

    def execute(self, args = None):
        daemons = self.getDaemonProfiles()
        for daemon in daemons:
            self.verifyDaemon(daemon)

    def getRunningDaemons(self, args = None):
        running_daemons = 0
        daemons = self.getDaemonProfiles()
        for daemon in daemons:
            running_daemons += 1 if self.verifyDaemon(daemon, warning=False) else 0
        return running_daemons

    def getDaemonProfiles(self):
        daemons = []
        daemons.append('syncd,/var/run/synosyncfolder.pid,/tmp/cloud-syncservice'.split(','))

        # 3.0
        if GLOBALS.version < 12000:
            daemons.append('cloud-cached,/var/run/cloud-cached.pid,/tmp/cloud-cached-socket'.split(','))

        daemons.append('cloud-authd,/var/run/cloud-authd.pid,/tmp/cloud-authd-socket'.split(','))
        daemons.append('cloud-vmtouchd,/var/run/cloud-vmtouchd.pid,/tmp/cloud-vmtouchd.sock'.split(','))

        # 2.0
        if GLOBALS.version > 10700:
            daemons.append(['cloud-workerd', '/run/SynologyDrive/cloud-workerd.pid', None])
            daemons.append('redis,/run/SynologyDrive/redis.pid,/run/SynologyDrive/redis.sock'.split(','))

        daemons.append('syno-cloud-clientd,/var/run/cloud-clientd.pid,/tmp/cloud-clientd-control'.split(','))

        return daemons

    def verifyDaemon(self, daemon, warning = True):
        name, pid_file, sock = daemon

        try:
            if not os.path.isfile(pid_file):
                raise RuntimeError('pid {} not exist'.format(pid_file))

            pid = int(subprocess.check_output(['cat', pid_file]).decode("utf8").rstrip())

            try:
                os.kill(pid, 0)
            except:
                raise RuntimeError('no such process: {}'.format(name))

            if sock != None:
                stat_result = os.stat(sock)
                if not (stat_result.st_mode & stat.S_IFSOCK):
                    raise RuntimeError('no such socket: {}'.format(sock))

        except RuntimeError as e:
            if warning:
                print('Error occurred: ', e)
            return False

        else:
            if warning:
                print('"{}" is good'.format(name))
            return True

class LogParser(ToolInterface):
    def getSubparserName(self):
        return 'log'

    def addSubparser(self, subparsers):
        parser = subparsers.add_parser(self.getSubparserName(), help="let you quickly grep across all logs")

        # grep related
        parser.add_argument('-l', '--lines', type=int, nargs='?', default=25, help="lines")
        parser.add_argument('--no-color', action='store_true', help="disable color highlight")
        parser.add_argument('--rm', action='store_true', help="cleanup after query")
        parser.add_argument('-u', '--force-update', action='store_true', help="force update database even if exists")

        kw_group = parser.add_argument_group('keywords')
        kw_group.add_argument('-k', '--keywords', type=str, nargs='+', help="keywords")
        kw_group.add_argument('-o', '--keywords-or', action='store_true', help="keywords using or operations")
        kw_group.add_argument('-v', '--keywords-without', type=str, nargs='+', help="keywords excludes")

        # datetime related
        dt_group = parser.add_argument_group('datetime')
        dt_group.add_argument('-a', '--after-date', type=validDate, help="after date(format: YYYY/MM/DD or 3d)")
        dt_group.add_argument('-A', '--after-hour', type=validTime, help="after time(format: H:M:S / 3h / 3m)")
        dt_group.add_argument('-b', '--before-date', type=validDate, help="before date(format: YYYY/MM/DD or 3d / 3h / 3m)")
        dt_group.add_argument('-B', '--before-hour', type=validTime, help="before time(format: H:M:S)")

        # file related
        file_group = parser.add_argument_group('files')
        file_group.add_argument('-m', '--messages', action='store_true', help="with /var/log/messages")
        file_group.add_argument('-d', '--synologydrive', action='store_true', help="with /var/log/synologydrive.log")
        file_group.add_argument('-s', '--syncfolder', action='store_true', help="with /volume*/@synologydrive/log/syncfolder.log")
        file_group.add_argument('-c', '--client', action='store_true', help="with /volume*/@synologydrive/log/client.log")
        file_group.add_argument('-f', '--extra-files', type=str, nargs='+', default=[], help="extra files")

    def execute(self, args):
        self.args = args
        self.checkCondition()

        if len(self.args.extra_files) > 0:
            self.openDB()
            self.insertLogs()
            self.performQuery()

        if self.args.rm:
            self.cleanUp()

    def checkCondition(self):
        args = self.args
        if args.messages:
            args.extra_files.append('/var/log/messages')

        if args.synologydrive:
            args.extra_files.append('/var/log/synologydrive.log')

        if args.syncfolder:
            args.extra_files.append(GLOBALS.repo_path + '../log/syncfolder.log')

        if args.client:
            args.extra_files.append(GLOBALS.repo_path + '../log/client.log')

        if len(args.extra_files) == 0 and not self.args.rm:
            raise RuntimeError('need at least one file')

    def getDBPath(self):
        return GLOBALS.getVolTmp() + 'drive-debug.sqlite'

    def cleanUp(self):
        path = self.getDBPath()
        if os.path.isfile(path):
            os.unlink(path)
            print('cleanuped database "{}" successfully.'.format(path))

    def openDB(self):
        db_path = self.getDBPath()
        db_exists = os.path.isfile(db_path)

        self.db = sqlite3.connect(db_path)
        self.db.text_factory = str

        if db_exists:
            return

        self.db.executescript("""
            PRAGMA journal_mode = WAL; PRAGMA synchronous = OFF; PRAGMA temp_store = MEMORY;
            CREATE TABLE logs(
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                time INTEGER NOT NULL,
                file TEXT,
                file_rotation INTEGER NOT NULL,
                msg TEXT
            );
            CREATE INDEX logs_time_index ON logs(time ASC, file_rotation DESC, id ASC);
            CREATE INDEX logs_file_index ON logs(file);
        """)

    def insertLogs(self):
        self.cursor = self.db.cursor()

        for base_file in self.args.extra_files:
            if self.args.force_update or not self.isInsertedBefore(base_file):
                self.insertLogsFromBaseFile(base_file)

        self.db.commit()
        self.cursor = None

    def isInsertedBefore(self, base_file):
        self.cursor.execute('SELECT EXISTS(SELECT 1 FROM logs WHERE file = ?)', [base_file])
        return self.cursor.fetchone()[0] == 1

    def insertLogsFromBaseFile(self, base_file):
        self.TryDropPreviousLogs(base_file)
        self.insertLogsFromFile(base_file, base_file, 0)
        i = 1

        while self.insertLogsFromFile('{}_{}'.format(base_file, i - 1), base_file, i) or \
            self.insertLogsFromXZ('{}.{}.xz'.format(base_file, i), base_file, i):
            i += 1

    def TryDropPreviousLogs(self, base_file):
        self.cursor.execute('DELETE FROM logs WHERE file = ?', [base_file])

    def insertLogsFromFile(self, file, base_file, file_rotation):
        if not os.path.isfile(file):
            return False
        print('parsing: ' + file)

        with open(file, 'r') as f:
            self.insertLogsFromFd(f, base_file, file_rotation)

        return True

    def insertLogsFromXZ(self, file, base_file, file_rotation):
        if not os.path.isfile(file):
            return False
        print('parsing: ' + file)

        popen = subprocess.Popen(['xz', '-d', '-c', '-T', '4', file], stdout=subprocess.PIPE)
        self.insertLogsFromFd(popen.stdout, base_file, file_rotation)
        popen.terminate()

        return True

    def insertLogsFromFd(self, file, base_file, file_rotation):
        last_line = file.readline().rstrip()
        last_time = self.parseTime(last_line)

        for line in file:
            line = line.rstrip()
            time = self.parseTime(line)

            if time:
                if not last_time:
                    print('Skipped line: ' + last_line)
                else:
                    self.cursor.execute('INSERT INTO logs (time, file, file_rotation, msg) VALUES(?,?,?,?);', (last_time, base_file, file_rotation, last_line))
                last_line = line
                last_time = time
            else:
                last_line += '\n' + line

        if last_time:
            self.cursor.execute('INSERT INTO logs (time, file, file_rotation, msg) VALUES(?,?,?,?);', (last_time, base_file, file_rotation, last_line))

    def parseTime(self, line):
        for fmt in ('%Y-%m-%dT%H:%M:%S', '%Y/%m/%d %H:%M:%S'):
            try:
                time = datetime.datetime.strptime(line[:19], fmt)
                return int(time.strftime('%s'))
            except ValueError:
                pass

    def performQuery(self):
        query = "SELECT file, msg FROM (SELECT * FROM logs WHERE 1 = 1"

        if self.args.keywords and len(self.args.keywords) > 0:
            kwqueries = []
            for k in self.args.keywords:
                kwqueries.append("msg LIKE '%{}%'".format(k))

            join = " OR " if self.args.keywords_or else " AND "
            query += " AND ({})".format(join.join(kwqueries))

        if self.args.keywords_without and len(self.args.keywords_without) > 0:
            kwqueries = []
            for k in self.args.keywords_without:
                kwqueries.append("msg NOT LIKE '%{}%'".format(k))

            query += " AND {}".format(" AND ".join(kwqueries))

        if self.args.after_date or self.args.after_hour:
            query += " AND time >= " + self.handleDateHour(self.args.after_date, self.args.after_hour, True)

        if self.args.before_date or self.args.before_hour:
            query += " AND time <= " + self.handleDateHour(self.args.before_date, self.args.before_hour, False)

        query += " AND file IN ({})".format(','.join(['?'] * len(self.args.extra_files)))

        query += " ORDER BY time DESC, file_rotation ASC, id DESC"
        query += " LIMIT " + str(self.args.lines)
        query += ") ORDER BY time ASC, file_rotation DESC, id ASC"

        cursor = self.db.cursor()

        maxlength = 0
        for file in self.args.extra_files:
            filelen = len(file.split('/')[-1])
            if filelen > maxlength:
                maxlength = filelen

        for row in cursor.execute(query, self.args.extra_files):
            file, msg = row
            fmt = '{:' + str(maxlength) + '} : {}'
            print(fmt.format(self.toColor(bcolors.OKGREEN, file.split('/')[-1]), self.keywordHighlight(msg)))

    def handleDateHour(self, date, hour, is_after):
        if not date:
            date = datetime.datetime.today()

        if not hour:
            hour = datetime.time(0, 0, 0) if is_after else datetime.time(23,59,59)

        d = datetime.datetime.combine(date, hour)
        return d.strftime('%s')

    def keywordHighlight(self, msg):
        if self.args.no_color:
            return msg

        if not hasattr(self, 'date_re'):
            self.date_re = re.compile('(\d{4}.\d{2}.\d{2}T?\d{2}:\d{2}:\d{2}(?:\+\d{2}:\d{2}))')

        msg = self.date_re.sub(bcolors.OKBLUE + '\\1' + bcolors.ENDC, msg)

        if not self.args.keywords:
            return msg

        for kw in self.args.keywords:
            msg = msg.replace(kw, self.toColor(bcolors.HEADER, kw))

        return msg

    def toColor(self, color, msg):
        if self.args.no_color:
            return msg
        return color + msg + bcolors.ENDC

class Database(ToolInterface):
    def getSubparserName(self):
        return 'database'

    def addSubparser(self, subparsers):
        parser = subparsers.add_parser(self.getSubparserName(), help="check or fix databases")
        group = parser.add_mutually_exclusive_group(required=True)
        group.add_argument('--stats', action='store_true', help="databases stats")
        group.add_argument('--check', action='store_true', help="check databases")
        group.add_argument('--fix-corrupt', action='store_true', help="fix corrupt database")
        parser.add_argument('--path', type=str, help="custom database path, if not specified, will perform to all databases")

    def execute(self, args = None):
        if args.stats:
            self.stats(args)
        elif args.check:
            self.check(args)
        elif args.fix_corrupt:
            self.fix_corrupt(args)

    def stats(self, args):
        databases = [args.path] if args.path != None else self.enumViewDatabase()

        for db in databases:
            print('Database: {}'.format(db))
            node_table = sqlite(db, 'SELECT COUNT(*) FROM node_table')
            version_table = sqlite(db, 'SELECT COUNT(*) FROM version_table')
            db_size = os.stat(db).st_size / 1024.0 / 1024
            print('files: {}, versions: {}, size: {} MB'.format(node_table, version_table, db_size))

    def check(self, args):
        databases = [args.path] if args.path != None else self.enumDatabase()

        for db in databases:
            print('Checking database: {}'.format(db))
            sqlite(db, 'PRAGMA integrity_check(1)', output=True, line=True)
            print('')

    def check_db_integrity(self, db_path):
        try:
            ok = sqlite(db_path, 'PRAGMA integrity_check(1)')
            return True if ok == 'ok' else False
        except subprocess.CalledProcessError as e:
            print('Failed to check integerity for ', db_path, ':\n', e.output)

        return False

    def fix_corrupt(self, args):
        daemon_count = DaemonStatusChecker().getRunningDaemons()
        if daemon_count != 0:
            raise RuntimeError('some daemon is running, maybe drive is still started?')

        fixed = False

        databases = [args.path] if args.path != None else self.enumDatabase()

        for db in databases:
            ok = self.check_db_integrity(db)
            if ok is not True:
                db_new = db + '.new'
                print('{} is corrupted, try to fix it'.format(db))

                sqlite_source_process = subprocess.Popen(["sqlite3", db, '.dump'], stdout=subprocess.PIPE)
                subprocess.call(["sqlite3", db_new], stdin=sqlite_source_process.stdout)
                sqlite_source_process.stdout.close()

                ok = self.check_db_integrity(db_new)
                if ok is not True:
                    print('fix failed, will leave it alone')
                    os.unlink(db_new)
                else:
                    db_bak = db + '.bak'
                    os.rename(db, db_bak)
                    os.rename(db_new, db)
                    print('fix successfully, old file will be placed in {}'.format(db_bak))
                    fixed = True
            else:
                print('db is ok: {}'.format(db))

        if fixed:
            print('Warning: we are not sure if drive can work successfully after fixed, if user feels strange then asks if he wanna full uninstall and reinstall drive.')

    def enumDatabase(self):
        for db in GLOBALS.databases:
            yield db

        for filename in os.listdir(GLOBALS.repo_path + 'view'):
            yield GLOBALS.repo_path + 'view/' + filename + '/view-db.sqlite'

        for filename in os.listdir(GLOBALS.repo_path + 'file'):
            yield GLOBALS.repo_path + 'file/' + filename + '/file-db.sqlite'

    def enumViewDatabase(self):
        for filename in os.listdir(GLOBALS.repo_path + 'view'):
            yield GLOBALS.repo_path + 'view/' + filename + '/view-db.sqlite'

class bcolors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'

def validDate(s):
    short_form = re.search('(\d+)d', s)
    if short_form:
        return (datetime.datetime.today() - datetime.timedelta(days = int(short_form.group(1)))).date()

    try:
        return datetime.datetime.strptime(s, "%Y/%m/%d").date()
    except ValueError:
        msg = "Not a valid date: '{0}'.".format(s)
        raise argparse.ArgumentTypeError(msg)

def validTime(s):
    short_form = re.search('(\d+)([hms])', s)
    if short_form:
        type = short_form.group(2)
        if type == 'h':
            return (datetime.datetime.today() - datetime.timedelta(hours = int(short_form.group(1)))).time()
        elif type == 'm':
            return (datetime.datetime.today() - datetime.timedelta(minutes = int(short_form.group(1)))).time()
        elif type == 's':
            return (datetime.datetime.today() - datetime.timedelta(seconds = int(short_form.group(1)))).time()

    try:
        return datetime.datetime.strptime(s, "%H:%M:%S").time()
    except ValueError:
        msg = "Not a valid time: '{0}'.".format(s)
        raise argparse.ArgumentTypeError(msg)

def getFileBasicInfo(file_dict, columns='sync_id,max_id,owner,capabilities,parent_id,permanent_link,path,display_path,file_id,removed,name,content_type'):
    want_keys = columns.split(',')
    return {k:v for k, v in filter(lambda t: t[0] in want_keys, file_dict.items())}

def getFileFullPath(permanent_id):
    result = webapi('SYNO.SynologyDrive.Files', 'get', 1, 'root', ['path="id:{}"'.format(permanent_id)])
    return result['data']['dsm_path']

def sqlite(db, sql, output=False, line=False, header=False):
    if not os.path.isfile(db):
        raise RuntimeError("File not found " + db)

    cmd = ['sqlite3']
    if line:
        cmd.append('-line')
    elif header:
        cmd.append('-header')

    cmd += [db, sql]

    if output:
        subprocess.call(cmd)
    else:
        return subprocess.check_output(cmd).decode("utf8").rstrip()

def webapi(api, method, version, runner="root", params=[]):
    prepend = ["/usr/syno/bin/synowebapi"]
    prepend.append("--exec")
    prepend.append("api=" + api)
    prepend.append("method=" + method)
    prepend.append("version=" + str(version))
    prepend.append("runner=" + runner)
    return json.loads(subprocess.check_output(prepend + params, stderr=GLOBALS.fnull).decode("utf8"))

if __name__ == "__main__":
    main()

Youez - 2016 - github.com/yon3zu
LinuXploit