playdate_device/
install.rs1use std::ffi::OsStr;
2use std::path::{Path, PathBuf};
3
4use futures::{FutureExt, Stream, StreamExt, TryFutureExt};
5
6use crate::device::query::Query;
7use crate::error::Error;
8use crate::mount::MountedDevice;
9use crate::mount;
10
11
12type Result<T = (), E = Error> = std::result::Result<T, E>;
13
14
15pub struct MountedDevicePath {
17 drive: MountedDevice,
18 path: String,
19}
20
21pub struct MountedDevicePathBorrowed<'dev> {
23 drive: &'dev MountedDevice,
24 path: String,
25}
26
27impl<'dev> MountedDevicePathBorrowed<'dev> {
28 pub fn drive(&self) -> &MountedDevice { self.drive }
29
30 pub fn path_local(&self) -> &str { &self.path }
32 pub fn path_abs(&self) -> PathBuf { self.drive.handle.path().join(&self.path) }
34
35 pub fn into_path(self) -> String { self.path }
36 pub fn into_parts(self) -> (&'dev MountedDevice, String) { (self.drive, self.path) }
37
38 pub fn to_owned_replacing(self) -> impl FnOnce(MountedDevice) -> MountedDevicePath {
39 let (_, path) = self.into_parts();
40 move |drive| MountedDevicePath { drive, path }
41 }
42}
43
44impl MountedDevicePath {
45 pub fn drive(&self) -> &MountedDevice { &self.drive }
46 pub fn drive_mut(&mut self) -> &mut MountedDevice { &mut self.drive }
47
48 pub fn path_local(&self) -> &str { &self.path }
50 pub fn path_abs(&self) -> PathBuf { self.drive.handle.path().join(&self.path) }
52
53 pub fn into_path(self) -> String { self.path }
54 pub fn into_parts(self) -> (MountedDevice, String) { (self.drive, self.path) }
55}
56
57
58#[cfg_attr(feature = "tracing", tracing::instrument(skip(drive)))]
62pub async fn install<'dev>(drive: &'dev MountedDevice,
63 path: &Path,
64 force: bool)
65 -> Result<MountedDevicePathBorrowed<'dev>> {
66 #[cfg(all(feature = "tokio", not(feature = "async-std")))]
67 use tokio::process::Command;
68 #[cfg(feature = "async-std")]
69 use async_std::process::Command;
70 #[cfg(all(not(feature = "tokio"), not(feature = "async-std")))]
71 use std::process::Command;
72
73
74 let retry = mount::fs_available_wait_time();
75 mount::wait_fs_available_with_user(drive, retry).await?;
76 validate_host_package(path).await?;
77
78 trace!(
79 "Installing: {} -> {}",
80 path.display(),
81 drive.handle.path().display()
82 );
83
84 let games = drive.handle.path().join("Games");
85
86 let cp = || {
87 async {
88 if cfg!(unix) {
89 let mut cmd = Command::new("cp");
90 cmd.arg("-r");
91
92 if force {
93 cmd.arg("-f");
94 }
95
96 cmd.arg(path);
97 cmd.arg(&games);
98
99 #[cfg(feature = "tokio")]
100 cmd.status().await?.exit_ok()?;
101 #[cfg(not(feature = "tokio"))]
102 cmd.status()?.exit_ok()?;
103 } else if cfg!(windows) {
104 let mut cmd = Command::new("xcopy");
106 cmd.arg(path);
107 cmd.arg(games.join(path.file_name().unwrap()));
108
109 cmd.args(["/S", "/E", "/H", "/I"]);
110 if force {
111 cmd.arg("/Y");
112 }
113
114 #[cfg(feature = "tokio")]
115 cmd.status().await?.exit_ok()?;
116 #[cfg(not(feature = "tokio"))]
117 cmd.status()?.exit_ok()?;
118 } else {
119 unreachable!("Unsupported OS")
120 }
121 Ok::<_, Error>(())
122 }
123 };
124
125 if !path.is_dir() {
126 #[cfg(feature = "tokio")]
127 {
128 tokio::fs::copy(path, games.join(path.file_name().unwrap())).map_ok(|bytes| trace!("copied {bytes}"))
129 .inspect_err(|err| error!("{err}"))
130 .or_else(|_| cp())
131 .await?;
132 };
133 #[cfg(not(feature = "tokio"))]
134 {
135 std::fs::copy(path, games.join(path.file_name().unwrap())).map(|bytes| trace!("copied {bytes}"))
136 .inspect_err(|err| error!("{err}"))
137 .or_else(|_| {
138 futures_lite::future::block_on(cp())
139 })?;
140 }
141 } else {
142 cp().await?;
143 }
144
145 let path = format!("/Games/{}", path.file_name().unwrap().to_string_lossy());
147 Ok(MountedDevicePathBorrowed { drive, path })
148}
149
150
151#[cfg_attr(feature = "tracing", tracing::instrument())]
155pub async fn mount_and_install(query: Query,
156 path: &Path,
157 force: bool)
158 -> Result<impl Stream<Item = Result<MountedDevicePath>> + '_> {
159 validate_host_package(path).await?;
160
161 let fut = mount::mount_and(query, true).await?.flat_map(move |res| {
164 async move {
165 match res {
166 Ok(drive) => {
167 let path = install(&drive, path, force).await?;
168 Ok(path.to_owned_replacing()(drive))
169 },
170 Err(err) => Err(err),
171 }
172 }.into_stream()
173 });
174 Ok(fut)
175}
176
177
178#[cfg_attr(feature = "tracing", tracing::instrument())]
180pub async fn validate_host_package(path: &Path) -> Result<()> {
181 use std::io::{Error, ErrorKind};
182
183 if !path.try_exists()? {
184 return Err(Error::new(ErrorKind::NotFound, "package not found").into());
185 }
186
187 (path.is_dir() ||
188 path.extension() == Some(OsStr::new("pdz")) ||
189 path.extension() == Some(OsStr::new("pdx")))
190 .then_some(())
191 .ok_or_else(|| Error::new(ErrorKind::InvalidData, "invalid package").into())
192}