Spaces:
Paused
Paused
ehl0wr0ld Rafael Uzarowski commited on
Better download (#628)
Browse files* fix: rfc for get_workdir_files was failing
* fix: make downloads chunked with progress
---------
Co-authored-by: Rafael Uzarowski <uzarowski.rafael@proton.me>
python/api/download_work_dir_file.py
CHANGED
|
@@ -1,10 +1,77 @@
|
|
| 1 |
import base64
|
| 2 |
from io import BytesIO
|
|
|
|
|
|
|
| 3 |
|
| 4 |
-
from
|
|
|
|
| 5 |
from python.helpers import files, runtime
|
| 6 |
from python.api import file_info
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
class DownloadFile(ApiHandler):
|
|
@@ -32,31 +99,27 @@ class DownloadFile(ApiHandler):
|
|
| 32 |
if runtime.is_development():
|
| 33 |
b64 = await runtime.call_development_function(fetch_file, zip_file)
|
| 34 |
file_data = BytesIO(base64.b64decode(b64))
|
| 35 |
-
return
|
| 36 |
file_data,
|
| 37 |
-
|
| 38 |
-
download_name=os.path.basename(zip_file),
|
| 39 |
)
|
| 40 |
else:
|
| 41 |
-
return
|
| 42 |
zip_file,
|
| 43 |
-
|
| 44 |
-
download_name=f"{os.path.basename(file_path)}.zip",
|
| 45 |
)
|
| 46 |
elif file["is_file"]:
|
| 47 |
if runtime.is_development():
|
| 48 |
b64 = await runtime.call_development_function(fetch_file, file["abs_path"])
|
| 49 |
file_data = BytesIO(base64.b64decode(b64))
|
| 50 |
-
return
|
| 51 |
file_data,
|
| 52 |
-
|
| 53 |
-
download_name=os.path.basename(file_path),
|
| 54 |
)
|
| 55 |
else:
|
| 56 |
-
return
|
| 57 |
file["abs_path"],
|
| 58 |
-
|
| 59 |
-
download_name=os.path.basename(file["file_name"]),
|
| 60 |
)
|
| 61 |
raise Exception(f"File {file_path} not found")
|
| 62 |
|
|
|
|
| 1 |
import base64
|
| 2 |
from io import BytesIO
|
| 3 |
+
import mimetypes
|
| 4 |
+
import os
|
| 5 |
|
| 6 |
+
from flask import Response
|
| 7 |
+
from python.helpers.api import ApiHandler, Input, Output, Request
|
| 8 |
from python.helpers import files, runtime
|
| 9 |
from python.api import file_info
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def stream_file_download(file_source, download_name, chunk_size=8192):
|
| 13 |
+
"""
|
| 14 |
+
Create a streaming response for file downloads that shows progress in browser.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
file_source: Either a file path (str) or BytesIO object
|
| 18 |
+
download_name: Name for the downloaded file
|
| 19 |
+
chunk_size: Size of chunks to stream (default 8192 bytes)
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
Flask Response object with streaming content
|
| 23 |
+
"""
|
| 24 |
+
# Calculate file size for Content-Length header
|
| 25 |
+
if isinstance(file_source, str):
|
| 26 |
+
# File path - get size from filesystem
|
| 27 |
+
file_size = os.path.getsize(file_source)
|
| 28 |
+
elif isinstance(file_source, BytesIO):
|
| 29 |
+
# BytesIO object - get size from buffer
|
| 30 |
+
current_pos = file_source.tell()
|
| 31 |
+
file_source.seek(0, 2) # Seek to end
|
| 32 |
+
file_size = file_source.tell()
|
| 33 |
+
file_source.seek(current_pos) # Restore original position
|
| 34 |
+
else:
|
| 35 |
+
raise ValueError(f"Unsupported file source type: {type(file_source)}")
|
| 36 |
+
|
| 37 |
+
def generate():
|
| 38 |
+
if isinstance(file_source, str):
|
| 39 |
+
# File path - open and stream from disk
|
| 40 |
+
with open(file_source, 'rb') as f:
|
| 41 |
+
while True:
|
| 42 |
+
chunk = f.read(chunk_size)
|
| 43 |
+
if not chunk:
|
| 44 |
+
break
|
| 45 |
+
yield chunk
|
| 46 |
+
elif isinstance(file_source, BytesIO):
|
| 47 |
+
# BytesIO object - stream from memory
|
| 48 |
+
file_source.seek(0) # Ensure we're at the beginning
|
| 49 |
+
while True:
|
| 50 |
+
chunk = file_source.read(chunk_size)
|
| 51 |
+
if not chunk:
|
| 52 |
+
break
|
| 53 |
+
yield chunk
|
| 54 |
+
|
| 55 |
+
# Detect content type based on file extension
|
| 56 |
+
content_type, _ = mimetypes.guess_type(download_name)
|
| 57 |
+
if not content_type:
|
| 58 |
+
content_type = 'application/octet-stream'
|
| 59 |
+
|
| 60 |
+
# Create streaming response with proper headers for immediate streaming
|
| 61 |
+
response = Response(
|
| 62 |
+
generate(),
|
| 63 |
+
content_type=content_type,
|
| 64 |
+
direct_passthrough=True, # Prevent Flask from buffering the response
|
| 65 |
+
headers={
|
| 66 |
+
'Content-Disposition': f'attachment; filename="{download_name}"',
|
| 67 |
+
'Content-Length': str(file_size), # Critical for browser progress bars
|
| 68 |
+
'Cache-Control': 'no-cache',
|
| 69 |
+
'X-Accel-Buffering': 'no', # Disable nginx buffering
|
| 70 |
+
'Accept-Ranges': 'bytes' # Allow browser to resume downloads
|
| 71 |
+
}
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
return response
|
| 75 |
|
| 76 |
|
| 77 |
class DownloadFile(ApiHandler):
|
|
|
|
| 99 |
if runtime.is_development():
|
| 100 |
b64 = await runtime.call_development_function(fetch_file, zip_file)
|
| 101 |
file_data = BytesIO(base64.b64decode(b64))
|
| 102 |
+
return stream_file_download(
|
| 103 |
file_data,
|
| 104 |
+
download_name=os.path.basename(zip_file)
|
|
|
|
| 105 |
)
|
| 106 |
else:
|
| 107 |
+
return stream_file_download(
|
| 108 |
zip_file,
|
| 109 |
+
download_name=f"{os.path.basename(file_path)}.zip"
|
|
|
|
| 110 |
)
|
| 111 |
elif file["is_file"]:
|
| 112 |
if runtime.is_development():
|
| 113 |
b64 = await runtime.call_development_function(fetch_file, file["abs_path"])
|
| 114 |
file_data = BytesIO(base64.b64decode(b64))
|
| 115 |
+
return stream_file_download(
|
| 116 |
file_data,
|
| 117 |
+
download_name=os.path.basename(file_path)
|
|
|
|
| 118 |
)
|
| 119 |
else:
|
| 120 |
+
return stream_file_download(
|
| 121 |
file["abs_path"],
|
| 122 |
+
download_name=os.path.basename(file["file_name"])
|
|
|
|
| 123 |
)
|
| 124 |
raise Exception(f"File {file_path} not found")
|
| 125 |
|
python/api/get_work_dir_files.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
from python.helpers.api import ApiHandler, Request, Response
|
| 2 |
from python.helpers.file_browser import FileBrowser
|
| 3 |
from python.helpers import runtime
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
class GetWorkDirFiles(ApiHandler):
|
|
@@ -20,7 +21,7 @@ class GetWorkDirFiles(ApiHandler):
|
|
| 20 |
|
| 21 |
# browser = FileBrowser()
|
| 22 |
# result = browser.get_files(current_path)
|
| 23 |
-
result = await runtime.call_development_function(get_files, current_path)
|
| 24 |
|
| 25 |
return {"data": result}
|
| 26 |
|
|
|
|
| 1 |
from python.helpers.api import ApiHandler, Request, Response
|
| 2 |
from python.helpers.file_browser import FileBrowser
|
| 3 |
from python.helpers import runtime
|
| 4 |
+
import python.api.get_work_dir_files as get_work_dir_files_module
|
| 5 |
|
| 6 |
|
| 7 |
class GetWorkDirFiles(ApiHandler):
|
|
|
|
| 21 |
|
| 22 |
# browser = FileBrowser()
|
| 23 |
# result = browser.get_files(current_path)
|
| 24 |
+
result = await runtime.call_development_function(get_work_dir_files_module.get_files, current_path)
|
| 25 |
|
| 26 |
return {"data": result}
|
| 27 |
|