synctoweb.py (Source)

#!/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 = 'hx0050@staff.uni-due.de:/homes/hx0050/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 # 'staff.uni-due.de'
UPLOAD_DIRECTORY = None # '/homes/hx0050/public_html/live'
SSH_USERNAME = None # 'hx0050'
# 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)
    # SCPCLient takes a paramiko transport as an argument
    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()