Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
matthoskins1980 committed Feb 4, 2016
1 parent 4147ced commit f00a9a9
Show file tree
Hide file tree
Showing 7 changed files with 922 additions and 0 deletions.
20 changes: 20 additions & 0 deletions demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from flask import Flask, render_template, send_from_directory
from flask.ext.tus import tus_manager
import os

app = Flask(__name__)
tm = tus_manager(app)

@app.route("/demo")
def demo():
return render_template("demo.html")

# serve the uploaded files
@app.route('/uploads/<path:filename>', methods=['GET'])
def download(filename):
uploads = os.path.join(app.root_path, app.config['TUS_UPLOADSDIR'])
return send_from_directory(directory=uploads, filename=filename)

if __name__ == '__main__':
app.run( host='0.0.0.0', debug=True )

184 changes: 184 additions & 0 deletions flask_tus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
from flask import request, jsonify, make_response, current_app
import base64
import os
import redis
import uuid

# Find the stack on which we want to store the database connection.
# Starting with Flask 0.9, the _app_ctx_stack is the correct one,
# before that we need to use the _request_ctx_stack.
try:
from flask import _app_ctx_stack as stack
except ImportError:
from flask import _request_ctx_stack as stack

class tus_manager(object):

def __init__(self, app=None):
self.app = app
if app is not None:
self.init_app(app)

def init_app(self, app):
app.config.setdefault('TUS_ROOTDIR', '/file-upload')
app.config.setdefault('TUS_UPLOADSDIR', 'uploads')

self.tus_api_version = '1.0.0'
self.tus_api_version_supported = '1.0.0'
self.tus_api_extensions = ['creation', 'termination']
self.tus_max_file_size = 4294967296 # 4GByte

# Use the newstyle teardown_appcontext if it's available,
# otherwise fall back to the request context
if hasattr(app, 'teardown_appcontext'):
app.teardown_appcontext(self.teardown)
else:
app.teardown_request(self.teardown)

# register the two file upload endpoints
app.add_url_rule(app.config['TUS_ROOTDIR'], 'file-upload', self.tus_file_upload, methods=['OPTIONS', 'POST'])
app.add_url_rule('{}/<resource_id>'.format( app.config['TUS_ROOTDIR'] ), 'file-upload-chunk', self.tus_file_upload_chunk, methods=['HEAD', 'PATCH', 'DELETE'])

# handle redis server connection
def redis_connect(self):
return redis.Redis()

# handle teardown of redis connection
def teardown(self, app):
# ctx = stack.top
# if hasattr(ctx, 'tus_redis'):
# ctx.tus_redis.disconnect()
pass

@property
def redis_connection(self):
ctx = stack.top
if ctx is not None:
if not hasattr(ctx, 'tus_redis'):
ctx.tus_redis = self.redis_connect()
return ctx.tus_redis


def tus_file_upload(self):
response = make_response("", 200)

if request.headers.get("Tus-Resumable") is not None:
response.headers['Tus-Resumable'] = self.tus_api_version
response.headers['Tus-Version'] = self.tus_api_version_supported

if request.method == 'OPTIONS':
response.headers['Tus-Extension'] = ",".join(self.tus_api_extensions)
response.headers['Tus-Max-Size'] = self.tus_max_file_size

response.status_code = 204
return response

# process upload metadata
metadata = {}
for kv in request.headers.get("Upload-Metadata", None).split(","):
(key, value) = kv.split(" ")
metadata[key] = base64.b64decode(value)

file_size = int(request.headers.get("Upload-Length", "0"))
resource_id = uuid.uuid4()

p = self.redis_connection.pipeline()
p.setex("file-uploads/{}/filename".format(resource_id), "{}".format(metadata.get("filename")), 3600)
p.setex("file-uploads/{}/file_size".format(resource_id), file_size, 3600)
p.setex("file-uploads/{}/offset".format(resource_id), 0, 3600)
p.setex("file-uploads/{}/upload-metadata".format(resource_id), request.headers.get("Upload-Metadata"), 3600)
p.execute()

try:
f = open("{}/{}".format(self.app.config['TUS_UPLOADSDIR'], resource_id), "wb")
f.seek( file_size - 1)
f.write("\0")
f.close()
except IOError as e:
self.app.logger.error("Unable to create file: {}".format(e))
response.status_code = 500
return response

response.status_code = 201
response.headers['Location'] = '{}/{}'.format(self.app.config['TUS_ROOTDIR'], resource_id)
response.autocorrect_location_header = False

else:
self.app.logger.warning("Received File upload for unsupported file transfer protocol")
response.data = "Received File upload for unsupported file transfer protocol"
response.status_code = 500

return response

def tus_file_upload_chunk(self, resource_id):
response = make_response("", 204)
response.headers['Tus-Resumable'] = self.tus_api_version
response.headers['Tus-Version'] = self.tus_api_version_supported

offset = self.redis_connection.get("file-uploads/{}/offset".format( resource_id ))
self.app.logger.info( offset );

if request.method == 'HEAD':
offset = self.redis_connection.get("file-uploads/{}/offset".format( resource_id ))
if offset is None:
response.status_code = 404
return response

