Skip to content

Commit 3a75bff

Browse files
Merge pull request #135 from ComputerScienceHouse/develop
feat: chunked uploads
2 parents 7019001 + 5b7d1f2 commit 3a75bff

3 files changed

Lines changed: 141 additions & 14 deletions

File tree

gallery/__init__.py

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import flask_migrate
4141
import requests
4242
from werkzeug.utils import secure_filename
43+
import shutil
4344

4445
app = Flask(__name__)
4546
app.config.update({
@@ -1056,7 +1057,7 @@ def render_dir(dir_id: int, auth_dict: Optional[Dict[str, Any]] = None):
10561057
dir_model = Directory.query.filter(Directory.id == dir_id).first()
10571058
if dir_model is None:
10581059
abort(404)
1059-
description = dir_model.description
1060+
description = dir_model.description or ""
10601061
display_description = len(description) > 0
10611062

10621063
display_parent = True
@@ -1104,7 +1105,7 @@ def render_file(file_id: int, auth_dict: Optional[Dict[str, Any]] = None):
11041105
if gallery_lockdown and (not auth_dict['is_eboard'] and not auth_dict['is_rtp']):
11051106
abort(405)
11061107

1107-
description = file_model.caption
1108+
description = file_model.caption or ""
11081109
display_description = len(description) > 0
11091110
display_parent = True
11101111
if file_model is None or file_model.parent is None:
@@ -1139,6 +1140,106 @@ def render_file(file_id: int, auth_dict: Optional[Dict[str, Any]] = None):
11391140
lockdown=gallery_lockdown)
11401141

11411142

1143+
@app.route('/upload/chunk', methods=['POST'])
1144+
@auth.oidc_auth('default')
1145+
@gallery_auth
1146+
def upload_chunk(auth_dict: Optional[Dict[str, Any]] = None):
1147+
chunk = request.files.get('gallery-upload')
1148+
if chunk is None:
1149+
return jsonify({'error': 'no chunk'}), 400
1150+
1151+
dz_uuid = request.form.get('dzuuid')
1152+
chunk_index = int(request.form.get('dzchunkindex', 0))
1153+
chunk_size = int(request.form.get('dzchunksize', 0))
1154+
filename = secure_filename(request.form.get('dzfilename', ''))
1155+
1156+
if not dz_uuid or not filename:
1157+
return jsonify({'error': 'missing chunk metadata'}), 400
1158+
1159+
chunk_dir = os.path.join(tempfile.gettempdir(), 'chonks', dz_uuid)
1160+
os.makedirs(chunk_dir, exist_ok=True)
1161+
1162+
out_path = os.path.join(chunk_dir, 'assembled')
1163+
with open(out_path, 'ab') as out:
1164+
out.write(chunk.read())
1165+
1166+
return jsonify({'status': 'ok', 'chunk': chunk_index}), 200
1167+
1168+
1169+
@app.route('/upload/chunk/finalize', methods=['POST'])
1170+
@auth.oidc_auth('default')
1171+
@gallery_auth
1172+
def finalize_upload(auth_dict: Optional[Dict[str, Any]] = None):
1173+
assert auth_dict is not None
1174+
owner = auth_dict['uuid']
1175+
parent = request.form.get('parent_id')
1176+
dz_uuid = request.form.get('dzuuid')
1177+
filename = secure_filename(request.form.get('filename', ''))
1178+
total_chunks = int(request.form.get('dztotalchunkcount', 0))
1179+
1180+
if not all([parent, dz_uuid, filename, total_chunks]):
1181+
return jsonify({'error': 'missing parameters'}), 400
1182+
1183+
chunk_dir = os.path.join(tempfile.gettempdir(), 'chonks', dz_uuid)
1184+
1185+
upload_status: Dict[str, Any] = {}
1186+
upload_status['redirect'] = '/view/dir/' + str(parent)
1187+
errors: List[str] = []
1188+
success: List[Dict[str, Any]] = []
1189+
1190+
file_model = File.query.filter(File.parent == parent) \
1191+
.filter(File.name == filename).first()
1192+
if file_model is not None:
1193+
errors.append(filename)
1194+
upload_status['error'] = errors
1195+
upload_status['success'] = success
1196+
return jsonify(upload_status), 200
1197+
1198+
dir_path = tempfile.mkdtemp()
1199+
filepath = os.path.join(dir_path, filename)
1200+
try:
1201+
assembled_path = os.path.join(chunk_dir, 'assembled')
1202+
if not os.path.exists(assembled_path):
1203+
return jsonify({'error': 'assembled file missing'}), 400
1204+
shutil.move(assembled_path, filepath)
1205+
try:
1206+
mime, file_model = add_file(filename, dir_path, parent, '', owner)
1207+
except OSError as e:
1208+
if e.errno == 28:
1209+
return jsonify({'error': 'storage full'}), 507
1210+
raise
1211+
if file_model is None:
1212+
errors.append(filename)
1213+
else:
1214+
with open(filepath, 'rb') as f_hnd:
1215+
storage_interface.put(
1216+
'files/{}'.format(file_model.s3_id),
1217+
f_hnd,
1218+
filename,
1219+
mime
1220+
)
1221+
os.remove(filepath)
1222+
1223+
thumb_path = os.path.join(dir_path, file_model.thumbnail_uuid)
1224+
with open(thumb_path, 'rb') as f_hnd:
1225+
storage_interface.put(
1226+
'thumbnails/' + file_model.s3_id,
1227+
f_hnd,
1228+
'thumb_' + filename + '.' + thumb_path.split('.')[-1],
1229+
'image/gif' if thumb_path.endswith('.gif') else 'image/jpeg'
1230+
)
1231+
os.remove(thumb_path)
1232+
success.append({'name': file_model.name, 'id': file_model.id})
1233+
finally:
1234+
shutil.rmtree(chunk_dir, ignore_errors=True)
1235+
shutil.rmtree(dir_path, ignore_errors=True)
1236+
1237+
upload_status['error'] = errors
1238+
upload_status['success'] = success
1239+
refresh_default_thumbnails()
1240+
return jsonify(upload_status), 200
1241+
1242+
11421243
@app.route("/view/random_file")
11431244
@auth.oidc_auth('default')
11441245
def get_random_file():

gallery/templates/view_dir.html

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -245,34 +245,60 @@ <h4 class="modal-title">Upload</h4>
245245
Dropzone.autoDiscover = false;
246246

247247
$(function() {
248-
248+
249249
var myDropzone = new Dropzone('div#galleryUpload', {
250-
url: '/upload',
250+
url: '/upload/chunk',
251251
paramName: "gallery-upload",
252252
uploadMultiple: false,
253253
autoProcessQueue: false,
254254
parallelUploads: 3,
255-
success: afterUpload,
256-
maxFilesize: 1024,
255+
maxFilesize: 5 * 1024,
256+
chunking: true,
257+
forceChunking: true,
258+
chunkSize: 10 * 1024 * 1024,
259+
retryChunks: true,
260+
retryChunksLimit: 3,
261+
timeout: 0,
257262
init: function() {
258263
this.on('addedfile', function(file) {
259264
console.log(file.name + ": " + file.size);
260-
if (file.size > (1024 * 1024 * 1024)) {
261-
var warning = "<div class='alert alert-dismissible alert-danger' id='upload-alert'><button type='button' class='close' data-dismiss='alert'>&times;</button><span class='glyphicon glyphicon-exclamation-sign'></span> File \"<strong>" + file.name + "</strong>\" is too large, max filesize is 1GB.</div>";
262-
$('#upload').after(warning);
263-
this.removeFile(file);
264-
}
265265
});
266266

267267
this.on('accept', function(file, done) {
268268
done();
269269
});
270270

271271
this.on('sending', function(file, xhr, formData) {
272-
formData.append('parent_id', "{{directory.id}}");
272+
formData.append('parent_id', '{{ directory.id }}');
273+
formData.append('dzfilename', file.name);
274+
});
275+
276+
this.on('success', function(file, response) {
277+
if (file.upload.chunked && file.upload.chunkIndex < file.upload.totalChunkCount - 1) {
278+
return;
279+
}
280+
var formData = new FormData();
281+
formData.append('parent_id', '{{ directory.id }}');
282+
formData.append('dzuuid', file.upload.uuid);
283+
formData.append('filename', file.name);
284+
formData.append('dztotalchunkcount', file.upload.totalChunkCount);
285+
fetch('/upload/chunk/finalize', {
286+
method: 'POST',
287+
body: formData,
288+
})
289+
.then(function(r) { return r.json(); })
290+
.then(function(data) {
291+
console.log('finalize response:', JSON.stringify(data));
292+
afterUpload(file, data);
293+
})
294+
.catch(function(err) {
295+
console.error('Finalize error:', err);
296+
});
273297
});
274298

275-
this.on('complete', function(file) {
299+
this.on('error', function(file, message) {
300+
var warning = "<div class='alert alert-dismissible alert-danger' id='upload-alert'><button type='button' class='close' data-dismiss='alert'>&times;</button><span class='glyphicon glyphicon-exclamation-sign'></span> Upload failed for <strong>" + file.name + "</strong>: " + (typeof message === 'object' ? (message.error || JSON.stringify(message)) : message) + "</div>";
301+
$('#upload').after(warning);
276302
this.removeFile(file);
277303
});
278304
}

gallery/templates/view_file.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959

6060
{% elif file.mimetype.split('/')[0] == "video" %}
6161
<video id="file-content" controls>
62-
<source src="/api/file/get/{{file.id}}">
62+
<source src="/api/file/get/{{file.id}}" type="{{ file.mimetype }}">
6363
</video>
6464

6565
{% elif file.mimetype == "application/pdf" or file.mimetype == "text/plain" %}

0 commit comments

Comments
 (0)