|  | #!/usr/bin/python3
 | 
|  | 
 | 
|  | from glob import glob
 | 
|  | import hashlib
 | 
|  | import json
 | 
|  | import os
 | 
|  | import sys
 | 
|  | from time import sleep, time
 | 
|  | 
 | 
|  | from watchdog.observers import Observer
 | 
|  | from watchdog.observers.api import EventQueue
 | 
|  | from watchdog.events import PatternMatchingEventHandler
 | 
|  | 
 | 
|  | # ---------------- Configuration ---------------------------------------------
 | 
|  | 
 | 
|  | # Replace the RSYNC_TO string appropriately (and set up public key
 | 
|  | # authentication for ssh), or set RSYNC_TO to None if you want to use scp
 | 
|  | # instead.
 | 
|  | RSYNC_TO = 'hx1234@staff.uni-due.de:/homes/hx1234/public_html/live'
 | 
|  | 
 | 
|  | # If you want to use scp to upload files, install the paramiko and scp packages
 | 
|  | # and set the server address and directory on the server here. (You must create
 | 
|  | # the directory on the server beforehand.)
 | 
|  | SERVER = None  # e.g., 'staff.uni-due.de'
 | 
|  | UPLOAD_DIRECTORY = None  # e.g., '/homes/hx1234/public_html/live'
 | 
|  | SSH_USERNAME = None  # e.g., 'hx1234'
 | 
|  | 
 | 
|  | # If you want to use scp to upload files, set up public key authentication for
 | 
|  | # ssh (preferably - if an SSH agent is running, this should be used
 | 
|  | # automatically by paramiko; if your private key is password protected and no
 | 
|  | # SSH agent is running, you need to provide the password for the key here), or
 | 
|  | # replace None in the next line by your password.
 | 
|  | SSH_PASSWORD = None
 | 
|  | 
 | 
|  | # -----------------------------------------------------------------------------
 | 
|  | 
 | 
|  | if not RSYNC_TO:
 | 
|  |     # open ssh connection
 | 
|  |     from paramiko import SSHClient
 | 
|  |     from scp import SCPClient
 | 
|  | 
 | 
|  |     ssh = SSHClient()
 | 
|  |     ssh.load_system_host_keys()
 | 
|  |     ssh.connect(SERVER, username=SSH_USERNAME, password=SSH_PASSWORD)
 | 
|  |     scp = SCPClient(ssh.get_transport())
 | 
|  | 
 | 
|  | # SOURCE_DIR will be watched for changes to *.xopp files.
 | 
|  | SOURCE_DIR = sys.argv[1]
 | 
|  | 
 | 
|  | # In OUTPUT_DIR, a directory "png-source" will be created which will hold
 | 
|  | # the png files exported by xournal++, and a directory "png-upload" will be
 | 
|  | # created which holds the files that should be uploaded to the web server.
 | 
|  | OUTPUT_DIR = sys.argv[2]
 | 
|  | 
 | 
|  | # The title to be displayed on the web page.
 | 
|  | TITLE = sys.argv[3]
 | 
|  | 
 | 
|  | # List of files we uploaded already
 | 
|  | uploaded_files = []
 | 
|  | 
 | 
|  | 
 | 
|  | def md5sum_file(filename, blocksize=65536):
 | 
|  |     '''Computes the md5 hash of the given file and returns the first ten digits
 | 
|  |     of its hexadecimal presentation.'''
 | 
|  | 
 | 
|  |     hash = hashlib.md5()
 | 
|  |     with open(filename, "rb") as f:
 | 
|  |         for block in iter(lambda: f.read(blocksize), b""):
 | 
|  |             hash.update(block)
 | 
|  |     return hash.hexdigest()[:10]
 | 
|  | 
 | 
|  | 
 | 
|  | def upload_files():
 | 
|  |     slides = []
 | 
|  | 
 | 
|  |     # Go through the new slides and add hash codes. This ensures that a slide
 | 
|  |     # gets a new name when changed. This ensures that only changed/new slides
 | 
|  |     # are uploaded to the web server and also avoids that the web page is not
 | 
|  |     # updated as it should because of caching.
 | 
|  | 
 | 
|  |     for path in sorted(glob(os.path.join(OUTPUT_DIR, 'png-source', '*.png'))):
 | 
|  |         MD5SUM = md5sum_file(path)
 | 
|  |         fn = os.path.basename(path)
 | 
|  |         os.rename(
 | 
|  |             path,
 | 
|  |             os.path.join(OUTPUT_DIR, 'png-upload', '%s-%s.png' % (fn, MD5SUM, )))
 | 
|  |         slides.append((fn, MD5SUM, ))
 | 
|  | 
 | 
|  |     # Compute a timestamp and store it together with the current list of slides.
 | 
|  |     # The javascript running on the web page can compare its own timestamp with
 | 
