diff options
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | db.py | 45 | ||||
-rwxr-xr-x | fbin.py | 267 | ||||
-rw-r--r-- | static/style.css | 20 | ||||
-rw-r--r-- | templates/base.tmpl | 43 | ||||
-rw-r--r-- | templates/help.tmpl | 10 | ||||
-rw-r--r-- | templates/login.tmpl | 14 | ||||
-rw-r--r-- | templates/my.tmpl | 14 | ||||
-rw-r--r-- | templates/register.tmpl | 16 | ||||
-rw-r--r-- | templates/upload.tmpl | 23 | ||||
-rw-r--r-- | templates/uploaded.tmpl | 11 |
11 files changed, 467 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..418bb19 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.swp +*.pyc +settings.py +/images @@ -0,0 +1,45 @@ +from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, Index, ForeignKey +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, relation, backref +from sqlalchemy.orm.exc import NoResultFound +from sqlalchemy.exc import IntegrityError +from sqlalchemy.sql import and_ +import settings + +engine = create_engine(settings.db_path) + +Base = declarative_base(bind = engine) + +class User(Base): + __tablename__ = 'users' + + id = Column(Integer, primary_key = True) + username = Column(String, unique = True, index = True) + password = Column(String) + files = relation('File', backref = 'user', order_by = 'File.date.desc()') + + def __init__(self, username, password): + self.username = username + self.password = password + +class File(Base): + __tablename__ = 'files' + + id = Column(Integer, primary_key = True) + hash = Column(String, unique = True, index = True) + filename = Column(String) + date = Column(DateTime) + user_id = Column(Integer, ForeignKey('users.id'), nullable = True) + + def __init__(self, hash, filename, date, user_id = None): + self.hash = hash + self.filename = filename + self.date = date + self.user_id = user_id + + def html(self): + return '<a href="/f/{hash}/{filename}">{filename}</a> on {date}'.format( + hash = self.hash, filename = self.filename, date = self.date.strftime('%Y-%m-%d %H:%M:%S UTC')) + +Base.metadata.create_all() +Session = sessionmaker(bind = engine, autoflush = True, autocommit = False) @@ -0,0 +1,267 @@ +#!/usr/bin/env python2 + +import templates +import settings, db, os, random, datetime, shutil, mimetypes, cgi, tempfile, hashlib, Cookie + +base62_alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + +if not os.path.isdir(settings.file_directory): + os.mkdir(settings.file_directory) + +class Application(object): + def get_user(self, username, password): + session = db.Session() + try: + user = session.query(db.User).filter(db.and_(db.User.username == username, db.User.password == password)).one() + except db.NoResultFound: + return None + finally: + session.close() + + return user + + def add_user(self, username, password): + session = db.Session() + try: + user = db.User(username, password) + session.add(user) + session.commit() + except db.IntegrityError: + return None + finally: + session.close() + + return user + + def get_file_path(self, hash): + session = db.Session() + try: + file = session.query(db.File).filter(db.File.hash == hash).one() + except db.NoResultFound: + return None + finally: + session.close() + + return os.path.join(settings.file_directory, hash + os.path.splitext(file.filename)[1]) + + def add_file(self, path, filename, user = None): + hash = ''.join(random.choice(base62_alphabet) for x in xrange(5)) + new_path = os.path.join(settings.file_directory, hash + os.path.splitext(filename)[1]) + shutil.copyfile(path, new_path) + + session = db.Session() + try: + file = db.File(hash, filename, datetime.datetime.utcnow(), user.id if user else None) + session.add(file) + session.commit() + finally: + session.close() + + return hash + + def get_files(self, user): + session = db.Session() + try: + session.add(user) + files = user.files + except db.NoResultFound: + return [] + finally: + session.close() + + return files + + def validate_cookie(self, cookie): + if not cookie: + return None + uid = int(cookie['uid'].value) + identifier = cookie['identifier'].value + + session = db.Session() + try: + user = session.query(db.User).filter(db.User.id == uid).one() + except db.NoResultFound: + return None + finally: + session.close() + + digest = hashlib.md5(str(uid) + user.password).hexdigest() + return user if (digest == identifier) else None + + def file(self, environ, start_response, path): + hash = path[1] + filename = self.get_file_path(hash) + if filename == None: + start_response('404 Not Found', [('Content-Type', 'text/html')]) + return ['<h1>Not Found</h1><p>The file you requested does not exist.</p>'] + + mime = mimetypes.guess_type(filename, strict = False)[0] or 'application/octet-stream' + + start_response('200 OK', [('Content-Type', mime), ('Content-Length', str(os.path.getsize(filename)))]) + return open(filename, 'rb') + + def upload(self, environ, start_response, path): + c = Cookie.SimpleCookie(environ['HTTP_COOKIE'] if 'HTTP_COOKIE' in environ else None) + user = self.validate_cookie(c) + form = cgi.FieldStorage(fp = environ['wsgi.input'], environ = environ) + if environ['REQUEST_METHOD'] != 'POST' or not 'file' in form or not 'filename' in form: + start_response('200 OK', [('Content-Type', 'text/html')]) + return str(templates.upload(searchList = {'user': user})) + + filename = form.getvalue('filename') + + temp = tempfile.NamedTemporaryFile(mode = 'wb', prefix = 'fbin', delete = True) + temp.write(form.getvalue('file')) + temp.flush() + + hash = self.add_file(temp.name, filename, user) + + temp.close() + + if 'redir' in form: + start_response('302 Found', [('Content-Type', 'text/html'), ('Location', '/f/{0}/{1}'.format(hash, filename))]) + return ['<a href="/f/{0}/{1}">{1}</a>'.format(hash, filename)] + else: + start_response('200 OK', [('Content-Type', 'text/html')]) + return str(templates.uploaded(searchList = { + 'user': user, + 'hash': hash, + 'filename': filename, + 'scheme': environ['wsgi.url_scheme'], + 'host': environ['HTTP_HOST'], + })) + + def login(self, environ, start_response, path): + c = Cookie.SimpleCookie(environ['HTTP_COOKIE'] if 'HTTP_COOKIE' in environ else None) + user = self.validate_cookie(c) + form = cgi.FieldStorage(fp = environ['wsgi.input'], environ = environ) + if environ['REQUEST_METHOD'] != 'POST' or not 'username' in form or not 'password' in form: + start_response('200 OK', [('Content-Type', 'text/html')]) + return str(templates.login(searchList = { + 'user': user, + 'error': None, + })) + + username = form.getvalue('username') + password = hashlib.md5(form.getvalue('password')).hexdigest() + + user = self.get_user(username, password) + + if user == None: + start_response('200 OK', [('Content-Type', 'text/html')]) + return str(templates.login(searchList = { + 'user': user, + 'error': 'Login failed', + })) + + c = Cookie.SimpleCookie() + c['uid'] = user.id + c['identifier'] = hashlib.md5(str(user.id) + password).hexdigest() + + dt = datetime.datetime.utcnow() + datetime.timedelta(days = 30) + expires = dt.strftime('%a, %d-%b-%y %H:%M:%S GMT') + c['uid']['expires'] = expires + c['identifier']['expires'] = expires + + start_response('302 Found', [('Location', '/u'), ('Set-Cookie', c['uid'].OutputString()), ('Set-Cookie', c['identifier'].OutputString())]) + return [] + + def register(self, environ, start_response, path): + c = Cookie.SimpleCookie(environ['HTTP_COOKIE'] if 'HTTP_COOKIE' in environ else None) + user = self.validate_cookie(c) + form = cgi.FieldStorage(fp = environ['wsgi.input'], environ = environ) + if environ['REQUEST_METHOD'] != 'POST' or not 'username' in form or not 'password' in form or not 'password2' in form: + start_response('200 OK', [('Content-Type', 'text/html')]) + return str(templates.register(searchList = { + 'user': user, + 'error': None, + })) + + username = form.getvalue('username') + password = form.getvalue('password') + password2 = form.getvalue('password2') + if password != password2: + start_response('200 OK', [('Content-Type', 'text/html')]) + return str(templates.register(searchList = { + 'user': user, + 'error': 'Passwords doesn\'t match', + })) + + user = self.add_user(username, hashlib.md5(password).hexdigest()) + if not user: + start_response('200 OK', [('Content-Type', 'text/html')]) + return str(templates.register(searchList = { + 'user': None, + 'error': 'Username already taken.', + })) + + start_response('302 Found', [('Location', '/l')]) + return [] + + def logout(self, environ, start_response, path): + c = Cookie.SimpleCookie() + expires = datetime.datetime.utcfromtimestamp(0).strftime('%a, %d-%b-%y %H:%M:%S GMT') + c['uid'] = 0 + c['uid']['expires'] = expires + c['identifier'] = '' + c['identifier']['expires'] = expires + start_response('302 Found', [('Set-Cookie', c['uid'].OutputString()), ('Set-Cookie', c['identifier'].OutputString()), ('Location', '/')]) + return [] + + def static(self, environ, start_response, path): + filename = path[1] + if not filename in ('style.css',): + start_response('404 Not Found', []) + return [] + + mime = mimetypes.guess_type(filename, strict = False)[0] or 'application/octet-stream' + start_response('200 OK', [('Content-Type', mime)]) + return open('static/' + filename, 'rb') + + def help(self, environ, start_response, path): + c = Cookie.SimpleCookie(environ['HTTP_COOKIE'] if 'HTTP_COOKIE' in environ else None) + user = self.validate_cookie(c) + start_response('200 OK', [('Content-Type', 'text/html')]) + return str(templates.help(searchList = { + 'user': user, + 'scheme': environ['wsgi.url_scheme'], + 'host': environ['HTTP_HOST'], + })) + + def my_files(self, environ, start_response, path): + c = Cookie.SimpleCookie(environ['HTTP_COOKIE'] if 'HTTP_COOKIE' in environ else None) + user = self.validate_cookie(c) + if user == None: + start_response('200 OK', [('Content-Type', 'text/html')]) + return ['Not logged in.'] + files = self.get_files(user) + start_response('200 OK', [('Content-Type', 'text/html')]) + return str(templates.my(searchList = { + 'user': user, + 'files': files, + })) + + f = file + u = upload + l = login + s = static + h = help + m = my_files + o = logout + r = register + + def __call__(self, environ, start_response): + path = environ['PATH_INFO'].split('/')[1:] + module = path[0] + if len(module) and module in 'fulshmor': + return getattr(self, module)(environ, start_response, path) + else: + start_response('302 Found', [('Location', '/u')]) + return [] + +if __name__ == '__main__': + from wsgiref.simple_server import make_server, WSGIServer + # enable IPv6 + WSGIServer.address_family |= 10 + http = make_server('', 8000, Application()) + http.serve_forever() diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..f281482 --- /dev/null +++ b/static/style.css @@ -0,0 +1,20 @@ +* { margin: 0px; padding: 0px; font-family: sans-serif; } +html, body { background-color: #fff; color: #333; height: 100%; } +div#page-header { background-color: #ccc; color: #333; padding: 1em; } +div#page-header a { color: inherit; text-decoration: none; } +div#page { min-height: 100%; height: auto !important; height: 100%; margin: 0 auto -2em; } +div#page-content { padding: 1em; } +div#page-wrapper { height: 2em; } +div#page-footer { background-color: #333; color: #666; height: 1em; padding: .5em; text-align: center; } +ul { list-style-type: none; } +li { margin-left: 1em; } +p { margin-bottom: 1em; } +code { font-family: monospace; margin: 1em; display: block; } + +.error { color: red; } + +p.login { font-size: small; margin: .5em 0 0 1em; } +form { border: 1px solid #888; display: inline-block; margin: .5em; padding: .5em; } + +div#page-footer ul, div#page-footer li { margin: 0; display: inline; } +div#page-footer a { color: #ccc; text-decoration: none; margin: 0 .5em 0 .5em; } diff --git a/templates/base.tmpl b/templates/base.tmpl new file mode 100644 index 0000000..5b710f8 --- /dev/null +++ b/templates/base.tmpl @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <title>$title</title> + <link rel="StyleSheet" href="/s/style.css" type="text/css" /> +#block head +#end block + </head> + <body> + <div id="page"> + <div id="page-header"> + <h1><a href="/">$header</a></h1> + </div> + <p class="login">#slurp +#if $user +Logged in as $user.username.#slurp +#else +Not logged in.#slurp +#end if +</p> + <div id="page-content"> +#block content +#end block + <div id="page-wrapper"></div> + </div> + </div> + <div id="page-footer"> + <ul> + <li><a href="/u">upload</a></li> +#if $user + <li><a href="/o">logout</a></li> + <li><a href="/m">myfiles</a></li> +#else + <li><a href="/l">login</a></li> + <li><a href="/r">register</a></li> +#end if + <li><a href="/h">help</a></li> + </ul> + </div> + </body> +</html> diff --git a/templates/help.tmpl b/templates/help.tmpl new file mode 100644 index 0000000..48d12bc --- /dev/null +++ b/templates/help.tmpl @@ -0,0 +1,10 @@ +#def title: help +#def header: help +#extends templates.base +#def content + <p>Usage: POST to <a href="$scheme://$host/u">$scheme://$host/u</a> with filedata given to "file" and original filename to "filename". + Login is sent by cookies with user id in "uid" and an identifier which is md5(uid+md5(password)).</p> + <p>cURL example: + <code>curl -b 'uid=42; identifier=3858f62230ac3c915f300c664312c63f' -F 'file=@image.png' -F 'filename=image.png' http://myhost/u</code> + Here user id is 42 and the password is "foobar".</p> +#end def diff --git a/templates/login.tmpl b/templates/login.tmpl new file mode 100644 index 0000000..6baee6c --- /dev/null +++ b/templates/login.tmpl @@ -0,0 +1,14 @@ +#def title: login +#def header: login +#extends templates.base +#def content +#set error = $error or '' + <div class="error">$error</div> + <form method="post" action="/l"> + <p>username</p> + <p><input type="text" id="username" name="username" /></p> + <p>password</p> + <p><input type="password" id="password" name="password" /></p> + <p><input type="submit" value="Upload" /></p> + </form> +#end def diff --git a/templates/my.tmpl b/templates/my.tmpl new file mode 100644 index 0000000..b1fb2cc --- /dev/null +++ b/templates/my.tmpl @@ -0,0 +1,14 @@ +#def title: myfiles +#def header: myfiles +#extends templates.base +#def content +<p>Your uploads:</p> +<ul> +#for file in $files + <li>$file.html</li> +#end for +#if not len($files) + <li><em>(No file uploads yet.)</em></li> +#end if +</ul> +#end def diff --git a/templates/register.tmpl b/templates/register.tmpl new file mode 100644 index 0000000..2a9d1a9 --- /dev/null +++ b/templates/register.tmpl @@ -0,0 +1,16 @@ +#def title: register +#def header: register +#extends templates.base +#def content +#set error = $error or '' + <div class="error">$error</div> + <form method="post" action="/r"> + <p>username</p> + <p><input type="text" id="username" name="username" /></p> + <p>password</p> + <p><input type="password" id="password" name="password" /></p> + <p>repeat password</p> + <p><input type="password" id="password2" name="password2" /></p> + <p><input type="submit" value="Register" /></p> + </form> +#end def diff --git a/templates/upload.tmpl b/templates/upload.tmpl new file mode 100644 index 0000000..49b0212 --- /dev/null +++ b/templates/upload.tmpl @@ -0,0 +1,23 @@ +#def title: upload +#def header: upload +#extends templates.base +#def head + <script type="text/javascript"> + function file_changed() { + s = document.getElementById('file').value; + i = s.lastIndexOf('/'); + if(i < 0) + i = s.lastIndexOf('\\'); + if(i >= 0) + s = s.substr(i+1); + document.getElementById('filename').value = s; + } + </script> +#end def +#def content + <form method="post" action="/u" enctype="multipart/form-data"> + <input type="hidden" id="filename" name="filename" /> + <p><input type="file" id="file" name="file" onchange="file_changed()" /></p> + <p><input type="submit" value="Upload" /></p> + </form> +#end def diff --git a/templates/uploaded.tmpl b/templates/uploaded.tmpl new file mode 100644 index 0000000..e36fc53 --- /dev/null +++ b/templates/uploaded.tmpl @@ -0,0 +1,11 @@ +#def title: upload +#def header: upload +#extends templates.base +#def content + <p>Your file has been uploaded: <a href="/f/$hash/$filename">$scheme://$host/f/$hash/$filename</a>. +#if $user + <p>Your file will also appear in <a href="/m">your file list</a>.</p> +#else + <p>If you were <a href="/r">registered</a> and <a href="/l">logged in</a>, your file would also appear in <a href="/m">your file list</a>.</p> +#end if +#end def |