playdate_device/
install.rs

1use 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
15/// On-device path with owned mounted device.
16pub struct MountedDevicePath {
17	drive: MountedDevice,
18	path: String,
19}
20
21/// On-device path with borrowed mounted device.
22pub 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	/// Local on-device path.
31	pub fn path_local(&self) -> &str { &self.path }
32	/// Absolute on-host path.
33	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	/// Local on-device path.
49	pub fn path_local(&self) -> &str { &self.path }
50	/// Absolute on-host path.
51	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/// Install package on the device.
59///
60/// `path` is a host filesystem path to pdx.
61#[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				// xcopy c:\test c:\test2\test /S /E /H /I /Y
105				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	// on-dev-path:
146	let path = format!("/Games/{}", path.file_name().unwrap().to_string_lossy());
147	Ok(MountedDevicePathBorrowed { drive, path })
148}
149
150
151/// 1. Mount if needed
152/// 1. Wait for FS to become available
153/// 1. Install package
154#[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	// TODO: Check query is path and this is mounted volume.
162
163	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/// Validate path - pdz or pdx-dir.
179#[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}