diff --git a/ContentConverter.py b/ContentConverter.py new file mode 100644 index 0000000..f007d03 --- /dev/null +++ b/ContentConverter.py @@ -0,0 +1,130 @@ +import os +import subprocess + +contentDirectory = "content" +renderedDirectory = "webResources" + +# Pairs of extension, pandoc read type +convertableContentTypes = [(".org", "org")] +contentExtensions = [] +for contentType in convertableContentTypes: + contentExtensions.append(contentType[0]) +contentExtensions = tuple(contentExtensions) + +renderedContentTypes = [(".html", "html")] +defaultRenderedTypeIndex = 0 +renderedContentExtensions = [] +for contentType in renderedContentTypes: + renderedContentExtensions.append(contentType[0]) +renderedContentExtensions = tuple(renderedContentExtensions) + +def getContentReadType(contentFilename): + contentExtension = contentFilename[contentFilename.rfind("."):] + for contentType in convertableContentTypes: + if contentExtension == contentType[0]: + return contentType[1] + +# Strip the content directory +def getContentLocalName(contentFilename): + return contentFilename[len(contentDirectory):] + +# Strip the rendered directory +def getRenderedLocalName(filename): + return filename[len(renderedDirectory):] + +def stripExtension(filename): + return filename[:filename.rfind(".")] + +def contentFilenameToRenderedFilename(contentFilename): + return "{}{}{}".format(renderedDirectory, + stripExtension(getContentLocalName(contentFilename)), + renderedContentTypes[defaultRenderedTypeIndex][0]) + +def renderContent(contentFilename): + # Content modified recently; render it + print("\tRendering {}".format(contentFilename)) + + outputFilename = contentFilenameToRenderedFilename(contentFilename) + + # TODO: Support multiple output formats? + subprocess.run(["pandoc", + "-r", getContentReadType(contentFilename), + "-w", renderedContentTypes[defaultRenderedTypeIndex][1], + "-o", outputFilename, contentFilename]) + +# List of all content files +contentCache = [] + +renderedCache = [] + +renderedDictionary = {} + +def updateRenderedCache(): + global renderedCache + renderedCache = [] + # Get all rendered files + for root, dirs, files in os.walk(renderedDirectory): + for file in files: + if file.endswith(renderedContentExtensions): + renderedFile = os.path.join(root, file) + renderedCache.append(renderedFile) + # The path actually used to look up the content (strip '/' from front) + contentPath = getRenderedLocalName(stripExtension(renderedFile))[1:] + print("\t'{}' = '{}'".format(contentPath, renderedFile)) + # No use for the value yet, we just want fast key lookups + renderedDictionary[contentPath] = renderedFile + +def getRenderedBody(contentPath): + body = None + if contentPath in renderedDictionary: + renderedFilename = renderedDictionary[contentPath] + renderedFile = open(renderedFilename) + body = renderedFile.readlines() + body = "".join(body) + renderedFile.close() + return body + +def checkForContentChange(): + global contentCache + + print("Checking for content updates...") + + # Get all content files + contentCache = [] + for root, dirs, files in os.walk(contentDirectory): + for file in files: + if file.endswith(contentExtensions): + contentCache.append(os.path.join(root, file)) + + updateRenderedCache() + + numRenderedFiles = 0 + + # Compare timestamps to determine which files need regeneration + for contentFilename in contentCache: + contentModified = os.path.getmtime(contentFilename) + contentLocalName = getContentLocalName(contentFilename) + + renderedFileFound = False + for renderedFile in renderedCache: + if stripExtension(renderedFile[len(renderedDirectory):]) != stripExtension(contentLocalName): + continue + + renderedFileFound = True + renderedModified = os.path.getmtime(renderedFile) + + # Modified content + if contentModified > renderedModified: + renderContent(contentFilename) + numRenderedFiles += 1 + + # New content file + if not renderedFileFound: + renderContent(contentFilename) + numRenderedFiles += 1 + + # We generated new content; make sure the cache is up-to-date + if numRenderedFiles: + updateRenderedCache() + + print("Checking for content updates done. Rendered {} files".format(numRenderedFiles)) diff --git a/Generate_Certificates.sh b/Generate_Certificates.sh new file mode 100755 index 0000000..24af5e5 --- /dev/null +++ b/Generate_Certificates.sh @@ -0,0 +1,3 @@ +#!/bin/bash +mkdir certificates +openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout certificates/server_jupyter_based.crt.key -out certificates/server_jupyter_based.crt.pem diff --git a/README.md b/ReadMe.org similarity index 100% rename from README.md rename to ReadMe.org diff --git a/SimpleBlogServer.py b/SimpleBlogServer.py new file mode 100644 index 0000000..5b30fd1 --- /dev/null +++ b/SimpleBlogServer.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python + +import tornado.ioloop +import tornado.web +import tornado.websocket +import tornado.httpclient +import tornado.httpserver +import tornado.gen + +import os +from datetime import datetime + +import ContentConverter + +# +# Tornado handlers +# + +class HomeHandler(tornado.web.RequestHandler): + def get(self): + self.write(''' +

