1use tracing::{debug, info};
4use uuid::Uuid;
5
6use crate::{client::DaytonaClient, error::Result, models::workspace::*};
7
8pub struct WorkspaceManager<'a> {
10 client: &'a DaytonaClient,
11}
12
13impl<'a> WorkspaceManager<'a> {
14 pub fn new(client: &'a DaytonaClient) -> Self {
16 Self { client }
17 }
18
19 pub async fn create(&self, params: CreateWorkspaceParams) -> Result<Workspace> {
21 info!("Creating new workspace: {}", params.name);
22
23 let response = self
24 .client
25 .build_request(reqwest::Method::POST, "/workspace")
26 .json(¶ms)
27 .send()
28 .await?;
29
30 let workspace = self.client.handle_response::<Workspace>(response).await?;
31 info!("Created workspace: {} ({})", workspace.name, workspace.id);
32
33 Ok(workspace)
34 }
35
36 pub async fn get(&self, workspace_id: &Uuid) -> Result<Workspace> {
38 debug!("Getting workspace: {}", workspace_id);
39
40 let response = self
41 .client
42 .build_request(
43 reqwest::Method::GET,
44 &format!("/workspace/{}", workspace_id),
45 )
46 .send()
47 .await?;
48
49 self.client.handle_response(response).await
50 }
51
52 pub async fn list(&self) -> Result<Vec<Workspace>> {
54 debug!("Listing workspaces");
55
56 let response = self
57 .client
58 .build_request(reqwest::Method::GET, "/workspace")
59 .send()
60 .await?;
61
62 self.client.handle_response(response).await
63 }
64
65 pub async fn update(
67 &self,
68 workspace_id: &Uuid,
69 params: UpdateWorkspaceParams,
70 ) -> Result<Workspace> {
71 info!("Updating workspace: {}", workspace_id);
72
73 let response = self
74 .client
75 .build_request(
76 reqwest::Method::PUT,
77 &format!("/workspace/{}", workspace_id),
78 )
79 .json(¶ms)
80 .send()
81 .await?;
82
83 self.client.handle_response(response).await
84 }
85
86 pub async fn delete(&self, workspace_id: &Uuid) -> Result<()> {
88 self.delete_with_force(workspace_id, false).await
89 }
90
91 pub async fn delete_with_force(&self, workspace_id: &Uuid, force: bool) -> Result<()> {
93 info!("Deleting workspace: {} (force: {})", workspace_id, force);
94
95 let url = if force {
96 format!("/workspace/{}?force=true", workspace_id)
97 } else {
98 format!("/workspace/{}", workspace_id)
99 };
100
101 let response = self
102 .client
103 .build_request(reqwest::Method::DELETE, &url)
104 .send()
105 .await?;
106
107 if response.status().is_success() {
108 Ok(())
109 } else {
110 let error_text = response.text().await?;
111 Err(crate::error::DaytonaError::RequestFailed(format!(
112 "Failed to delete workspace: {}",
113 error_text
114 )))
115 }
116 }
117
118 pub async fn start(&self, workspace_id: &Uuid) -> Result<()> {
120 info!("Starting workspace: {}", workspace_id);
121
122 let response = self
123 .client
124 .build_request(
125 reqwest::Method::POST,
126 &format!("/workspace/{}/start", workspace_id),
127 )
128 .send()
129 .await?;
130
131 if response.status().is_success() {
132 Ok(())
133 } else {
134 let error_text = response.text().await?;
135 Err(crate::error::DaytonaError::RequestFailed(format!(
136 "Failed to start workspace: {}",
137 error_text
138 )))
139 }
140 }
141
142 pub async fn stop(&self, workspace_id: &Uuid) -> Result<()> {
144 info!("Stopping workspace: {}", workspace_id);
145
146 let response = self
147 .client
148 .build_request(
149 reqwest::Method::POST,
150 &format!("/workspace/{}/stop", workspace_id),
151 )
152 .send()
153 .await?;
154
155 if response.status().is_success() {
156 Ok(())
157 } else {
158 let error_text = response.text().await?;
159 Err(crate::error::DaytonaError::RequestFailed(format!(
160 "Failed to stop workspace: {}",
161 error_text
162 )))
163 }
164 }
165
166 pub async fn archive(&self, workspace_id: &Uuid) -> Result<()> {
168 info!("Archiving workspace: {}", workspace_id);
169
170 let response = self
171 .client
172 .build_request(
173 reqwest::Method::POST,
174 &format!("/workspace/{}/archive", workspace_id),
175 )
176 .send()
177 .await?;
178
179 if response.status().is_success() {
180 Ok(())
181 } else {
182 let error_text = response.text().await?;
183 Err(crate::error::DaytonaError::RequestFailed(format!(
184 "Failed to archive workspace: {}",
185 error_text
186 )))
187 }
188 }
189
190 pub async fn restore(&self, workspace_id: &Uuid) -> Result<()> {
192 info!("Restoring workspace: {}", workspace_id);
193
194 let response = self
195 .client
196 .build_request(
197 reqwest::Method::POST,
198 &format!("/workspace/{}/restore", workspace_id),
199 )
200 .send()
201 .await?;
202
203 if response.status().is_success() {
204 Ok(())
205 } else {
206 let error_text = response.text().await?;
207 Err(crate::error::DaytonaError::RequestFailed(format!(
208 "Failed to restore workspace: {}",
209 error_text
210 )))
211 }
212 }
213
214 pub async fn backup(
216 &self,
217 workspace_id: &Uuid,
218 name: String,
219 description: Option<String>,
220 ) -> Result<WorkspaceBackup> {
221 info!("Creating backup for workspace: {}", workspace_id);
222
223 let body = serde_json::json!({
224 "name": name,
225 "description": description
226 });
227
228 let response = self
229 .client
230 .build_request(
231 reqwest::Method::POST,
232 &format!("/workspace/{}/backup", workspace_id),
233 )
234 .json(&body)
235 .send()
236 .await?;
237
238 self.client.handle_response(response).await
239 }
240
241 pub async fn list_backups(&self, workspace_id: &Uuid) -> Result<Vec<WorkspaceBackup>> {
243 debug!("Listing backups for workspace: {}", workspace_id);
244
245 let response = self
246 .client
247 .build_request(
248 reqwest::Method::GET,
249 &format!("/workspace/{}/backup", workspace_id),
250 )
251 .send()
252 .await?;
253
254 self.client.handle_response(response).await
255 }
256
257 pub async fn restore_from_backup(&self, workspace_id: &Uuid, backup_id: &Uuid) -> Result<()> {
259 info!(
260 "Restoring workspace {} from backup {}",
261 workspace_id, backup_id
262 );
263
264 let response = self
265 .client
266 .build_request(
267 reqwest::Method::POST,
268 &format!("/workspace/{}/backup/{}/restore", workspace_id, backup_id),
269 )
270 .send()
271 .await?;
272
273 if response.status().is_success() {
274 Ok(())
275 } else {
276 let error_text = response.text().await?;
277 Err(crate::error::DaytonaError::RequestFailed(format!(
278 "Failed to restore from backup: {}",
279 error_text
280 )))
281 }
282 }
283
284 pub async fn get_port_preview(&self, workspace_id: &Uuid, port: u16) -> Result<PortPreview> {
286 debug!(
287 "Getting port preview for workspace {} port {}",
288 workspace_id, port
289 );
290
291 let response = self
292 .client
293 .build_request(
294 reqwest::Method::GET,
295 &format!("/workspace/{}/port/{}/preview", workspace_id, port),
296 )
297 .send()
298 .await?;
299
300 self.client.handle_response(response).await
301 }
302
303 pub async fn create_port_preview(
305 &self,
306 workspace_id: &Uuid,
307 port: u16,
308 public: bool,
309 ) -> Result<PortPreview> {
310 info!(
311 "Creating port preview for workspace {} port {}",
312 workspace_id, port
313 );
314
315 let body = serde_json::json!({ "public": public });
316
317 let response = self
318 .client
319 .build_request(
320 reqwest::Method::POST,
321 &format!("/workspace/{}/port/{}/preview", workspace_id, port),
322 )
323 .json(&body)
324 .send()
325 .await?;
326
327 self.client.handle_response(response).await
328 }
329
330 pub async fn get_build_logs(&self, workspace_id: &Uuid, follow: bool) -> Result<String> {
332 debug!(
333 "Getting build logs for workspace: {} (follow: {})",
334 workspace_id, follow
335 );
336
337 let url = if follow {
338 format!("/workspace/{}/build-logs?follow=true", workspace_id)
339 } else {
340 format!("/workspace/{}/build-logs", workspace_id)
341 };
342
343 let response = self
344 .client
345 .build_request(reqwest::Method::GET, &url)
346 .send()
347 .await?;
348
349 if response.status().is_success() {
350 Ok(response.text().await?)
351 } else {
352 let error_text = response.text().await?;
353 Err(crate::error::DaytonaError::RequestFailed(format!(
354 "Failed to get build logs: {}",
355 error_text
356 )))
357 }
358 }
359
360 pub async fn replace_labels(
362 &self,
363 workspace_id: &Uuid,
364 labels: std::collections::HashMap<String, String>,
365 ) -> Result<()> {
366 info!("Replacing labels for workspace: {}", workspace_id);
367
368 let response = self
369 .client
370 .build_request(
371 reqwest::Method::PUT,
372 &format!("/workspace/{}/labels", workspace_id),
373 )
374 .json(&labels)
375 .send()
376 .await?;
377
378 if response.status().is_success() {
379 Ok(())
380 } else {
381 let error_text = response.text().await?;
382 Err(crate::error::DaytonaError::RequestFailed(format!(
383 "Failed to replace labels: {}",
384 error_text
385 )))
386 }
387 }
388
389 pub async fn set_auto_archive_interval(&self, workspace_id: &Uuid, minutes: u32) -> Result<()> {
391 info!(
392 "Setting auto-archive interval for workspace {}: {} minutes",
393 workspace_id, minutes
394 );
395
396 let body = serde_json::json!({ "minutes": minutes });
397
398 let response = self
399 .client
400 .build_request(
401 reqwest::Method::POST,
402 &format!("/workspace/{}/auto-archive", workspace_id),
403 )
404 .json(&body)
405 .send()
406 .await?;
407
408 if response.status().is_success() {
409 Ok(())
410 } else {
411 let error_text = response.text().await?;
412 Err(crate::error::DaytonaError::RequestFailed(format!(
413 "Failed to set auto-archive interval: {}",
414 error_text
415 )))
416 }
417 }
418
419 pub async fn set_autostop_interval(&self, workspace_id: &Uuid, minutes: u32) -> Result<()> {
421 info!(
422 "Setting auto-stop interval for workspace {}: {} minutes",
423 workspace_id, minutes
424 );
425
426 let body = serde_json::json!({ "minutes": minutes });
427
428 let response = self
429 .client
430 .build_request(
431 reqwest::Method::POST,
432 &format!("/workspace/{}/auto-stop", workspace_id),
433 )
434 .json(&body)
435 .send()
436 .await?;
437
438 if response.status().is_success() {
439 Ok(())
440 } else {
441 let error_text = response.text().await?;
442 Err(crate::error::DaytonaError::RequestFailed(format!(
443 "Failed to set auto-stop interval: {}",
444 error_text
445 )))
446 }
447 }
448
449 pub async fn update_public_status(&self, workspace_id: &Uuid, is_public: bool) -> Result<()> {
451 info!(
452 "Updating public status for workspace {}: {}",
453 workspace_id, is_public
454 );
455
456 let body = serde_json::json!({ "public": is_public });
457
458 let response = self
459 .client
460 .build_request(
461 reqwest::Method::PATCH,
462 &format!("/workspace/{}/public", workspace_id),
463 )
464 .json(&body)
465 .send()
466 .await?;
467
468 if response.status().is_success() {
469 Ok(())
470 } else {
471 let error_text = response.text().await?;
472 Err(crate::error::DaytonaError::RequestFailed(format!(
473 "Failed to update public status: {}",
474 error_text
475 )))
476 }
477 }
478
479 pub async fn wait_for_state(
481 &self,
482 workspace_id: &Uuid,
483 target_state: WorkspaceState,
484 max_wait_seconds: u64,
485 ) -> Result<Workspace> {
486 let start = std::time::Instant::now();
487 let max_duration = std::time::Duration::from_secs(max_wait_seconds);
488
489 loop {
490 let workspace = self.get(workspace_id).await?;
491
492 if matches!(workspace.state, ref s if std::mem::discriminant(s) == std::mem::discriminant(&target_state))
493 {
494 return Ok(workspace);
495 }
496
497 if let WorkspaceState::Error = workspace.state {
498 return Err(crate::error::DaytonaError::RequestFailed(format!(
499 "Workspace entered error state: {:?}",
500 workspace.error_reason
501 )));
502 }
503
504 if start.elapsed() > max_duration {
505 return Err(crate::error::DaytonaError::RequestFailed(format!(
506 "Timeout waiting for workspace to reach {:?} state",
507 target_state
508 )));
509 }
510
511 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
512 }
513 }
514}
515
516impl DaytonaClient {
517 pub fn workspaces(&self) -> WorkspaceManager<'_> {
519 WorkspaceManager::new(self)
520 }
521}