Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit17f8e93

Browse files
chore: add agent endpoint for querying file system (#16736)
Closescoder/internal#382
1 parenteddccbc commit17f8e93

File tree

5 files changed

+395
-3
lines changed

5 files changed

+395
-3
lines changed

‎agent/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func (a *agent) apiHandler() http.Handler {
4141
r.Get("/api/v0/containers",ch.ServeHTTP)
4242
r.Get("/api/v0/listening-ports",lp.handler)
4343
r.Get("/api/v0/netcheck",a.HandleNetcheck)
44+
r.Post("/api/v0/list-directory",a.HandleLS)
4445
r.Get("/debug/logs",a.HandleHTTPDebugLogs)
4546
r.Get("/debug/magicsock",a.HandleHTTPDebugMagicsock)
4647
r.Get("/debug/magicsock/debug-logging/{state}",a.HandleHTTPMagicsockDebugLoggingState)

‎agent/ls.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package agent
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"os"
7+
"path/filepath"
8+
"regexp"
9+
"runtime"
10+
"strings"
11+
12+
"github.com/shirou/gopsutil/v4/disk"
13+
"golang.org/x/xerrors"
14+
15+
"github.com/coder/coder/v2/coderd/httpapi"
16+
"github.com/coder/coder/v2/codersdk"
17+
)
18+
19+
varWindowsDriveRegex=regexp.MustCompile(`^[a-zA-Z]:\\$`)
20+
21+
func (*agent)HandleLS(rw http.ResponseWriter,r*http.Request) {
22+
ctx:=r.Context()
23+
24+
varqueryLSRequest
25+
if!httpapi.Read(ctx,rw,r,&query) {
26+
return
27+
}
28+
29+
resp,err:=listFiles(query)
30+
iferr!=nil {
31+
status:=http.StatusInternalServerError
32+
switch {
33+
caseerrors.Is(err,os.ErrNotExist):
34+
status=http.StatusNotFound
35+
caseerrors.Is(err,os.ErrPermission):
36+
status=http.StatusForbidden
37+
default:
38+
}
39+
httpapi.Write(ctx,rw,status, codersdk.Response{
40+
Message:err.Error(),
41+
})
42+
return
43+
}
44+
45+
httpapi.Write(ctx,rw,http.StatusOK,resp)
46+
}
47+
48+
funclistFiles(queryLSRequest) (LSResponse,error) {
49+
varfullPath []string
50+
switchquery.Relativity {
51+
caseLSRelativityHome:
52+
home,err:=os.UserHomeDir()
53+
iferr!=nil {
54+
returnLSResponse{},xerrors.Errorf("failed to get user home directory: %w",err)
55+
}
56+
fullPath= []string{home}
57+
caseLSRelativityRoot:
58+
ifruntime.GOOS=="windows" {
59+
iflen(query.Path)==0 {
60+
returnlistDrives()
61+
}
62+
if!WindowsDriveRegex.MatchString(query.Path[0]) {
63+
returnLSResponse{},xerrors.Errorf("invalid drive letter %q",query.Path[0])
64+
}
65+
}else {
66+
fullPath= []string{"/"}
67+
}
68+
default:
69+
returnLSResponse{},xerrors.Errorf("unsupported relativity type %q",query.Relativity)
70+
}
71+
72+
fullPath=append(fullPath,query.Path...)
73+
fullPathRelative:=filepath.Join(fullPath...)
74+
absolutePathString,err:=filepath.Abs(fullPathRelative)
75+
iferr!=nil {
76+
returnLSResponse{},xerrors.Errorf("failed to get absolute path of %q: %w",fullPathRelative,err)
77+
}
78+
79+
f,err:=os.Open(absolutePathString)
80+
iferr!=nil {
81+
returnLSResponse{},xerrors.Errorf("failed to open directory %q: %w",absolutePathString,err)
82+
}
83+
deferf.Close()
84+
85+
stat,err:=f.Stat()
86+
iferr!=nil {
87+
returnLSResponse{},xerrors.Errorf("failed to stat directory %q: %w",absolutePathString,err)
88+
}
89+
90+
if!stat.IsDir() {
91+
returnLSResponse{},xerrors.Errorf("path %q is not a directory",absolutePathString)
92+
}
93+
94+
// `contents` may be partially populated even if the operation fails midway.
95+
contents,_:=f.ReadDir(-1)
96+
respContents:=make([]LSFile,0,len(contents))
97+
for_,file:=rangecontents {
98+
respContents=append(respContents,LSFile{
99+
Name:file.Name(),
100+
AbsolutePathString:filepath.Join(absolutePathString,file.Name()),
101+
IsDir:file.IsDir(),
102+
})
103+
}
104+
105+
absolutePath:=pathToArray(absolutePathString)
106+
107+
returnLSResponse{
108+
AbsolutePath:absolutePath,
109+
AbsolutePathString:absolutePathString,
110+
Contents:respContents,
111+
},nil
112+
}
113+
114+
funclistDrives() (LSResponse,error) {
115+
partitionStats,err:=disk.Partitions(true)
116+
iferr!=nil {
117+
returnLSResponse{},xerrors.Errorf("failed to get partitions: %w",err)
118+
}
119+
contents:=make([]LSFile,0,len(partitionStats))
120+
for_,a:=rangepartitionStats {
121+
// Drive letters on Windows have a trailing separator as part of their name.
122+
// i.e. `os.Open("C:")` does not work, but `os.Open("C:\\")` does.
123+
name:=a.Mountpoint+string(os.PathSeparator)
124+
contents=append(contents,LSFile{
125+
Name:name,
126+
AbsolutePathString:name,
127+
IsDir:true,
128+
})
129+
}
130+
131+
returnLSResponse{
132+
AbsolutePath: []string{},
133+
AbsolutePathString:"",
134+
Contents:contents,
135+
},nil
136+
}
137+
138+
funcpathToArray(pathstring) []string {
139+
out:=strings.FieldsFunc(path,func(rrune)bool {
140+
returnr==os.PathSeparator
141+
})
142+
// Drive letters on Windows have a trailing separator as part of their name.
143+
// i.e. `os.Open("C:")` does not work, but `os.Open("C:\\")` does.
144+
ifruntime.GOOS=="windows"&&len(out)>0 {
145+
out[0]+=string(os.PathSeparator)
146+
}
147+
returnout
148+
}
149+
150+
typeLSRequeststruct {
151+
// e.g. [], ["repos", "coder"],
152+
Path []string`json:"path"`
153+
// Whether the supplied path is relative to the user's home directory,
154+
// or the root directory.
155+
RelativityLSRelativity`json:"relativity"`
156+
}
157+
158+
typeLSResponsestruct {
159+
AbsolutePath []string`json:"absolute_path"`
160+
// Returned so clients can display the full path to the user, and
161+
// copy it to configure file sync
162+
// e.g. Windows: "C:\\Users\\coder"
163+
// Linux: "/home/coder"
164+
AbsolutePathStringstring`json:"absolute_path_string"`
165+
Contents []LSFile`json:"contents"`
166+
}
167+
168+
typeLSFilestruct {
169+
Namestring`json:"name"`
170+
// e.g. "C:\\Users\\coder\\hello.txt"
171+
// "/home/coder/hello.txt"
172+
AbsolutePathStringstring`json:"absolute_path_string"`
173+
IsDirbool`json:"is_dir"`
174+
}
175+
176+
typeLSRelativitystring
177+
178+
const (
179+
LSRelativityRootLSRelativity="root"
180+
LSRelativityHomeLSRelativity="home"
181+
)

‎agent/ls_internal_test.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package agent
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"runtime"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
funcTestListFilesNonExistentDirectory(t*testing.T) {
13+
t.Parallel()
14+
15+
query:=LSRequest{
16+
Path: []string{"idontexist"},
17+
Relativity:LSRelativityHome,
18+
}
19+
_,err:=listFiles(query)
20+
require.ErrorIs(t,err,os.ErrNotExist)
21+
}
22+
23+
funcTestListFilesPermissionDenied(t*testing.T) {
24+
t.Parallel()
25+
26+
ifruntime.GOOS=="windows" {
27+
t.Skip("creating an unreadable-by-user directory is non-trivial on Windows")
28+
}
29+
30+
home,err:=os.UserHomeDir()
31+
require.NoError(t,err)
32+
33+
tmpDir:=t.TempDir()
34+
35+
reposDir:=filepath.Join(tmpDir,"repos")
36+
err=os.Mkdir(reposDir,0o000)
37+
require.NoError(t,err)
38+
39+
rel,err:=filepath.Rel(home,reposDir)
40+
require.NoError(t,err)
41+
42+
query:=LSRequest{
43+
Path:pathToArray(rel),
44+
Relativity:LSRelativityHome,
45+
}
46+
_,err=listFiles(query)
47+
require.ErrorIs(t,err,os.ErrPermission)
48+
}
49+
50+
funcTestListFilesNotADirectory(t*testing.T) {
51+
t.Parallel()
52+
53+
home,err:=os.UserHomeDir()
54+
require.NoError(t,err)
55+
56+
tmpDir:=t.TempDir()
57+
58+
filePath:=filepath.Join(tmpDir,"file.txt")
59+
err=os.WriteFile(filePath, []byte("content"),0o600)
60+
require.NoError(t,err)
61+
62+
rel,err:=filepath.Rel(home,filePath)
63+
require.NoError(t,err)
64+
65+
query:=LSRequest{
66+
Path:pathToArray(rel),
67+
Relativity:LSRelativityHome,
68+
}
69+
_,err=listFiles(query)
70+
require.ErrorContains(t,err,"is not a directory")
71+
}
72+
73+
funcTestListFilesSuccess(t*testing.T) {
74+
t.Parallel()
75+
76+
tc:= []struct {
77+
namestring
78+
baseFuncfunc(t*testing.T)string
79+
relativityLSRelativity
80+
}{
81+
{
82+
name:"home",
83+
baseFunc:func(t*testing.T)string {
84+
home,err:=os.UserHomeDir()
85+
require.NoError(t,err)
86+
returnhome
87+
},
88+
relativity:LSRelativityHome,
89+
},
90+
{
91+
name:"root",
92+
baseFunc:func(*testing.T)string {
93+
ifruntime.GOOS=="windows" {
94+
return""
95+
}
96+
return"/"
97+
},
98+
relativity:LSRelativityRoot,
99+
},
100+
}
101+
102+
// nolint:paralleltest // Not since Go v1.22.
103+
for_,tc:=rangetc {
104+
t.Run(tc.name,func(t*testing.T) {
105+
t.Parallel()
106+
107+
base:=tc.baseFunc(t)
108+
tmpDir:=t.TempDir()
109+
110+
reposDir:=filepath.Join(tmpDir,"repos")
111+
err:=os.Mkdir(reposDir,0o755)
112+
require.NoError(t,err)
113+
114+
downloadsDir:=filepath.Join(tmpDir,"Downloads")
115+
err=os.Mkdir(downloadsDir,0o755)
116+
require.NoError(t,err)
117+
118+
textFile:=filepath.Join(tmpDir,"file.txt")
119+
err=os.WriteFile(textFile, []byte("content"),0o600)
120+
require.NoError(t,err)
121+
122+
varqueryComponents []string
123+
// We can't get an absolute path relative to empty string on Windows.
124+
ifruntime.GOOS=="windows"&&base=="" {
125+
queryComponents=pathToArray(tmpDir)
126+
}else {
127+
rel,err:=filepath.Rel(base,tmpDir)
128+
require.NoError(t,err)
129+
queryComponents=pathToArray(rel)
130+
}
131+
132+
query:=LSRequest{
133+
Path:queryComponents,
134+
Relativity:tc.relativity,
135+
}
136+
resp,err:=listFiles(query)
137+
require.NoError(t,err)
138+
139+
require.Equal(t,tmpDir,resp.AbsolutePathString)
140+
require.ElementsMatch(t, []LSFile{
141+
{
142+
Name:"repos",
143+
AbsolutePathString:reposDir,
144+
IsDir:true,
145+
},
146+
{
147+
Name:"Downloads",
148+
AbsolutePathString:downloadsDir,
149+
IsDir:true,
150+
},
151+
{
152+
Name:"file.txt",
153+
AbsolutePathString:textFile,
154+
IsDir:false,
155+
},
156+
},resp.Contents)
157+
})
158+
}
159+
}
160+
161+
funcTestListFilesListDrives(t*testing.T) {
162+
t.Parallel()
163+
164+
ifruntime.GOOS!="windows" {
165+
t.Skip("skipping test on non-Windows OS")
166+
}
167+
168+
query:=LSRequest{
169+
Path: []string{},
170+
Relativity:LSRelativityRoot,
171+
}
172+
resp,err:=listFiles(query)
173+
require.NoError(t,err)
174+
require.Contains(t,resp.Contents,LSFile{
175+
Name:"C:\\",
176+
AbsolutePathString:"C:\\",
177+
IsDir:true,
178+
})
179+
180+
query=LSRequest{
181+
Path: []string{"C:\\"},
182+
Relativity:LSRelativityRoot,
183+
}
184+
resp,err=listFiles(query)
185+
require.NoError(t,err)
186+
187+
query=LSRequest{
188+
Path:resp.AbsolutePath,
189+
Relativity:LSRelativityRoot,
190+
}
191+
resp,err=listFiles(query)
192+
require.NoError(t,err)
193+
// System directory should always exist
194+
require.Contains(t,resp.Contents,LSFile{
195+
Name:"Windows",
196+
AbsolutePathString:"C:\\Windows",
197+
IsDir:true,
198+
})
199+
200+
query=LSRequest{
201+
// Network drives are not supported.
202+
Path: []string{"\\sshfs\\work"},
203+
Relativity:LSRelativityRoot,
204+
}
205+
resp,err=listFiles(query)
206+
require.ErrorContains(t,err,"drive")
207+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp