Skip to content

Commit fd0317f

Browse files
authored
Ignore "System Volume Information" folder when detecting empty disks
1 parent cd0c06f commit fd0317f

1 file changed

Lines changed: 212 additions & 82 deletions

File tree

src/disk.rs

Lines changed: 212 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,225 @@
1-
use std::env::current_dir;
2-
use std::fs;
1+
use std::path::Path;
2+
use std::vec::Vec;
33

4-
use sysinfo::{Disk, Disks};
4+
use futures::future::try_join_all;
55

6-
use console::Style;
7-
use indicatif::DecimalBytes;
6+
use anyhow::{Context, Error, Result, anyhow};
7+
8+
use clap::{Arg, ArgAction, Command, crate_version};
89

910
use log::debug;
1011

11-
// Print disks list as a table
12-
pub fn print_disks() {
13-
println!(
14-
"{0: ^20} | {1: ^30} | {2: ^6} | {3: ^9} | {4: ^10} | {5: ^5} ",
15-
"Name", "Location", "Type", "Removable", "Avail.", "Empty"
16-
);
17-
let red = Style::new().red();
18-
let green = Style::new().green();
19-
let disks = Disks::new_with_refreshed_list();
20-
for disk in &disks {
21-
let disk_removable = if disk.is_removable() {
22-
green.apply_to("Yes")
23-
} else {
24-
red.apply_to("No")
25-
};
26-
let file_system_str = disk.file_system().to_string_lossy();
27-
let file_system = if file_system_str.eq_ignore_ascii_case("vfat")
28-
|| file_system_str.eq_ignore_ascii_case("fat32")
29-
{
30-
green.apply_to(file_system_str)
31-
} else {
32-
red.apply_to(file_system_str)
33-
};
34-
35-
let empty = if let Ok(files) = fs::read_dir(disk.mount_point()) {
36-
if files.count() == 0 {
37-
green.apply_to("Yes")
38-
} else {
39-
red.apply_to("No")
12+
use reqwest::Client;
13+
14+
use indicatif::{DecimalBytes, MultiProgress};
15+
16+
mod disk;
17+
mod download;
18+
mod interact;
19+
mod psa;
20+
21+
#[tokio::main]
22+
async fn main() -> Result<(), Error> {
23+
env_logger::init();
24+
25+
let mut map_info = "Sets the map to check for update. Supported maps:".to_string();
26+
for map in psa::MAPS {
27+
map_info = format!("{}\n - {}: {}", map_info, map.get_code(), map.get_name());
28+
}
29+
30+
let matches = Command::new("PSA firmware update.")
31+
.version(crate_version!())
32+
.about("CLI alternative to Peugeot/Citroën/Opel/DS update applications for car infotainment system (NAC/RCC firmware and navigation maps), hopefully more robust. Supports for resume of downloads.")
33+
.arg(Arg::new("VIN")
34+
.help("Vehicle Identification Number (VIN) to check for update")
35+
.required(false)
36+
.index(1))
37+
.arg(Arg::new("map")
38+
.help(map_info)
39+
.required(false)
40+
.long("map")
41+
.action(ArgAction::Set))
42+
.arg(Arg::new("silent")
43+
.help("Sets silent (non-interactive) mode")
44+
.required(false)
45+
.long("silent")
46+
.action(ArgAction::SetTrue))
47+
.arg(Arg::new("download")
48+
.help("Automatically proceed with download of updates. Previous downloads will be resumed.")
49+
.required(false)
50+
.long("download")
51+
.action(ArgAction::SetTrue))
52+
.arg(Arg::new("extract")
53+
.help("Location where to extract update files. Should be the root of an empty FAT32 USB drive.")
54+
.required(false)
55+
.long("extract")
56+
.action(ArgAction::Set))
57+
.arg(Arg::new("sequential-download")
58+
.help("Forces sequential download of updates. By default updates are downloaded concurrently.")
59+
.required(false)
60+
.long("sequential-download")
61+
.action(ArgAction::SetTrue))
62+
.get_matches();
63+
64+
let interactive = !matches.get_flag("silent");
65+
let vin = matches.get_one::<String>("VIN").map(|s| s.to_uppercase());
66+
let vin_provided_as_arg = vin.is_some();
67+
let map = matches.get_one::<String>("map").map(|s| s.as_str());
68+
let download = matches.get_flag("download");
69+
let sequential_download = matches.get_flag("sequential-download");
70+
let extract_location = matches.get_one::<String>("extract").map(|s| s.as_str());
71+
72+
// Vin not provided on command line, asking interactively
73+
let vin = if !vin_provided_as_arg && interactive {
74+
interact::prompt("Please enter VIN").ok()
75+
} else {
76+
vin.map(|v| v.to_string())
77+
};
78+
if vin.is_none() {
79+
return Err(anyhow!("No VIN provided"));
80+
}
81+
let vin = vin.unwrap();
82+
83+
let client = Client::builder()
84+
// Dummy user agent to make cloudfront proxy happy when downloading firmware files
85+
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36")
86+
.build()
87+
.context("Failed to create HTTP client")?;
88+
let device_info = psa::request_device_information(&client, &vin).await?;
89+
let is_nac: bool = device_info
90+
.devices
91+
.map(|l| l.iter().any(|d| d.ecu_type.contains("NAC")))
92+
== Some(true);
93+
94+
// Maps not provided on command line, asking interactively for NAC
95+
let map = if map.is_none() && is_nac && interactive {
96+
interact::select_map()?
97+
} else {
98+
map
99+
};
100+
101+
let update_response = psa::request_available_updates(&client, &vin, map).await?;
102+
103+
if update_response.software.is_none() {
104+
println!("No update found");
105+
return Ok(());
106+
}
107+
108+
let mut selected_updates: Vec<psa::SoftwareUpdate> = Vec::new();
109+
let mut total_update_size = 0_u64;
110+
111+
let mut software_list: Vec<psa::Software> = update_response
112+
.software
113+
.expect("Expected at least a software in server response");
114+
115+
// For NAC, let's sort in reverse order of software type to display firmware (ovip) first, then map (map)
116+
software_list.sort_by(|u1, u2| u2.software_type.cmp(&u1.software_type));
117+
118+
for software in software_list {
119+
for update in &software.update {
120+
// An empty update can be sent by the server when there is no available update
121+
if !update.update_id.is_empty() {
122+
psa::print(&software, update);
123+
if download || (interactive && interact::confirm("Download update?")?) {
124+
selected_updates.push(update.clone());
125+
let update_size = match update.update_size.parse() {
126+
Ok(size) => size,
127+
Err(_) => {
128+
debug!("Failed to parse update size: {}", update.update_size);
129+
0
130+
}
131+
};
132+
total_update_size += update_size;
133+
}
40134
}
41-
} else {
42-
red.apply_to("N/A")
43-
};
44-
45-
println!(
46-
"{0: <20} | {1: <30} | {2: <6} | {3: <9} | {4: >10} | {5: <5}",
47-
disk.name().to_string_lossy(),
48-
disk.mount_point().to_string_lossy(),
49-
file_system,
50-
disk_removable,
51-
DecimalBytes(disk.available_space()).to_string(),
52-
empty
53-
);
135+
}
54136
}
55-
}
56137

57-
// Available disk space in current directory
58-
pub fn get_current_dir_available_space() -> Option<u64> {
59-
let cwd_result = current_dir();
60-
if cwd_result.is_err() {
61-
debug!(
62-
"Failed to retrieve information about current working directory: {}",
63-
cwd_result.err().unwrap()
64-
);
65-
return None;
138+
if selected_updates.is_empty() {
139+
println!("No update selected for download");
140+
return Ok(());
66141
}
67-
let cwd = cwd_result.ok().unwrap();
68-
let mut cwd_disk: Option<&Disk> = None;
69-
// Lookup disk whose mount point is parent of cwd
70-
// In case there are multiple candidates, pick up the "nearest" parent of cwd
71-
let disks = Disks::new_with_refreshed_list();
72-
for disk in &disks {
73-
debug!("Disk {disk:?}");
74-
if cwd.starts_with(disk.mount_point())
75-
&& (cwd_disk.is_none()
76-
|| disk
77-
.mount_point()
78-
.starts_with(cwd_disk.unwrap().mount_point()))
79-
{
80-
cwd_disk = Some(disk);
142+
143+
// Check available disk size
144+
let disk_space = disk::get_current_dir_available_space();
145+
if let Some(space) = disk_space
146+
&& space < total_update_size
147+
{
148+
interact::warn(&format!(
149+
"Not enough space on disk to proceed with download. Available disk space in current directory: {}",
150+
DecimalBytes(space)
151+
));
152+
if interactive && !(interact::confirm("Continue anyway?")?) {
153+
return Ok(());
81154
}
82155
}
83-
if cwd_disk.is_none() {
84-
debug!(
85-
"Failed to retrieve disk information for current working directory: {}",
86-
cwd.to_string_lossy()
87-
);
88-
return None;
156+
157+
let multi_progress = MultiProgress::new();
158+
159+
let downloaded_updates: Vec<psa::DownloadedUpdate> = if sequential_download {
160+
// Download sequentially
161+
let mut result: Vec<psa::DownloadedUpdate> = Vec::new();
162+
for update in selected_updates {
163+
result.push(psa::download_update(&client, &update, &multi_progress).await?);
164+
}
165+
result
166+
} else {
167+
// Download concurrently
168+
let downloads = selected_updates
169+
.iter()
170+
.map(|update| psa::download_update(&client, update, &multi_progress));
171+
try_join_all(downloads).await?
172+
};
173+
174+
let mut extract_location = extract_location.map(str::to_string);
175+
if interactive && extract_location.is_none() {
176+
if !interact::confirm(
177+
"To proceed to extraction of update(s), please insert an empty USB disk formatted as FAT32. Continue?",
178+
)? {
179+
return Ok(());
180+
}
181+
182+
// Listing available disks for extraction
183+
// TODO check destination available space.
184+
disk::print_disks();
185+
let location = interact::prompt(
186+
"Location where to extract the update files (IMPORTANT: Should be the root of an EMPTY USB device formatted as FAT32)",
187+
)?;
188+
if !location.is_empty() {
189+
extract_location = Some(location);
190+
}
89191
}
90-
debug!(
91-
"Current working directory maps to disk {}",
92-
cwd_disk.unwrap().name().to_string_lossy()
93-
);
94-
Some(cwd_disk.unwrap().available_space())
192+
193+
match extract_location {
194+
Some(location) => {
195+
let destination_path = Path::new(&location);
196+
if !destination_path.is_dir() {
197+
return Err(anyhow!(
198+
"Destination does not exist or is not a directory: {}",
199+
destination_path.to_string_lossy()
200+
));
201+
}
202+
for update in downloaded_updates {
203+
psa::extract_update(&update, destination_path)
204+
.context("Failed to extract update")?;
205+
}
206+
println!(
207+
"The update can be applied on the car infotainment system following vendor instructions."
208+
);
209+
if is_nac {
210+
println!(
211+
"For example, for Peugeot NAC: https://web.archive.org/web/20220719220945/https://media-ct-ndp.peugeot.com/file/38/2/map-software-rcc-en.632382.pdf"
212+
);
213+
} else {
214+
println!(
215+
"For example, for Peugeot RCC: https://web.archive.org/web/20230602131011/https://media-ct-ndp.peugeot.com/file/38/0/map-software-nac-en.632380.pdf"
216+
);
217+
}
218+
}
219+
None => {
220+
println!("No location, skipping extraction");
221+
}
222+
}
223+
224+
Ok(())
95225
}

0 commit comments

Comments
 (0)