Initial commit: the Python 2.7 version based on web.py

This commit is contained in:
Thomas Kolb 2020-06-29 21:35:54 +02:00
commit 37f600d9c2
7 changed files with 380 additions and 0 deletions

19
config.py.template Normal file
View file

@ -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"

265
main.py Executable file
View file

@ -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()

4
paste.wsgi Normal file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import main
application = main.app.wsgifunc()

7
setup.sql Normal file
View file

@ -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));

29
templates/cleanup.html Normal file
View file

@ -0,0 +1,29 @@
$def with (stage, result)
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Pastebin - Cleanup</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" >
</head>
<body>
<h1>Pastebin - Cleanup</h1>
$if stage == 1:
<p>
This will delete all database entries of files which do not exist any
more.
</p>
<p>
<form action="/cleanup" method="GET">
Do you want to clean the database?
<input type="hidden" name="doit" value="True">
<input type="submit" value="Yes, I want!">
</form>
</p>
$else:
<p>
Result of cleanup: $result
</p>
</body>
</html>

49
templates/index.html Normal file
View file

@ -0,0 +1,49 @@
$def with (host, maxsize)
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Pastebin</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" >
</head>
<body>
<h1>Pastebin</h1>
<p>
Welcome to my Pastebin. You can upload any file here and a short link
will be generated that you can send to others.
</p>
<p>
<form action="/p" method="POST" enctype="multipart/form-data">
Select a file to upload:
<input type="file" name="content">
<input type="submit" value="Upload">
</form>
</p>
<hr>
<h2>Additional Hints</h2>
<p>Here are some hints for features which may not be obvious:</p>
<ul>
<li>
You can use this tool directly from your command line using
<a href="http://curl.haxx.se">curl</a>. Its easiest when you define an
alias like this:
<pre>alias paste="curl -F 'content=@-' http://$host/p"</pre>
Then you can do something like
<pre>paste &lt; some_file</pre>
and the link to your file will be printed directly to your shell.
</li>
<li>
The server will try to detect the type of the uploaded data
automatically and generate an appropriate extension for the file. If
the servers guess about the extension is wrong, you can change it.
The server will then set a MIME-Type associated with the file
extension on retrieval.
</li>
<li>
The maximum upload size is currently set to $maxsize bytes.
</li>
</ul>
</body>
</html>

7
utils.py Normal file
View file

@ -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)