Skip to content

Commit 7a4b0a2

Browse files
Local upload tracking (#1092)
* cleanup `upload_video` arguments * wip * wip * make `progress_upload` required for instant mode * fix uploading of thumbnails * absolute mess * store recordings into project meta? * cursed realtime upload progress * break out API definitions + use tracing for logging in `upload.rs` * working stream-based uploader * bring back progress tracking to upload * implement `from_file` abstraction * abstract another endpoint into `api` module * abstract more API + retry on S3 upload request * finish overhauling upload code * restructure project file again * UI for uploading state + errors * show recording and pending status * merge in changes from #1077 * upload progress working in sync pogggg * wip * polish off todo's in `recordings.tsx` * a bunch of random fixes * fixes * fix * resumable system * format * feature flag the new uploader * fixes to some remaining issues * fix button visibility on `CapCard` * wip * Clippy improvements * cleanup * fix CI * fix Typescript * fixes * make `CapCard` more conservative with upload status * CodeRabbit fixes * fix some `CapCard` states * fix retry policy * wip * fixes * emit first chunk multiple times * potentially fix the missing video toolbar sometimes * improve chunk emit logic * fix retry policy for s3 upload * stttttrrrriiiinnnnggg
1 parent 92b587f commit 7a4b0a2

File tree

29 files changed

+2781
-1202
lines changed

29 files changed

+2781
-1202
lines changed

Cargo.lock

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ wgpu.workspace = true
104104
bytemuck = "1.23.1"
105105
kameo = "0.17.2"
106106
tauri-plugin-sentry = "0.5.0"
107+
thiserror.workspace = true
108+
bytes = "1.10.1"
109+
async-stream = "0.3.6"
107110

108111
[target.'cfg(target_os = "macos")'.dependencies]
109112
core-graphics = "0.24.0"

apps/desktop/src-tauri/src/api.rs

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
//! TODO: We should investigate generating this with OpenAPI.
2+
//! This will come part of the EffectTS rewrite work.
3+
4+
use serde::{Deserialize, Serialize};
5+
use serde_json::json;
6+
use tauri::AppHandle;
7+
8+
use crate::web_api::ManagerExt;
9+
10+
pub async fn upload_multipart_initiate(app: &AppHandle, video_id: &str) -> Result<String, String> {
11+
#[derive(Deserialize)]
12+
#[serde(rename_all = "camelCase")]
13+
pub struct Response {
14+
upload_id: String,
15+
}
16+
17+
let resp = app
18+
.authed_api_request("/api/upload/multipart/initiate", |c, url| {
19+
c.post(url)
20+
.header("Content-Type", "application/json")
21+
.json(&serde_json::json!({
22+
"videoId": video_id,
23+
"contentType": "video/mp4"
24+
}))
25+
})
26+
.await
27+
.map_err(|err| format!("api/upload_multipart_initiate/request: {err}"))?;
28+
29+
if !resp.status().is_success() {
30+
let status = resp.status();
31+
let error_body = resp
32+
.text()
33+
.await
34+
.unwrap_or_else(|_| "<no response body>".to_string());
35+
return Err(format!(
36+
"api/upload_multipart_initiate/{status}: {error_body}"
37+
));
38+
}
39+
40+
resp.json::<Response>()
41+
.await
42+
.map_err(|err| format!("api/upload_multipart_initiate/response: {err}"))
43+
.map(|data| data.upload_id)
44+
}
45+
46+
pub async fn upload_multipart_presign_part(
47+
app: &AppHandle,
48+
video_id: &str,
49+
upload_id: &str,
50+
part_number: u32,
51+
md5_sum: &str,
52+
) -> Result<String, String> {
53+
#[derive(Deserialize)]
54+
#[serde(rename_all = "camelCase")]
55+
pub struct Response {
56+
presigned_url: String,
57+
}
58+
59+
let resp = app
60+
.authed_api_request("/api/upload/multipart/presign-part", |c, url| {
61+
c.post(url)
62+
.header("Content-Type", "application/json")
63+
.json(&serde_json::json!({
64+
"videoId": video_id,
65+
"uploadId": upload_id,
66+
"partNumber": part_number,
67+
"md5Sum": md5_sum
68+
}))
69+
})
70+
.await
71+
.map_err(|err| format!("api/upload_multipart_presign_part/request: {err}"))?;
72+
73+
if !resp.status().is_success() {
74+
let status = resp.status();
75+
let error_body = resp
76+
.text()
77+
.await
78+
.unwrap_or_else(|_| "<no response body>".to_string());
79+
return Err(format!(
80+
"api/upload_multipart_presign_part/{status}: {error_body}"
81+
));
82+
}
83+
84+
resp.json::<Response>()
85+
.await
86+
.map_err(|err| format!("api/upload_multipart_presign_part/response: {err}"))
87+
.map(|data| data.presigned_url)
88+
}
89+
90+
#[derive(Serialize)]
91+
#[serde(rename_all = "camelCase")]
92+
pub struct UploadedPart {
93+
pub part_number: u32,
94+
pub etag: String,
95+
pub size: usize,
96+
#[serde(skip)]
97+
pub total_size: u64,
98+
}
99+
100+
#[derive(Serialize, Debug, Clone)]
101+
#[serde(rename_all = "camelCase")]
102+
pub struct S3VideoMeta {
103+
#[serde(rename = "durationInSecs")]
104+
pub duration_in_secs: f64,
105+
pub width: u32,
106+
pub height: u32,
107+
#[serde(skip_serializing_if = "Option::is_none")]
108+
pub fps: Option<f32>,
109+
}
110+
111+
pub async fn upload_multipart_complete(
112+
app: &AppHandle,
113+
video_id: &str,
114+
upload_id: &str,
115+
parts: &[UploadedPart],
116+
meta: Option<S3VideoMeta>,
117+
) -> Result<Option<String>, String> {
118+
#[derive(Serialize)]
119+
#[serde(rename_all = "camelCase")]
120+
pub struct MultipartCompleteRequest<'a> {
121+
video_id: &'a str,
122+
upload_id: &'a str,
123+
parts: &'a [UploadedPart],
124+
#[serde(flatten)]
125+
meta: Option<S3VideoMeta>,
126+
}
127+
128+
#[derive(Deserialize)]
129+
pub struct Response {
130+
location: Option<String>,
131+
}
132+
133+
let resp = app
134+
.authed_api_request("/api/upload/multipart/complete", |c, url| {
135+
c.post(url)
136+
.header("Content-Type", "application/json")
137+
.json(&MultipartCompleteRequest {
138+
video_id,
139+
upload_id,
140+
parts,
141+
meta,
142+
})
143+
})
144+
.await
145+
.map_err(|err| format!("api/upload_multipart_complete/request: {err}"))?;
146+
147+
if !resp.status().is_success() {
148+
let status = resp.status();
149+
let error_body = resp
150+
.text()
151+
.await
152+
.unwrap_or_else(|_| "<no response body>".to_string());
153+
return Err(format!(
154+
"api/upload_multipart_complete/{status}: {error_body}"
155+
));
156+
}
157+
158+
resp.json::<Response>()
159+
.await
160+
.map_err(|err| format!("api/upload_multipart_complete/response: {err}"))
161+
.map(|data| data.location)
162+
}
163+
164+
#[derive(Serialize)]
165+
#[serde(rename_all = "lowercase")]
166+
pub enum PresignedS3PutRequestMethod {
167+
#[allow(unused)]
168+
Post,
169+
Put,
170+
}
171+
172+
#[derive(Serialize)]
173+
#[serde(rename_all = "camelCase")]
174+
pub struct PresignedS3PutRequest {
175+
pub video_id: String,
176+
pub subpath: String,
177+
pub method: PresignedS3PutRequestMethod,
178+
#[serde(flatten)]
179+
pub meta: Option<S3VideoMeta>,
180+
}
181+
182+
pub async fn upload_signed(app: &AppHandle, body: PresignedS3PutRequest) -> Result<String, String> {
183+
#[derive(Deserialize)]
184+
struct Data {
185+
url: String,
186+
}
187+
188+
#[derive(Deserialize)]
189+
#[serde(rename_all = "camelCase")]
190+
pub struct Response {
191+
presigned_put_data: Data,
192+
}
193+
194+
let resp = app
195+
.authed_api_request("/api/upload/signed", |client, url| {
196+
client.post(url).json(&body)
197+
})
198+
.await
199+
.map_err(|err| format!("api/upload_signed/request: {err}"))?;
200+
201+
if !resp.status().is_success() {
202+
let status = resp.status();
203+
let error_body = resp
204+
.text()
205+
.await
206+
.unwrap_or_else(|_| "<no response body>".to_string());
207+
return Err(format!("api/upload_signed/{status}: {error_body}"));
208+
}
209+
210+
resp.json::<Response>()
211+
.await
212+
.map_err(|err| format!("api/upload_signed/response: {err}"))
213+
.map(|data| data.presigned_put_data.url)
214+
}
215+
216+
pub async fn desktop_video_progress(
217+
app: &AppHandle,
218+
video_id: &str,
219+
uploaded: u64,
220+
total: u64,
221+
) -> Result<(), String> {
222+
let resp = app
223+
.authed_api_request("/api/desktop/video/progress", |client, url| {
224+
client.post(url).json(&json!({
225+
"videoId": video_id,
226+
"uploaded": uploaded,
227+
"total": total,
228+
"updatedAt": chrono::Utc::now().to_rfc3339()
229+
}))
230+
})
231+
.await
232+
.map_err(|err| format!("api/desktop_video_progress/request: {err}"))?;
233+
234+
if !resp.status().is_success() {
235+
let status = resp.status();
236+
let error_body = resp
237+
.text()
238+
.await
239+
.unwrap_or_else(|_| "<no response body>".to_string());
240+
return Err(format!("api/desktop_video_progress/{status}: {error_body}"));
241+
}
242+
243+
Ok(())
244+
}

apps/desktop/src-tauri/src/general_settings.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ pub struct GeneralSettingsStore {
9797
pub enable_new_recording_flow: bool,
9898
#[serde(default)]
9999
pub post_deletion_behaviour: PostDeletionBehaviour,
100+
#[serde(default = "default_enable_new_uploader", skip_serializing_if = "no")]
101+
pub enable_new_uploader: bool,
100102
}
101103

102104
fn default_enable_native_camera_preview() -> bool {
@@ -108,6 +110,10 @@ fn default_enable_new_recording_flow() -> bool {
108110
cfg!(debug_assertions)
109111
}
110112

113+
fn default_enable_new_uploader() -> bool {
114+
cfg!(debug_assertions)
115+
}
116+
111117
fn no(_: &bool) -> bool {
112118
false
113119
}
@@ -155,6 +161,7 @@ impl Default for GeneralSettingsStore {
155161
auto_zoom_on_clicks: false,
156162
enable_new_recording_flow: default_enable_new_recording_flow(),
157163
post_deletion_behaviour: PostDeletionBehaviour::DoNothing,
164+
enable_new_uploader: default_enable_new_uploader(),
158165
}
159166
}
160167
}

0 commit comments

Comments
 (0)