From 4989a8e572ea666d3e392a503ee6831b8a9386f9 Mon Sep 17 00:00:00 2001 From: Jon Bergli Heier Date: Tue, 15 Feb 2011 23:04:18 +0100 Subject: Initial commit. --- .gitignore | 4 + db.py | 45 ++++++++ fbin.py | 267 ++++++++++++++++++++++++++++++++++++++++++++++++ static/style.css | 20 ++++ templates/base.tmpl | 43 ++++++++ templates/help.tmpl | 10 ++ templates/login.tmpl | 14 +++ templates/my.tmpl | 14 +++ templates/register.tmpl | 16 +++ templates/upload.tmpl | 23 +++++ templates/uploaded.tmpl | 11 ++ 11 files changed, 467 insertions(+) create mode 100644 .gitignore create mode 100644 db.py create mode 100755 fbin.py create mode 100644 static/style.css create mode 100644 templates/base.tmpl create mode 100644 templates/help.tmpl create mode 100644 templates/login.tmpl create mode 100644 templates/my.tmpl create mode 100644 templates/register.tmpl create mode 100644 templates/upload.tmpl create mode 100644 templates/uploaded.tmpl 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 diff --git a/db.py b/db.py new file mode 100644 index 0000000..762fc07 --- /dev/null +++ b/db.py @@ -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 '{filename} 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) diff --git a/fbin.py b/fbin.py new file mode 100755 index 0000000..9a1a4df --- /dev/null +++ b/fbin.py @@ -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 ['

Not Found

The file you requested does not exist.

'] + + 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 ['{1}'.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 @@ + + + + + $title + +#block head +#end block + + +
+ + +
+#block content +#end block +
+
+
+ + + 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 +

Usage: POST to $scheme://$host/u 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)).

+

cURL example: + curl -b 'uid=42; identifier=3858f62230ac3c915f300c664312c63f' -F 'file=@image.png' -F 'filename=image.png' http://myhost/u + Here user id is 42 and the password is "foobar".

+#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 '' +
$error
+
+

username

+

+

password

+

+

+
+#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 +

Your uploads:

+ +#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 '' +
$error
+
+

username

+

+

password

+

+

repeat password

+

+

+
+#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 + +#end def +#def content +
+ +

+

+
+#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 +

Your file has been uploaded: $scheme://$host/f/$hash/$filename. +#if $user +

Your file will also appear in your file list.

+#else +

If you were registered and logged in, your file would also appear in your file list.

+#end if +#end def -- cgit v1.2.3