Skip to content

Commit 85e81ac

Browse files
committed
feat(daemon,sdk): add depth-aware recursive file listing
Signed-off-by: MDzaja <mirkodzaja0@gmail.com>
1 parent 760db6b commit 85e81ac

37 files changed

Lines changed: 533 additions & 92 deletions

File tree

apps/daemon/pkg/toolbox/docs/docs.go

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/daemon/pkg/toolbox/docs/swagger.json

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/daemon/pkg/toolbox/docs/swagger.yaml

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/daemon/pkg/toolbox/fs/list_files.go

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,138 @@
44
package fs
55

66
import (
7+
"errors"
8+
iofs "io/fs"
79
"net/http"
810
"os"
911
"path/filepath"
12+
"strconv"
13+
"strings"
1014

15+
"github.com/daytonaio/daemon/internal/util"
1116
"github.com/gin-gonic/gin"
1217
)
1318

1419
// ListFiles godoc
1520
//
1621
// @Summary List files and directories
17-
// @Description List files and directories in the specified path
22+
// @Description List files and directories in the specified path. Use the optional depth
23+
// @Description parameter to list recursively: depth=1 (default) lists the directory's
24+
// @Description entries, depth=2 also includes their children, and so on.
1825
// @Tags file-system
1926
// @Produce json
2027
// @Param path query string false "Directory path to list (defaults to working directory)"
28+
// @Param depth query int false "How many levels deep to list (default: 1, must be >= 1)"
2129
// @Success 200 {array} FileInfo
2230
// @Router /files [get]
2331
//
2432
// @id ListFiles
2533
func ListFiles(c *gin.Context) {
26-
path := c.Query("path")
27-
if path == "" {
28-
path = "."
34+
root := c.Query("path")
35+
if root == "" {
36+
root = "."
2937
}
3038

31-
files, err := os.ReadDir(path)
32-
if err != nil {
33-
if os.IsNotExist(err) {
34-
c.AbortWithError(http.StatusNotFound, err)
39+
depth := 1
40+
if depthStr := c.Query("depth"); depthStr != "" {
41+
parsed, err := strconv.Atoi(depthStr)
42+
if err != nil || parsed < 1 {
43+
c.AbortWithError(http.StatusBadRequest, errors.New("depth must be an integer >= 1"))
3544
return
3645
}
37-
if os.IsPermission(err) {
38-
c.AbortWithError(http.StatusForbidden, err)
39-
return
40-
}
41-
c.AbortWithError(http.StatusBadRequest, err)
46+
depth = parsed
47+
}
48+
49+
stripPath := util.ClientRejectsUnknownResponseFields(c.Request.Header)
50+
51+
// depth=1 uses the original os.ReadDir code path to avoid behavioural
52+
// regressions for the default (most common) case.
53+
if depth == 1 {
54+
listFilesShallow(c, root, stripPath)
4255
return
4356
}
57+
listFilesRecursive(c, root, depth, stripPath)
58+
}
4459

45-
var fileInfos = make([]FileInfo, 0)
60+
func listFilesShallow(c *gin.Context, path string, stripPath bool) {
61+
files, err := os.ReadDir(path)
62+
if err != nil {
63+
abortWithFsError(c, err)
64+
return
65+
}
66+
67+
fileInfos := make([]FileInfo, 0)
4668
for _, file := range files {
47-
info, err := getFileInfo(filepath.Join(path, file.Name()))
69+
fullPath := filepath.Join(path, file.Name())
70+
info, err := getFileInfo(fullPath)
4871
if err != nil {
4972
continue
5073
}
74+
if !stripPath {
75+
info.Path = fullPath
76+
}
5177
fileInfos = append(fileInfos, info)
5278
}
5379

5480
c.JSON(http.StatusOK, fileInfos)
5581
}
82+
83+
// listFilesRecursive returns a flat listing up to depth levels below root;
84+
// unreadable subtrees are skipped and symlinks are not followed.
85+
func listFilesRecursive(c *gin.Context, root string, depth int, stripPath bool) {
86+
if _, err := os.ReadDir(root); err != nil {
87+
abortWithFsError(c, err)
88+
return
89+
}
90+
91+
fileInfos := make([]FileInfo, 0)
92+
_ = filepath.WalkDir(root, func(entryPath string, d iofs.DirEntry, err error) error {
93+
if err != nil {
94+
if d != nil && d.IsDir() {
95+
return filepath.SkipDir
96+
}
97+
return nil
98+
}
99+
if entryPath == root {
100+
return nil
101+
}
102+
103+
rel, relErr := filepath.Rel(root, entryPath)
104+
if relErr != nil {
105+
return nil
106+
}
107+
entryDepth := strings.Count(rel, string(os.PathSeparator)) + 1
108+
if entryDepth > depth {
109+
if d.IsDir() {
110+
return filepath.SkipDir
111+
}
112+
return nil
113+
}
114+
115+
if info, infoErr := getFileInfo(entryPath); infoErr == nil {
116+
if !stripPath {
117+
info.Path = entryPath
118+
}
119+
fileInfos = append(fileInfos, info)
120+
}
121+
122+
if d.IsDir() && entryDepth >= depth {
123+
return filepath.SkipDir
124+
}
125+
return nil
126+
})
127+
128+
c.JSON(http.StatusOK, fileInfos)
129+
}
130+
131+
func abortWithFsError(c *gin.Context, err error) {
132+
if os.IsNotExist(err) {
133+
c.AbortWithError(http.StatusNotFound, err)
134+
return
135+
}
136+
if os.IsPermission(err) {
137+
c.AbortWithError(http.StatusForbidden, err)
138+
return
139+
}
140+
c.AbortWithError(http.StatusBadRequest, err)
141+
}

apps/daemon/pkg/toolbox/fs/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import "time"
77

88
type FileInfo struct {
99
Name string `json:"name" validate:"required"`
10+
// Full path of the entry
11+
Path string `json:"path,omitempty"`
1012
Size int64 `json:"size" validate:"required"`
1113
Mode string `json:"mode" validate:"required"`
1214
// Deprecated: ModTime uses Go's time.String() layout which is not a standard format.

apps/docs/src/content/docs/en/go-sdk/daytona.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ result, err := sandbox.Process.ExecuteCommand(ctx, "ls -la")
149149
- [func \(f \*FileSystemService\) DownloadFileStream\(ctx context.Context, remotePath string, opts ...DownloadStreamOption\) \(io.ReadCloser, error\)](<#FileSystemService.DownloadFileStream>)
150150
- [func \(f \*FileSystemService\) FindFiles\(ctx context.Context, path, pattern string\) \(any, error\)](<#FileSystemService.FindFiles>)
151151
- [func \(f \*FileSystemService\) GetFileInfo\(ctx context.Context, path string\) \(\*types.FileInfo, error\)](<#FileSystemService.GetFileInfo>)
152-
- [func \(f \*FileSystemService\) ListFiles\(ctx context.Context, path string\) \(\[\]\*types.FileInfo, error\)](<#FileSystemService.ListFiles>)
152+
- [func \(f \*FileSystemService\) ListFiles\(ctx context.Context, path string, opts ...func\(\*options.ListFiles\)\) \(\[\]\*types.FileInfo, error\)](<#FileSystemService.ListFiles>)
153153
- [func \(f \*FileSystemService\) MoveFiles\(ctx context.Context, source, destination string\) error](<#FileSystemService.MoveFiles>)
154154
- [func \(f \*FileSystemService\) ReplaceInFiles\(ctx context.Context, files \[\]string, pattern, newValue string\) \(any, error\)](<#FileSystemService.ReplaceInFiles>)
155155
- [func \(f \*FileSystemService\) SearchFiles\(ctx context.Context, path, pattern string\) \(any, error\)](<#FileSystemService.SearchFiles>)
@@ -1737,7 +1737,7 @@ Returns an error if the path doesn't exist.
17371737
### func \(\*FileSystemService\) ListFiles
17381738

17391739
```go
1740-
func (f *FileSystemService) ListFiles(ctx context.Context, path string) ([]*types.FileInfo, error)
1740+
func (f *FileSystemService) ListFiles(ctx context.Context, path string, opts ...func(*options.ListFiles)) ([]*types.FileInfo, error)
17411741
```
17421742

17431743
ListFiles lists files and directories in the specified path.

apps/docs/src/content/docs/en/go-sdk/options.mdx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ opts := options.Apply(
5252
- [func WithCreatePtySize\(ptySize types.PtySize\) func\(\*CreatePty\)](<#WithCreatePtySize>)
5353
- [func WithCustomContext\(contextID string\) func\(\*RunCode\)](<#WithCustomContext>)
5454
- [func WithCwd\(cwd string\) func\(\*ExecuteCommand\)](<#WithCwd>)
55+
- [func WithDepth\(depth int32\) func\(\*ListFiles\)](<#WithDepth>)
5556
- [func WithEnv\(env map\[string\]string\) func\(\*RunCode\)](<#WithEnv>)
5657
- [func WithExecuteTimeout\(timeout time.Duration\) func\(\*ExecuteCommand\)](<#WithExecuteTimeout>)
5758
- [func WithExtraIndexURLs\(urls ...string\) func\(\*PipInstall\)](<#WithExtraIndexURLs>)
@@ -87,6 +88,7 @@ opts := options.Apply(
8788
- [type GitDeleteBranch](<#GitDeleteBranch>)
8889
- [type GitPull](<#GitPull>)
8990
- [type GitPush](<#GitPush>)
91+
- [type ListFiles](<#ListFiles>)
9092
- [type PipInstall](<#PipInstall>)
9193
- [type PtySession](<#PtySession>)
9294
- [type RunCode](<#RunCode>)
@@ -289,6 +291,23 @@ result, err := sandbox.Process.ExecuteCommand(ctx, "ls -la",
289291
)
290292
```
291293

294+
<a name="WithDepth"></a>
295+
## func WithDepth
296+
297+
```go
298+
func WithDepth(depth int32) func(*ListFiles)
299+
```
300+
301+
WithDepth sets how many levels deep to list. Depth 1 \(the default\) lists the directory's entries, depth 2 also includes their children, and so on.
302+
303+
Example:
304+
305+
```
306+
files, err := sandbox.FileSystem.ListFiles(ctx, "/home/user",
307+
options.WithDepth(3),
308+
)
309+
```
310+
292311
<a name="WithEnv"></a>
293312
## func WithEnv
294313

@@ -891,6 +910,17 @@ type GitPush struct {
891910
}
892911
```
893912

913+
<a name="ListFiles"></a>
914+
## type ListFiles
915+
916+
ListFiles holds optional parameters for \[daytona.FileSystemService.ListFiles\].
917+
918+
```go
919+
type ListFiles struct {
920+
Depth *int32 // How many levels deep to list (default: 1, must be >= 1)
921+
}
922+
```
923+
894924
<a name="PipInstall"></a>
895925
## type PipInstall
896926

apps/docs/src/content/docs/en/go-sdk/types.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ FileInfo represents file metadata
231231
```go
232232
type FileInfo struct {
233233
Name string
234+
Path string
234235
Size int64
235236
Mode string
236237
ModifiedTime time.Time

apps/docs/src/content/docs/en/java-sdk/file-system.mdx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,26 @@ Lists files and directories under a path.
194194

195195
- `io.daytona.sdk.exception.DaytonaException` - if listing fails
196196

197+
#### listFiles()
198+
```java
199+
public List<FileInfo> listFiles(String path, Integer depth)
200+
```
201+
202+
Lists files and directories under a path, optionally recursing into subdirectories.
203+
204+
**Parameters**:
205+
206+
- `path` _String_ - directory path
207+
- `depth` _Integer_ - how many levels deep to list: depth=1 (default) lists the directory's entries, depth=2 also includes their children, and so on; must be >= 1. Each returned entry carries a full path field.
208+
209+
**Returns**:
210+
211+
- `List\<FileInfo\>` - file metadata entries
212+
213+
**Throws**:
214+
215+
- `io.daytona.sdk.exception.DaytonaException` - if listing fails
216+
197217
#### getFileDetails()
198218
```java
199219
public FileInfo getFileDetails(String path)

0 commit comments

Comments
 (0)