Skip to content

Commit 5a00a15

Browse files
committed
fix(backup): skip locked files on Windows instead of aborting (#147)
Backing up a Windows user folder aborted the whole run when any file was held open by another process (NTUSER.DAT, browser/Outlook DBs, app lock files), surfacing as os error 32 (ERROR_SHARING_VIOLATION). Extend the existing soft-error classifier is_soft_backup_io_error() to treat ERROR_SHARING_VIOLATION (32) and ERROR_LOCK_VIOLATION (33) as per-file soft skips, matching the warn/skip/continue handling already given to macOS dataless and Windows cloud-file cases. The backup degrades to a partial snapshot with a per-file warning instead of failing. Update the stale soft-error enumerations in the walk/pipeline doc comments and add #[cfg(windows)] tests for both codes.
1 parent 5db0e0a commit 5a00a15

4 files changed

Lines changed: 43 additions & 15 deletions

File tree

crates/vykar-core/src/commands/backup/pipeline/mod.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,10 @@ pub(super) enum ProcessedEntry {
8181
NonFile {
8282
item: crate::snapshot::item::Item,
8383
},
84-
/// A file skipped due to a soft error (permission denied, not found,
85-
/// or drift detected between walk and open / during read). No data
86-
/// was committed for this file.
84+
/// A file skipped due to a soft error (e.g. permission denied, not found,
85+
/// EIO, Windows cloud-file or locked-file failure, or drift detected
86+
/// between walk and open / during read). No data was committed for this
87+
/// file.
8788
Skipped {
8889
path: String,
8990
/// Pre-formatted reason (avoids carrying `VykarError` across threads).
@@ -99,7 +100,8 @@ pub(super) enum ProcessedEntry {
99100
},
100101
/// The walker reported a soft error before it could materialize an
101102
/// `Item` (e.g. directory-iteration `EACCES`, Windows unsupported
102-
/// reparse tag, cloud-file unavailable). Carries the failing path and
103+
/// reparse tag, cloud-file unavailable, locked file held by another
104+
/// process). Carries the failing path and
103105
/// pre-formatted reason so the consumer can emit a path-bearing
104106
/// warning. Mirrors `sequential.rs` `WalkEvent::Skipped` handling.
105107
WalkSkip {

crates/vykar-core/src/commands/backup/walk/inode_walk.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ pub(super) fn rel_path_from_abs(abs_source: &Path, abs_path: &Path) -> String {
107107
pub(in crate::commands::backup) enum WalkEvent {
108108
Entry(WalkedEntry),
109109
/// A soft error occurred (permission denied, not found, EIO, or a
110-
/// Windows-specific unsupported-reparse / cloud-file failure). Carries
110+
/// Windows-specific unsupported-reparse / cloud-file / locked-file
111+
/// (sharing or lock violation) failure). Carries
111112
/// the failing path and a pre-formatted reason so consumers can surface
112113
/// a path-bearing warning rather than just bumping an opaque counter.
113114
Skipped {

crates/vykar-core/src/commands/backup/walk/mod.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,9 @@ pub(super) enum Materialized {
118118
abs_path: PathBuf,
119119
metadata: fs::MetadataSummary,
120120
},
121-
/// Soft I/O error (e.g. permission denied on readlink, Windows
122-
/// unsupported reparse tag) — caller should count as error and surface
123-
/// `path` + `reason` in a path-bearing warning.
121+
/// Soft I/O error from the read_link step (e.g. permission denied on
122+
/// readlink, Windows unsupported reparse tag) — caller should count as
123+
/// error and surface `path` + `reason` in a path-bearing warning.
124124
SoftError { path: PathBuf, reason: String },
125125
/// Unsupported file type (block device, FIFO, etc.) — silent skip.
126126
Unsupported,
@@ -239,7 +239,8 @@ pub(super) enum WalkEntry {
239239
item: Item,
240240
},
241241
/// A file that was skipped due to a soft error (permission denied, not
242-
/// found, EIO, Windows unsupported reparse, cloud-file). Carries the
242+
/// found, EIO, Windows unsupported reparse, cloud-file, locked file).
243+
/// Carries the
243244
/// failing path (snapshot/abs string form) and a pre-formatted reason
244245
/// so consumers can surface a path-bearing warning.
245246
Skipped {

crates/vykar-types/src/error.rs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ fn is_eio(e: &std::io::Error) -> bool {
183183
/// surface from `File::open` / `read` / `fstat` on cloud placeholders when
184184
/// the provider can't materialize content. List intentionally narrow;
185185
/// verified against winerror.h, expand only on concrete user reports.
186+
/// - **`ERROR_SHARING_VIOLATION` (32) and `ERROR_LOCK_VIOLATION` (33)** for
187+
/// files locked by another process — pervasive in a user folder (NTUSER.DAT,
188+
/// browser/Outlook DBs, app lock files):
189+
/// - `32 ERROR_SHARING_VIOLATION` — file held open by another process
190+
/// - `33 ERROR_LOCK_VIOLATION` — byte-range lock held (LockFileEx)
186191
pub fn is_soft_backup_io_error(e: &std::io::Error) -> bool {
187192
if matches!(
188193
e.kind(),
@@ -197,14 +202,16 @@ pub fn is_soft_backup_io_error(e: &std::io::Error) -> bool {
197202
if e.raw_os_error().is_none() && e.to_string().contains("Unsupported reparse point type") {
198203
return true;
199204
}
200-
// (b) cloud-file / can't-access codes from winerror.h:
205+
// (b) cloud-file / can't-access and sharing/lock codes from winerror.h:
201206
// 1920 ERROR_CANT_ACCESS_FILE
202207
// 362 ERROR_CLOUD_FILE_PROVIDER_NOT_RUNNING
203208
// 395 ERROR_CLOUD_FILE_ACCESS_DENIED
204209
// 404 ERROR_CLOUD_FILE_PROVIDER_TERMINATED
210+
// 32 ERROR_SHARING_VIOLATION (file held open by another process)
211+
// 33 ERROR_LOCK_VIOLATION (byte-range lock held, LockFileEx)
205212
if matches!(
206213
e.raw_os_error(),
207-
Some(1920) | Some(362) | Some(395) | Some(404)
214+
Some(1920) | Some(362) | Some(395) | Some(404) | Some(32) | Some(33)
208215
) {
209216
return true;
210217
}
@@ -215,8 +222,9 @@ pub fn is_soft_backup_io_error(e: &std::io::Error) -> bool {
215222
impl VykarError {
216223
/// Returns `true` for I/O errors that indicate a file was unreadable
217224
/// (permission denied, file vanished, EIO, or a Windows-specific
218-
/// unsupported-reparse / cloud-file failure) **before** any data was
219-
/// committed. These are safe to skip for partial-backup support.
225+
/// unsupported-reparse / cloud-file / locked-file (sharing or lock
226+
/// violation) failure) **before** any data was committed. These are safe to
227+
/// skip for partial-backup support.
220228
pub fn is_soft_file_error(&self) -> bool {
221229
match self {
222230
VykarError::Io(e) => is_soft_backup_io_error(e),
@@ -296,8 +304,24 @@ mod tests {
296304

297305
#[test]
298306
#[cfg(windows)]
299-
fn non_soft_io_error_outside_cloud_file_list() {
300-
// 361 is just outside the cloud-file range we recognise; treat as hard.
307+
fn soft_io_error_sharing_violation() {
308+
// ERROR_SHARING_VIOLATION — file held open by another process.
309+
let e = std::io::Error::from_raw_os_error(32);
310+
assert!(is_soft_backup_io_error(&e));
311+
}
312+
313+
#[test]
314+
#[cfg(windows)]
315+
fn soft_io_error_lock_violation() {
316+
// ERROR_LOCK_VIOLATION — byte-range lock held (LockFileEx).
317+
let e = std::io::Error::from_raw_os_error(33);
318+
assert!(is_soft_backup_io_error(&e));
319+
}
320+
321+
#[test]
322+
#[cfg(windows)]
323+
fn non_soft_io_error_outside_windows_allowlist() {
324+
// 361 is just outside the codes we recognise; treat as hard.
301325
let e = std::io::Error::from_raw_os_error(361);
302326
assert!(!is_soft_backup_io_error(&e));
303327
}

0 commit comments

Comments
 (0)