{}

+ '''.format("Hello, world!")) + +class BlogHandler(tornado.web.RequestHandler): + def get(self, request): + contentTitle = "Blog: " + request + renderedBody = ContentConverter.getRenderedBody(request) + if not renderedBody: + renderedBody = "

The post under '{}' does not exist.

".format(request) + + self.render("templates/BlogPost.html", title=contentTitle, postBody=renderedBody) + +# +# Startup +# + +def make_app(): + return tornado.web.Application([ + # Home page + (r'/', HomeHandler), + + (r'/blog/(.*)', BlogHandler), + + # # Handles messages for run script + # (r'/runScriptWebSocket', RunScriptWebSocket), + + # Upload handler + # (r'/upload', UploadHandler), + + # # Don't change this 'output' here without changing the other places as well + # (r'/output/(.*)', tornado.web.StaticFileHandler, {'path' : 'output'}), + + # Static files. Keep this at the bottom because it handles everything else + # TODO put these in a subdir so everything isn't accessible + (r'/webResources/(.*)', tornado.web.StaticFileHandler, {'path' : 'webResources'}), + ], + xsrf_cookies=True, + cookie_secret='this is my org blog') + +if __name__ == '__main__': + + # Before startup, convert anything which needs to be converted + ContentConverter.checkForContentChange() + + port = 8888 + print('\nStarting Simple Org Blog Server on port {}...'.format(port)) + app = make_app() + + # + # Notes on SSL + # + # Certificate generation (for localhost) (didn't actually work): + # https://medium.freecodecamp.org/how-to-get-https-working-on-your-local-development-environment-in-5-minutes-7af615770eec?gi=bd966500e56a + # Tornado instructions: + # https://stackoverflow.com/questions/18307131/how-to-create-https-tornado-server + # Note that I added the rootCA to Certificates trust in Firefox Preferences as well (didn't do anything) + # + # What I actually did: + # openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout certificates/server_jupyter_based.crt.key -out certificates/server_jupyter_based.crt.pem + # (from https://jupyter-notebook.readthedocs.io/en/latest/public_server.html) + # I then had to tell Firefox to trust this certificate even though it is self-signing (because + # I want a free certificate for this non-serious project) + useSSL = True + if useSSL: + app.listen(port, ssl_options={"certfile":"certificates/server_jupyter_based.crt.pem", + "keyfile":"certificates/server_jupyter_based.crt.key"}) + else: + # Show the warning only if SSL is not enabled + print('\n\tWARNING: Do NOT run this server on the internet (e.g. port-forwarded)' + ' nor when\n\t connected to an insecure LAN! It is not protected against malicious use.\n') + + app.listen(port) + + ioLoop = tornado.ioloop.IOLoop.current() + + # Periodically check to see if anything has changed which needs to be converted to .html + # Check every fifteen minutes + checkForContentChangeCallback = tornado.ioloop.PeriodicCallback(ContentConverter.checkForContentChange, + 15 * 60 * 1000) + checkForContentChangeCallback.start() + + ioLoop.start() diff --git a/content/TestPost.org b/content/TestPost.org new file mode 100644 index 0000000..50e613b --- /dev/null +++ b/content/TestPost.org @@ -0,0 +1,6 @@ +* Test post +This is a test post. + +This is an update. + +Here's another update diff --git a/templates/BlogPost.html b/templates/BlogPost.html new file mode 100644 index 0000000..9b64a6e --- /dev/null +++ b/templates/BlogPost.html @@ -0,0 +1,9 @@ + + + {{ title }} + + + + {{ postBody }} + + diff --git a/webResources/TestPost.html b/webResources/TestPost.html new file mode 100644 index 0000000..9d91c86 --- /dev/null +++ b/webResources/TestPost.html @@ -0,0 +1,4 @@ +

Test post

+

This is a test post.

+

This is an update.

+

Here's another update

diff --git a/webResources/styles.css b/webResources/styles.css new file mode 100644 index 0000000..4e32acd --- /dev/null +++ b/webResources/styles.css @@ -0,0 +1,29 @@ +body { + margin: 0 auto; + width: 80%; + height: 100%; + background-color: #333333; + max-width: 800px; +} + +p, +h1, +h2, +h3, +a, +li { + color: #cccccc; +} + +label { + font-size: large; + color: #cccccc; +} + +li { + margin-top: 0px; + margin-bottom: 0px; + margin-left: 20px; + font-size: small; + color: #aaaaaa; +}