figpack 0.2.27__py3-none-any.whl → 0.2.40__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- figpack/__init__.py +1 -1
- figpack/cli.py +214 -2
- figpack/core/_bundle_utils.py +12 -8
- figpack/core/_file_handler.py +4 -1
- figpack/core/_save_figure.py +12 -8
- figpack/core/_server_manager.py +106 -3
- figpack/core/_show_view.py +1 -1
- figpack/core/_upload_bundle.py +63 -51
- figpack/core/_view_figure.py +15 -10
- figpack/core/_zarr_consolidate.py +185 -0
- figpack/core/extension_view.py +8 -4
- figpack/core/figpack_extension.py +1 -1
- figpack/core/figpack_view.py +29 -13
- figpack/core/zarr.py +2 -2
- figpack/figpack-figure-dist/assets/{index-DnHZdWys.js → index-ST_DU17U.js} +39 -39
- figpack/figpack-figure-dist/index.html +2 -2
- figpack/views/Box.py +2 -2
- figpack/views/CaptionedView.py +64 -0
- figpack/views/Iframe.py +43 -0
- figpack/views/Image.py +1 -2
- figpack/views/Markdown.py +7 -3
- figpack/views/PlotlyExtension/PlotlyExtension.py +12 -12
- figpack/views/Spectrogram.py +2 -0
- figpack/views/TimeseriesGraph.py +84 -15
- figpack/views/__init__.py +2 -0
- {figpack-0.2.27.dist-info → figpack-0.2.40.dist-info}/METADATA +22 -1
- figpack-0.2.40.dist-info/RECORD +50 -0
- figpack-0.2.27.dist-info/RECORD +0 -47
- {figpack-0.2.27.dist-info → figpack-0.2.40.dist-info}/WHEEL +0 -0
- {figpack-0.2.27.dist-info → figpack-0.2.40.dist-info}/entry_points.txt +0 -0
- {figpack-0.2.27.dist-info → figpack-0.2.40.dist-info}/licenses/LICENSE +0 -0
- {figpack-0.2.27.dist-info → figpack-0.2.40.dist-info}/top_level.txt +0 -0
figpack/__init__.py
CHANGED
figpack/cli.py
CHANGED
|
@@ -18,6 +18,7 @@ import requests
|
|
|
18
18
|
from . import __version__
|
|
19
19
|
from .core._server_manager import CORSRequestHandler
|
|
20
20
|
from .core._view_figure import serve_files, view_figure
|
|
21
|
+
from .core._upload_bundle import _upload_bundle, get_figure_by_source_url
|
|
21
22
|
from .extensions import ExtensionManager
|
|
22
23
|
|
|
23
24
|
MAX_WORKERS_FOR_DOWNLOAD = 16
|
|
@@ -249,6 +250,188 @@ def handle_extensions_command(args):
|
|
|
249
250
|
print("Use 'figpack extensions <command> --help' for more information.")
|
|
250
251
|
|
|
251
252
|
|
|
253
|
+
def handle_upload_from_source_url(args) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Handle the upload-from-source-url command
|
|
256
|
+
|
|
257
|
+
Downloads a tar.gz/tgz file from a URL, extracts it, and uploads it as a new figure
|
|
258
|
+
with the source URL set.
|
|
259
|
+
"""
|
|
260
|
+
import os
|
|
261
|
+
|
|
262
|
+
source_url = args.source_url
|
|
263
|
+
title = args.title if hasattr(args, "title") else None
|
|
264
|
+
|
|
265
|
+
# Get API key from environment variable
|
|
266
|
+
api_key = os.environ.get("FIGPACK_API_KEY")
|
|
267
|
+
if not api_key:
|
|
268
|
+
print(
|
|
269
|
+
"Error: FIGPACK_API_KEY environment variable must be set to upload figures."
|
|
270
|
+
)
|
|
271
|
+
sys.exit(1)
|
|
272
|
+
|
|
273
|
+
# Validate URL format
|
|
274
|
+
if not (source_url.endswith(".tar.gz") or source_url.endswith(".tgz")):
|
|
275
|
+
print(f"Error: Source URL must point to a .tar.gz or .tgz file: {source_url}")
|
|
276
|
+
sys.exit(1)
|
|
277
|
+
|
|
278
|
+
print(f"Downloading archive from: {source_url}")
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
# Download the archive
|
|
282
|
+
response = requests.get(source_url, timeout=120, stream=True)
|
|
283
|
+
response.raise_for_status()
|
|
284
|
+
|
|
285
|
+
# Create temporary file for the archive
|
|
286
|
+
with tempfile.NamedTemporaryFile(
|
|
287
|
+
suffix=".tar.gz", delete=False
|
|
288
|
+
) as temp_archive:
|
|
289
|
+
archive_path = temp_archive.name
|
|
290
|
+
|
|
291
|
+
# Download with progress indication
|
|
292
|
+
total_size = int(response.headers.get("content-length", 0))
|
|
293
|
+
downloaded_size = 0
|
|
294
|
+
|
|
295
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
296
|
+
if chunk:
|
|
297
|
+
temp_archive.write(chunk)
|
|
298
|
+
downloaded_size += len(chunk)
|
|
299
|
+
if total_size > 0:
|
|
300
|
+
progress = (downloaded_size / total_size) * 100
|
|
301
|
+
print(
|
|
302
|
+
f"Downloaded: {downloaded_size / (1024*1024):.2f} MB ({progress:.1f}%)",
|
|
303
|
+
end="\r",
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if total_size > 0:
|
|
307
|
+
print() # New line after progress
|
|
308
|
+
print(f"Download complete: {downloaded_size / (1024*1024):.2f} MB")
|
|
309
|
+
|
|
310
|
+
# Extract archive to temporary directory
|
|
311
|
+
print("Extracting archive...")
|
|
312
|
+
with tempfile.TemporaryDirectory() as extract_dir:
|
|
313
|
+
extract_path = pathlib.Path(extract_dir)
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
with tarfile.open(archive_path, "r:gz") as tar:
|
|
317
|
+
tar.extractall(path=extract_path)
|
|
318
|
+
print(f"Extracted to temporary directory")
|
|
319
|
+
except Exception as e:
|
|
320
|
+
print(f"Error: Failed to extract archive: {e}")
|
|
321
|
+
sys.exit(1)
|
|
322
|
+
finally:
|
|
323
|
+
# Clean up downloaded archive
|
|
324
|
+
import os
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
os.unlink(archive_path)
|
|
328
|
+
except Exception:
|
|
329
|
+
pass
|
|
330
|
+
|
|
331
|
+
# Upload the extracted files
|
|
332
|
+
print(f"Uploading figure with source URL: {source_url}")
|
|
333
|
+
try:
|
|
334
|
+
figure_url = _upload_bundle(
|
|
335
|
+
str(extract_path),
|
|
336
|
+
api_key=api_key,
|
|
337
|
+
title=title,
|
|
338
|
+
source_url=source_url,
|
|
339
|
+
)
|
|
340
|
+
print(f"\nFigure uploaded successfully!")
|
|
341
|
+
print(f"Figure URL: {figure_url}")
|
|
342
|
+
except Exception as e:
|
|
343
|
+
print(f"Error: Failed to upload figure: {e}")
|
|
344
|
+
sys.exit(1)
|
|
345
|
+
|
|
346
|
+
except requests.exceptions.RequestException as e:
|
|
347
|
+
print(f"Error: Failed to download archive from {source_url}: {e}")
|
|
348
|
+
sys.exit(1)
|
|
349
|
+
except Exception as e:
|
|
350
|
+
print(f"Error: {e}")
|
|
351
|
+
sys.exit(1)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def handle_find_by_source_url(args) -> None:
|
|
355
|
+
"""
|
|
356
|
+
Handle the find-by-source-url command
|
|
357
|
+
|
|
358
|
+
Queries the API for a figure URL by its source URL.
|
|
359
|
+
"""
|
|
360
|
+
source_url = args.source_url
|
|
361
|
+
|
|
362
|
+
print(f"Querying for figure with source URL: {source_url}")
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
figure_url = get_figure_by_source_url(source_url)
|
|
366
|
+
|
|
367
|
+
if figure_url:
|
|
368
|
+
print(f"Figure found: {figure_url}")
|
|
369
|
+
else:
|
|
370
|
+
print(f"No figure found with source URL: {source_url}")
|
|
371
|
+
sys.exit(1)
|
|
372
|
+
except Exception as e:
|
|
373
|
+
print(f"Error: {e}")
|
|
374
|
+
sys.exit(1)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def download_and_view_archive(url: str, port: int = None) -> None:
|
|
378
|
+
"""
|
|
379
|
+
Download a tar.gz/tgz archive from a URL and view it
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
url: URL to the tar.gz or tgz file
|
|
383
|
+
port: Optional port number to serve on
|
|
384
|
+
"""
|
|
385
|
+
if not (url.endswith(".tar.gz") or url.endswith(".tgz")):
|
|
386
|
+
print(f"Error: URL must point to a .tar.gz or .tgz file: {url}")
|
|
387
|
+
sys.exit(1)
|
|
388
|
+
|
|
389
|
+
print(f"Downloading archive from: {url}")
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
response = requests.get(url, timeout=60, stream=True)
|
|
393
|
+
response.raise_for_status()
|
|
394
|
+
|
|
395
|
+
# Create a temporary file to store the downloaded archive
|
|
396
|
+
with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as temp_file:
|
|
397
|
+
temp_path = temp_file.name
|
|
398
|
+
|
|
399
|
+
# Download with progress indication
|
|
400
|
+
total_size = int(response.headers.get("content-length", 0))
|
|
401
|
+
downloaded_size = 0
|
|
402
|
+
|
|
403
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
404
|
+
if chunk:
|
|
405
|
+
temp_file.write(chunk)
|
|
406
|
+
downloaded_size += len(chunk)
|
|
407
|
+
if total_size > 0:
|
|
408
|
+
progress = (downloaded_size / total_size) * 100
|
|
409
|
+
print(
|
|
410
|
+
f"Downloaded: {downloaded_size / (1024*1024):.2f} MB ({progress:.1f}%)",
|
|
411
|
+
end="\r",
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
if total_size > 0:
|
|
415
|
+
print() # New line after progress
|
|
416
|
+
print(f"Download complete: {downloaded_size / (1024*1024):.2f} MB")
|
|
417
|
+
|
|
418
|
+
# Now view the downloaded file
|
|
419
|
+
try:
|
|
420
|
+
view_figure(temp_path, port=port)
|
|
421
|
+
finally:
|
|
422
|
+
# Clean up the temporary file after viewing
|
|
423
|
+
import os
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
os.unlink(temp_path)
|
|
427
|
+
except Exception:
|
|
428
|
+
pass
|
|
429
|
+
|
|
430
|
+
except requests.exceptions.RequestException as e:
|
|
431
|
+
print(f"Error: Failed to download archive from {url}: {e}")
|
|
432
|
+
sys.exit(1)
|
|
433
|
+
|
|
434
|
+
|
|
252
435
|
def main():
|
|
253
436
|
"""Main CLI entry point"""
|
|
254
437
|
parser = argparse.ArgumentParser(
|
|
@@ -270,7 +453,7 @@ def main():
|
|
|
270
453
|
view_parser = subparsers.add_parser(
|
|
271
454
|
"view", help="Extract and serve a figure archive locally"
|
|
272
455
|
)
|
|
273
|
-
view_parser.add_argument("archive", help="Path to the tar.gz archive file")
|
|
456
|
+
view_parser.add_argument("archive", help="Path or URL to the tar.gz archive file")
|
|
274
457
|
view_parser.add_argument(
|
|
275
458
|
"--port", type=int, help="Port number to serve on (default: auto-select)"
|
|
276
459
|
)
|
|
@@ -312,14 +495,43 @@ def main():
|
|
|
312
495
|
"extensions", nargs="+", help="Extension package names to uninstall"
|
|
313
496
|
)
|
|
314
497
|
|
|
498
|
+
# Upload from URL command
|
|
499
|
+
upload_from_source_url_parser = subparsers.add_parser(
|
|
500
|
+
"upload-from-source-url",
|
|
501
|
+
help="Download a tar.gz/tgz file and upload it as a new figure",
|
|
502
|
+
)
|
|
503
|
+
upload_from_source_url_parser.add_argument(
|
|
504
|
+
"source_url",
|
|
505
|
+
help="URL to the tar.gz or tgz file (will be set as the figure's source URL)",
|
|
506
|
+
)
|
|
507
|
+
upload_from_source_url_parser.add_argument(
|
|
508
|
+
"--title", help="Optional title for the figure"
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
# Find by source URL command
|
|
512
|
+
find_by_source_url_parser = subparsers.add_parser(
|
|
513
|
+
"find-by-source-url", help="Get the figure URL for a given source URL"
|
|
514
|
+
)
|
|
515
|
+
find_by_source_url_parser.add_argument(
|
|
516
|
+
"source_url", help="The source URL to search for"
|
|
517
|
+
)
|
|
518
|
+
|
|
315
519
|
args = parser.parse_args()
|
|
316
520
|
|
|
317
521
|
if args.command == "download":
|
|
318
522
|
download_figure(args.figure_url, args.dest)
|
|
319
523
|
elif args.command == "view":
|
|
320
|
-
|
|
524
|
+
# Check if archive argument is a URL
|
|
525
|
+
if args.archive.startswith("http://") or args.archive.startswith("https://"):
|
|
526
|
+
download_and_view_archive(args.archive, port=args.port)
|
|
527
|
+
else:
|
|
528
|
+
view_figure(args.archive, port=args.port)
|
|
321
529
|
elif args.command == "extensions":
|
|
322
530
|
handle_extensions_command(args)
|
|
531
|
+
elif args.command == "upload-from-source-url":
|
|
532
|
+
handle_upload_from_source_url(args)
|
|
533
|
+
elif args.command == "find-by-source-url":
|
|
534
|
+
handle_find_by_source_url(args)
|
|
323
535
|
else:
|
|
324
536
|
parser.print_help()
|
|
325
537
|
|
figpack/core/_bundle_utils.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import pathlib
|
|
3
3
|
import json
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Optional, List
|
|
5
5
|
|
|
6
6
|
import zarr
|
|
7
7
|
|
|
@@ -9,12 +9,13 @@ from .figpack_view import FigpackView
|
|
|
9
9
|
from .figpack_extension import FigpackExtension
|
|
10
10
|
from .extension_view import ExtensionView
|
|
11
11
|
from .zarr import Group, _check_zarr_version
|
|
12
|
+
from ._zarr_consolidate import consolidate_zarr_chunks
|
|
12
13
|
|
|
13
14
|
thisdir = pathlib.Path(__file__).parent.resolve()
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def prepare_figure_bundle(
|
|
17
|
-
view: FigpackView, tmpdir: str, *, title: str, description: str = None
|
|
18
|
+
view: FigpackView, tmpdir: str, *, title: str, description: Optional[str] = None
|
|
18
19
|
) -> None:
|
|
19
20
|
"""
|
|
20
21
|
Prepare a figure bundle in the specified temporary directory.
|
|
@@ -51,8 +52,8 @@ def prepare_figure_bundle(
|
|
|
51
52
|
# because we only support version 2 on the frontend right now.
|
|
52
53
|
|
|
53
54
|
if _check_zarr_version() == 3:
|
|
54
|
-
old_default_zarr_format = zarr.config.get("default_zarr_format")
|
|
55
|
-
zarr.config.set({"default_zarr_format": 2})
|
|
55
|
+
old_default_zarr_format = zarr.config.get("default_zarr_format") # type: ignore
|
|
56
|
+
zarr.config.set({"default_zarr_format": 2}) # type: ignore
|
|
56
57
|
|
|
57
58
|
try:
|
|
58
59
|
# Write the view data to the Zarr group
|
|
@@ -72,15 +73,18 @@ def prepare_figure_bundle(
|
|
|
72
73
|
# Generate extension manifest
|
|
73
74
|
_write_extension_manifest(required_extensions, tmpdir)
|
|
74
75
|
|
|
76
|
+
# Create the .zmetadata file
|
|
75
77
|
zarr.consolidate_metadata(zarr_group._zarr_group.store)
|
|
76
78
|
|
|
77
79
|
# It's important that we remove all the metadata files except for the
|
|
78
|
-
# consolidated one
|
|
79
|
-
# once we start editing the zarr data from the browser.
|
|
80
|
+
# consolidated one so there is a single source of truth.
|
|
80
81
|
_remove_metadata_files_except_consolidated(pathlib.Path(tmpdir) / "data.zarr")
|
|
82
|
+
|
|
83
|
+
# Consolidate zarr chunks into larger files to reduce upload count
|
|
84
|
+
consolidate_zarr_chunks(pathlib.Path(tmpdir) / "data.zarr")
|
|
81
85
|
finally:
|
|
82
86
|
if _check_zarr_version() == 3:
|
|
83
|
-
zarr.config.set({"default_zarr_format": old_default_zarr_format})
|
|
87
|
+
zarr.config.set({"default_zarr_format": old_default_zarr_format}) # type: ignore
|
|
84
88
|
|
|
85
89
|
|
|
86
90
|
def _remove_metadata_files_except_consolidated(zarr_dir: pathlib.Path) -> None:
|
|
@@ -107,7 +111,7 @@ def _remove_metadata_files_except_consolidated(zarr_dir: pathlib.Path) -> None:
|
|
|
107
111
|
print(f"Warning: could not remove file {file_path}: {e}")
|
|
108
112
|
|
|
109
113
|
|
|
110
|
-
def _discover_required_extensions(view: FigpackView) ->
|
|
114
|
+
def _discover_required_extensions(view: FigpackView) -> List[str]:
|
|
111
115
|
"""
|
|
112
116
|
Recursively discover all extensions required by a view and its children
|
|
113
117
|
|
figpack/core/_file_handler.py
CHANGED
|
@@ -42,6 +42,9 @@ class FileUploadCORSRequestHandler(CORSRequestHandler):
|
|
|
42
42
|
"Accept-Ranges, Content-Encoding, Content-Length, Content-Range",
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
+
# Always send Accept-Ranges header to indicate byte-range support
|
|
46
|
+
self.send_header("Accept-Ranges", "bytes")
|
|
47
|
+
|
|
45
48
|
# Prevent browser caching - important for when we are editing figures in place
|
|
46
49
|
# This ensures the browser always fetches the latest version of files
|
|
47
50
|
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
@@ -187,6 +190,6 @@ class FileUploadCORSRequestHandler(CORSRequestHandler):
|
|
|
187
190
|
self.send_error(500, f"Internal Server Error: {str(e)}")
|
|
188
191
|
return False
|
|
189
192
|
|
|
190
|
-
def log_message(self,
|
|
193
|
+
def log_message(self, format, *args):
|
|
191
194
|
"""Override to suppress default logging (same as parent class)."""
|
|
192
195
|
pass
|
figpack/core/_save_figure.py
CHANGED
|
@@ -5,7 +5,9 @@ from ._bundle_utils import prepare_figure_bundle
|
|
|
5
5
|
from .figpack_view import FigpackView
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def _save_figure(
|
|
8
|
+
def _save_figure(
|
|
9
|
+
view: FigpackView, output_path: str, *, title: str, description: str = ""
|
|
10
|
+
) -> None:
|
|
9
11
|
"""
|
|
10
12
|
Save the figure to a folder or a .tar.gz file
|
|
11
13
|
|
|
@@ -13,19 +15,21 @@ def _save_figure(view: FigpackView, output_path: str, *, title: str):
|
|
|
13
15
|
view: FigpackView instance to save
|
|
14
16
|
output_path: Output path (destination folder or .tar.gz file path)
|
|
15
17
|
"""
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
|
|
18
|
+
output_path_2 = pathlib.Path(output_path)
|
|
19
|
+
if (output_path_2.suffix == ".gz" and output_path_2.suffixes[-2] == ".tar") or (
|
|
20
|
+
output_path_2.suffix == ".tgz"
|
|
19
21
|
):
|
|
20
22
|
# It's a .tar.gz file
|
|
21
23
|
with tempfile.TemporaryDirectory(prefix="figpack_save_") as tmpdir:
|
|
22
|
-
prepare_figure_bundle(view, tmpdir, title=title)
|
|
24
|
+
prepare_figure_bundle(view, tmpdir, title=title, description=description)
|
|
23
25
|
# Create tar.gz file
|
|
24
26
|
import tarfile
|
|
25
27
|
|
|
26
|
-
with tarfile.open(
|
|
28
|
+
with tarfile.open(output_path_2, "w:gz") as tar:
|
|
27
29
|
tar.add(tmpdir, arcname=".")
|
|
28
30
|
else:
|
|
29
31
|
# It's a folder
|
|
30
|
-
|
|
31
|
-
prepare_figure_bundle(
|
|
32
|
+
output_path_2.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
prepare_figure_bundle(
|
|
34
|
+
view, str(output_path_2), title=title, description=description
|
|
35
|
+
)
|
figpack/core/_server_manager.py
CHANGED
|
@@ -29,6 +29,9 @@ class CORSRequestHandler(SimpleHTTPRequestHandler):
|
|
|
29
29
|
"Accept-Ranges, Content-Encoding, Content-Length, Content-Range",
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
+
# Always send Accept-Ranges header to indicate byte-range support
|
|
33
|
+
self.send_header("Accept-Ranges", "bytes")
|
|
34
|
+
|
|
32
35
|
# Prevent browser caching - important for when we are editing figures in place
|
|
33
36
|
# This ensures the browser always fetches the latest version of files
|
|
34
37
|
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
@@ -45,7 +48,100 @@ class CORSRequestHandler(SimpleHTTPRequestHandler):
|
|
|
45
48
|
"""Reject PUT requests when file upload is not enabled."""
|
|
46
49
|
self.send_error(405, "Method Not Allowed")
|
|
47
50
|
|
|
48
|
-
def
|
|
51
|
+
def do_GET(self):
|
|
52
|
+
"""Handle GET requests with support for Range requests."""
|
|
53
|
+
# Translate path and check if file exists
|
|
54
|
+
path = self.translate_path(self.path)
|
|
55
|
+
|
|
56
|
+
# Check if path is a file
|
|
57
|
+
if not os.path.isfile(path):
|
|
58
|
+
# Let parent class handle directories and 404s
|
|
59
|
+
return super().do_GET()
|
|
60
|
+
|
|
61
|
+
# Check for Range header
|
|
62
|
+
range_header = self.headers.get("Range")
|
|
63
|
+
|
|
64
|
+
if range_header is None:
|
|
65
|
+
# No range request, use parent's implementation
|
|
66
|
+
return super().do_GET()
|
|
67
|
+
|
|
68
|
+
# Parse range header
|
|
69
|
+
try:
|
|
70
|
+
# Range header format: "bytes=start-end"
|
|
71
|
+
if not range_header.startswith("bytes="):
|
|
72
|
+
# Invalid range format, ignore and serve full file
|
|
73
|
+
return super().do_GET()
|
|
74
|
+
|
|
75
|
+
range_spec = range_header[6:] # Remove "bytes=" prefix
|
|
76
|
+
|
|
77
|
+
# Get file size
|
|
78
|
+
file_size = os.path.getsize(path)
|
|
79
|
+
|
|
80
|
+
# Parse range specification
|
|
81
|
+
if "-" not in range_spec:
|
|
82
|
+
# Invalid format
|
|
83
|
+
self.send_error(400, "Invalid Range header")
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
range_parts = range_spec.split("-", 1)
|
|
87
|
+
|
|
88
|
+
# Determine start and end positions
|
|
89
|
+
if range_parts[0]: # Start position specified
|
|
90
|
+
start = int(range_parts[0])
|
|
91
|
+
if range_parts[1]: # End position also specified
|
|
92
|
+
end = int(range_parts[1])
|
|
93
|
+
else: # Open-ended range (e.g., "1024-")
|
|
94
|
+
end = file_size - 1
|
|
95
|
+
else: # Suffix range (e.g., "-500" means last 500 bytes)
|
|
96
|
+
if not range_parts[1]:
|
|
97
|
+
self.send_error(400, "Invalid Range header")
|
|
98
|
+
return
|
|
99
|
+
suffix_length = int(range_parts[1])
|
|
100
|
+
start = max(0, file_size - suffix_length)
|
|
101
|
+
end = file_size - 1
|
|
102
|
+
|
|
103
|
+
# Validate range
|
|
104
|
+
if start < 0 or end >= file_size or start > end:
|
|
105
|
+
self.send_response(416, "Range Not Satisfiable")
|
|
106
|
+
self.send_header("Content-Range", f"bytes */{file_size}")
|
|
107
|
+
self.end_headers()
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
# Calculate content length
|
|
111
|
+
content_length = end - start + 1
|
|
112
|
+
|
|
113
|
+
# Guess content type
|
|
114
|
+
import mimetypes
|
|
115
|
+
|
|
116
|
+
content_type = mimetypes.guess_type(path)[0] or "application/octet-stream"
|
|
117
|
+
|
|
118
|
+
# Send 206 Partial Content response
|
|
119
|
+
self.send_response(206, "Partial Content")
|
|
120
|
+
self.send_header("Content-Type", content_type)
|
|
121
|
+
self.send_header("Content-Length", str(content_length))
|
|
122
|
+
self.send_header("Content-Range", f"bytes {start}-{end}/{file_size}")
|
|
123
|
+
self.end_headers()
|
|
124
|
+
|
|
125
|
+
# Send the requested byte range
|
|
126
|
+
with open(path, "rb") as f:
|
|
127
|
+
f.seek(start)
|
|
128
|
+
remaining = content_length
|
|
129
|
+
while remaining > 0:
|
|
130
|
+
chunk_size = min(8192, remaining)
|
|
131
|
+
chunk = f.read(chunk_size)
|
|
132
|
+
if not chunk:
|
|
133
|
+
break
|
|
134
|
+
self.wfile.write(chunk)
|
|
135
|
+
remaining -= len(chunk)
|
|
136
|
+
|
|
137
|
+
except ValueError:
|
|
138
|
+
# Invalid range values
|
|
139
|
+
self.send_error(400, "Invalid Range header")
|
|
140
|
+
except Exception as e:
|
|
141
|
+
# Log error and return 500
|
|
142
|
+
self.send_error(500, f"Internal Server Error: {str(e)}")
|
|
143
|
+
|
|
144
|
+
def log_message(self, format, *args):
|
|
49
145
|
pass
|
|
50
146
|
|
|
51
147
|
|
|
@@ -189,6 +285,7 @@ class ProcessServerManager:
|
|
|
189
285
|
and self._server_thread.is_alive()
|
|
190
286
|
and (allow_origin is None or self._allow_origin == allow_origin)
|
|
191
287
|
):
|
|
288
|
+
assert self._port is not None
|
|
192
289
|
return f"http://localhost:{self._port}", self._port
|
|
193
290
|
|
|
194
291
|
# Stop existing server if settings are incompatible
|
|
@@ -209,7 +306,7 @@ class ProcessServerManager:
|
|
|
209
306
|
if enable_file_upload:
|
|
210
307
|
from ._file_handler import FileUploadCORSRequestHandler
|
|
211
308
|
|
|
212
|
-
def
|
|
309
|
+
def handler_factory_enable_upload(*args, **kwargs):
|
|
213
310
|
return FileUploadCORSRequestHandler(
|
|
214
311
|
*args,
|
|
215
312
|
directory=str(temp_dir),
|
|
@@ -219,6 +316,11 @@ class ProcessServerManager:
|
|
|
219
316
|
**kwargs,
|
|
220
317
|
)
|
|
221
318
|
|
|
319
|
+
assert port is not None
|
|
320
|
+
self._server = ThreadingHTTPServer(
|
|
321
|
+
("0.0.0.0", port), handler_factory_enable_upload
|
|
322
|
+
)
|
|
323
|
+
|
|
222
324
|
else:
|
|
223
325
|
|
|
224
326
|
def handler_factory(*args, **kwargs):
|
|
@@ -226,7 +328,8 @@ class ProcessServerManager:
|
|
|
226
328
|
*args, directory=str(temp_dir), allow_origin=allow_origin, **kwargs
|
|
227
329
|
)
|
|
228
330
|
|
|
229
|
-
|
|
331
|
+
assert port is not None
|
|
332
|
+
self._server = ThreadingHTTPServer(("0.0.0.0", port), handler_factory)
|
|
230
333
|
self._port = port
|
|
231
334
|
self._allow_origin = allow_origin
|
|
232
335
|
|
figpack/core/_show_view.py
CHANGED