Skip to content

Commit 7459971

Browse files
fix: handle readonly errors (#204)
Signed-off-by: Thomas Mauran <thomasmauran@yahoo.com>
1 parent dae35fb commit 7459971

File tree

2 files changed

+75
-15
lines changed

2 files changed

+75
-15
lines changed

src/app.rs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ use log::LevelFilter;
1616
use shakmaty::{Color, Move, Position};
1717
use std::error;
1818
use std::fs::{self, File};
19-
use std::io::Write;
19+
use std::io::{self, Write};
2020
use std::net::{IpAddr, UdpSocket};
21+
use std::path::Path;
2122
use std::sync::mpsc::{channel, Receiver};
2223
use std::thread::sleep;
2324
use std::time::Duration;
@@ -1378,6 +1379,29 @@ impl App {
13781379
}
13791380
}
13801381

1382+
/// Checks if an IO error indicates a read-only filesystem or permission issue.
1383+
/// This handles both EROFS (error code 30) and permission denied errors.
1384+
fn is_readonly_error(e: &io::Error) -> bool {
1385+
// EROFS = 30 on Unix systems (Read-only file system)
1386+
e.raw_os_error() == Some(30) || e.kind() == io::ErrorKind::PermissionDenied
1387+
}
1388+
1389+
/// Handles config file write errors gracefully, logging appropriate warnings.
1390+
/// This allows the application to work with read-only config files (e.g., from NixOS/home-manager).
1391+
fn handle_config_write_error(&self, e: io::Error, config_path: &Path) {
1392+
if Self::is_readonly_error(&e) {
1393+
log::warn!(
1394+
"Config file at {:?} is read-only. Settings changes will not be persisted.",
1395+
config_path
1396+
);
1397+
} else {
1398+
log::warn!(
1399+
"Could not write to config file at {:?}: {}. Settings changes will not be persisted.",
1400+
config_path, e
1401+
);
1402+
}
1403+
}
1404+
13811405
pub fn update_config(&self) {
13821406
let Ok(config_dir) = config_dir() else {
13831407
log::error!("Failed to get config directory");
@@ -1396,10 +1420,17 @@ impl App {
13961420
config.lichess_token = self.lichess_token.clone();
13971421
config.sound_enabled = Some(self.sound_enabled);
13981422

1399-
if let Ok(mut file) = File::create(&config_path) {
1400-
let toml_string = toml::to_string(&config).unwrap_or_default();
1401-
if let Err(e) = file.write_all(toml_string.as_bytes()) {
1402-
log::error!("Failed to write config: {}", e);
1423+
// Try to write the config file, but don't fail if it's read-only
1424+
// This allows the application to work with read-only config files (e.g., from NixOS/home-manager)
1425+
let toml_string = toml::to_string(&config).unwrap_or_default();
1426+
match File::create(&config_path) {
1427+
Ok(mut file) => {
1428+
if let Err(e) = file.write_all(toml_string.as_bytes()) {
1429+
self.handle_config_write_error(e, &config_path);
1430+
}
1431+
}
1432+
Err(e) => {
1433+
self.handle_config_write_error(e, &config_path);
14031434
}
14041435
}
14051436
}

src/main.rs

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use chess_tui::ui::tui::Tui;
1313
use clap::Parser;
1414
use log::LevelFilter;
1515
use std::fs::{self, File};
16-
use std::io::Write;
16+
use std::io::{self, Write};
1717
use std::panic;
1818
use std::path::Path;
1919

@@ -312,13 +312,31 @@ fn main() -> AppResult<()> {
312312
Ok(())
313313
}
314314

315-
fn config_create(args: &Args, folder_path: &Path, config_path: &Path) -> AppResult<()> {
316-
std::fs::create_dir_all(folder_path)?;
315+
/// Checks if an IO error indicates a read-only filesystem or permission issue.
316+
/// This handles both EROFS (error code 30) and permission denied errors.
317+
fn is_readonly_error(e: &io::Error) -> bool {
318+
// EROFS = 30 on Unix systems (Read-only file system)
319+
e.raw_os_error() == Some(30) || e.kind() == io::ErrorKind::PermissionDenied
320+
}
317321

318-
if !config_path.exists() {
319-
//write to console
320-
File::create(config_path)?;
322+
/// Handles config file write errors gracefully, printing appropriate warnings.
323+
/// This allows the application to work with read-only config files (e.g., from NixOS/home-manager).
324+
fn handle_config_write_error(e: io::Error, config_path: &Path) {
325+
if is_readonly_error(&e) {
326+
eprintln!(
327+
"Warning: Config file at {:?} is read-only. The application will continue with the existing read-only config.",
328+
config_path
329+
);
330+
} else {
331+
eprintln!(
332+
"Warning: Could not write to config file at {:?}: {}. The application will continue with the existing config.",
333+
config_path, e
334+
);
321335
}
336+
}
337+
338+
fn config_create(args: &Args, folder_path: &Path, config_path: &Path) -> AppResult<()> {
339+
std::fs::create_dir_all(folder_path)?;
322340

323341
// Attempt to read the configuration file and parse it as a TOML Value.
324342
// If we encounter any issues (like the file not being readable or not being valid TOML), we start with a new, empty TOML table instead.
@@ -374,14 +392,25 @@ fn config_create(args: &Args, folder_path: &Path, config_path: &Path) -> AppResu
374392
config.sound_enabled = Some(false);
375393
}
376394

395+
// Try to write the config file, but don't fail if it's read-only
396+
// This allows the application to work with read-only config files (e.g., from NixOS/home-manager)
377397
let toml_string = toml::to_string(&config).map_err(|e| {
378-
std::io::Error::new(
379-
std::io::ErrorKind::InvalidData,
398+
io::Error::new(
399+
io::ErrorKind::InvalidData,
380400
format!("Failed to serialize config to TOML: {e}"),
381401
)
382402
})?;
383-
let mut file = File::create(config_path)?;
384-
file.write_all(toml_string.as_bytes())?;
403+
404+
match File::create(config_path) {
405+
Ok(mut file) => {
406+
if let Err(e) = file.write_all(toml_string.as_bytes()) {
407+
handle_config_write_error(e, config_path);
408+
}
409+
}
410+
Err(e) => {
411+
handle_config_write_error(e, config_path);
412+
}
413+
}
385414

386415
Ok(())
387416
}

0 commit comments

Comments
 (0)