From f3d570bc0267ddef743e977bce6096c7a846c24d Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Wed, 31 May 2023 13:54:43 +0200 Subject: [PATCH] Make filer.Filer return fs.DirEntry from ReadDir This allows for compatibility with the stdlib functions in io/fs. --- internal/filer_test.go | 25 ++++++++---- libs/filer/dbfs_client.go | 59 +++++++++++++++++++++++---- libs/filer/filer.go | 20 +-------- libs/filer/workspace_files_client.go | 61 ++++++++++++++++++++++++---- 4 files changed, 123 insertions(+), 42 deletions(-) diff --git a/internal/filer_test.go b/internal/filer_test.go index 6244a5c474..29d08b3eb5 100644 --- a/internal/filer_test.go +++ b/internal/filer_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "io/fs" "net/http" "strings" "testing" @@ -75,6 +76,7 @@ func runFilerReadWriteTest(t *testing.T, ctx context.Context, f filer.Filer) { func runFilerReadDirTest(t *testing.T, ctx context.Context, f filer.Filer) { var err error + var info fs.FileInfo // We start with an empty directory. entries, err := f.ReadDir(ctx, ".") @@ -105,23 +107,32 @@ func runFilerReadDirTest(t *testing.T, ctx context.Context, f filer.Filer) { entries, err = f.ReadDir(ctx, ".") require.NoError(t, err) assert.Len(t, entries, 2) - assert.Equal(t, "dir", entries[0].Name) - assert.Equal(t, "hello.txt", entries[1].Name) - assert.Greater(t, entries[1].ModTime.Unix(), int64(0)) + assert.Equal(t, "dir", entries[0].Name()) + assert.True(t, entries[0].IsDir()) + assert.Equal(t, "hello.txt", entries[1].Name()) + assert.False(t, entries[1].IsDir()) + info, err = entries[1].Info() + require.NoError(t, err) + assert.Greater(t, info.ModTime().Unix(), int64(0)) // Expect two entries in the directory. entries, err = f.ReadDir(ctx, "/dir") require.NoError(t, err) assert.Len(t, entries, 2) - assert.Equal(t, "a", entries[0].Name) - assert.Equal(t, "world.txt", entries[1].Name) - assert.Greater(t, entries[1].ModTime.Unix(), int64(0)) + assert.Equal(t, "a", entries[0].Name()) + assert.True(t, entries[0].IsDir()) + assert.Equal(t, "world.txt", entries[1].Name()) + assert.False(t, entries[1].IsDir()) + info, err = entries[1].Info() + require.NoError(t, err) + assert.Greater(t, info.ModTime().Unix(), int64(0)) // Expect a single entry in the nested path. entries, err = f.ReadDir(ctx, "/dir/a/b") require.NoError(t, err) assert.Len(t, entries, 1) - assert.Equal(t, "c", entries[0].Name) + assert.Equal(t, "c", entries[0].Name()) + assert.True(t, entries[0].IsDir()) } func temporaryWorkspaceDir(t *testing.T, w *databricks.WorkspaceClient) string { diff --git a/libs/filer/dbfs_client.go b/libs/filer/dbfs_client.go index 2e32421074..7a3084ac65 100644 --- a/libs/filer/dbfs_client.go +++ b/libs/filer/dbfs_client.go @@ -4,6 +4,7 @@ import ( "context" "errors" "io" + "io/fs" "net/http" "path" "sort" @@ -15,6 +16,52 @@ import ( "golang.org/x/exp/slices" ) +// Type that implements fs.DirEntry for DBFS. +type dbfsDirEntry struct { + dbfsFileInfo +} + +func (entry dbfsDirEntry) Type() fs.FileMode { + typ := fs.ModePerm + if entry.fi.IsDir { + typ |= fs.ModeDir + } + return typ +} + +func (entry dbfsDirEntry) Info() (fs.FileInfo, error) { + return entry.dbfsFileInfo, nil +} + +// Type that implements fs.FileInfo for DBFS. +type dbfsFileInfo struct { + fi files.FileInfo +} + +func (info dbfsFileInfo) Name() string { + return path.Base(info.fi.Path) +} + +func (info dbfsFileInfo) Size() int64 { + return info.fi.FileSize +} + +func (info dbfsFileInfo) Mode() fs.FileMode { + return fs.ModePerm +} + +func (info dbfsFileInfo) ModTime() time.Time { + return time.UnixMilli(info.fi.ModificationTime) +} + +func (info dbfsFileInfo) IsDir() bool { + return info.fi.IsDir +} + +func (info dbfsFileInfo) Sys() any { + return nil +} + // DbfsClient implements the [Filer] interface for the DBFS backend. type DbfsClient struct { workspaceClient *databricks.WorkspaceClient @@ -152,7 +199,7 @@ func (w *DbfsClient) Delete(ctx context.Context, name string) error { }) } -func (w *DbfsClient) ReadDir(ctx context.Context, name string) ([]FileInfo, error) { +func (w *DbfsClient) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) { absPath, err := w.root.Join(name) if err != nil { return nil, err @@ -175,17 +222,13 @@ func (w *DbfsClient) ReadDir(ctx context.Context, name string) ([]FileInfo, erro return nil, err } - info := make([]FileInfo, len(res.Files)) + info := make([]fs.DirEntry, len(res.Files)) for i, v := range res.Files { - info[i] = FileInfo{ - Name: path.Base(v.Path), - Size: v.FileSize, - ModTime: time.UnixMilli(v.ModificationTime), - } + info[i] = dbfsDirEntry{dbfsFileInfo: dbfsFileInfo{fi: v}} } // Sort by name for parity with os.ReadDir. - sort.Slice(info, func(i, j int) bool { return info[i].Name < info[j].Name }) + sort.Slice(info, func(i, j int) bool { return info[i].Name() < info[j].Name() }) return info, nil } diff --git a/libs/filer/filer.go b/libs/filer/filer.go index 88de7e466a..61412b97e8 100644 --- a/libs/filer/filer.go +++ b/libs/filer/filer.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "io" - "time" + "io/fs" ) type WriteMode int @@ -14,22 +14,6 @@ const ( CreateParentDirectories = iota << 1 ) -// FileInfo abstracts over file information from different file systems. -// Inspired by https://pkg.go.dev/io/fs#FileInfo. -type FileInfo struct { - // The type of the file in workspace. - Type string - - // Base name. - Name string - - // Size in bytes. - Size int64 - - // Modification time. - ModTime time.Time -} - type FileAlreadyExistsError struct { path string } @@ -68,7 +52,7 @@ type Filer interface { Delete(ctx context.Context, path string) error // Return contents of directory at `path`. - ReadDir(ctx context.Context, path string) ([]FileInfo, error) + ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error) // Creates directory at `path`, creating any intermediate directories as required. Mkdir(ctx context.Context, path string) error diff --git a/libs/filer/workspace_files_client.go b/libs/filer/workspace_files_client.go index df2c0bdbb9..b9f0f3db3c 100644 --- a/libs/filer/workspace_files_client.go +++ b/libs/filer/workspace_files_client.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "io/fs" "net/http" "net/url" "path" @@ -20,6 +21,53 @@ import ( "golang.org/x/exp/slices" ) +// Type that implements fs.DirEntry for WSFS. +type wsfsDirEntry struct { + wsfsFileInfo +} + +func (entry wsfsDirEntry) Type() fs.FileMode { + return entry.wsfsFileInfo.Mode() +} + +func (entry wsfsDirEntry) Info() (fs.FileInfo, error) { + return entry.wsfsFileInfo, nil +} + +// Type that implements fs.FileInfo for WSFS. +type wsfsFileInfo struct { + oi workspace.ObjectInfo +} + +func (info wsfsFileInfo) Name() string { + return path.Base(info.oi.Path) +} + +func (info wsfsFileInfo) Size() int64 { + return info.oi.Size +} + +func (info wsfsFileInfo) Mode() fs.FileMode { + switch info.oi.ObjectType { + case workspace.ObjectTypeDirectory: + return fs.ModeDir + default: + return fs.ModePerm + } +} + +func (info wsfsFileInfo) ModTime() time.Time { + return time.UnixMilli(info.oi.ModifiedAt) +} + +func (info wsfsFileInfo) IsDir() bool { + return info.oi.ObjectType == workspace.ObjectTypeDirectory +} + +func (info wsfsFileInfo) Sys() any { + return nil +} + // WorkspaceFilesClient implements the files-in-workspace API. // NOTE: This API is available for files under /Repos if a workspace has files-in-repos enabled. @@ -165,7 +213,7 @@ func (w *WorkspaceFilesClient) Delete(ctx context.Context, name string) error { return err } -func (w *WorkspaceFilesClient) ReadDir(ctx context.Context, name string) ([]FileInfo, error) { +func (w *WorkspaceFilesClient) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) { absPath, err := w.root.Join(name) if err != nil { return nil, err @@ -189,18 +237,13 @@ func (w *WorkspaceFilesClient) ReadDir(ctx context.Context, name string) ([]File return nil, err } - info := make([]FileInfo, len(objects)) + info := make([]fs.DirEntry, len(objects)) for i, v := range objects { - info[i] = FileInfo{ - Type: string(v.ObjectType), - Name: path.Base(v.Path), - Size: v.Size, - ModTime: time.UnixMilli(v.ModifiedAt), - } + info[i] = wsfsDirEntry{wsfsFileInfo{oi: v}} } // Sort by name for parity with os.ReadDir. - sort.Slice(info, func(i, j int) bool { return info[i].Name < info[j].Name }) + sort.Slice(info, func(i, j int) bool { return info[i].Name() < info[j].Name() }) return info, nil }