commit 37f600d9c2ab35fd8a10066d60b04087e670701c
Author: Thomas Kolb
Date: Mon Jun 29 21:35:54 2020 +0200
Initial commit: the Python 2.7 version based on web.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.
+
+
+
+
+ $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.
+
+
+
+
+
+ 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)