else:
response.status_code = 200
response.headers['Upload-Offset'] = offset
response.headers['Cache-Control'] = 'no-store'

return response

if request.method == 'DELETE':
os.unlink("{}/{}".format( self.app.config['TUS_UPLOADSDIR'], resource_id))

p = self.redis_connection.pipeline()
p.delete("file-uploads/{}/filename".format(resource_id))
p.delete("file-uploads/{}/file_size".format(resource_id))
p.delete("file-uploads/{}/offset".format(resource_id))
p.delete("file-uploads/{}/upload-metadata".format(resource_id))
p.execute()

response.status_code = 204
return respose

filename = self.redis_connection.get("file-uploads/{}/filename".format( resource_id ))
if filename is None or os.path.lexists("{}/{}".format(self.app.config['TUS_UPLOADSDIR'], resource_id )) is False:
response.status_code = 410
return response

file_offset = int(request.headers.get("Upload-Offset", 0))
chunk_size = int(request.headers.get("Content-Length", 0))
file_size = int( self.redis_connection.get( "file-uploads/{}/file_size".format( resource_id )) )

if request.headers.get("Upload-Offset") != self.redis_connection.get( "file-uploads/{}/offset".format( resource_id )): # check to make sure we're in sync
response.status_code = 409 # HTTP 409 Conflict
return response

try:
f = open( "{}/{}".format(self.app.config['TUS_UPLOADSDIR'], resource_id), "r+b")
except IOError:
f = open( "{}/{}".format(self.app.config['TUS_UPLOADSDIR'], resource_id), "wb")
finally:
f.seek( file_offset )
f.write(request.data)
f.close()

new_offset = self.redis_connection.incrby( "file-uploads/{}/offset".format( resource_id ), chunk_size)
response.headers['Upload-Offset'] = new_offset

if file_size == new_offset: # file transfer complete, rename from resource id to actual filename
filename_parts = os.path.splitext(filename)
counter = 1
while True:
if os.path.lexists( "{}/{}".format(self.app.config['TUS_UPLOADSDIR'], filename)):
filename = "{}{}.{}".format( filename_parts[0], filename_parts[1], counter )
counter += 1
else:
break

os.rename( "{}/{}".format( self.app.config['TUS_UPLOADSDIR'], resource_id ), "{}/{}".format( self.app.config['TUS_UPLOADSDIR'], filename ))

return response
37 changes: 37 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
Flask-Tus
-------------
Implements the tus.io server-side file-upload protocol
visit http://tus.io for more information
"""
from setuptools import setup


setup(
name='Flask-Tus',
version='0.1.0',
url='http://github.com/matthoskins1980/Flask-Tus/',
license='MIT',
author='Matt Hoskins',
author_email='[email protected]',
description='TUS protocol implementation',
long_description=__doc__,
py_modules=['flask_tus'],
zip_safe=False,
include_package_data=True,
platforms='any',
install_requires=[
'Flask',
'Redis'
],
classifiers=[
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Software Development :: Libraries :: Python Modules'
]
)
11 changes: 11 additions & 0 deletions static/css/demo.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
body {
padding-top: 40px;
}

.progress {
height: 32px;
}

a.btn {
margin-bottom: 2px;
}
68 changes: 68 additions & 0 deletions static/js/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
var upload = null
var stopBtn = document.querySelector("#stop-btn")
var resumeCheckbox = document.querySelector("#resume")
var input = document.querySelector("input[type=file]")
var progress = document.querySelector(".progress")
var progressBar = progress.querySelector(".bar")
var alertBox = document.querySelector("#support-alert")
var chunkInput = document.querySelector("#chunksize")
var endpointInput = document.querySelector("#endpoint")

if (!tus.isSupported) {
alertBox.className = alertBox.className.replace("hidden", "")
}

stopBtn.addEventListener("click", function(e) {
e.preventDefault()

if (upload) {
upload.abort()
}
})

input.addEventListener("change", function(e) {
var file = e.target.files[0]
console.log("selected file", file)

stopBtn.classList.remove("disabled")
var endpoint = endpointInput.value
var chunkSize = parseInt(chunkInput.value, 10)
if (isNaN(chunkSize)) {
chunkSize = Infinity
}

var options = {
endpoint: endpoint,
resume: !resumeCheckbox.checked,
chunkSize: chunkSize,
metadata: {
filename: file.name
},
onError: function(error) {
reset()
alert("Failed because: " + error)
},
onProgress: function(bytesUploaded, bytesTotal) {
var percentage = (bytesUploaded / bytesTotal * 100).toFixed(2)
progressBar.style.width = percentage + "%"
console.log(bytesUploaded, bytesTotal, percentage + "%")
},
onSuccess: function() {
reset()
var anchor = document.createElement("a")
anchor.textContent = "Download " + upload.file.name + " (" + upload.file.size + " bytes)"
anchor.href = "/uploads/" + upload.file.name
anchor.className = "btn btn-success"
e.target.parentNode.appendChild(anchor)
}
}

upload = new tus.Upload(file, options)
upload.start()
})

function reset() {
input.value = ""
stopBtn.classList.add("disabled")
progress.classList.remove("active")
}
Loading

0 comments on commit f00a9a9

Please sign in to comment.