diff --git a/CanvasSync/GUI/canvas_logo.png b/CanvasSync/GUI/canvas_logo.png new file mode 100644 index 0000000..a0a64cf Binary files /dev/null and b/CanvasSync/GUI/canvas_logo.png differ diff --git a/CanvasSync/GUI/macos_statusbar.py b/CanvasSync/GUI/macos_statusbar.py new file mode 100755 index 0000000..3240bb0 --- /dev/null +++ b/CanvasSync/GUI/macos_statusbar.py @@ -0,0 +1,143 @@ +""" +This script is the actual MacOS Statusbar for CanvasSync. +Execute this script to run the CanvasSync Statusbar. +Run startup_installer.py to add the CanvasSync Statsubar to the system StartUp. +""" +from rumps import * +import pathlib +import sys +import subprocess +import AppKit +import configparser +import os.path +import datetime +import keyring +import pymsgbox + + +class config_file: + def __init__(self, filepath): + self.filepath = filepath + if not os.path.exists(self.filepath): + open(self.filepath, 'a').close() + self.config = configparser.ConfigParser() + self.config.read(filepath) + + def read(self, section, parameter): + if not self.config.has_option(section, parameter): + return None + else: + return self.config[section][parameter] + + def write(self, section, parameter, value): + if not self.config.has_section(section): + self.config.add_section(section) + self.config.set(section, parameter, value) + with open(self.filepath, 'w') as configfile: + self.config.write(configfile) + + +from tkinter import * + +def pop_up(): + app = Tk() + app.title("CanvasSync Password") + label = Label(app, text="Please enter the CanvasSync Password:") + label.grid() + pwd = StringVar() + e1 = Entry(app, width=40, textvariable=pwd) + btn = Button(app, text="Ok", command=app.destroy) + btn.grid(row=2) + e1.grid(row=1) + + app.mainloop() + return pwd.get() + +def savepassword(): + response = str(pop_up()) + keyring.set_password('CanvasSync', 'xkcd', response) + + +def changeSchedule(ToNew): + """ + This function changes the scheduler settings file so that the selected interval in the settings file is saved there. + Arguments: + ToNew (str): The interval changed to/the new interval. + """ + config.write('Settings', 'interval', ToNew) + + +def adjust_interval(self): + # Change tick-mark in Statusbar on change + app.menu["Automatic sync"]['hourly'].state = 0 + app.menu["Automatic sync"]['every 6 hours'].state = 0 + app.menu["Automatic sync"]['daily'].state = 0 + app.menu["Automatic sync"]['Do not sync'].state = 0 + self.state = 1 + # Save setting in Scheduler-Settings-File + if self.title == 'hourly': + changeSchedule('hourly') + elif self.title == 'every 6 hours': + changeSchedule('every 6 hours') + elif self.title == 'daily': + changeSchedule('daily') + elif self.title == 'Do not sync': + changeSchedule('Do not sync') + + +@clicked("Sync now") +def prefs(_): + # Manual Sync now + rumps.notification("Canvas Sync", "Syncing...", "The Canvas sync was started manually.") + # Add folder of canvas module to search directory + path = str(pathlib.Path(__file__).parent.parent.parent) + "/bin/" + sys.path.insert(0, path) + # Actual syncing + import canvas + settings = canvas.Settings() + password = keyring.get_password('CanvasSync', 'xkcd') + canvas.do_sync(settings, password) + rumps.notification("Canvas Sync", "Finished Synchronisation", "The Manual Canvas-Sync-task was finished.") + config.write('Last run', 'time', str(datetime.datetime.now().strftime("%d.%m.%Y %H:%M:%S"))) + + +@clicked("Preferences") +def sayhi(_): + # Will be added later on + rumps.alert("Not yet available! In next version...") + + +hourly = MenuItem('hourly', callback=adjust_interval) +every6 = MenuItem('every 6 hours', callback=adjust_interval) +daily = MenuItem('daily', callback=adjust_interval) +no_sync = MenuItem('Do not sync', callback=adjust_interval) + +if __name__ == "__main__": + if keyring.get_password('CanvasSync', 'xkcd') is None: + savepassword() + + config = config_file(str(pathlib.Path(__file__).parent.parent) + "/scheduler/scheduler.ini") + interval = config.read('Settings', 'interval') + + # Do not appear in Mac Dock + info = AppKit.NSBundle.mainBundle().infoDictionary() + info["LSBackgroundOnly"] = "1" + + # Set Statusbar Properties + app = App('Canvas Sync', icon='canvas_logo.png') + app.menu = [("Sync now"), ("Automatic sync", [hourly, every6, daily, None, no_sync]), "Preferences"] + path = str(pathlib.Path(__file__).parent.parent) + '/scheduler/scheduler.py' + + # Run Scheduler as independent subprocess + #subprocess.run(["python", path]) + from subprocess import Popen, PIPE + p = subprocess.Popen([sys.executable, path], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Get Current Sync-Setting from file and select the selected in Statusbar + if not interval == None: + app.menu["Automatic sync"][interval].state = 1 + + # Run Statusbar + app.run() diff --git a/CanvasSync/GUI/startup_installer.py b/CanvasSync/GUI/startup_installer.py new file mode 100644 index 0000000..f19a1ab --- /dev/null +++ b/CanvasSync/GUI/startup_installer.py @@ -0,0 +1,98 @@ +""" +This script is to add/remove the CanvasSync Statusbar in MacOS to the LaunchAgent so that is automatically starts at startup. +Run this file or the function ActivateStartUpStatusbar() to add it to startup. +Run the function DeactivateStartUpStatusbar() to remove it from startup. +""" + +import pathlib +import os +import sys +import platform + +def CreateStartupFile(): + with open(str(pathlib.Path(__file__).parent) + '/startup_template.plist', 'r') as file: + script = str(file.read()) + + script = script.replace('[@replaceCanvasPath]', str(pathlib.Path(__file__).parent)) + script = script.replace('[@replacePythonPath]', str(sys.executable)) + + path_s = str(os.path.expanduser('~')) + "/Library/LaunchAgents/com.CanvasSync.Statusbar.plist" + with open(path_s, 'w') as file: + file.write(script) + +def AddWinTaskScheduler(wkdir, command, enabled): + import datetime + import win32com.client + + scheduler = win32com.client.Dispatch('Schedule.Service') + scheduler.Connect() + root_folder = scheduler.GetFolder('\\') + task_def = scheduler.NewTask(0) + + # Create trigger + #end_time = datetime.datetime.now() + TASK_TRIGGER_TIME = 9 + trigger = task_def.Triggers.Create(TASK_TRIGGER_TIME) + #trigger.EndBoundary = end_time.isoformat() + trigger.ExecutionTimeLimit = "PT5M" + trigger.Id = "LogonTriggerId" + import win32api + user = win32api.GetUserName() + trigger.UserId = user + trigger.ExecutionTimeLimit = "P0M2DT0H0M" + trigger.enabled = enabled + + # Create action + TASK_ACTION_EXEC = 0 + action = task_def.Actions.Create(TASK_ACTION_EXEC) + action.ID = 'DO NOTHING' + action.Path = 'cmd.exe' + action.Arguments = '/c "' + str(command) + '"' + action.WorkingDirectory = str(wkdir) + + # Set parameters + task_def.RegistrationInfo.Description = 'Run CanvasSync at System Startup' + task_def.Settings.Enabled = True + task_def.Settings.StopIfGoingOnBatteries = False + task_def.Settings.DisallowStartIfOnBatteries = False + task_def.Settings.Hidden = True + + # Register task + # If task already exists, it will be updated + TASK_CREATE_OR_UPDATE = 6 + TASK_LOGON_NONE = 0 + root_folder.RegisterTaskDefinition( + 'CanvasSync', # Task name + task_def, + TASK_CREATE_OR_UPDATE, + '', # No user + '', # No password + TASK_LOGON_NONE) + + + +def ActivateStartUpStatusbar(): + # ToDo: In Windows subpress feedback e.g. when asking if run this package that is not in PATH. + if platform.system() == 'Windows': + path = str(pathlib.Path(__file__).parent) + AddWinTaskScheduler(path, r'python .\windows_systemtray.py -y', True) + + else: #MacOS + CreateStartupFile() + load_command = "launchctl load " + str(os.path.expanduser('~')) + "/Library/LaunchAgents/com.CanvasSync.Statusbar.plist" + os.system(load_command) + +def DeactivateStartUpStatusbar(): + if platform.system() == 'Windows': + path = str(pathlib.Path(__file__).parent) + AddWinTaskScheduler(path, r'python .\windows_systemtray.py -y', False) + else: # MacOS + unload_command = "launchctl unload " + str(os.path.expanduser('~')) + "/Library/LaunchAgents/com.CanvasSync.Statusbar.plist" + stop_command = "launchctl stop " + str(os.path.expanduser('~')) + "/Library/LaunchAgents/com.CanvasSync.Statusbar.plist" + os.system(unload_command) + os.system(stop_command) + + +# If main module +if __name__ == u"__main__": + ActivateStartUpStatusbar() diff --git a/CanvasSync/GUI/startup_template.plist b/CanvasSync/GUI/startup_template.plist new file mode 100644 index 0000000..c20a35b --- /dev/null +++ b/CanvasSync/GUI/startup_template.plist @@ -0,0 +1,19 @@ + + + + + Label + com.CanvasSync.Statusbar + ProgramArguments + + [@replacePythonPath] + [@replaceCanvasPath]/macos_statusbar.py + + StandardErrorPath + [@replaceCanvasPath]/statusbar.err + StandardOutPath + [@replaceCanvasPath]/statusbar.out + KeepAlive + + + diff --git a/CanvasSync/GUI/windows_systemtray.py b/CanvasSync/GUI/windows_systemtray.py new file mode 100644 index 0000000..a726add --- /dev/null +++ b/CanvasSync/GUI/windows_systemtray.py @@ -0,0 +1,131 @@ +from PIL import Image +import pathlib, configparser, os, sys, datetime, keyring, subprocess +from pystray import Icon as icon, Menu as menu, MenuItem as item +import pystray +import PIL + +class config_file: + def __init__(self, filepath): + self.filepath = filepath + if not os.path.exists(self.filepath): + open(self.filepath, 'a').close() + self.config = configparser.ConfigParser() + self.config.read(filepath) + + def read(self, section, parameter): + if not self.config.has_option(section, parameter): + return None + else: + return self.config[section][parameter] + + def write(self, section, parameter, value): + if not self.config.has_section(section): + self.config.add_section(section) + self.config.set(section, parameter, value) + with open(self.filepath, 'w') as configfile: + self.config.write(configfile) + + +def set_state(v): + def inner(icon, item): + global state + state = v + config.write('Settings', 'interval', state_dict[v]) + return inner + +from tkinter import * +def get_state(v): + def inner(item): + #return state == v + return config.read('Settings', 'interval') == state_dict[v] + return inner + + +def pop_up(): + app = Tk() + app.title("CanvasSync Password") + label = Label(app, text="Please enter the CanvasSync Password:") + label.grid() + pwd = StringVar() + e1 = Entry(app, width=40, textvariable=pwd) + btn = Button(app, text="Ok", command=app.destroy) + btn.grid(row=2) + e1.grid(row=1) + + app.mainloop() + return pwd.get() + + +def savepassword(): + response = str(pop_up()) + keyring.set_password('CanvasSync', 'xkcd', response) + +def runSync(): + print('sync') + + # Import Canvas Module + path = str(pathlib.Path(__file__).parent.parent.parent) + "/bin/" + sys.path.insert(0, path) + import canvas + + settings = canvas.Settings() + password = keyring.get_password('CanvasSync', 'xkcd') + canvas.do_sync(settings, password) + config.write('Last run', 'time', str(datetime.datetime.now().strftime("%d.%m.%Y %H:%M:%S"))) + +def preferences(): + pass + +def quit_icon(): + #ToDo: Quit App does not work at the moment. + icon.stop() + +dir_path = os.path.dirname(os.path.realpath(__file__)) + +if __name__ == '__main__': + image = PIL.Image.open('canvas_logo.png') + + state = 0 + state_dict = { + 1: 'hourly', + 2: 'every 6 hours', + 3: 'daily', + 4: 'Do not sync' + } + + if keyring.get_password('CanvasSync', 'xkcd') is None: + savepassword() + + # Run Scheduler as independent subprocess + path = str(os.path.abspath(os.path.join(os.path.join(os.path.abspath(__file__), '..'), '..'))) + '\\scheduler\\scheduler.py' + subprocess.Popen(["python", path]) + + config = config_file(str(os.path.abspath(os.path.join(os.path.join(os.path.abspath(__file__), '..'), '..'))) + "\\scheduler\\scheduler.ini") + icon('test', image, menu=menu( + item('Sync now', runSync), + item( + 'Automatic sync', + menu( + item( + 'hourly', + set_state(1), + checked=get_state(1), + radio=True), + item( + 'every 6 hours', + set_state(2), + checked=get_state(2), + radio=True), + item( + 'daily', + set_state(3), + checked=get_state(3), + radio=True), + item( + 'Do not sync', + set_state(4), + checked=get_state(4), + radio=True))), + item('Preferences', preferences), + item('Quit now', quit_icon) + )).run() \ No newline at end of file diff --git a/CanvasSync/scheduler/scheduler.py b/CanvasSync/scheduler/scheduler.py new file mode 100644 index 0000000..7f5db0b --- /dev/null +++ b/CanvasSync/scheduler/scheduler.py @@ -0,0 +1,91 @@ +""" +This Script is the Scheduler for the automatic Canvas Sync. It constanly runs and checks every full hour if it needs to sync. +It checks it every hour because the user could have changed the settings inbetween the last and the current run. +""" +import datetime, time +import pathlib +import sys +import configparser +import os.path +import keyring +import platform +#import pymsgbox + +class config_file: + def __init__(self, filepath): + self.filepath = filepath + if not os.path.exists(self.filepath): + open(self.filepath, 'a').close() + self.config = configparser.ConfigParser() + self.config.read(filepath) + + def read(self, section, parameter): + if not self.config.has_option(section, parameter): + return None + else: + return self.config[section][parameter] + + def write(self, section, parameter, value): + if not self.config.has_section(section): + self.config.add_section(section) + self.config.set(section, parameter, value) + with open(self.filepath, 'w') as configfile: + self.config.write(configfile) + + +def runSync(): + print('sync') + if keyring.get_password('CanvasSync', 'xkcd') == None: + raise Exception("Error: Password for CanvasSync not saved in Keychain") + settings = canvas.Settings() + password = keyring.get_password('CanvasSync', 'xkcd') + canvas.do_sync(settings, password) + config.write('Last run', 'time', str(datetime.datetime.now().strftime("%d.%m.%Y %H:%M:%S"))) + + +if __name__ == '__main__': + # Import Canvas Module + path = str(pathlib.Path(__file__).parent.parent.parent) + "/bin/" + sys.path.insert(0, path) + # Set workind dir for windows + # ToDo: Check if still works with MacOS + if platform.system() == 'Windows': + os.chdir(path) + import canvas + + # The Check if has to sync + dailysync = False + while True: + # read Config + config = config_file(str(pathlib.Path(__file__).parent) + "/scheduler.ini") + interval = config.read('Settings', 'interval') + print('interval ' + str(interval)) + last_sync = config.read('Last run', 'time') + if last_sync == None: + last_sync = '01.01.2000 01:01:01' + last_sync = datetime.datetime.strptime(last_sync, "%d.%m.%Y %H:%M:%S") + print('last sync ' + str(last_sync.strftime("%d.%m.%Y %H:%M:%S"))) + current_time = datetime.datetime.now() + print('current time ' + str(current_time.strftime("%d.%m.%Y %H:%M:%S"))) + if interval == 'daily': + next_sync = last_sync + datetime.timedelta(days=1) + elif interval == 'Do not sync': + #do nothing + next_sync = last_sync + datetime.timedelta(hours=1) + pass + elif interval == 'hourly': + next_sync = last_sync + datetime.timedelta(hours=1) + elif interval == 'every 6 hours': + next_sync = last_sync + datetime.timedelta(hours=6) + else: + next_sync = last_sync + datetime.timedelta(minutes=10) + if next_sync < current_time and interval != 'Do not sync': + runSync() + else: + print('next sync ' + str(next_sync.strftime("%d.%m.%Y %H:%M:%S"))) + # Calc time till next full hour and wait till then + waitseconds = (next_sync - current_time).total_seconds() + if waitseconds > 3600: + waitseconds = 3600 + print('waiting ' + str(waitseconds) + ' seconds') + time.sleep(waitseconds) \ No newline at end of file diff --git a/README.md b/README.md index 3c4e630..8a34d86 100755 --- a/README.md +++ b/README.md @@ -84,6 +84,19 @@ The authentication token is stored in an local file encrypted using a private pa specify the password whenever CanvasSync is launched to synchronize at a later time. Passwords and/or auth tokens are cannot and will not be shared with third parties. +Statusbar +---------- +For MacOS CanvasSync has also a statusbar, for Windows a icon at the taskbar. From here you can initalise a Synchronisation manually or setup an automatic sync. + + + +To run the Statusbar simply execute ```/GUI/macos_statusbar.py``` or ```/GUI/windows_systemtray.py```. You can also add it to the system startup by executing ```/GUI/startup_installer.py```. This script also contains a function to remove the statusbar from the system startup again. + +Still ToDo: +- quiting does not work +- preferences GUI still has to be added +(On MacOS the you have to grant CanvasSync access to access the KeyChain. CanvasSync saves the password here. ToDo: Make PopUp to give access if CanvasSync does not have access.) + Disclaimer ---------- Please note that by using CanvasSync the user allows the software to authenticate with the Canvas server on the users diff --git a/bin/canvas.py b/bin/canvas.py index a64fc36..f142b26 100755 --- a/bin/canvas.py +++ b/bin/canvas.py @@ -72,6 +72,7 @@ def run_canvas_sync(): and initializes the program """ + # Executed by command line # Get command line arguments (C-style) try: opts, args = getopt.getopt(sys.argv[1:], u"hsiSp:", [u"help", u"setup", u"info", u"sync", u"password"]) @@ -80,6 +81,7 @@ def run_canvas_sync(): print(err) usage.help() + # Parse the command line arguments and act accordingly setup = False show_info = False diff --git a/resources/macos_statusbar.png b/resources/macos_statusbar.png new file mode 100644 index 0000000..49f903c Binary files /dev/null and b/resources/macos_statusbar.png differ diff --git a/resources/windows_systemtray.png b/resources/windows_systemtray.png new file mode 100644 index 0000000..e3c8d54 Binary files /dev/null and b/resources/windows_systemtray.png differ