| 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 : |
#!/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()