From 4c20469b78bde36e92b5c30ce8489b8e83aee939 Mon Sep 17 00:00:00 2001 From: Marcel Farres Date: Thu, 18 Dec 2025 10:12:10 -0500 Subject: [PATCH] fix: add Sync() calls to persist filesystem changes to storage - Add Sync() after Format, Unmount, Remove, Rename, Mkdir - Add Sync() after File.Close, File.Sync, File.Truncate - Sync on OpenFile with O_CREATE/O_TRUNC flags - Fix cache invalidation in lfs_bd_flush - Add TestDirectoryPersistence and NestedDirectories tests" --- littlefs/go_lfs.go | 88 ++++++++++++++++++++++++++++++++++++----- littlefs/go_lfs_test.go | 62 ++++++++++++++++++++++++++++- littlefs/lfs.c | 4 ++ 3 files changed, 144 insertions(+), 10 deletions(-) diff --git a/littlefs/go_lfs.go b/littlefs/go_lfs.go index 1e94f5d..fa02069 100644 --- a/littlefs/go_lfs.go +++ b/littlefs/go_lfs.go @@ -186,24 +186,48 @@ func (l *LFS) Mount() error { } func (l *LFS) Format() error { - return errval(C.lfs_format(l.lfs, l.cfg)) + if err := errval(C.lfs_format(l.lfs, l.cfg)); err != nil { + return err + } + if syncer, ok := l.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } + return nil } func (l *LFS) Unmount() error { - return errval(C.lfs_unmount(l.lfs)) + if err := errval(C.lfs_unmount(l.lfs)); err != nil { + return err + } + if syncer, ok := l.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } + return nil } func (l *LFS) Remove(path string) error { cs := cstring(path) defer C.free(unsafe.Pointer(cs)) - return errval(C.lfs_remove(l.lfs, cs)) + if err := errval(C.lfs_remove(l.lfs, cs)); err != nil { + return err + } + if syncer, ok := l.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } + return nil } func (l *LFS) Rename(oldPath string, newPath string) error { cs1, cs2 := cstring(oldPath), cstring(newPath) defer C.free(unsafe.Pointer(cs1)) defer C.free(unsafe.Pointer(cs2)) - return errval(C.lfs_rename(l.lfs, cs1, cs2)) + if err := errval(C.lfs_rename(l.lfs, cs1, cs2)); err != nil { + return err + } + if syncer, ok := l.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } + return nil } func (l *LFS) Stat(path string) (os.FileInfo, error) { @@ -223,7 +247,13 @@ func (l *LFS) Stat(path string) (os.FileInfo, error) { func (l *LFS) Mkdir(path string, _ os.FileMode) error { cs := (*C.char)(cstring(path)) defer C.free(unsafe.Pointer(cs)) - return errval(C.lfs_mkdir(l.lfs, cs)) + if err := errval(C.lfs_mkdir(l.lfs, cs)); err != nil { + return err + } + if syncer, ok := l.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } + return nil } func (l *LFS) Open(path string) (tinyfs.File, error) { @@ -261,6 +291,24 @@ func (l *LFS) OpenFile(path string, flags int) (tinyfs.File, error) { return nil, err } + if flags&(os.O_CREATE|os.O_TRUNC) != 0 { + if flags&os.O_TRUNC != 0 { + if err := errval(C.lfs_file_sync(l.lfs, file.fileptr())); err != nil { + file.Close() + return nil, err + } + } + if syncer, ok := l.dev.(tinyfs.Syncer); ok { + if err := syncer.Sync(); err != nil { + // Should we close and return error? + // Maybe just log? But we can't log. + // For now let's just return error and close. + file.Close() + return nil, err + } + } + } + return file, nil } @@ -305,14 +353,21 @@ func (f *File) Close() error { C.free(f.hndl) f.hndl = nil }() + var err error switch f.typ { case fileTypeReg: - return errval(C.lfs_file_close(f.lfs.lfs, f.fileptr())) + err = errval(C.lfs_file_close(f.lfs.lfs, f.fileptr())) case fileTypeDir: - return errval(C.lfs_dir_close(f.lfs.lfs, f.dirptr())) + err = errval(C.lfs_dir_close(f.lfs.lfs, f.dirptr())) default: panic("lfs: unknown typ for file handle") } + if err != nil { + return err + } + if syncer, ok := f.lfs.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } } return nil } @@ -373,12 +428,27 @@ func (f *File) Stat() (os.FileInfo, error) { // Sync synchronizes to storage so that any pending writes are written out. func (f *File) Sync() error { - return errval(C.lfs_file_sync(f.lfs.lfs, f.fileptr())) + if err := errval(C.lfs_file_sync(f.lfs.lfs, f.fileptr())); err != nil { + return err + } + if syncer, ok := f.lfs.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } + return nil } // Truncate the size of the file to the specified size func (f *File) Truncate(size uint32) error { - return errval(C.lfs_file_truncate(f.lfs.lfs, f.fileptr(), C.lfs_off_t(size))) + if err := errval(C.lfs_file_truncate(f.lfs.lfs, f.fileptr(), C.lfs_off_t(size))); err != nil { + return err + } + if err := errval(C.lfs_file_sync(f.lfs.lfs, f.fileptr())); err != nil { + return err + } + if syncer, ok := f.lfs.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } + return nil } func (f *File) Write(buf []byte) (n int, err error) { diff --git a/littlefs/go_lfs_test.go b/littlefs/go_lfs_test.go index 9d41735..41f68a0 100644 --- a/littlefs/go_lfs_test.go +++ b/littlefs/go_lfs_test.go @@ -211,7 +211,15 @@ func TestDirectories(t *testing.T) { }) t.Run("NestedDirectories", func(t *testing.T) { - + check(t, fs.Mkdir("parent", 0777)) + check(t, fs.Mkdir("parent/child", 0777)) + check(t, fs.Mkdir("parent/child/grandchild", 0777)) + // Verify they exist + info, err := fs.Stat("parent/child/grandchild") + check(t, err) + if !info.IsDir() { + t.Error("expected directory") + } }) t.Run("MultiBlockDirectory", func(t *testing.T) { @@ -337,3 +345,55 @@ func check(t *testing.T, err error) { t.Fatal(err) } } + +// TestDirectoryPersistence verifies that directories persist after unmount/remount. +// This is a regression test for the Sync() fix that ensures filesystem changes +// are flushed to the underlying block device. +func TestDirectoryPersistence(t *testing.T) { + bd := tinyfs.NewMemoryDevice(testPageSize, testBlockSize, testBlockCount) + fs := New(bd).Configure(defaultConfig) + + // Format and mount + if err := fs.Format(); err != nil { + t.Fatalf("Format failed: %v", err) + } + if err := fs.Mount(); err != nil { + t.Fatalf("Mount failed: %v", err) + } + + // Create directory + dirName := "persist_test" + if err := fs.Mkdir(dirName, 0755); err != nil { + t.Fatalf("Mkdir failed: %v", err) + } + + // Verify immediate existence + info, err := fs.Stat(dirName) + if err != nil { + t.Fatalf("Stat after mkdir failed: %v", err) + } + if !info.IsDir() { + t.Fatalf("Expected directory, got file") + } + + // Unmount + if err := fs.Unmount(); err != nil { + t.Fatalf("Unmount failed: %v", err) + } + + // Create new filesystem instance and remount (simulates reboot) + fs2 := New(bd).Configure(defaultConfig) + if err := fs2.Mount(); err != nil { + t.Fatalf("Remount failed: %v", err) + } + defer fs2.Unmount() + + // Verify directory persisted + info2, err := fs2.Stat(dirName) + if err != nil { + t.Fatalf("Directory '%s' lost after remount: %v", dirName, err) + } + if !info2.IsDir() { + t.Fatalf("'%s' exists but is not a directory", dirName) + } +} diff --git a/littlefs/lfs.c b/littlefs/lfs.c index 95c241e..8f6aff3 100644 --- a/littlefs/lfs.c +++ b/littlefs/lfs.c @@ -185,6 +185,10 @@ static int lfs_bd_flush(lfs_t *lfs, return err; } + if (pcache->block != LFS_BLOCK_NULL && pcache->block == rcache->block) { + lfs_cache_drop(lfs, rcache); + } + if (validate) { // check data on disk lfs_cache_drop(lfs, rcache);