Skip to content

Commit 76e461f

Browse files
committed
make it a runtime thing
1 parent 3ffa9bb commit 76e461f

File tree

7 files changed

+179
-215
lines changed

7 files changed

+179
-215
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import JSON5 from "json5";
2+
import env from "$/env.json";
3+
4+
/**
5+
* Upload files to the server.
6+
*
7+
* @param state The state to apply the delta to.
8+
* @param handler The handler to use.
9+
* @param upload_id The upload id to use.
10+
* @param on_upload_progress The function to call on upload progress.
11+
* @param socket the websocket connection
12+
* @param extra_headers Extra headers to send with the request.
13+
* @param refs The refs object to store the abort controller in.
14+
* @param getBackendURL Function to get the backend URL.
15+
* @param getToken Function to get the Reflex token.
16+
*
17+
* @returns The response from posting to the UPLOADURL endpoint.
18+
*/
19+
export const uploadFiles = async (
20+
handler,
21+
files,
22+
upload_id,
23+
on_upload_progress,
24+
extra_headers,
25+
socket,
26+
refs,
27+
getBackendURL,
28+
getToken
29+
) => {
30+
// return if there's no file to upload
31+
if (files === undefined || files.length === 0) {
32+
return false;
33+
}
34+
35+
const upload_ref_name = `__upload_controllers_${upload_id}`;
36+
37+
if (refs[upload_ref_name]) {
38+
console.log("Upload already in progress for ", upload_id);
39+
return false;
40+
}
41+
42+
// Track how many partial updates have been processed for this upload.
43+
let resp_idx = 0;
44+
const eventHandler = (progressEvent) => {
45+
const event_callbacks = socket._callbacks.$event;
46+
// Whenever called, responseText will contain the entire response so far.
47+
const chunks = progressEvent.event.target.responseText.trim().split("\n");
48+
// So only process _new_ chunks beyond resp_idx.
49+
chunks.slice(resp_idx).map((chunk_json) => {
50+
try {
51+
const chunk = JSON5.parse(chunk_json);
52+
event_callbacks.map((f, ix) => {
53+
f(chunk)
54+
.then(() => {
55+
if (ix === event_callbacks.length - 1) {
56+
// Mark this chunk as processed.
57+
resp_idx += 1;
58+
}
59+
})
60+
.catch((e) => {
61+
if (progressEvent.progress === 1) {
62+
// Chunk may be incomplete, so only report errors when full response is available.
63+
console.log("Error processing chunk", chunk, e);
64+
}
65+
return;
66+
});
67+
});
68+
} catch (e) {
69+
if (progressEvent.progress === 1) {
70+
console.log("Error parsing chunk", chunk_json, e);
71+
}
72+
return;
73+
}
74+
});
75+
};
76+
77+
const controller = new AbortController();
78+
const formdata = new FormData();
79+
80+
// Add the token and handler to the file name.
81+
files.forEach((file) => {
82+
formdata.append("files", file, file.path || file.name);
83+
});
84+
85+
// Send the file to the server.
86+
refs[upload_ref_name] = controller;
87+
88+
return new Promise((resolve, reject) => {
89+
const xhr = new XMLHttpRequest();
90+
91+
// Set up event handlers
92+
xhr.onload = function () {
93+
if (xhr.status >= 200 && xhr.status < 300) {
94+
resolve({
95+
data: xhr.responseText,
96+
status: xhr.status,
97+
statusText: xhr.statusText,
98+
headers: {
99+
get: (name) => xhr.getResponseHeader(name),
100+
},
101+
});
102+
} else {
103+
reject(new Error(`HTTP error! status: ${xhr.status}`));
104+
}
105+
};
106+
107+
xhr.onerror = function () {
108+
reject(new Error("Network error"));
109+
};
110+
111+
xhr.onabort = function () {
112+
reject(new Error("Upload aborted"));
113+
};
114+
115+
// Handle upload progress
116+
if (on_upload_progress) {
117+
xhr.upload.onprogress = function (event) {
118+
if (event.lengthComputable) {
119+
const progressEvent = {
120+
loaded: event.loaded,
121+
total: event.total,
122+
progress: event.loaded / event.total,
123+
};
124+
on_upload_progress(progressEvent);
125+
}
126+
};
127+
}
128+
129+
// Handle download progress with streaming response parsing
130+
xhr.onprogress = function (event) {
131+
if (eventHandler) {
132+
const progressEvent = {
133+
event: {
134+
target: {
135+
responseText: xhr.responseText,
136+
},
137+
},
138+
progress: event.lengthComputable ? event.loaded / event.total : 0,
139+
};
140+
eventHandler(progressEvent);
141+
}
142+
};
143+
144+
// Handle abort controller
145+
controller.signal.addEventListener("abort", () => {
146+
xhr.abort();
147+
});
148+
149+
// Configure and send request
150+
xhr.open("POST", getBackendURL(env.UPLOAD));
151+
xhr.setRequestHeader("Reflex-Client-Token", getToken());
152+
xhr.setRequestHeader("Reflex-Event-Handler", handler);
153+
for (const [key, value] of Object.entries(extra_headers || {})) {
154+
xhr.setRequestHeader(key, value);
155+
}
156+
157+
try {
158+
xhr.send(formdata);
159+
} catch (error) {
160+
reject(error);
161+
}
162+
})
163+
.catch((error) => {
164+
console.log("Upload error:", error.message);
165+
return false;
166+
})
167+
.finally(() => {
168+
delete refs[upload_ref_name];
169+
});
170+
};

