diff --git a/samples/python-flask/app.py b/samples/python-flask/app.py new file mode 100644 index 00000000..b614bd25 --- /dev/null +++ b/samples/python-flask/app.py @@ -0,0 +1,100 @@ +from flask import Flask, render_template, request, abort, make_response, jsonify +import os + +app = Flask(__name__, static_folder='static', static_url_path='/static') +app.debug = True + +temp_base = os.path.expanduser("/home/tmp/uploads") + + +# landing page +@app.route("/api/upload") +def resumable_example(): + return render_template("index.html") + + +# resumable.js uses a GET request to check if it uploaded the file already. +# NOTE: your validation here needs to match whatever you do in the POST (otherwise it will NEVER find the files) +@app.route("/upload", methods=['GET']) +def resumable_get(): + resumableIdentfier = request.args.get('resumableIdentifier', type=str) + resumableFilename = request.args.get('resumableFilename', type=str) + resumableChunkNumber = request.args.get('resumableChunkNumber', type=int) + + if not resumableIdentfier or not resumableFilename or not resumableChunkNumber: + # Parameters are missing or invalid + abort(500, 'Parameter error') + + # chunk folder path based on the parameters + temp_dir = os.path.join(temp_base, resumableIdentfier) + + # chunk path based on the parameters + chunk_file = os.path.join(temp_dir, get_chunk_name(resumableFilename, resumableChunkNumber)) + app.logger.debug('Getting chunk: %s', chunk_file) + + if os.path.isfile(chunk_file): + # Let resumable.js know this chunk already exists + return 'OK' + else: + # Let resumable.js know this chunk does not exists and needs to be uploaded + abort(404, 'Not found') + + +# if it didn't already upload, resumable.js sends the file here +@app.route("/upload", methods=['POST']) +def resumable_post(): + resumableTotalChunks = request.form.get('resumableTotalChunks', type=int) + resumableChunkNumber = request.form.get('resumableChunkNumber', default=1, type=int) + resumableFilename = request.form.get('resumableFilename', default='error', type=str) + resumableIdentfier = request.form.get('resumableIdentifier', default='error', type=str) + + # get the chunk data + chunk_data = request.files['file'] + + # make our temp directory + temp_dir = os.path.join(temp_base, resumableIdentfier) + if not os.path.isdir(temp_dir): + os.makedirs(temp_dir, 0777) + + # save the chunk data + chunk_name = get_chunk_name(resumableFilename, resumableChunkNumber) + chunk_file = os.path.join(temp_dir, chunk_name) + chunk_data.save(chunk_file) + app.logger.debug('Saved chunk: %s', chunk_file) + + # check if the upload is complete + chunk_paths = [os.path.join(temp_dir, get_chunk_name(resumableFilename, x)) for x in range(1, resumableTotalChunks+1)] + upload_complete = all([os.path.exists(p) for p in chunk_paths]) + + # combine all the chunks to create the final file + if upload_complete: + target_file_name = os.path.join(temp_base, resumableFilename) + with open(target_file_name, "ab") as target_file: + for p in chunk_paths: + stored_chunk_file_name = p + stored_chunk_file = open(stored_chunk_file_name, 'rb') + target_file.write(stored_chunk_file.read()) + stored_chunk_file.close() + os.unlink(stored_chunk_file_name) + target_file.close() + os.rmdir(temp_dir) + app.logger.debug('File saved to: %s', target_file_name) + + return 'OK' + + +@app.route("/api/resumable.js", methods=['GET']) +def resumable_js(): + js_file = open('../../resumable.js', 'rb') + resp = make_response() + resp.headers["content-type"] = "application/javascript" + resp.set_data(js_file.read()) + return resp + + +def get_chunk_name(uploaded_filename, chunk_number): + return uploaded_filename + "_part_%03d" % chunk_number + + +if __name__ == '__main__': + app.run() diff --git a/samples/python-flask/requirements.txt b/samples/python-flask/requirements.txt new file mode 100644 index 00000000..e31f8ac6 --- /dev/null +++ b/samples/python-flask/requirements.txt @@ -0,0 +1,2 @@ +Flask + diff --git a/samples/python-flask/static/cancel.png b/samples/python-flask/static/cancel.png new file mode 100644 index 00000000..f5a10aba Binary files /dev/null and b/samples/python-flask/static/cancel.png differ diff --git a/samples/python-flask/static/pause.png b/samples/python-flask/static/pause.png new file mode 100644 index 00000000..53eada6f Binary files /dev/null and b/samples/python-flask/static/pause.png differ diff --git a/samples/python-flask/static/resume.png b/samples/python-flask/static/resume.png new file mode 100644 index 00000000..b150936d Binary files /dev/null and b/samples/python-flask/static/resume.png differ diff --git a/samples/python-flask/static/style.css b/samples/python-flask/static/style.css new file mode 100644 index 00000000..6b447b13 --- /dev/null +++ b/samples/python-flask/static/style.css @@ -0,0 +1,51 @@ +/* Reset */ +body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,th,var{font-style:normal;font-weight:normal;}ol,ul {list-style:none;}caption,th {text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym {border:0;} + +/* Baseline */ +body, p, h1, h2, h3, h4, h5, h6 {font:normal 12px/1.3em Helvetica, Arial, sans-serif; color:#333; } +h1 {font-size:22px; font-weight:bold;} +h2 {font-size:19px; font-weight:bold;} +h3 {font-size:16px; font-weight:bold;} +h4 {font-size:14px; font-weight:bold;} +h5 {font-size:12px; font-weight:bold;} +p {margin:10px 0;} + + +body {text-align:center; margin:40px;} +#frame {margin:0 auto; width:800px; text-align:left;} + + + +/* Uploader: Drag & Drop */ +.resumable-error {display:none; font-size:14px; font-style:italic;} +.resumable-drop {padding:15px; font-size:13px; text-align:center; color:#666; font-weight:bold;background-color:#eee; border:2px dashed #aaa; border-radius:10px; margin-top:40px; z-index:9999; display:none;} +.dragover {padding:30px; color:#555; background-color:#ddd; border:1px solid #999;} + +/* Uploader: Progress bar */ +.resumable-progress {margin:30px 0 30px 0; width:100%; display:none;} +.progress-container {height:7px; background:#9CBD94; position:relative; } +.progress-bar {position:absolute; top:0; left:0; bottom:0; background:#45913A; width:0;} +.progress-text {font-size:11px; line-height:9px; padding-left:10px;} +.progress-pause {padding:0 0 0 7px;} +.progress-resume-link {display:none;} +.is-paused .progress-resume-link {display:inline;} +.is-paused .progress-pause-link {display:none;} +.is-complete .progress-pause {display:none;} + +/* Uploader: List of items being uploaded */ +.resumable-list {overflow:auto; margin-right:-20px; display:none;} +.uploader-item {width:148px; height:90px; background-color:#666; position:relative; border:2px solid black; float:left; margin:0 6px 6px 0;} +.uploader-item-thumbnail {width:100%; height:100%; position:absolute; top:0; left:0;} +.uploader-item img.uploader-item-thumbnail {opacity:0;} +.uploader-item-creating-thumbnail {padding:0 5px; font-size:9px; color:white;} +.uploader-item-title {position:absolute; font-size:9px; line-height:11px; padding:3px 50px 3px 5px; bottom:0; left:0; right:0; color:white; background-color:rgba(0,0,0,0.6); min-height:27px;} +.uploader-item-status {position:absolute; bottom:3px; right:3px;} + +/* Uploader: Hover & Active status */ +.uploader-item:hover, .is-active .uploader-item {border-color:#4a873c; cursor:pointer; } +.uploader-item:hover .uploader-item-title, .is-active .uploader-item .uploader-item-title {background-color:rgba(74,135,60,0.8);} + +/* Uploader: Error status */ +.is-error .uploader-item:hover, .is-active.is-error .uploader-item {border-color:#900;} +.is-error .uploader-item:hover .uploader-item-title, .is-active.is-error .uploader-item .uploader-item-title {background-color:rgba(153,0,0,0.6);} +.is-error .uploader-item-creating-thumbnail {display:none;} diff --git a/samples/python-flask/templates/index.html b/samples/python-flask/templates/index.html new file mode 100644 index 00000000..4c1c3e5a --- /dev/null +++ b/samples/python-flask/templates/index.html @@ -0,0 +1,116 @@ + + + + Resumable.js - Multiple simultaneous, stable and resumable uploads via the HTML5 File API + + + + +
+ +

Resumable.js

+

It's a JavaScript library providing multiple simultaneous, stable and resumable uploads via the HTML5 File API.

+ +

The library is designed to introduce fault-tolerance into the upload of large files through HTTP. This is done by splitting each files into small chunks; whenever the upload of a chunk fails, uploading is retried until the procedure completes. This allows uploads to automatically resume uploading after a network connection is lost either locally or to the server. Additionally, it allows for users to pause and resume uploads without loosing state.

+ +

Resumable.js relies on the HTML5 File API and the ability to chunks files into smaller pieces. Currently, this means that support is limited to Firefox 4+ and Chrome 11+.

+ +
+ +

Demo

+ + + +
+ Your browser, unfortunately, is not supported by Resumable.js. The library requires support for the HTML5 File API along with file slicing. +
+ +
+ Drop video files here to upload or select from your computer +
+ +
+ + + + + + +
+ + + +
+
+ + + + + +
+ + + + +