hrobot/api/storagebox/
mod.rs

1//! Storagebox structs and implementation.
2use crate::{error::Error, AsyncRobot};
3
4use super::{
5    wrapper::{Empty, List, Single},
6    UnauthenticatedRequest,
7};
8
9mod models;
10pub use models::*;
11use serde::Serialize;
12
13fn list_storageboxes() -> UnauthenticatedRequest<List<StorageBoxReference>> {
14    UnauthenticatedRequest::from("https://robot-ws.your-server.de/storagebox")
15}
16
17fn get_storagebox(storagebox: StorageBoxId) -> UnauthenticatedRequest<Single<StorageBox>> {
18    UnauthenticatedRequest::from(&format!(
19        "https://robot-ws.your-server.de/storagebox/{storagebox}"
20    ))
21}
22
23fn reset_password(storagebox: StorageBoxId) -> UnauthenticatedRequest<Single<String>> {
24    UnauthenticatedRequest::from(&format!(
25        "https://robot-ws.your-server.de/storagebox/{storagebox}/password"
26    ))
27    .with_method("POST")
28}
29
30fn rename_storagebox(
31    storagebox: StorageBoxId,
32    name: &str,
33) -> Result<UnauthenticatedRequest<Single<StorageBox>>, serde_html_form::ser::Error> {
34    #[derive(Serialize)]
35    struct RenameBox<'a> {
36        storagebox_name: &'a str,
37    }
38    UnauthenticatedRequest::from(&format!(
39        "https://robot-ws.your-server.de/storagebox/{storagebox}"
40    ))
41    .with_method("POST")
42    .with_body(RenameBox {
43        storagebox_name: name,
44    })
45}
46
47fn toggle_service(
48    storagebox: StorageBoxId,
49    service: &str,
50    enabled: bool,
51) -> UnauthenticatedRequest<Single<StorageBox>> {
52    UnauthenticatedRequest::from(&format!(
53        "https://robot-ws.your-server.de/storagebox/{storagebox}"
54    ))
55    .with_method("POST")
56    .with_serialized_body(format!("{service}={enabled}"))
57}
58
59fn configure_accessibility(
60    storagebox: StorageBoxId,
61    accessibility: Accessibility,
62) -> Result<UnauthenticatedRequest<Single<StorageBox>>, serde_html_form::ser::Error> {
63    UnauthenticatedRequest::from(&format!(
64        "https://robot-ws.your-server.de/storagebox/{storagebox}"
65    ))
66    .with_method("POST")
67    .with_body(accessibility)
68}
69
70fn list_snapshots(storagebox: StorageBoxId) -> UnauthenticatedRequest<List<Snapshot>> {
71    UnauthenticatedRequest::from(&format!(
72        "https://robot-ws.your-server.de/storagebox/{storagebox}/snapshot"
73    ))
74}
75
76fn create_snapshot(storagebox: StorageBoxId) -> UnauthenticatedRequest<Single<CreatedSnapshot>> {
77    UnauthenticatedRequest::from(&format!(
78        "https://robot-ws.your-server.de/storagebox/{storagebox}/snapshot"
79    ))
80    .with_method("POST")
81}
82
83fn delete_snapshot(storagebox: StorageBoxId, snapshot_name: &str) -> UnauthenticatedRequest<Empty> {
84    UnauthenticatedRequest::from(&format!(
85        "https://robot-ws.your-server.de/storagebox/{storagebox}/snapshot/{snapshot_name}"
86    ))
87    .with_method("DELETE")
88}
89
90fn revert_to_snapshot(
91    storagebox: StorageBoxId,
92    snapshot_name: &str,
93) -> UnauthenticatedRequest<Empty> {
94    UnauthenticatedRequest::from(&format!(
95        "https://robot-ws.your-server.de/storagebox/{storagebox}/snapshot/{snapshot_name}"
96    ))
97    .with_method("POST")
98    .with_serialized_body("revert=true".to_string())
99}
100
101fn change_snapshot_comment(
102    storagebox: StorageBoxId,
103    snapshot_name: &str,
104    comment: &str,
105) -> Result<UnauthenticatedRequest<Empty>, serde_html_form::ser::Error> {
106    #[derive(Serialize)]
107    struct ChangeComment<'a> {
108        comment: &'a str,
109    }
110    UnauthenticatedRequest::from(&format!(
111        "https://robot-ws.your-server.de/storagebox/{storagebox}/snapshot/{snapshot_name}/comment"
112    ))
113    .with_method("POST")
114    .with_body(ChangeComment { comment })
115}
116
117fn get_snapshot_plan(storagebox: StorageBoxId) -> UnauthenticatedRequest<Single<SnapshotPlan>> {
118    UnauthenticatedRequest::from(&format!(
119        "https://robot-ws.your-server.de/storagebox/{storagebox}/snapshotplan"
120    ))
121}
122
123fn update_snapshot_plan(
124    storagebox: StorageBoxId,
125    plan: SnapshotPlan,
126) -> Result<UnauthenticatedRequest<Single<SnapshotPlan>>, serde_html_form::ser::Error> {
127    UnauthenticatedRequest::from(&format!(
128        "https://robot-ws.your-server.de/storagebox/{storagebox}/snapshotplan"
129    ))
130    .with_method("POST")
131    .with_body(plan)
132}
133
134fn list_subaccounts(storagebox: StorageBoxId) -> UnauthenticatedRequest<List<Subaccount>> {
135    UnauthenticatedRequest::from(&format!(
136        "https://robot-ws.your-server.de/storagebox/{storagebox}/subaccount"
137    ))
138}
139
140#[derive(Serialize)]
141struct SubaccountConfig<'a> {
142    #[serde(skip_serializing_if = "Option::is_none")]
143    homedirectory: Option<&'a str>,
144
145    #[serde(skip_serializing_if = "Option::is_none")]
146    samba: Option<bool>,
147
148    #[serde(skip_serializing_if = "Option::is_none")]
149    ssh: Option<bool>,
150
151    #[serde(skip_serializing_if = "Option::is_none")]
152    webdav: Option<bool>,
153
154    #[serde(skip_serializing_if = "Option::is_none")]
155    readonly: Option<Permission>,
156
157    #[serde(skip_serializing_if = "Option::is_none")]
158    comment: Option<&'a str>,
159}
160
161fn create_subaccount(
162    storagebox: StorageBoxId,
163    home_directory: &str,
164    accessibility: Accessibility,
165    read_only: Permission,
166    comment: Option<&str>,
167) -> Result<UnauthenticatedRequest<Single<CreatedSubaccount>>, serde_html_form::ser::Error> {
168    UnauthenticatedRequest::from(&format!(
169        "https://robot-ws.your-server.de/storagebox/{storagebox}/subaccount"
170    ))
171    .with_method("POST")
172    .with_body(SubaccountConfig {
173        homedirectory: Some(home_directory),
174        samba: Some(accessibility.samba),
175        ssh: Some(accessibility.ssh),
176        webdav: Some(accessibility.webdav),
177        readonly: Some(read_only),
178        comment,
179    })
180}
181
182fn update_subaccount(
183    storagebox: StorageBoxId,
184    subaccount: &SubaccountId,
185    home_directory: Option<&str>,
186    accessibility: Option<&Accessibility>,
187    read_only: Option<Permission>,
188    comment: Option<&str>,
189) -> Result<UnauthenticatedRequest<Empty>, serde_html_form::ser::Error> {
190    UnauthenticatedRequest::from(&format!(
191        "https://robot-ws.your-server.de/storagebox/{storagebox}/subaccount/{subaccount}"
192    ))
193    .with_method("PUT")
194    .with_body(SubaccountConfig {
195        homedirectory: home_directory,
196        samba: accessibility.map(|a| a.samba),
197        ssh: accessibility.map(|a| a.ssh),
198        webdav: accessibility.map(|a| a.webdav),
199        readonly: read_only,
200        comment,
201    })
202}
203
204fn delete_subaccount(
205    storagebox: StorageBoxId,
206    subaccount: SubaccountId,
207) -> UnauthenticatedRequest<Empty> {
208    UnauthenticatedRequest::from(&format!(
209        "https://robot-ws.your-server.de/storagebox/{storagebox}/subaccount/{subaccount}"
210    ))
211    .with_method("DELETE")
212}
213
214fn reset_subaccount_password(
215    storagebox: StorageBoxId,
216    subaccount: &SubaccountId,
217) -> UnauthenticatedRequest<Single<String>> {
218    UnauthenticatedRequest::from(&format!(
219        "https://robot-ws.your-server.de/storagebox/{storagebox}/subaccount/{subaccount}/password"
220    ))
221    .with_method("POST")
222}
223
224impl AsyncRobot {
225    /// List all storageboxes associated with this account.
226    ///
227    /// Note that this function returns a truncated version of the [`StorageBox`] which
228    /// does not contain disk usage and service accessibility information.
229    ///
230    /// # Example
231    /// ```rust,no_run
232    /// # #[tokio::main]
233    /// # async fn main() {
234    /// # let _ = dotenvy::dotenv().ok();
235    /// let robot = hrobot::AsyncRobot::default();
236    /// robot.list_storageboxes().await.unwrap();
237    /// # }
238    /// ```
239    pub async fn list_storageboxes(&self) -> Result<Vec<StorageBoxReference>, Error> {
240        Ok(self.go(list_storageboxes()).await?.0)
241    }
242
243    /// Get a single storagebox.
244    ///
245    /// # Example
246    /// ```rust,no_run
247    /// # use hrobot::api::storagebox::StorageBoxId;
248    /// # #[tokio::main]
249    /// # async fn main() {
250    /// # let _ = dotenvy::dotenv().ok();
251    /// let robot = hrobot::AsyncRobot::default();
252    /// robot.get_storagebox(StorageBoxId(1234)).await.unwrap();
253    /// # }
254    /// ```
255    pub async fn get_storagebox(&self, id: StorageBoxId) -> Result<StorageBox, Error> {
256        Ok(self.go(get_storagebox(id)).await?.0)
257    }
258
259    /// Rename storagebox.
260    ///
261    /// # Example
262    /// ```rust,no_run
263    /// # use hrobot::api::storagebox::StorageBoxId;
264    /// # #[tokio::main]
265    /// # async fn main() {
266    /// # let _ = dotenvy::dotenv().ok();
267    /// let robot = hrobot::AsyncRobot::default();
268    /// robot.rename_storagebox(StorageBoxId(1234), "my-backups").await.unwrap();
269    /// # }
270    /// ```
271    pub async fn rename_storagebox(
272        &self,
273        id: StorageBoxId,
274        name: &str,
275    ) -> Result<StorageBox, Error> {
276        Ok(self.go(rename_storagebox(id, name)?).await?.0)
277    }
278
279    /// Configure storagebox accessibility.
280    ///
281    /// # Example
282    /// ```rust,no_run
283    /// # use hrobot::api::storagebox::{StorageBoxId, Accessibility};
284    /// # #[tokio::main]
285    /// # async fn main() {
286    /// # let _ = dotenvy::dotenv().ok();
287    /// let robot = hrobot::AsyncRobot::default();
288    /// robot.configure_storagebox_accessibility(StorageBoxId(1234), Accessibility {
289    ///     webdav: false,
290    ///     samba: false,
291    ///     ssh: false,
292    ///     external_reachability: false,
293    /// }).await.unwrap();
294    /// # }
295    /// ```
296    pub async fn configure_storagebox_accessibility(
297        &self,
298        id: StorageBoxId,
299        accessibility: Accessibility,
300    ) -> Result<StorageBox, Error> {
301        Ok(self
302            .go(configure_accessibility(id, accessibility)?)
303            .await?
304            .0)
305    }
306
307    /// Enable Samba (SMB) access to the storagebox.
308    ///
309    /// # Example
310    /// ```rust,no_run
311    /// # use hrobot::api::storagebox::StorageBoxId;
312    /// # #[tokio::main]
313    /// # async fn main() {
314    /// # let _ = dotenvy::dotenv().ok();
315    /// let robot = hrobot::AsyncRobot::default();
316    /// robot.enable_storagebox_samba(StorageBoxId(1234)).await.unwrap();
317    /// # }
318    /// ```
319    pub async fn enable_storagebox_samba(&self, id: StorageBoxId) -> Result<StorageBox, Error> {
320        Ok(self.go(toggle_service(id, "samba", true)).await?.0)
321    }
322
323    /// Disable Samba (SMB) access to the storagebox.
324    ///
325    /// # Example
326    /// ```rust,no_run
327    /// # use hrobot::api::storagebox::StorageBoxId;
328    /// # #[tokio::main]
329    /// # async fn main() {
330    /// # let _ = dotenvy::dotenv().ok();
331    /// let robot = hrobot::AsyncRobot::default();
332    /// robot.disable_storagebox_samba(StorageBoxId(1234)).await.unwrap();
333    /// # }
334    /// ```
335    pub async fn disable_storagebox_samba(&self, id: StorageBoxId) -> Result<StorageBox, Error> {
336        Ok(self.go(toggle_service(id, "samba", false)).await?.0)
337    }
338
339    /// Enable WebDAV access to the storagebox.
340    ///
341    /// # Example
342    /// ```rust,no_run
343    /// # use hrobot::api::storagebox::StorageBoxId;
344    /// # #[tokio::main]
345    /// # async fn main() {
346    /// # let _ = dotenvy::dotenv().ok();
347    /// let robot = hrobot::AsyncRobot::default();
348    /// robot.enable_storagebox_webdav(StorageBoxId(1234)).await.unwrap();
349    /// # }
350    /// ```
351    pub async fn enable_storagebox_webdav(&self, id: StorageBoxId) -> Result<StorageBox, Error> {
352        Ok(self.go(toggle_service(id, "webdav", true)).await?.0)
353    }
354
355    /// Disable WebDAV access to the storagebox.
356    ///
357    /// # Example
358    /// ```rust,no_run
359    /// # use hrobot::api::storagebox::StorageBoxId;
360    /// # #[tokio::main]
361    /// # async fn main() {
362    /// # let _ = dotenvy::dotenv().ok();
363    /// let robot = hrobot::AsyncRobot::default();
364    /// robot.disable_storagebox_webdav(StorageBoxId(1234)).await.unwrap();
365    /// # }
366    /// ```
367    pub async fn disable_storagebox_webdav(&self, id: StorageBoxId) -> Result<StorageBox, Error> {
368        Ok(self.go(toggle_service(id, "webdav", false)).await?.0)
369    }
370
371    /// Enable SSH access to the storagebox.
372    ///
373    /// # Example
374    /// ```rust,no_run
375    /// # use hrobot::api::storagebox::StorageBoxId;
376    /// # #[tokio::main]
377    /// # async fn main() {
378    /// # let _ = dotenvy::dotenv().ok();
379    /// let robot = hrobot::AsyncRobot::default();
380    /// robot.enable_storagebox_ssh(StorageBoxId(1234)).await.unwrap();
381    /// # }
382    /// ```
383    pub async fn enable_storagebox_ssh(&self, id: StorageBoxId) -> Result<StorageBox, Error> {
384        Ok(self.go(toggle_service(id, "ssh", true)).await?.0)
385    }
386
387    /// Disable SSH access to the storagebox.
388    ///
389    /// # Example
390    /// ```rust,no_run
391    /// # use hrobot::api::storagebox::StorageBoxId;
392    /// # #[tokio::main]
393    /// # async fn main() {
394    /// # let _ = dotenvy::dotenv().ok();
395    /// let robot = hrobot::AsyncRobot::default();
396    /// robot.disable_storagebox_ssh(StorageBoxId(1234)).await.unwrap();
397    /// # }
398    /// ```
399    pub async fn disable_storagebox_ssh(&self, id: StorageBoxId) -> Result<StorageBox, Error> {
400        Ok(self.go(toggle_service(id, "ssh", false)).await?.0)
401    }
402
403    /// Enable external reachability for the storagebox.
404    ///
405    /// External reachability means that the enabled services are reachable
406    /// outside of Hetzner's networks. Without this enabled, you won't be able
407    /// to log into the storagebox from anything other than Hetzner Cloud
408    /// or Hetzner's Dedicated Servers.
409    ///
410    /// # Example
411    /// ```rust,no_run
412    /// # use hrobot::api::storagebox::StorageBoxId;
413    /// # #[tokio::main]
414    /// # async fn main() {
415    /// # let _ = dotenvy::dotenv().ok();
416    /// let robot = hrobot::AsyncRobot::default();
417    /// robot.enable_storagebox_external_reachability(StorageBoxId(1234)).await.unwrap();
418    /// # }
419    /// ```
420    pub async fn enable_storagebox_external_reachability(
421        &self,
422        id: StorageBoxId,
423    ) -> Result<StorageBox, Error> {
424        Ok(self
425            .go(toggle_service(id, "external_reachability", true))
426            .await?
427            .0)
428    }
429
430    /// Disable external reachability for to the storagebox.
431    ///
432    /// External reachability means that the enabled services are reachable
433    /// outside of Hetzner's networks. Without this enabled, you won't be able
434    /// to log into the storagebox from anything other than Hetzner Cloud
435    /// or Hetzner's Dedicated Servers.
436    ///
437    /// # Example
438    /// ```rust,no_run
439    /// # use hrobot::api::storagebox::StorageBoxId;
440    /// # #[tokio::main]
441    /// # async fn main() {
442    /// # let _ = dotenvy::dotenv().ok();
443    /// let robot = hrobot::AsyncRobot::default();
444    /// robot.disable_storagebox_external_reachability(StorageBoxId(1234)).await.unwrap();
445    /// # }
446    /// ```
447    pub async fn disable_storagebox_external_reachability(
448        &self,
449        id: StorageBoxId,
450    ) -> Result<StorageBox, Error> {
451        Ok(self
452            .go(toggle_service(id, "external_reachability", false))
453            .await?
454            .0)
455    }
456
457    /// Enable snapshot directory visibility for the storagebox.
458    ///
459    /// When enabled, mounts a directory containing storage box snapshots
460    /// in /.zfs or /home/.zfs depending on access method.
461    ///
462    /// Read more at: <https://docs.hetzner.com/robot/storage-box/snapshots/>
463    ///
464    /// # Example
465    /// ```rust,no_run
466    /// # use hrobot::api::storagebox::StorageBoxId;
467    /// # #[tokio::main]
468    /// # async fn main() {
469    /// # let _ = dotenvy::dotenv().ok();
470    /// let robot = hrobot::AsyncRobot::default();
471    /// robot.enable_storagebox_snapshot_directory(StorageBoxId(1234)).await.unwrap();
472    /// # }
473    /// ```
474    pub async fn enable_storagebox_snapshot_directory(
475        &self,
476        id: StorageBoxId,
477    ) -> Result<StorageBox, Error> {
478        Ok(self.go(toggle_service(id, "zfs", true)).await?.0)
479    }
480
481    /// Disable snapshot directory visibility for the storagebox.
482    ///
483    /// When enabled, mounts a directory containing storage box snapshots
484    /// in /.zfs or /home/.zfs depending on access method.
485    ///
486    /// Read more at: <https://docs.hetzner.com/robot/storage-box/snapshots/>
487    ///
488    /// # Example
489    /// ```rust,no_run
490    /// # use hrobot::api::storagebox::StorageBoxId;
491    /// # #[tokio::main]
492    /// # async fn main() {
493    /// # let _ = dotenvy::dotenv().ok();
494    /// let robot = hrobot::AsyncRobot::default();
495    /// robot.disable_storagebox_snapshot_directory(StorageBoxId(1234)).await.unwrap();
496    /// # }
497    /// ```
498    pub async fn disable_storagebox_snapshot_directory(
499        &self,
500        id: StorageBoxId,
501    ) -> Result<StorageBox, Error> {
502        Ok(self.go(toggle_service(id, "zfs", false)).await?.0)
503    }
504
505    /// Reset storagebox password, returning the new password.
506    ///
507    /// # Example
508    /// ```rust,no_run
509    /// # use hrobot::api::storagebox::StorageBoxId;
510    /// # #[tokio::main]
511    /// # async fn main() {
512    /// # let _ = dotenvy::dotenv().ok();
513    /// let robot = hrobot::AsyncRobot::default();
514    /// robot.reset_storagebox_password(StorageBoxId(1234)).await.unwrap();
515    /// # }
516    /// ```
517    pub async fn reset_storagebox_password(&self, id: StorageBoxId) -> Result<String, Error> {
518        Ok(self.go(reset_password(id)).await?.0)
519    }
520
521    /// List snapshots for storagebox.
522    ///
523    /// # Example
524    /// ```rust,no_run
525    /// # use hrobot::api::storagebox::StorageBoxId;
526    /// # #[tokio::main]
527    /// # async fn main() {
528    /// # let _ = dotenvy::dotenv().ok();
529    /// let robot = hrobot::AsyncRobot::default();
530    /// robot.list_snapshots(StorageBoxId(1234)).await.unwrap();
531    /// # }
532    /// ```
533    pub async fn list_snapshots(&self, id: StorageBoxId) -> Result<Vec<Snapshot>, Error> {
534        Ok(self.go(list_snapshots(id)).await?.0)
535    }
536
537    /// Create a new snapshot of the storagebox.
538    ///
539    /// # Example
540    /// ```rust,no_run
541    /// # use hrobot::api::storagebox::StorageBoxId;
542    /// # #[tokio::main]
543    /// # async fn main() {
544    /// # let _ = dotenvy::dotenv().ok();
545    /// let robot = hrobot::AsyncRobot::default();
546    /// robot.create_snapshot(StorageBoxId(1234)).await.unwrap();
547    /// # }
548    /// ```
549    pub async fn create_snapshot(&self, id: StorageBoxId) -> Result<CreatedSnapshot, Error> {
550        Ok(self.go(create_snapshot(id)).await?.0)
551    }
552
553    /// Delete a snapshot of the storagebox.
554    ///
555    /// Snapshots are named after the timestamp at which they are created
556    /// with an implicit timezone of UTC. The safest way to target a snapshot
557    /// for deletion, is to first retrieve it, and use its name from there.
558    ///
559    /// If you otherwise know the timestamp, but not the name of the snapshot,
560    /// you can format it as "YYYY-MM-DDThh-mm-ss", with an assumed UTC timezone.
561    ///
562    /// # Example
563    /// ```rust,no_run
564    /// # use hrobot::api::storagebox::StorageBoxId;
565    /// # #[tokio::main]
566    /// # async fn main() {
567    /// # let _ = dotenvy::dotenv().ok();
568    /// let robot = hrobot::AsyncRobot::default();
569    /// robot.delete_snapshot(
570    ///     StorageBoxId(1234),
571    ///     "2015-12-21T13-13-03"
572    /// ).await.unwrap();
573    /// # }
574    /// ```
575    pub async fn delete_snapshot(
576        &self,
577        id: StorageBoxId,
578        snapshot_name: &str,
579    ) -> Result<(), Error> {
580        self.go(delete_snapshot(id, snapshot_name))
581            .await?
582            .throw_away();
583        Ok(())
584    }
585
586    /// Revert storagebox to a snapshot.
587    ///
588    /// # Example
589    /// ```rust,no_run
590    /// # use hrobot::api::storagebox::StorageBoxId;
591    /// # #[tokio::main]
592    /// # async fn main() {
593    /// # let _ = dotenvy::dotenv().ok();
594    /// let robot = hrobot::AsyncRobot::default();
595    /// robot.revert_to_snapshot(
596    ///     StorageBoxId(1234),
597    ///     "2015-12-21T13-13-03"
598    /// ).await.unwrap();
599    /// # }
600    /// ```
601    pub async fn revert_to_snapshot(
602        &self,
603        id: StorageBoxId,
604        snapshot_name: &str,
605    ) -> Result<(), Error> {
606        self.go(revert_to_snapshot(id, snapshot_name))
607            .await?
608            .throw_away();
609        Ok(())
610    }
611
612    /// Change snapshot comment.
613    ///
614    /// # Example
615    /// ```rust,no_run
616    /// # use hrobot::api::storagebox::StorageBoxId;
617    /// # #[tokio::main]
618    /// # async fn main() {
619    /// # let _ = dotenvy::dotenv().ok();
620    /// let robot = hrobot::AsyncRobot::default();
621    /// robot.change_snapshot_comment(
622    ///     StorageBoxId(1234),
623    ///     "2015-12-21T13-13-03",
624    ///     "Last backup before upgrade to 2.0"
625    /// ).await.unwrap();
626    /// # }
627    /// ```
628    pub async fn change_snapshot_comment(
629        &self,
630        id: StorageBoxId,
631        snapshot_name: &str,
632        comment: &str,
633    ) -> Result<(), Error> {
634        self.go(change_snapshot_comment(id, snapshot_name, comment)?)
635            .await?
636            .throw_away();
637        Ok(())
638    }
639
640    /// Update snapshot plan for storagebox
641    ///
642    /// # Example
643    /// ```rust,no_run
644    /// # use hrobot::api::storagebox::StorageBoxId;
645    /// # #[tokio::main]
646    /// # async fn main() {
647    /// # let _ = dotenvy::dotenv().ok();
648    /// let robot = hrobot::AsyncRobot::default();
649    /// robot.get_snapshot_plan(StorageBoxId(1234)).await.unwrap();
650    /// # }
651    /// ```
652    pub async fn get_snapshot_plan(&self, id: StorageBoxId) -> Result<SnapshotPlan, Error> {
653        Ok(self.go(get_snapshot_plan(id)).await?.0)
654    }
655
656    /// Update snapshot plan.
657    ///
658    /// # Example
659    /// ```rust,no_run
660    /// # use hrobot::api::storagebox::{StorageBoxId, SnapshotPlan};
661    /// # use hrobot::time::Weekday;
662    /// # #[tokio::main]
663    /// # async fn main() {
664    /// # let _ = dotenvy::dotenv().ok();
665    /// let robot = hrobot::AsyncRobot::default();
666    /// robot.update_snapshot_plan(
667    ///     StorageBoxId(1234),
668    ///     SnapshotPlan::weekly(Weekday::Monday, 10, 0)
669    /// ).await.unwrap();
670    /// # }
671    /// ```
672    pub async fn update_snapshot_plan(
673        &self,
674        id: StorageBoxId,
675        plan: SnapshotPlan,
676    ) -> Result<SnapshotPlan, Error> {
677        Ok(self.go(update_snapshot_plan(id, plan)?).await?.0)
678    }
679
680    /// List sub-accounts for storagebox.
681    ///
682    /// # Example
683    /// ```rust,no_run
684    /// # use hrobot::api::storagebox::StorageBoxId;
685    /// # #[tokio::main]
686    /// # async fn main() {
687    /// # let _ = dotenvy::dotenv().ok();
688    /// let robot = hrobot::AsyncRobot::default();
689    /// robot.list_subaccounts(StorageBoxId(1234)).await.unwrap();
690    /// # }
691    /// ```
692    pub async fn list_subaccounts(&self, id: StorageBoxId) -> Result<Vec<Subaccount>, Error> {
693        Ok(self.go(list_subaccounts(id)).await?.0)
694    }
695
696    /// Create sub-account.
697    ///
698    /// # Example
699    /// ```rust,no_run
700    /// # use hrobot::api::storagebox::{StorageBoxId, SubaccountId, Permission, Accessibility};
701    /// # #[tokio::main]
702    /// # async fn main() {
703    /// # let _ = dotenvy::dotenv().ok();
704    /// let robot = hrobot::AsyncRobot::default();
705    /// robot.create_subaccount(
706    ///     StorageBoxId(1234),
707    ///     "/home/test-user",
708    ///     Accessibility::default(), // default disables all access.
709    ///     Permission::ReadOnly,
710    ///     None
711    /// ).await.unwrap();
712    /// # }
713    /// ```
714    pub async fn create_subaccount(
715        &self,
716        storagebox: StorageBoxId,
717        home_directory: &str,
718        accessibility: Accessibility,
719        permissions: Permission,
720        comment: Option<&str>,
721    ) -> Result<CreatedSubaccount, Error> {
722        Ok(self
723            .go(create_subaccount(
724                storagebox,
725                home_directory,
726                accessibility,
727                permissions,
728                comment,
729            )?)
730            .await?
731            .0)
732    }
733
734    /// Change home directory of storagebox sub-account
735    ///
736    /// # Example
737    /// ```rust,no_run
738    /// # use hrobot::api::storagebox::{StorageBoxId, SubaccountId, Accessibility};
739    /// # #[tokio::main]
740    /// # async fn main() {
741    /// # let _ = dotenvy::dotenv().ok();
742    /// let robot = hrobot::AsyncRobot::default();
743    /// robot.set_subaccount_home_directory(
744    ///     StorageBoxId(1234),
745    ///     &SubaccountId("u1234-sub1".to_string()),
746    ///     "/homedirs/sub1"
747    /// ).await.unwrap();
748    /// # }
749    /// ```
750    pub async fn set_subaccount_home_directory(
751        &self,
752        storagebox: StorageBoxId,
753        subaccount: &SubaccountId,
754        home_directory: &str,
755    ) -> Result<(), Error> {
756        self.go(update_subaccount(
757            storagebox,
758            subaccount,
759            Some(home_directory),
760            None,
761            None,
762            None,
763        )?)
764        .await?
765        .throw_away();
766        Ok(())
767    }
768
769    /// Change sub-account comment/description.
770    ///
771    /// # Example
772    /// ```rust,no_run
773    /// # use hrobot::api::storagebox::{StorageBoxId, SubaccountId, Permission};
774    /// # #[tokio::main]
775    /// # async fn main() {
776    /// # let _ = dotenvy::dotenv().ok();
777    /// let robot = hrobot::AsyncRobot::default();
778    /// robot.update_subaccount(
779    ///     StorageBoxId(1234),
780    ///     &SubaccountId("u1234-sub1".to_string()),
781    ///     "/new/home/dir",
782    ///     None, // Keep old accessibility options
783    ///     Some(Permission::ReadWrite),
784    ///     Some("Sub-account used for accessing backups")
785    /// ).await.unwrap();
786    /// # }
787    /// ```
788    pub async fn update_subaccount(
789        &self,
790        storagebox: StorageBoxId,
791        subaccount: &SubaccountId,
792        home_directory: &str,
793        accessibility: Option<&Accessibility>,
794        permissions: Option<Permission>,
795        comment: Option<&str>,
796    ) -> Result<(), Error> {
797        self.go(update_subaccount(
798            storagebox,
799            subaccount,
800            Some(home_directory),
801            accessibility,
802            permissions,
803            comment,
804        )?)
805        .await?
806        .throw_away();
807        Ok(())
808    }
809
810    /// Reset sub-account password.
811    ///
812    /// # Example
813    /// ```rust,no_run
814    /// # use hrobot::api::storagebox::{StorageBoxId, SubaccountId};
815    /// # #[tokio::main]
816    /// # async fn main() {
817    /// # let _ = dotenvy::dotenv().ok();
818    /// let robot = hrobot::AsyncRobot::default();
819    /// let password = robot.reset_subaccount_password(
820    ///     StorageBoxId(1234),
821    ///     &SubaccountId("u1234-sub1".to_string()),
822    /// ).await.unwrap();
823    ///
824    /// println!("new password: {password}");
825    /// # }
826    /// ```
827    pub async fn reset_subaccount_password(
828        &self,
829        storagebox: StorageBoxId,
830        subaccount: &SubaccountId,
831    ) -> Result<String, Error> {
832        Ok(self
833            .go(reset_subaccount_password(storagebox, subaccount))
834            .await?
835            .0)
836    }
837
838    /// Delete sub-account.
839    ///
840    /// # Example
841    /// ```rust,no_run
842    /// # use hrobot::api::storagebox::{StorageBoxId, SubaccountId};
843    /// # #[tokio::main]
844    /// # async fn main() {
845    /// # let _ = dotenvy::dotenv().ok();
846    /// let robot = hrobot::AsyncRobot::default();
847    /// robot.delete_subaccount(
848    ///     StorageBoxId(1234),
849    ///     SubaccountId("u1234-sub1".to_string()),
850    /// ).await.unwrap();
851    /// # }
852    /// ```
853    pub async fn delete_subaccount(
854        &self,
855        storagebox: StorageBoxId,
856        subaccount: SubaccountId,
857    ) -> Result<(), Error> {
858        self.go(delete_subaccount(storagebox, subaccount))
859            .await?
860            .throw_away();
861        Ok(())
862    }
863}