Exported csv file name now contains the project name (including non-ascii names)

This commit is contained in:
allegroai 2023-07-26 18:37:20 +03:00
parent 5c80336aa9
commit 3ad636c468
3 changed files with 40 additions and 6 deletions

View File

@ -4,4 +4,5 @@ tags_cache {
download { download {
redis_timeout_sec: 300 redis_timeout_sec: 300
batch_size: 500 batch_size: 500
max_project_name_length: 60
} }

View File

@ -1,8 +1,10 @@
import unicodedata
from functools import partial from functools import partial
from flask import request, Response, redirect from flask import request, Response, redirect
from werkzeug.datastructures import ImmutableMultiDict from werkzeug.datastructures import ImmutableMultiDict
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
from werkzeug.urls import url_quote
from apiserver.apierrors import APIError from apiserver.apierrors import APIError
from apiserver.apierrors.base import BaseError from apiserver.apierrors.base import BaseError
@ -44,9 +46,17 @@ class RequestHandlers:
else: else:
headers = None headers = None
if call.result.filename: if call.result.filename:
headers = { try:
"Content-Disposition": f"attachment; filename={call.result.filename}" call.result.filename.encode("ascii")
} except UnicodeEncodeError:
simple = unicodedata.normalize("NFKD", call.result.filename)
simple = simple.encode("ascii", "ignore").decode("ascii")
# safe = RFC 5987 attr-char
quoted = url_quote(call.result.filename, safe="")
filenames = f"filename={simple}; filename*=UTF-8''{quoted}"
else:
filenames = f"filename={call.result.filename}"
headers = {"Content-Disposition": "attachment; " + filenames}
response = Response( response = Response(
content, content,

View File

@ -177,6 +177,9 @@ def _get_download_getter_fn(
return getter return getter
download_conf = config.get("services.organization.download")
@endpoint("organization.prepare_download_for_get_all") @endpoint("organization.prepare_download_for_get_all")
def prepare_download_for_get_all( def prepare_download_for_get_all(
call: APICall, company: str, request: PrepareDownloadForGetAll call: APICall, company: str, request: PrepareDownloadForGetAll
@ -194,7 +197,7 @@ def prepare_download_for_get_all(
redis.setex( redis.setex(
f"get_all_download_{call.id}", f"get_all_download_{call.id}",
int(config.get("services.organization.download.redis_timeout_sec", 300)), int(download_conf.get("redis_timeout_sec", 300)),
json.dumps(call.data), json.dumps(call.data),
) )
@ -248,7 +251,7 @@ def download_for_get_all(call: APICall, company, request: DownloadForGetAll):
with ThreadPoolExecutor(1) as pool: with ThreadPoolExecutor(1) as pool:
page = 0 page = 0
page_size = int( page_size = int(
config.get("services.organization.download.batch_size", 500) download_conf.get("batch_size", 500)
) )
future = pool.submit(get_fn, page, page_size) future = pool.submit(get_fn, page, page_size)
@ -270,6 +273,26 @@ def download_for_get_all(call: APICall, company, request: DownloadForGetAll):
if page == 0: if page == 0:
yield csv.writer(SingleLine()).writerow(projection) yield csv.writer(SingleLine()).writerow(projection)
call.result.filename = f"{request.entity_type}_export.csv" def get_project_name() -> Optional[str]:
projects = call_data.get("project")
if not projects or not isinstance(projects, (list, str)):
return
if isinstance(projects, list):
if len(projects) > 1:
return
projects = projects[0]
if projects is None:
return "root"
project: Project = Project.objects(id=projects).only("basename").first()
if not project:
return
return project.basename[:download_conf.get("max_project_name_length", 60)]
call.result.filename = "-".join(
filter(
None, ("clearml", get_project_name(), f"{request.entity_type}s.csv")
)
)
call.result.content_type = "text/csv" call.result.content_type = "text/csv"
call.result.raw_data = stream_with_context(generate()) call.result.raw_data = stream_with_context(generate())