From 37f600d9c2ab35fd8a10066d60b04087e670701c Mon Sep 17 00:00:00 2001 From: Thomas Kolb Date: Mon, 29 Jun 2020 21:35:54 +0200 Subject: [PATCH] Initial commit: the Python 2.7 version based on web.py --- config.py.template | 19 +++ main.py | 265 +++++++++++++++++++++++++++++++++++++++++ paste.wsgi | 4 + setup.sql | 7 ++ templates/cleanup.html | 29 +++++ templates/index.html | 49 ++++++++ utils.py | 7 ++ 7 files changed, 380 insertions(+) create mode 100644 config.py.template create mode 100755 main.py create mode 100644 paste.wsgi create mode 100644 setup.sql create mode 100644 templates/cleanup.html create mode 100644 templates/index.html create mode 100644 utils.py diff --git a/config.py.template b/config.py.template new file mode 100644 index 0000000..07f325a --- /dev/null +++ b/config.py.template @@ -0,0 +1,19 @@ +import os + +CONF_INSTALL_DIR = os.path.dirname(os.path.abspath(__file__)) + +CONF_DATA_DIR = os.path.join(CONF_INSTALL_DIR, "pastes") +CONF_TEMPLATE_DIR = os.path.join(CONF_INSTALL_DIR, "templates") + +CONF_MAX_UPLOAD_SIZE = 256 * 1024 * 1024 + +CONF_DEFAULT_TYPE = "text/plain; charset=utf-8" +CONF_DEFAULT_CHARSET = "utf-8" + +CONF_TIME_FORMAT = "%c" + +CONF_SQL_DRIVER = "mysql" +CONF_SQL_USER = "pastebin" +CONF_SQL_PASSWORD = "T0pS3cret!!1" +CONF_SQL_DATABASE = "pastebin" +CONF_SQL_FILESTABLE = "pastebin_files" diff --git a/main.py b/main.py new file mode 100755 index 0000000..8837fa1 --- /dev/null +++ b/main.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python2 +# -- coding: utf-8 -- + +import web +import cgi +web.config.debug = True + +import sys +import os +import time + +import shutil +import hashlib +import magic +import mimetypes +from base64 import urlsafe_b64encode +from random import random +from StringIO import StringIO + +sys.path.append(os.path.dirname(__file__)) +from config import * +import utils + +render = web.template.render(CONF_TEMPLATE_DIR) +cgi.maxlen = CONF_MAX_UPLOAD_SIZE + +urls = ( + '/', 'Greeter', + '/p', 'Pastebin', + '/info', 'FileInfo', + '/cleanup', 'Cleanup', + '/([a-zA-Z0-9_.-]*)', 'Pastebin' + ) + +class Greeter: + def GET(self): + web.header("Content-Type", 'text/html') + return render.index(web.ctx.host, CONF_MAX_UPLOAD_SIZE) + +class Pastebin: + def __init__(self): + self.db = web.database( + dbn = CONF_SQL_DRIVER, + db = CONF_SQL_DATABASE, + user = CONF_SQL_USER, + pw = CONF_SQL_PASSWORD) + + self.magic = magic.Magic(mime = True) + + # this function returns (mime type, file extension) for the file with the + # given name or None, if the type is unknown + def detect_file_type(self, filename): + mime = self.magic.from_file(filename) + + # prevent weird extensions like ".ksh" for plain-text + if mime == "text/plain": + extension = ".txt" + else: + extension = mimetypes.guess_extension(mime) + return mime, extension + + def POST(self, doc = None): + web.header("Content-Type", 'text/plain') + + try: + x = web.input(content = {}) + except ValueError: + return "File too large!" + + if 'content' in x: + # open a temporary file + tempFileName = "/tmp/pastebin_" + web.ctx.ip + "_" + str(random()) + tempFile = open(tempFileName, "wb") + + # calculate md5 hash of the uploaded data while saving it to the + # temporary file + m = hashlib.md5() + for line in x['content'].file: + m.update(line) + tempFile.write(line) + + tempFile.close() + + # generate the paste's id + pasteid = urlsafe_b64encode(m.digest()[0:9]) + storeName = CONF_DATA_DIR + "/" + pasteid + + # get the mime type and extension from the data + mimeType, ext = self.detect_file_type(tempFileName) + if not ext: + ext = "" + + if not os.path.exists(storeName): + # this file does not yet exist + + # create the file's database entry + self.db.insert(CONF_SQL_FILESTABLE, + hash = pasteid, + detected_type = mimeType, + create_time = time.time(), + access_time = time.time()) + + # store the document permanently + shutil.move(tempFileName, storeName); + else: + # this file was uploaded before, so we just delete the temporary + # file + os.unlink(tempFileName); + + # return the generated URL + return "http://" + web.ctx.host + "/" + pasteid + ext + "\n" + else: + return "Nothing uploaded." + + def GET(self, doc = None): + if doc: + dotindex = doc.find(".") + if dotindex != -1: + fileName = doc[:dotindex] + fileExt = doc[dotindex+1:] + else: + fileName = doc + fileExt = None + + # set the content type from the user-specified file extension + mimeType, encoding = mimetypes.guess_type(doc); + if mimeType: + if mimeType[0:4] == "text": + mimeType += "; charset=" + CONF_DEFAULT_CHARSET + + web.header("Content-Type", mimeType) + if encoding: + web.header("Content-Encoding", encoding) + else: + web.header("Content-Type", CONF_DEFAULT_TYPE) + + storeName = CONF_DATA_DIR + "/" + fileName + + try: + f = open(storeName, "rb") + for line in f: + yield line + f.close() + + # update the corresponding access time and count + items = self.db.select(CONF_SQL_FILESTABLE, + where = "hash=" + web.db.sqlquote(fileName), + what = "access_count") + + if items: + oldcount = items[0]['access_count'] + + self.db.update(CONF_SQL_FILESTABLE, + where = "hash=" + web.db.sqlquote(fileName), + access_time = time.time(), + access_count = oldcount + 1) + else: + # The count could not be read. + # Probably there is no entry for this file -> create one + self.db.insert(CONF_SQL_FILESTABLE, + hash = fileName, + detected_type = mimeType, + create_time = os.path.getmtime(storeName), + access_time = time.time(), + access_count = 1) + except IOError: + yield "Cannot read \"" + fileName + "\"." + + else: + raise web.seeother("/") + +class FileInfo: + def __init__(self): + self.db = web.database( + dbn = CONF_SQL_DRIVER, + db = CONF_SQL_DATABASE, + user = CONF_SQL_USER, + pw = CONF_SQL_PASSWORD) + + def GET(self, doc = None): + web.header("Content-Type", 'text/plain') + + # generate a list of all files + items = self.db.select(CONF_SQL_FILESTABLE, + order = "access_time DESC") + + if not items: + return "There are no files in the database." + else: + # table header + outbuf = StringIO() + outbuf.write("%-12s | %-20s | %-8s | %-25s | %-16s | %-10s |\n" % ( + "Hash", "Detected Type", "Size", "Created", "Last Access", "# Accessed")) + outbuf.write("-"*12 + "-+-" + "-"*20 + "-+-" + "-"*8 + "-+-" + "-"*25 + + "-+-" + "-"*16 + "-+-" + "-"*10 + "-|\n") + + now = time.time() + + for i in items: + # determine file size (and existance) + storeName = CONF_DATA_DIR + "/" + i["hash"] + try: + fileSize = os.path.getsize(storeName) + sizeStr = utils.readable_size(fileSize) + except OSError: + sizeStr = "N/A" + + lastacc_delay = (now - i['access_time']) + lastacc_days = lastacc_delay / 86400 + lastacc_hours = (lastacc_delay / 3600) % 24 + lastacc_minutes = (lastacc_delay / 60) % 60 + + outbuf.write("%12s | %-20s | %8s | %25s | %16s | %10u |\n" % ( + i["hash"], + i["detected_type"], + sizeStr, + time.strftime(CONF_TIME_FORMAT, time.localtime(i['create_time'])), + "%ud, %2u:%02u ago" % (lastacc_days, lastacc_hours, lastacc_minutes), + i['access_count'])) + return outbuf.getvalue() + +class Cleanup: + def __init__(self): + self.db = web.database( + dbn = CONF_SQL_DRIVER, + db = CONF_SQL_DATABASE, + user = CONF_SQL_USER, + pw = CONF_SQL_PASSWORD) + + def GET(self): + web.header("Content-Type", 'text/html') + + getdata = web.input(doit = False) + + if not getdata.doit: + return render.cleanup(stage = 1, result = None) + else: + # generate a list of all files + items = self.db.select(CONF_SQL_FILESTABLE, + order = "access_time DESC") + + if not items: + return render.cleanup(stage = 2, result = "There are no files in the database.") + else: + deleted_entries = 0 + for i in items: + # determine file existance + storeName = CONF_DATA_DIR + "/" + i["hash"] + if not os.path.exists(storeName): + # this entry has no corresponding file -> delete it + deleted_entries += self.db.delete( + CONF_SQL_FILESTABLE, + where = "hash=" + web.db.sqlquote(i["hash"])) + + return render.cleanup( + stage = 2, + result = "Cleanup successful, " + str(deleted_entries) + " entries deleted.") + + +app = web.application(urls, globals()) + +if __name__ == "__main__": + app.run() + +application = app.wsgifunc() diff --git a/paste.wsgi b/paste.wsgi new file mode 100644 index 0000000..273f9d4 --- /dev/null +++ b/paste.wsgi @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import main +application = main.app.wsgifunc() diff --git a/setup.sql b/setup.sql new file mode 100644 index 0000000..44a8a43 --- /dev/null +++ b/setup.sql @@ -0,0 +1,7 @@ +CREATE TABLE files ( + hash CHAR(12) NOT NULL, + detected_type VARCHAR(64) NOT NULL, + create_time BIGINT UNSIGNED NOT NULL, + access_time BIGINT UNSIGNED NOT NULL, + access_count INT UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY(hash)); diff --git a/templates/cleanup.html b/templates/cleanup.html new file mode 100644 index 0000000..26e9c88 --- /dev/null +++ b/templates/cleanup.html @@ -0,0 +1,29 @@ +$def with (stage, result) + + + + + Pastebin - Cleanup + + + +

Pastebin - Cleanup

+ $if stage == 1: +

+ This will delete all database entries of files which do not exist any + more. +

+

+

+ Do you want to clean the database? + + +
+

+ $else: +

+ Result of cleanup: $result +

+ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..a6c5d39 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,49 @@ +$def with (host, maxsize) + + + + + Pastebin + + + +

Pastebin

+

+ Welcome to my Pastebin. You can upload any file here and a short link + will be generated that you can send to others. +

+

+

+ Select a file to upload: + + +
+

+
+

Additional Hints

+

Here are some hints for features which may not be obvious:

+ + + diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..33a43fd --- /dev/null +++ b/utils.py @@ -0,0 +1,7 @@ +def readable_size(size): + suffixes = "BKMGTP" + for suffix in suffixes: + if size < 1024: + return "%3.1f%c" % (size, suffix) + size /= 1024.0 + return "%3.1f%c" % (size, suffix)