From a4d9d829b2e2736c933b677dd8c19731249e76d3 Mon Sep 17 00:00:00 2001 From: Nicolas HOMBERG Date: Thu, 4 Sep 2025 23:29:42 +0200 Subject: [PATCH 01/21] download client side and bulk download --- src/apps/api/views/submissions.py | 73 +++++++++++++++-- src/apps/competitions/tasks.py | 82 +++++++++---------- src/static/js/ours/client.js | 55 ++++++++----- .../detail/submission_manager.tag | 6 +- 4 files changed, 142 insertions(+), 74 deletions(-) diff --git a/src/apps/api/views/submissions.py b/src/apps/api/views/submissions.py index afe14fb36..6549263e6 100644 --- a/src/apps/api/views/submissions.py +++ b/src/apps/api/views/submissions.py @@ -349,6 +349,7 @@ def re_run_many_submissions(self, request): submission.re_run() return Response({}) + @action(detail=False, methods=['get']) def download_many(self, request): """ @@ -363,12 +364,15 @@ def download_many(self, request): # Get submissions submissions = Submission.objects.filter(pk__in=pks).select_related( "owner", - "phase__competition", - "phase__competition__created_by", - ).prefetch_related("phase__competition__collaborators") + "phase", + "data", + ) + # .only("id","owner", "data__data_file") + # .prefetch_related("phase__competition__collaborators") if submissions.count() != len(pks): return Response({"error": "One or more submission IDs are invalid"}, status=404) + # NH : should should create a function for this ? # Check permissions if not request.user.is_authenticated: raise PermissionDenied("You must be logged in to download submissions") @@ -389,12 +393,63 @@ def download_many(self, request): "You do not have permission to download one or more of the requested submissions" ) - # Download - from competitions.tasks import stream_batch_download - in_memory_zip = stream_batch_download(pks) - response = StreamingHttpResponse(in_memory_zip, content_type='application/zip') - response['Content-Disposition'] = 'attachment; filename="bulk_submissions.zip"' - return response + files = [] + for sub in submissions: + file_path = sub.data.data_file.name.split('/')[-1] + short_name = f"{sub.id}_{sub.owner}_PhaseId{sub.phase.id}_{sub.data.created_when.strftime('%Y-%m-%d:%M-%S')}_{file_path}" + + url = SubmissionFilesSerializer(sub, context=self.get_serializer_context()).data['data_file'] + files.append({"name": short_name, "url": url}) + + return Response(files) + + + # @action(detail=False, methods=['get']) + # def download_many(self, request): + # """ + # Download a ZIP containing several submissions. + # """ + # pks = request.query_params.get('pks') + # if pks: + # pks = json.loads(pks) # Convert JSON string to list + # else: + # return Response({"error": "`pks` query parameter is required"}, status=400) + + # # Get submissions + # submissions = Submission.objects.filter(pk__in=pks).select_related( + # "owner", + # "phase__competition", + # "phase__competition__created_by", + # ).prefetch_related("phase__competition__collaborators") + # if submissions.count() != len(pks): + # return Response({"error": "One or more submission IDs are invalid"}, status=404) + + # # Check permissions + # if not request.user.is_authenticated: + # raise PermissionDenied("You must be logged in to download submissions") + # # Allow admins + # if request.user.is_superuser or request.user.is_staff: + # allowed = True + # else: + # # Build one Q object for "owner OR organizer" + # organiser_q = ( + # Q(phase__competition__created_by=request.user) | + # Q(phase__competition__collaborators=request.user) + # ) + # # Submissions that violate the rule + # disallowed = submissions.exclude(Q(owner=request.user) | organiser_q) + # allowed = not disallowed.exists() + # if not allowed: + # raise PermissionDenied( + # "You do not have permission to download one or more of the requested submissions" + # ) + + # # Download + # from competitions.tasks import stream_batch_download + # in_memory_zip = stream_batch_download(pks) + # response = StreamingHttpResponse(in_memory_zip, content_type='application/zip') + # response['Content-Disposition'] = 'attachment; filename="bulk_submissions.zip"' + # return response @action(detail=True, methods=('GET',)) def get_details(self, request, pk): diff --git a/src/apps/competitions/tasks.py b/src/apps/competitions/tasks.py index 9b685d1d7..9dd0e978f 100644 --- a/src/apps/competitions/tasks.py +++ b/src/apps/competitions/tasks.py @@ -280,47 +280,47 @@ def send_child_id(submission, child_id): }) -def retrieve_data(url, data=None): - with closing(urlopen(url, data)) as fp: - headers = fp.info() - - bs = 1024 * 8 - size = -1 - read = 0 - if "content-length" in headers: - size = int(headers["Content-Length"]) - - while True: - block = fp.read(bs) - if not block: - break - read += len(block) - yield(block) - - if size >= 0 and read < size: - raise ContentTooShortError( - "retrieval incomplete: got only %i out of %i bytes" - % (read, size)) - - -def zip_generator(submission_pks): - in_memory_zip = BytesIO() - with zipfile.ZipFile(in_memory_zip, 'w', zipfile.ZIP_DEFLATED) as zip_file: - for submission_id in submission_pks: - submission = Submission.objects.get(id=submission_id) - short_name = "ID_" + str(submission_id) + '_' + submission.data.data_file.name.split('/')[-1] - url = make_url_sassy(path=submission.data.data_file.name) - for block in retrieve_data(url): - zip_file.writestr(short_name, block) - - in_memory_zip.seek(0) - - return in_memory_zip - - -@app.task(queue='site-worker', soft_time_limit=60 * 60) -def stream_batch_download(submission_pks): - return zip_generator(submission_pks) +# def retrieve_data(url, data=None): +# with closing(urlopen(url, data)) as fp: +# headers = fp.info() + +# bs = 1024 * 8 +# size = -1 +# read = 0 +# if "content-length" in headers: +# size = int(headers["Content-Length"]) + +# while True: +# block = fp.read(bs) +# if not block: +# break +# read += len(block) +# yield(block) + +# if size >= 0 and read < size: +# raise ContentTooShortError( +# "retrieval incomplete: got only %i out of %i bytes" +# % (read, size)) + + +# def zip_generator(submission_pks): +# in_memory_zip = BytesIO() +# with zipfile.ZipFile(in_memory_zip, 'w', zipfile.ZIP_DEFLATED) as zip_file: +# for submission_id in submission_pks: +# submission = Submission.objects.get(id=submission_id) +# short_name = "ID_" + str(submission_id) + '_' + submission.data.data_file.name.split('/')[-1] +# url = make_url_sassy(path=submission.data.data_file.name) +# for block in retrieve_data(url): +# zip_file.writestr(short_name, block) + +# in_memory_zip.seek(0) + +# return in_memory_zip + + +# @app.task(queue='site-worker', soft_time_limit=60 * 60) +# def stream_batch_download(submission_pks): +# return zip_generator(submission_pks) @app.task(queue='site-worker', soft_time_limit=60) diff --git a/src/static/js/ours/client.js b/src/static/js/ours/client.js index 410ec34cf..963b11057 100644 --- a/src/static/js/ours/client.js +++ b/src/static/js/ours/client.js @@ -128,29 +128,42 @@ CODALAB.api = { return CODALAB.api.request('GET', `${URLS.API}submissions/${id}/get_detail_result/`) }, download_many_submissions: function (pks) { - console.log('Request bulk'); const params = new URLSearchParams({ pks: JSON.stringify(pks) }); const url = `${URLS.API}submissions/download_many/?${params}`; - return fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }).then(response => { - if (!response.ok) { - throw new Error('Network response was not ok ' + response.statusText); - } - return response.blob(); - }).then(blob => { - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = 'bulk_submissions.zip'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }).catch(error => { - console.error('Error downloading submissions:', error); - }); + // return CODALAB.api.request('GET', url) + CODALAB.api.request('GET', url) + .done(function(files) { + // files is already parsed JSON array + console.log("Files returned by server:", files); + + // Iterate files + files.forEach(file => { + console.log(file.name, file.url); + }); + // return files + + const zip = new JSZip(); + const fetchFiles = files.map(async file => { + const response = await fetch(file.url); + const blob = await response.blob(); + zip.file(file.name.replace(/[:/\\]/g, '_'), blob); + }); + + Promise.all(fetchFiles).then(() => { + zip.generateAsync({ type: 'blob' }).then(blob => { + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = 'bulk_submissions.zip'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }); + }); + + }) + .fail(function(err) { + console.error("Error downloading submissions:", err); + }); }, /*--------------------------------------------------------------------- diff --git a/src/static/riot/competitions/detail/submission_manager.tag b/src/static/riot/competitions/detail/submission_manager.tag index 74b746cc6..03e0c1c9e 100644 --- a/src/static/riot/competitions/detail/submission_manager.tag +++ b/src/static/riot/competitions/detail/submission_manager.tag @@ -571,9 +571,9 @@ self.bulk_download = function () { CODALAB.api.download_many_submissions(self.checked_submissions) - .catch(function (error) { - console.error('Error:', error); - }); + #.catch(function (error) { + # console.error('Error:', error); + #}); } From 5daa6f9528d2abb95de55d02755a50e779019bd7 Mon Sep 17 00:00:00 2001 From: Nicolas HOMBERG Date: Mon, 8 Sep 2025 16:44:57 +0200 Subject: [PATCH 02/21] working bulk download --- src/static/js/ours/client.js | 58 +++++------ .../detail/submission_manager.tag | 97 +++++++++++++++---- src/templates/base.html | 1 + 3 files changed, 107 insertions(+), 49 deletions(-) diff --git a/src/static/js/ours/client.js b/src/static/js/ours/client.js index 963b11057..a1f1ddebb 100644 --- a/src/static/js/ours/client.js +++ b/src/static/js/ours/client.js @@ -131,39 +131,39 @@ CODALAB.api = { const params = new URLSearchParams({ pks: JSON.stringify(pks) }); const url = `${URLS.API}submissions/download_many/?${params}`; // return CODALAB.api.request('GET', url) - CODALAB.api.request('GET', url) - .done(function(files) { - // files is already parsed JSON array - console.log("Files returned by server:", files); + return CODALAB.api.request('GET', url) + // .done(function(files) { + // // files is already parsed JSON array + // console.log("Files returned by server:", files); - // Iterate files - files.forEach(file => { - console.log(file.name, file.url); - }); - // return files + // // Iterate files + // files.forEach(file => { + // console.log(file.name, file.url); + // }); + // // return files - const zip = new JSZip(); - const fetchFiles = files.map(async file => { - const response = await fetch(file.url); - const blob = await response.blob(); - zip.file(file.name.replace(/[:/\\]/g, '_'), blob); - }); + // const zip = new JSZip(); + // const fetchFiles = files.map(async file => { + // const response = await fetch(file.url); + // const blob = await response.blob(); + // zip.file(file.name.replace(/[:/\\]/g, '_'), blob); + // }); - Promise.all(fetchFiles).then(() => { - zip.generateAsync({ type: 'blob' }).then(blob => { - const link = document.createElement('a'); - link.href = URL.createObjectURL(blob); - link.download = 'bulk_submissions.zip'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }); - }); + // Promise.all(fetchFiles).then(() => { + // zip.generateAsync({ type: 'blob' }).then(blob => { + // const link = document.createElement('a'); + // link.href = URL.createObjectURL(blob); + // link.download = 'bulk_submissions.zip'; + // document.body.appendChild(link); + // link.click(); + // document.body.removeChild(link); + // }); + // }); - }) - .fail(function(err) { - console.error("Error downloading submissions:", err); - }); + // }) + // .fail(function(err) { + // console.error("Error downloading submissions:", err); + // }); }, /*--------------------------------------------------------------------- diff --git a/src/static/riot/competitions/detail/submission_manager.tag b/src/static/riot/competitions/detail/submission_manager.tag index 03e0c1c9e..79d894bc3 100644 --- a/src/static/riot/competitions/detail/submission_manager.tag +++ b/src/static/riot/competitions/detail/submission_manager.tag @@ -1,29 +1,36 @@ -
-