@@ -34,6 +34,13 @@ class FileStore extends StoreAdapter {
3434
3535 // Ensure collection directory exists
3636 this . ensureDirectory ( )
37+
38+ // Detect whether the collection directory lives on a case-sensitive
39+ // filesystem, so a file rename that only changes letter case (e.g.
40+ // Login -> login) is handled correctly. On a case-insensitive FS (macOS
41+ // APFS and Windows NTFS defaults) such a rename points at the same physical
42+ // file, so writeAtomic + unlink would otherwise delete the just-written file.
43+ this . caseSensitive = this . detectCaseSensitive ( )
3744 }
3845
3946 ensureDirectory ( ) {
@@ -45,6 +52,33 @@ class FileStore extends StoreAdapter {
4552 }
4653 }
4754
55+ /**
56+ * Probe whether the collection directory lives on a case-sensitive filesystem
57+ * by writing a marker file and checking whether a differently-cased name
58+ * resolves to it. Returns true (case-sensitive) or false (case-insensitive),
59+ * defaulting to false if probing fails so we never unlink on a case-only rename.
60+ */
61+ detectCaseSensitive ( ) {
62+ const probeName = `.casesens-${ crypto . randomBytes ( 4 ) . toString ( 'hex' ) } `
63+ const probePath = path . join ( this . collectionPath , probeName )
64+ try {
65+ fs . writeFileSync ( probePath , '' )
66+ // On a case-insensitive FS the upper-cased name resolves to the same file.
67+ return ! fs . existsSync ( path . join ( this . collectionPath , probeName . toUpperCase ( ) ) )
68+ } catch {
69+ // Conservative default: treat as case-insensitive to avoid data loss.
70+ return false
71+ } finally {
72+ try {
73+ if ( fs . existsSync ( probePath ) ) {
74+ fs . unlinkSync ( probePath )
75+ }
76+ } catch {
77+ /* best-effort cleanup of the probe file */
78+ }
79+ }
80+ }
81+
4882 /**
4983 * Generate a unique ID similar to NeDB's format
5084 */
@@ -343,7 +377,13 @@ class FileStore extends StoreAdapter {
343377 reservedNames . add ( fileBaseName . toLowerCase ( ) )
344378
345379 this . writeAtomic ( targetPath , updatedDoc )
346- if ( targetPath !== entry . filePath && fs . existsSync ( entry . filePath ) ) {
380+ // Only unlink the previous file when it is a genuinely different physical
381+ // file. On a case-insensitive FS a case-only rename (Login -> login) points
382+ // at the same file, so unlinking would delete the document we just wrote.
383+ const pathsDiffer = this . caseSensitive
384+ ? targetPath !== entry . filePath
385+ : targetPath . toLowerCase ( ) !== entry . filePath . toLowerCase ( )
386+ if ( pathsDiffer && fs . existsSync ( entry . filePath ) ) {
347387 fs . unlinkSync ( entry . filePath )
348388 }
349389 }
0 commit comments