diff --git a/crates/engine/src/autosave.rs b/crates/engine/src/autosave.rs index 01899d2f..03b61dd3 100644 --- a/crates/engine/src/autosave.rs +++ b/crates/engine/src/autosave.rs @@ -180,7 +180,7 @@ impl Autosave { fn create(root: &Path, name: &str) -> Result { let stamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_millis(); let dir = root.join(format!("{} {stamp}-{}", sanitize_name(name), std::process::id())); - let lock = std::fs::create_dir_all(&dir).and_then(|()| std::fs::File::create(dir.join(LOCK))).map_err(|err| io_error(&dir, err))?; + let lock = create_folder(&dir).and_then(|()| std::fs::File::create(dir.join(LOCK))).map_err(|err| io_error(&dir, err))?; lock.try_lock().map_err(|err| EngineError::Io(format!("{}: {err}", dir.display())))?; Ok(Autosave { dir, lock: Some(lock), copies: BTreeMap::new(), next: 1, recording: false }) } @@ -210,9 +210,7 @@ impl Autosave { self.next += 1; let opts = EncodeOptions { format: FileFormat::Wav, bit_depth: BitDepth::Float32, dither: false, bwf: None }; let bytes = soundcraft_audio_io::encode(&audio.buffer, &opts).map_err(|err| EngineError::Io(err.to_string()))?; - std::fs::create_dir_all(&audio_dir) - .and_then(|()| write_synced(&audio_dir.join(&name), &bytes)) - .map_err(|err| io_error(&audio_dir, err))?; + create_folder(&audio_dir).and_then(|()| write_synced(&audio_dir.join(&name), &bytes)).map_err(|err| io_error(&audio_dir, err))?; name } }; @@ -395,6 +393,18 @@ pub fn discard(left: &Leftover) { let _ = std::fs::remove_dir_all(&left.dir); } +/// Create `dir` and its missing parents under the autosave root. On Unix every folder made here is +/// owner-only (`0o700`): autosaves and the takes of a session never saved are unsaved work, and +/// with the default mode other accounts on the machine could read them. A folder that already +/// exists keeps its mode. +pub fn create_folder(dir: &Path) -> std::io::Result<()> { + let mut builder = std::fs::DirBuilder::new(); + builder.recursive(true); + #[cfg(unix)] + std::os::unix::fs::DirBuilderExt::mode(&mut builder, 0o700); + builder.create(dir) +} + fn write_synced(path: &Path, bytes: &[u8]) -> std::io::Result<()> { let mut f = std::fs::File::create(path)?; f.write_all(bytes)?; @@ -511,6 +521,42 @@ mod tests { let _ = std::fs::remove_dir_all(&root); } + /// Autosaves are unsaved work: on Unix the folders the autosaver makes are owner-only, and an + /// autosave root that already exists keeps its mode. + #[cfg(unix)] + #[test] + fn the_autosaver_makes_owner_only_folders() { + use std::os::unix::fs::PermissionsExt as _; + let mode = |p: &Path| std::fs::metadata(p).unwrap().permissions().mode() & 0o777; + let base = folder("autosave-modes"); + let mut e = Engine::default(); + record(&mut e, 0.5); + // A root that doesn't exist yet is made with the first autosave, owner-only like the + // session's folder and the audio copied into it. + let new = base.join("Autosave"); + let w = Autosaver::new(new.clone()); + w.write(e.session_arc(), None, None); + w.flush().unwrap(); + let session = std::fs::read_dir(&new).unwrap().flatten().map(|d| d.path()).next().unwrap(); + assert_eq!((mode(&new), mode(&session), mode(&session.join("Audio Files"))), (0o700, 0o700, 0o700)); + w.close(false); + w.flush().unwrap(); + // A root that already exists keeps its mode; the folders made inside it are owner-only, + // also the one a session never saved records its takes into. + let old = base.join("old"); + std::fs::create_dir_all(&old).unwrap(); + std::fs::set_permissions(&old, std::fs::Permissions::from_mode(0o755)).unwrap(); + let w = Autosaver::new(old.clone()); + w.write(e.session_arc(), None, None); + w.flush().unwrap(); + let session = std::fs::read_dir(&old).unwrap().flatten().map(|d| d.path()).next().unwrap(); + create_folder(&old.join("Untitled")).unwrap(); + assert_eq!((mode(&old), mode(&session), mode(&old.join("Untitled"))), (0o755, 0o700, 0o700)); + w.close(false); + w.flush().unwrap(); + let _ = std::fs::remove_dir_all(&base); + } + #[test] fn a_take_still_recording_at_a_crash_is_placed_on_recovery() { use crate::io::Take; diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 3fdd9892..1db64e12 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -600,6 +600,11 @@ impl SoundApp { if plan.is_empty() { return Err("no audio track is record-enabled".into()); } + // The takes are unsaved work: the folder under the autosave root is owner-only, like the + // autosaves, before the recorder makes anything in it. + if self.engine.path.is_none() && self.autosave_dir.is_some() { + soundcraft_engine::autosave::create_folder(&unsaved_dir).map_err(|e| format!("{}: {e}", unsaved_dir.display()))?; + } let takes = plan .iter() .map(|(track, inputs, path)| soundcraft_engine::io::Take { track: *track, path: path.clone(), channels: inputs.len() })