summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJon Bergli Heier <snakebite@jvnv.net>2011-02-15 23:04:18 +0100
committerJon Bergli Heier <snakebite@jvnv.net>2011-02-15 23:04:18 +0100
commit4989a8e572ea666d3e392a503ee6831b8a9386f9 (patch)
treef1cf0bf4e6744acb45ad3cbd15023916762ecf37
Initial commit.
-rw-r--r--.gitignore4
-rw-r--r--db.py45
-rwxr-xr-xfbin.py267
-rw-r--r--static/style.css20
-rw-r--r--templates/base.tmpl43
-rw-r--r--templates/help.tmpl10
-rw-r--r--templates/login.tmpl14
-rw-r--r--templates/my.tmpl14
-rw-r--r--templates/register.tmpl16
-rw-r--r--templates/upload.tmpl23
-rw-r--r--templates/uploaded.tmpl11
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
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 '<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)
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 ['<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