{"record":{"id":"957f30790012d1cf","repo":"oldj/SwitchHosts","slug":"permissiondenied","errorCode":"PermissionDenied","errorMessage":"directory is not writable","messagePattern":"directory is not writable","errorType":"exception","errorClass":"StorageError","httpStatus":null,"severity":"error","filePath":"src-tauri/src/storage/paths.rs","lineNumber":83,"sourceCode":"\n    /// Like `ensure_dirs`, but also verifies every v5 directory is actually\n    /// writable (root, entries, internal, histories). `ensure_dirs` is a\n    /// no-op on already-existing dirs, so it can't tell a read-only sub-dir\n    /// from a usable one; this probes each with a temp file. Use before\n    /// committing to a data root (apply pre-check and startup), so an\n    /// unwritable target can't be saved and later crash data writes.\n    pub fn ensure_usable(&self) -> Result<(), StorageError> {\n        self.ensure_dirs()?;\n        for dir in [\n            &self.root,\n            &self.entries_dir,\n            &self.internal,\n            &self.histories_dir,\n        ] {\n            if !super::fs_copy::is_writable_dir(dir) {\n                return Err(StorageError::io(\n                    dir.display().to_string(),\n                    std::io::Error::new(\n                        std::io::ErrorKind::PermissionDenied,\n                        \"directory is not writable\",\n                    ),\n                ));\n            }\n        }\n        Ok(())\n    }\n\n    /// Remove leftover `.tmp` files from `atomic_write` that survived\n    /// a crash or force-kill. Each v5 directory is scanned for files\n    /// ending in `.tmp`; matches are deleted silently. This is safe\n    /// because `atomic_write` writes to `<target>.tmp` then renames\n    /// to `<target>` — a leftover `.tmp` is always a partial write\n    /// that never became the real file.\n    pub fn cleanup_tmp_files(&self) {\n        let dirs = [\n            &self.root,","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/oldj/SwitchHosts/blob/6ecea88d9291e0b127cfe745921738a66829a1ba/src-tauri/src/storage/paths.rs#L65-L101","documentation":"V5Paths::ensure_usable probes each v5 storage directory (root, entries, internal, histories) by actually writing a '.swh-write-probe.tmp' file via fs_copy::is_writable_dir (src-tauri/src/storage/fs_copy.rs:161). If any probe write fails, the dir is treated as unusable and the function returns StorageError::io wrapping io::ErrorKind::PermissionDenied with message 'directory is not writable'. This is deliberate: create_dir_all returns Ok on an existing read-only directory, so a real write probe is the only reliable check before later data writes would crash.","triggerScenarios":"Calling V5Paths::ensure_usable (directly, or transitively through AppState::bootstrap at startup) when any of root/entries_dir/internal/histories_dir exists but rejects writes: read-only volume (DMG mount, ro exFAT/network share), dir mode 555 owned by another user, Windows ACL deny, macOS SIP-protected location, or a dir the process lacks write perms for. The probe std::fs::write of dir.join(\".swh-write-probe.tmp\") fails for any of these and the error is emitted.","commonSituations":"User configured a custom SwitchHosts data directory on an external/USB/network drive mounted read-only or currently unavailable in ro mode; app data dir was chmod'ed or chown'ed away; running the app from a read-only installer image or sandboxed environment without write grants to the chosen data location; disk full or quota exceeded can also make the probe write fail.","solutions":["Check mount status and permissions of the data root: run `mount | grep <dir>` (macOS/Linux) or verify the drive isn't a read-only DMG/network share; remount read-write or pick a writable location.","Fix ownership/permissions: chmod u+w (or on Windows, grant Modify on the folder to the current user) on the data dir and its entries/internal/histories subdirs.","Point SwitchHosts at a different data directory (the app's recovery flow / 'Choose New Folder') on a local writable volume, then restart.","If this happens at every launch and blocks startup (see error 2), move the offending data dir aside and let the app recreate the v5 layout fresh."],"exampleFix":"# before\ndata-root -> /Volumes/SwitchHosts-DMG/data   (read-only mount)\n\n# after\n# remount writable or relocate the data dir\nmount -u rw /Volumes/SwitchHosts-DMG\n# or in-app: choose a new data folder on the local disk","handlingStrategy":"validation","validationCode":"use crate::storage::fs_copy::is_writable_dir;\n\n// Before relying on a chosen data root:\nlet candidate = std::path::PathBuf::from(\"/chosen/data-dir\");\nif !is_writable_dir(&candidate) {\n    eprintln!(\"data dir {} is not writable; pick another\", candidate.display());\n    // prompt user for a different location instead of proceeding\n}","typeGuard":"null","tryCatchPattern":"// Rust: handle the StorageError from ensure_usable and inspect the path\nmatch paths.ensure_usable() {\n    Ok(()) => {}\n    Err(StorageError::Io { path, .. }) if path.contains(\"not writable\") || true => {\n        // degrade gracefully: fall back to default root / show chooser dialog\n        log::warn!(\"storage root {path} unusable\");\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Pre-validate custom data directories with is_writable_dir before accepting them in settings, not after.","Avoid data roots on removable or network volumes unless write access is confirmed at selection time.","Re-run ensure_usable when a volume remounts or the app regains focus after sleep, since writability can change under you.","On Windows, verify ACL Modify rights rather than just folder existence when validating a user-picked path."],"tags":["rust","filesystem","permissions","storage","startup","tauri"],"backgroundTag":"directory-permission-denied","analyzedSha":"6ecea88d9291e0b127cfe745921738a66829a1ba","analyzedAt":"2026-08-16T21:20:57.238Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}