reflex/.templates/web/utils/state.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ export const applyRestEvent = async (event, socket, navigate, params) => {
432432
event.payload.files,
433433
event.payload.upload_id,
434434
event.payload.on_upload_progress,
435+
event.payload.extra_headers,
435436
socket,
436437
refs,
437438
getBackendURL,

reflex/app.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,11 +1422,6 @@ def _submit_work_without_advancing(
14221422
compile_results.append(
14231423
compiler.compile_contexts(self._state, self.theme),
14241424
)
1425-
compile_results.append(
1426-
compiler.compile_upload_js(
1427-
environment.REFLEX_UPLOAD_ENDPOINT_EXTRA_HEADERS.get().items()
1428-
)
1429-
)
14301425
if self.theme is not None:
14311426
# Fix #2992 by removing the top-level appearance prop
14321427
self.theme.appearance = None # pyright: ignore[reportAttributeAccessIssue]

reflex/compiler/compiler.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -145,18 +145,6 @@ def _compile_contexts(state: type[BaseState] | None, theme: Component | None) ->
145145
)
146146

147147

148-
def _compile_upload_js(extra_headers: Iterable[tuple[str, str]]) -> str:
149-
"""Compile the upload.js file.
150-
151-
Args:
152-
extra_headers: Extra headers to include in the upload request.
153-
154-
Returns:
155-
The compiled upload.js file.
156-
"""
157-
return templates.upload_js_template(extra_headers=extra_headers)
158-
159-
160148
def _compile_page(component: BaseComponent) -> str:
161149
"""Compile the component.
162150
@@ -559,20 +547,6 @@ def compile_contexts(
559547
return output_path, _compile_contexts(state, theme)
560548

561549

562-
def compile_upload_js(extra_headers: Iterable[tuple[str, str]]) -> tuple[str, str]:
563-
"""Compile the upload.js file.
564-
565-
Args:
566-
extra_headers: Extra headers to include in the upload request.
567-
568-
Returns:
569-
The path and code of the compiled upload.js file.
570-
"""
571-
output_path = utils.get_upload_js_path()
572-
573-
return output_path, _compile_upload_js(extra_headers=extra_headers)
574-
575-
576550
def compile_page(path: str, component: BaseComponent) -> tuple[str, str]:
577551
"""Compile a single page.
578552

0 commit comments

Comments
 (0)