File: //usr/syno/sbin/synonotifydbtransfer
#!/bin/python3
import argparse
import json
import subprocess
import sqlite3
import os
from os import walk
from os import path
from os import stat
from datetime import datetime
ETC_PATH = "/usr/syno/etc/"
DB_NAME = "dsmnotify.db"
DB_TMP_NAME = DB_NAME + ".tmp"
notify_per_user = 500
read_dsmnotify_list = []
class DbHandler:
def __init__(self, db_name=DB_NAME):
try:
self.db_path = ETC_PATH + db_name
self.db_conn = sqlite3.connect(self.db_path)
self.cur = self.db_conn.cursor()
self.create_table_if_not_exist()
except Exception as e:
print("error in __init__:", e)
def __del__(self):
try:
self.create_trigger_if_not_exist()
self.db_conn.close()
except Exception as e:
print("error in __del__:", e)
def has_timestamp(self):
self.cur.execute('PRAGMA table_info(message)')
for col in self.cur.fetchall():
if "timestamp" in col:
return True
return False
def update_timestamp(self):
try:
if not self.has_timestamp():
self.cur.execute('ALTER TABLE message ADD COLUMN timestamp INTEGER;')
message = {}
for row in self.cur.execute("SELECT message_id, message_content FROM message WHERE timestamp IS NULL;"):
message[str(row[0])] = str(json.loads(row[1])['time'])
for m_id in message:
self.cur.execute(f"UPDATE message SET timestamp={message[m_id]} WHERE message_id={m_id}")
self.cur.executescript("""DROP TRIGGER IF EXISTS 'insert_notifty' ; """)
self.create_trigger_if_not_exist()
return True
except Exception as e:
print(e)
return False
def insert_notify(self, notify_db):
msgAry = get_sorted_message(notify_db)
has_failed = False
try:
for arr in msgAry:
msg = arr[1]
self.cur.execute(
"INSERT OR IGNORE INTO message (message_content, message_counter, timestamp) VALUES (?, ?, ?);",
(msg, 0, str(json.loads(msg)['time'])),
)
self.cur.execute(
"SELECT message_id FROM message WHERE ? = message_content;",
(msg,),
)
res = self.cur.fetchone()
if res == None or len(res) != 1:
print('insert failed')
has_failed = True
continue
msg_id = res[0]
for uid in notify_db[msg]:
self.cur.execute(
"INSERT INTO dsmnotify(uid, message_id) SELECT ?, ? WHERE NOT EXISTS(SELECT 1 FROM dsmnotify WHERE uid = ? AND message_id = ?);",
(uid, msg_id, uid, msg_id),
)
self.cur.execute("DELETE FROM 'dsmnotify' WHERE (SELECT SUM(message_counter) FROM 'message') > ? AND _id IN (SELECT _id FROM 'dsmnotify' WHERE message_id = (SELECT message_id FROM 'message' ORDER BY timestamp LIMIT 1));",
(50000,),
)
self.db_conn.commit()
except Exception as e:
print("read /usr/syno/etc/dsmnotify.db failed", e)
return False
return has_failed == False
def create_table_if_not_exist(self):
self.cur.executescript("""
CREATE TABLE IF NOT EXISTS 'message' (
message_id INTEGER PRIMARY KEY,
message_content TEXT,
message_counter integer NOT NULL,
timestamp INTEGER,
UNIQUE(message_content)
);
CREATE TABLE IF NOT EXISTS 'dsmnotify' (
_id integer PRIMARY KEY,
uid integer NOT NULL,
message_id integer NOT NULL
);
""")
def create_trigger_if_not_exist(self):
self.cur.executescript("""
CREATE TRIGGER IF NOT EXISTS 'delete_message' AFTER DELETE ON 'dsmnotify'
BEGIN
UPDATE 'message' SET message_counter = message_counter - 1 WHERE OLD.message_id = message.message_id;
DELETE FROM 'message' WHERE message_counter < 1;
END;
CREATE TRIGGER IF NOT EXISTS 'insert_notifty' AFTER INSERT ON 'dsmnotify'
BEGIN
UPDATE 'message' SET message_counter = message_counter + 1 WHERE message.message_id = NEW.message_id;
DELETE FROM 'dsmnotify' WHERE _id IN
( SELECT _id FROM 'dsmnotify' INNER JOIN 'message' ON dsmnotify.message_id = message.message_id
WHERE uid = NEW.uid ORDER BY message.timestamp DESC limit -1 offset 500 ) ;
END;
""")
def read_user_notify_by_path(notify_db, path):
try:
uid = stat(path).st_uid
if uid > 2147483647:
uid = uid - 4294967296
for notify in json.load(open(path, encoding="utf8")):
str_notify = json.dumps(notify,
separators=(",", ":"),
ensure_ascii=False).encode("utf8")
if str_notify not in notify_db:
notify_db[str_notify] = set()
notify_db[str_notify].add(uid)
read_dsmnotify_list.append(path)
return True
except Exception as e:
print("read " + path + " failed", e)
return False
def read_user_notify_by_name(notify_db, name):
is_read = False
has_notify = False
pref_dir_path = (subprocess.Popen(
f"/usr/syno/bin/synopreferencedir --user-get-only '{name}'",
shell=True,
stdout=subprocess.PIPE,
).stdout.read().decode("utf-8"))
if len(pref_dir_path) == 0:
print("get " + name + "'s preference directory failed")
return False
for filepath in [
pref_dir_path + "/dsmnotify", pref_dir_path + "/dsmnotify.bkp"
]:
if path.exists(filepath):
has_notify = True
is_read = read_user_notify_by_path(notify_db, filepath) or is_read
if is_read != has_notify:
return False
return True
def read_all_users_notify(notify_db):
has_failed = False
for (dirpath, dirnames, filenames) in walk("/usr/syno/etc/preference/"):
for filename in filenames:
if filename == "dsmnotify" or filename == "dsmnotify.bkp":
has_failed = read_user_notify_by_path(notify_db, dirpath + "/" + filename) == False or has_failed
return not has_failed
def get_sorted_message(notify_db):
msgAry = []
for msg in notify_db:
msgAry.append([json.loads(msg)["time"], msg])
msgAry.sort()
return msgAry
def remove_redundant_notify(notify_db):
msgAry = get_sorted_message(notify_db)
userNotifyCounter = {}
for arr in reversed(msgAry):
temp_set = set()
msg = arr[1]
for uid in notify_db[msg]:
if uid not in userNotifyCounter:
userNotifyCounter[uid] = 0
if userNotifyCounter[uid] < notify_per_user:
userNotifyCounter[uid] = userNotifyCounter[uid] + 1
temp_set.add(uid)
if len(temp_set) == 0:
notify_db.pop(msg, None)
else:
notify_db[msg] = temp_set
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"-u",
"--user",
type=str,
action="append",
help="read local user's notifications",
)
parser.add_argument("-a",
"--all",
action="store_true",
help="read all users' notifications")
parser.add_argument("-t",
"--time",
action="store_true",
help="add timestamp into message")
parser.add_argument("-n",
"--num",
type=int,
default=500,
help="number of notifications per user (optional)")
args = parser.parse_args()
if args.num > 500 or args.num < 20:
print("ERROR: num shoud greater than 20 and less than 500\n")
parser.print_help()
else:
global notify_per_user
notify_per_user = args.num
notify_db = {}
if args.all == True:
if read_all_users_notify(notify_db) == False:
print('read all dsmnotify Failed')
return False
elif args.user:
for u in args.user:
if read_user_notify_by_name(notify_db, u) == False:
print('read ' + u + '\'s notify failed')
return False
elif args.time != True:
parser.print_help()
return False
if len(notify_db) == 0 and args.time == True and not os.path.exists(ETC_PATH + DB_NAME):
return True
try:
db_handler = DbHandler(DB_NAME)
db_handler.update_timestamp()
except Exception as e:
print("read /usr/syno/etc/dsmnotify.db failed", e)
return False
remove_redundant_notify(notify_db)
if db_handler.insert_notify(notify_db) == False:
print('insert notify failed')
return False
for path in read_dsmnotify_list:
if os.path.exists(path):
os.remove(path)
else:
print(path + " does not exist")
return True
if __name__ == "__main__":
try:
if not main():
exit(1)
except Exception as e:
exit(1)
exit(0)