|  |     # the one stored in the current version of the timestamp.kson file in order
 | 
|  |     # to decide whether the slides need to be updated (which is the case when
 | 
|  |     # those two timestamps differ).
 | 
|  |     timestamp = hashlib.md5(
 | 
|  |         ''.join(s[1] for s in slides).encode('utf-8')).hexdigest()
 | 
|  |     result = [timestamp, TITLE, ] + ["%s-%s.png" % s for s in slides]
 | 
|  |     with open(os.path.join(OUTPUT_DIR, 'png-upload', 'timestamp.json'), 'w') as f:
 | 
|  |         f.write(json.dumps(result))
 | 
|  |     # print(result)
 | 
|  | 
 | 
|  |     # Upload the timestamp.json file and the new slide files to the web server
 | 
|  |     if RSYNC_TO:
 | 
|  |         os.system(
 | 
|  |             f'command rsync -cr -e ssh {OUTPUT_DIR}/png-upload/ {RSYNC_TO}')
 | 
|  |     else:
 | 
|  |         scp.put(
 | 
|  |             os.path.join(OUTPUT_DIR, 'png-upload', 'timestamp.json'),
 | 
|  |             remote_path=UPLOAD_DIRECTORY)
 | 
|  | 
 | 
|  |         for s in result[2:]:
 | 
|  |             if s in uploaded_files:
 | 
|  |                 continue
 | 
|  | 
 | 
|  |             # print('Uploading', s)
 | 
|  |             scp.put(
 | 
|  |                 os.path.join(OUTPUT_DIR, 'png-upload', s),
 | 
|  |                 remote_path=UPLOAD_DIRECTORY)
 | 
|  |             uploaded_files.append(s)
 | 
|  | 
 | 
|  | 
 | 
|  | class MyEventHandler(PatternMatchingEventHandler):
 | 
|  | 
 | 
|  |     # (On my system at least, ) when a xournal file is "Saved as" (i.e., an
 | 
|  |     # on_created event happens), actually saving that file happens in several
 | 
|  |     # steps, so it also triggers an on_modified event shortly thereafter. It is
 | 
|  |     # therefore sufficient to listen to on_modified events only.
 | 
|  |     # def on_created(self, event):
 | 
|  |     #     queue.put(event)
 | 
|  | 
 | 
|  |     def on_modified(self, event):
 | 
|  |         queue.put(event)
 | 
|  | 
 | 
|  | 
 | 
|  | if __name__ == '__main__':
 | 
|  | 
 | 
|  |     # Create directories if necessary
 | 
|  |     for d in ['png-source', 'png-upload', ]:
 | 
|  |         if not os.path.exists(os.path.join(OUTPUT_DIR, d)):
 | 
|  |             try:
 | 
|  |                 os.mkdir(os.path.join(OUTPUT_DIR, d))
 | 
|  |             except:
 | 
|  |                 print('Failed to create directory', os.path.join(OUTPUT_DIR, d))
 | 
|  |                 sys.exit()
 | 
|  | 
 | 
|  |     event_handler = MyEventHandler(patterns=['*.xopp', '.*.xopp', ])
 | 
|  |     queue = EventQueue()
 | 
|  |     observer = Observer()
 | 
|  |     observer.schedule(event_handler, SOURCE_DIR, recursive=True)
 | 
|  |     observer.start()
 | 
|  |     print("Watching for changes ...")
 | 
|  | 
 | 
|  |     try:
 | 
|  |         while True:
 | 
|  |             # wait for 3 seconds between checking for file modification events
 | 
|  |             sleep(3)
 | 
|  | 
 | 
|  |             if not queue.empty():
 | 
|  |                 # (Auto-)saving a file triggers several file modification events
 | 
|  |                 # within 50 milliseconds or so. Therefore we wait here for
 | 
|  |                 # another second to ensure that we do not use a version of the
 | 
|  |                 # file that does not have all changes that were to be saved.
 | 
|  |                 sleep(1)
 | 
|  | 
 | 
|  |                 event = queue.get()
 | 
|  |                 print('File modification', time(), event)
 | 
|  | 
 | 
|  |                 if event.is_directory:
 | 
|  |                     continue
 | 
|  | 
 | 
|  |                 result = os.system('xournalpp --create-img=%s %s'
 | 
|  |                                    % (os.path.join(
 | 
|  |                                        OUTPUT_DIR,
 | 
|  |                                        'png-source',
 | 
|  |                                        'slide.png'),
 | 
|  |                                       event.src_path, ))
 | 
|  |                 if result == 0:
 | 
|  |                     upload_files()
 | 
|  |                 else:
 | 
|  |                     print("Conversion failed.")
 | 
|  |                     break
 | 
|  |     finally:
 | 
|  |         observer.stop()
 | 
|  |         observer.join()
 | 
|  |         if not RSYNC_TO:
 | 
|  |             scp.close()
 |