figpack 0.1.0__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.
Potentially problematic release.
This version of figpack might be problematic. Click here for more details.
- figpack/__init__.py +7 -0
- figpack/_show_view.py +133 -0
- figpack/_upload_view.py +334 -0
- figpack/figpack-gui-dist/assets/index-BrKvMWud.js +65 -0
- figpack/figpack-gui-dist/assets/index-CeWL3OeJ.css +1 -0
- figpack/figpack-gui-dist/index.html +14 -0
- figpack/figpack_view.py +62 -0
- figpack/views.py +250 -0
- figpack-0.1.0.dist-info/METADATA +33 -0
- figpack-0.1.0.dist-info/RECORD +14 -0
- figpack-0.1.0.dist-info/WHEEL +5 -0
- figpack-0.1.0.dist-info/licenses/LICENSE +201 -0
- figpack-0.1.0.dist-info/top_level.txt +2 -0
- figpack-gui/node_modules/flatted/python/flatted.py +149 -0
figpack/__init__.py
ADDED
figpack/_show_view.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from typing import Union
|
|
4
|
+
import zarr
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
import webbrowser
|
|
8
|
+
|
|
9
|
+
import pathlib
|
|
10
|
+
|
|
11
|
+
import threading
|
|
12
|
+
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
13
|
+
|
|
14
|
+
from .views import TimeseriesGraph
|
|
15
|
+
|
|
16
|
+
thisdir = pathlib.Path(__file__).parent.resolve()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _show_view(
|
|
20
|
+
view: TimeseriesGraph,
|
|
21
|
+
*,
|
|
22
|
+
open_in_browser: bool = False,
|
|
23
|
+
port: Union[int, None] = None,
|
|
24
|
+
allow_origin: Union[str, None] = None,
|
|
25
|
+
):
|
|
26
|
+
with tempfile.TemporaryDirectory(prefix="figpack_") as tmpdir:
|
|
27
|
+
html_dir = thisdir / "figpack-gui-dist"
|
|
28
|
+
if not os.path.exists(html_dir):
|
|
29
|
+
raise SystemExit(f"Error: directory not found: {html_dir}")
|
|
30
|
+
# copy all files in html_dir recursively to tmpdir
|
|
31
|
+
for item in html_dir.iterdir():
|
|
32
|
+
if item.is_file():
|
|
33
|
+
target = pathlib.Path(tmpdir) / item.name
|
|
34
|
+
target.write_bytes(item.read_bytes())
|
|
35
|
+
elif item.is_dir():
|
|
36
|
+
target = pathlib.Path(tmpdir) / item.name
|
|
37
|
+
target.mkdir(exist_ok=True)
|
|
38
|
+
for subitem in item.iterdir():
|
|
39
|
+
target_sub = target / subitem.name
|
|
40
|
+
target_sub.write_bytes(subitem.read_bytes())
|
|
41
|
+
|
|
42
|
+
# Write the graph data to the Zarr group
|
|
43
|
+
zarr_group = zarr.open_group(
|
|
44
|
+
pathlib.Path(tmpdir) / "data.zarr",
|
|
45
|
+
mode="w",
|
|
46
|
+
synchronizer=zarr.ThreadSynchronizer(),
|
|
47
|
+
)
|
|
48
|
+
view._write_to_zarr_group(zarr_group)
|
|
49
|
+
|
|
50
|
+
zarr.consolidate_metadata(zarr_group.store)
|
|
51
|
+
|
|
52
|
+
serve_files(
|
|
53
|
+
tmpdir,
|
|
54
|
+
port=port,
|
|
55
|
+
open_in_browser=open_in_browser,
|
|
56
|
+
allow_origin=allow_origin,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CORSRequestHandler(SimpleHTTPRequestHandler):
|
|
61
|
+
def __init__(self, *args, allow_origin=None, **kwargs):
|
|
62
|
+
self.allow_origin = allow_origin
|
|
63
|
+
super().__init__(*args, **kwargs)
|
|
64
|
+
|
|
65
|
+
# Serve only GET/HEAD/OPTIONS; add CORS headers on every response
|
|
66
|
+
def end_headers(self):
|
|
67
|
+
if self.allow_origin is not None:
|
|
68
|
+
self.send_header("Access-Control-Allow-Origin", self.allow_origin)
|
|
69
|
+
self.send_header("Vary", "Origin")
|
|
70
|
+
self.send_header("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
|
|
71
|
+
self.send_header("Access-Control-Allow-Headers", "Content-Type, Range")
|
|
72
|
+
self.send_header(
|
|
73
|
+
"Access-Control-Expose-Headers",
|
|
74
|
+
"Accept-Ranges, Content-Encoding, Content-Length, Content-Range",
|
|
75
|
+
)
|
|
76
|
+
super().end_headers()
|
|
77
|
+
|
|
78
|
+
def do_OPTIONS(self):
|
|
79
|
+
self.send_response(204, "No Content")
|
|
80
|
+
self.end_headers()
|
|
81
|
+
|
|
82
|
+
def log_message(self, fmt, *args):
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def serve_files(
|
|
87
|
+
tmpdir: str,
|
|
88
|
+
*,
|
|
89
|
+
port: Union[int, None],
|
|
90
|
+
open_in_browser: bool = False,
|
|
91
|
+
allow_origin: Union[str, None] = None,
|
|
92
|
+
):
|
|
93
|
+
# if port is None, find a free port
|
|
94
|
+
if port is None:
|
|
95
|
+
import socket
|
|
96
|
+
|
|
97
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
98
|
+
s.bind(("", 0))
|
|
99
|
+
port = s.getsockname()[1]
|
|
100
|
+
|
|
101
|
+
tmpdir = pathlib.Path(tmpdir)
|
|
102
|
+
tmpdir = tmpdir.resolve()
|
|
103
|
+
if not tmpdir.exists() or not tmpdir.is_dir():
|
|
104
|
+
raise SystemExit(f"Directory not found: {tmpdir}")
|
|
105
|
+
|
|
106
|
+
# Configure handler with directory and allow_origin
|
|
107
|
+
def handler_factory(*args, **kwargs):
|
|
108
|
+
return CORSRequestHandler(
|
|
109
|
+
*args, directory=str(tmpdir), allow_origin=allow_origin, **kwargs
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
httpd = ThreadingHTTPServer(("0.0.0.0", port), handler_factory)
|
|
113
|
+
print(f"Serving {tmpdir} at http://localhost:{port} (CORS → {allow_origin})")
|
|
114
|
+
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
115
|
+
thread.start()
|
|
116
|
+
|
|
117
|
+
if open_in_browser:
|
|
118
|
+
webbrowser.open(f"http://localhost:{port}")
|
|
119
|
+
print(f"Opening http://localhost:{port} in your browser.")
|
|
120
|
+
else:
|
|
121
|
+
print(
|
|
122
|
+
f"Open http://localhost:{port} in your browser to view the visualization."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
input("Press Enter to stop...\n")
|
|
127
|
+
except (KeyboardInterrupt, EOFError):
|
|
128
|
+
pass
|
|
129
|
+
finally:
|
|
130
|
+
print("Shutting down server...")
|
|
131
|
+
httpd.shutdown()
|
|
132
|
+
httpd.server_close()
|
|
133
|
+
thread.join()
|
figpack/_upload_view.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
import tempfile
|
|
6
|
+
import pathlib
|
|
7
|
+
import requests
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
|
|
10
|
+
import zarr
|
|
11
|
+
|
|
12
|
+
from .views import TimeseriesGraph
|
|
13
|
+
|
|
14
|
+
thisdir = pathlib.Path(__file__).parent.resolve()
|
|
15
|
+
|
|
16
|
+
FIGPACK_API_BASE_URL = "https://figpack-api.vercel.app"
|
|
17
|
+
TEMPORY_BASE_URL = "https://tempory.net/figpack/figures"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _upload_view(view: TimeseriesGraph) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Upload a figpack view to the cloud
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
view: The figpack view to upload
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
str: URL where the uploaded figure can be viewed
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
EnvironmentError: If FIGPACK_UPLOAD_PASSCODE is not set
|
|
32
|
+
Exception: If upload fails
|
|
33
|
+
"""
|
|
34
|
+
# Check for required environment variable
|
|
35
|
+
passcode = os.environ.get("FIGPACK_UPLOAD_PASSCODE")
|
|
36
|
+
if not passcode:
|
|
37
|
+
raise EnvironmentError(
|
|
38
|
+
"FIGPACK_UPLOAD_PASSCODE environment variable must be set"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Generate random figure ID
|
|
42
|
+
figure_id = str(uuid.uuid4())
|
|
43
|
+
print(f"Generated figure ID: {figure_id}")
|
|
44
|
+
|
|
45
|
+
with tempfile.TemporaryDirectory(prefix="figpack_upload_") as tmpdir:
|
|
46
|
+
# Prepare the figure bundle (reuse logic from _show_view)
|
|
47
|
+
print("Preparing figure bundle...")
|
|
48
|
+
_prepare_figure_bundle(view, tmpdir)
|
|
49
|
+
|
|
50
|
+
# Upload the bundle
|
|
51
|
+
print("Starting upload...")
|
|
52
|
+
_upload_bundle(tmpdir, figure_id, passcode)
|
|
53
|
+
|
|
54
|
+
# Return the final URL
|
|
55
|
+
figure_url = f"{TEMPORY_BASE_URL}/{figure_id}/index.html"
|
|
56
|
+
print(f"Upload completed successfully!")
|
|
57
|
+
print(f"Figure available at: {figure_url}")
|
|
58
|
+
return figure_url
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _prepare_figure_bundle(view: TimeseriesGraph, tmpdir: str) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Prepare the figure bundle in the temporary directory
|
|
64
|
+
This reuses the same logic as _show_view
|
|
65
|
+
"""
|
|
66
|
+
html_dir = thisdir / "figpack-gui-dist"
|
|
67
|
+
if not os.path.exists(html_dir):
|
|
68
|
+
raise SystemExit(f"Error: directory not found: {html_dir}")
|
|
69
|
+
|
|
70
|
+
# Copy all files in html_dir recursively to tmpdir
|
|
71
|
+
for item in html_dir.iterdir():
|
|
72
|
+
if item.is_file():
|
|
73
|
+
target = pathlib.Path(tmpdir) / item.name
|
|
74
|
+
target.write_bytes(item.read_bytes())
|
|
75
|
+
elif item.is_dir():
|
|
76
|
+
target = pathlib.Path(tmpdir) / item.name
|
|
77
|
+
target.mkdir(exist_ok=True)
|
|
78
|
+
for subitem in item.iterdir():
|
|
79
|
+
target_sub = target / subitem.name
|
|
80
|
+
target_sub.write_bytes(subitem.read_bytes())
|
|
81
|
+
|
|
82
|
+
# Write the graph data to the Zarr group
|
|
83
|
+
zarr_group = zarr.open_group(
|
|
84
|
+
pathlib.Path(tmpdir) / "data.zarr",
|
|
85
|
+
mode="w",
|
|
86
|
+
synchronizer=zarr.ThreadSynchronizer(),
|
|
87
|
+
)
|
|
88
|
+
view._write_to_zarr_group(zarr_group)
|
|
89
|
+
zarr.consolidate_metadata(zarr_group.store)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _upload_bundle(tmpdir: str, figure_id: str, passcode: str) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Upload the prepared bundle to the cloud
|
|
95
|
+
"""
|
|
96
|
+
tmpdir_path = pathlib.Path(tmpdir)
|
|
97
|
+
|
|
98
|
+
# First, upload initial figpack.json with "uploading" status
|
|
99
|
+
print("Uploading initial status...")
|
|
100
|
+
figpack_json = {
|
|
101
|
+
"status": "uploading",
|
|
102
|
+
"upload_started": datetime.now(timezone.utc).isoformat(),
|
|
103
|
+
"upload_updated": datetime.now(timezone.utc).isoformat(),
|
|
104
|
+
"figure_id": figure_id,
|
|
105
|
+
}
|
|
106
|
+
_upload_small_file(
|
|
107
|
+
figure_id, "figpack.json", json.dumps(figpack_json, indent=2), passcode
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Collect all files to upload
|
|
111
|
+
all_files = []
|
|
112
|
+
for file_path in tmpdir_path.rglob("*"):
|
|
113
|
+
if file_path.is_file():
|
|
114
|
+
relative_path = file_path.relative_to(tmpdir_path)
|
|
115
|
+
all_files.append((str(relative_path), file_path))
|
|
116
|
+
|
|
117
|
+
print(f"Found {len(all_files)} files to upload")
|
|
118
|
+
|
|
119
|
+
# Upload files
|
|
120
|
+
uploaded_count = 0
|
|
121
|
+
timer = time.time()
|
|
122
|
+
for relative_path, file_path in all_files:
|
|
123
|
+
# Skip the figpack.json since we already uploaded the initial version
|
|
124
|
+
if relative_path == "figpack.json":
|
|
125
|
+
continue
|
|
126
|
+
file_type = _determine_file_type(relative_path)
|
|
127
|
+
|
|
128
|
+
if file_type == "small":
|
|
129
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
130
|
+
content = f.read()
|
|
131
|
+
_upload_small_file(figure_id, relative_path, content, passcode)
|
|
132
|
+
else: # large file
|
|
133
|
+
_upload_large_file(figure_id, relative_path, file_path, passcode)
|
|
134
|
+
|
|
135
|
+
uploaded_count += 1
|
|
136
|
+
print(f"Uploaded {uploaded_count}/{len(all_files)-1}: {relative_path}")
|
|
137
|
+
elapsed_time = time.time() - timer
|
|
138
|
+
if elapsed_time > 60:
|
|
139
|
+
figpack_json = {
|
|
140
|
+
**figpack_json,
|
|
141
|
+
"status": "uploading",
|
|
142
|
+
"upload_progress": f"{uploaded_count}/{len(all_files)-1}",
|
|
143
|
+
"upload_updated": datetime.now(timezone.utc).isoformat(),
|
|
144
|
+
}
|
|
145
|
+
_upload_small_file(
|
|
146
|
+
figure_id,
|
|
147
|
+
"figpack.json",
|
|
148
|
+
json.dumps(figpack_json, indent=2),
|
|
149
|
+
passcode,
|
|
150
|
+
)
|
|
151
|
+
print(
|
|
152
|
+
f"Updated figpack.json with progress: {uploaded_count}/{len(all_files)-1}"
|
|
153
|
+
)
|
|
154
|
+
timer = time.time()
|
|
155
|
+
|
|
156
|
+
# Finally, upload completion status
|
|
157
|
+
print("Uploading completion status...")
|
|
158
|
+
figpack_json = {
|
|
159
|
+
**figpack_json,
|
|
160
|
+
"status": "completed",
|
|
161
|
+
"upload_completed": datetime.now(timezone.utc).isoformat(),
|
|
162
|
+
"expiration": (datetime.now(timezone.utc) + timedelta(days=1)).isoformat(),
|
|
163
|
+
"figure_id": figure_id,
|
|
164
|
+
"total_files": len(all_files),
|
|
165
|
+
}
|
|
166
|
+
_upload_small_file(
|
|
167
|
+
figure_id, "figpack.json", json.dumps(figpack_json, indent=2), passcode
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _determine_file_type(file_path: str) -> str:
|
|
172
|
+
"""
|
|
173
|
+
Determine if a file should be uploaded as small or large
|
|
174
|
+
Based on the validation logic in the API
|
|
175
|
+
"""
|
|
176
|
+
# Check exact matches first
|
|
177
|
+
if file_path == "figpack.json" or file_path == "index.html":
|
|
178
|
+
return "small"
|
|
179
|
+
|
|
180
|
+
# Check zarr metadata files
|
|
181
|
+
if (
|
|
182
|
+
file_path.endswith(".zattrs")
|
|
183
|
+
or file_path.endswith(".zgroup")
|
|
184
|
+
or file_path.endswith(".zarray")
|
|
185
|
+
or file_path.endswith(".zmetadata")
|
|
186
|
+
):
|
|
187
|
+
return "small"
|
|
188
|
+
|
|
189
|
+
# Check HTML files
|
|
190
|
+
if file_path.endswith(".html"):
|
|
191
|
+
return "small"
|
|
192
|
+
|
|
193
|
+
# Check data.zarr directory
|
|
194
|
+
if file_path.startswith("data.zarr/"):
|
|
195
|
+
file_name = file_path[len("data.zarr/") :]
|
|
196
|
+
# Check if it's a zarr chunk (numeric like 0.0.1)
|
|
197
|
+
if _is_zarr_chunk(file_name):
|
|
198
|
+
return "large"
|
|
199
|
+
# Check for zarr metadata files in subdirectories
|
|
200
|
+
if (
|
|
201
|
+
file_name.endswith(".zattrs")
|
|
202
|
+
or file_name.endswith(".zgroup")
|
|
203
|
+
or file_name.endswith(".zarray")
|
|
204
|
+
or file_name.endswith(".zmetadata")
|
|
205
|
+
):
|
|
206
|
+
return "small"
|
|
207
|
+
|
|
208
|
+
# Check assets directory
|
|
209
|
+
if file_path.startswith("assets/"):
|
|
210
|
+
file_name = file_path[len("assets/") :]
|
|
211
|
+
if file_name.endswith(".js") or file_name.endswith(".css"):
|
|
212
|
+
return "large"
|
|
213
|
+
|
|
214
|
+
# Default to large file
|
|
215
|
+
return "large"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _is_zarr_chunk(file_name: str) -> bool:
|
|
219
|
+
"""
|
|
220
|
+
Check if filename consists only of numbers and dots (zarr chunk pattern)
|
|
221
|
+
"""
|
|
222
|
+
for char in file_name:
|
|
223
|
+
if char != "." and not char.isdigit():
|
|
224
|
+
return False
|
|
225
|
+
return (
|
|
226
|
+
len(file_name) > 0
|
|
227
|
+
and not file_name.startswith(".")
|
|
228
|
+
and not file_name.endswith(".")
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _upload_small_file(
|
|
233
|
+
figure_id: str, file_path: str, content: str, passcode: str
|
|
234
|
+
) -> None:
|
|
235
|
+
"""
|
|
236
|
+
Upload a small file by sending content directly
|
|
237
|
+
"""
|
|
238
|
+
destination_url = f"{TEMPORY_BASE_URL}/{figure_id}/{file_path}"
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
content.encode("utf-8")
|
|
242
|
+
except Exception as e:
|
|
243
|
+
raise Exception(f"Content for {file_path} is not UTF-8 encodable: {e}")
|
|
244
|
+
payload = {
|
|
245
|
+
"destinationUrl": destination_url,
|
|
246
|
+
"passcode": passcode,
|
|
247
|
+
"content": content,
|
|
248
|
+
}
|
|
249
|
+
# check that payload is json serializable
|
|
250
|
+
try:
|
|
251
|
+
json.dumps(payload)
|
|
252
|
+
except Exception as e:
|
|
253
|
+
raise Exception(f"Payload for {file_path} is not JSON serializable: {e}")
|
|
254
|
+
|
|
255
|
+
response = requests.post(f"{FIGPACK_API_BASE_URL}/api/upload", json=payload)
|
|
256
|
+
|
|
257
|
+
if not response.ok:
|
|
258
|
+
try:
|
|
259
|
+
error_data = response.json()
|
|
260
|
+
error_msg = error_data.get("message", "Unknown error")
|
|
261
|
+
except:
|
|
262
|
+
error_msg = f"HTTP {response.status_code}"
|
|
263
|
+
raise Exception(f"Failed to upload {file_path}: {error_msg}")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _upload_large_file(
|
|
267
|
+
figure_id: str, file_path: str, local_file_path: pathlib.Path, passcode: str
|
|
268
|
+
) -> None:
|
|
269
|
+
"""
|
|
270
|
+
Upload a large file using signed URL
|
|
271
|
+
"""
|
|
272
|
+
destination_url = f"{TEMPORY_BASE_URL}/{figure_id}/{file_path}"
|
|
273
|
+
file_size = local_file_path.stat().st_size
|
|
274
|
+
|
|
275
|
+
# Get signed URL
|
|
276
|
+
payload = {
|
|
277
|
+
"destinationUrl": destination_url,
|
|
278
|
+
"passcode": passcode,
|
|
279
|
+
"size": file_size,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
response = requests.post(f"{FIGPACK_API_BASE_URL}/api/upload", json=payload)
|
|
283
|
+
|
|
284
|
+
if not response.ok:
|
|
285
|
+
try:
|
|
286
|
+
error_data = response.json()
|
|
287
|
+
error_msg = error_data.get("message", "Unknown error")
|
|
288
|
+
except:
|
|
289
|
+
error_msg = f"HTTP {response.status_code}"
|
|
290
|
+
raise Exception(f"Failed to get signed URL for {file_path}: {error_msg}")
|
|
291
|
+
|
|
292
|
+
response_data = response.json()
|
|
293
|
+
if not response_data.get("success"):
|
|
294
|
+
raise Exception(
|
|
295
|
+
f"Failed to get signed URL for {file_path}: {response_data.get('message', 'Unknown error')}"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
signed_url = response_data.get("signedUrl")
|
|
299
|
+
if not signed_url:
|
|
300
|
+
raise Exception(f"No signed URL returned for {file_path}")
|
|
301
|
+
|
|
302
|
+
# Upload file to signed URL
|
|
303
|
+
content_type = _determine_content_type(file_path)
|
|
304
|
+
with open(local_file_path, "rb") as f:
|
|
305
|
+
upload_response = requests.put(
|
|
306
|
+
signed_url, data=f, headers={"Content-Type": content_type}
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if not upload_response.ok:
|
|
310
|
+
raise Exception(
|
|
311
|
+
f"Failed to upload {file_path} to signed URL: HTTP {upload_response.status_code}"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _determine_content_type(file_path: str) -> str:
|
|
316
|
+
"""
|
|
317
|
+
Determine content type for upload based on file extension
|
|
318
|
+
"""
|
|
319
|
+
file_name = file_path.split("/")[-1]
|
|
320
|
+
extension = file_name.split(".")[-1] if "." in file_name else ""
|
|
321
|
+
|
|
322
|
+
content_type_map = {
|
|
323
|
+
"json": "application/json",
|
|
324
|
+
"html": "text/html",
|
|
325
|
+
"css": "text/css",
|
|
326
|
+
"js": "application/javascript",
|
|
327
|
+
"png": "image/png",
|
|
328
|
+
"zattrs": "application/json",
|
|
329
|
+
"zgroup": "application/json",
|
|
330
|
+
"zarray": "application/json",
|
|
331
|
+
"zmetadata": "application/json",
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return content_type_map.get(extension, "application/octet-stream")
|