forked from matthoskins1980/Flask-Tus
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4147ced
commit f00a9a9
Showing
7 changed files
with
922 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
Oops, something went wrong.