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

Commit163a149

Browse files
committed
chore: add agent endpoint for querying file system
1 parent7e33902 commit163a149

File tree

3 files changed

+309
-0
lines changed

3 files changed

+309
-0
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/ls",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: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package agent
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"os"
7+
"path/filepath"
8+
"runtime"
9+
10+
"golang.org/x/xerrors"
11+
12+
"github.com/coder/coder/v2/coderd/httpapi"
13+
"github.com/coder/coder/v2/codersdk"
14+
)
15+
16+
func (*agent)HandleLS(rw http.ResponseWriter,r*http.Request) {
17+
ctx:=r.Context()
18+
19+
varqueryLSQuery
20+
if!httpapi.Read(ctx,rw,r,&query) {
21+
return
22+
}
23+
24+
resp,err:=listFiles(query)
25+
iferr!=nil {
26+
switch {
27+
caseerrors.Is(err,os.ErrNotExist):
28+
httpapi.Write(ctx,rw,http.StatusNotFound, codersdk.Response{
29+
Message:"Directory does not exist",
30+
})
31+
caseerrors.Is(err,os.ErrPermission):
32+
httpapi.Write(ctx,rw,http.StatusForbidden, codersdk.Response{
33+
Message:"Permission denied",
34+
})
35+
default:
36+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
37+
Message:err.Error(),
38+
})
39+
}
40+
return
41+
}
42+
43+
httpapi.Write(ctx,rw,http.StatusOK,resp)
44+
}
45+
46+
funclistFiles(queryLSQuery) (LSResponse,error) {
47+
varbasestring
48+
switchquery.Relativity {
49+
caseLSRelativityHome:
50+
home,err:=os.UserHomeDir()
51+
iferr!=nil {
52+
returnLSResponse{},xerrors.Errorf("failed to get user home directory: %w",err)
53+
}
54+
base=home
55+
caseLSRelativityRoot:
56+
ifruntime.GOOS=="windows" {
57+
// TODO: Eventually, we could have a empty path with a root base
58+
// return all drives.
59+
// C drive should be good enough for now.
60+
base="C:\\"
61+
}else {
62+
base="/"
63+
}
64+
default:
65+
returnLSResponse{},xerrors.Errorf("unsupported relativity type %q",query.Relativity)
66+
}
67+
68+
fullPath:=append([]string{base},query.Path...)
69+
absolutePathString,err:=filepath.Abs(filepath.Join(fullPath...))
70+
iferr!=nil {
71+
returnLSResponse{},xerrors.Errorf("failed to get absolute path: %w",err)
72+
}
73+
74+
f,err:=os.Open(absolutePathString)
75+
iferr!=nil {
76+
returnLSResponse{},xerrors.Errorf("failed to open directory: %w",err)
77+
}
78+
deferf.Close()
79+
80+
stat,err:=f.Stat()
81+
iferr!=nil {
82+
returnLSResponse{},xerrors.Errorf("failed to stat directory: %w",err)
83+
}
84+
85+
if!stat.IsDir() {
86+
returnLSResponse{},xerrors.New("path is not a directory")
87+
}
88+
89+
// `contents` may be partially populated even if the operation fails midway.
90+
contents,_:=f.Readdir(-1)
91+
respContents:=make([]LSFile,0,len(contents))
92+
for_,file:=rangecontents {
93+
respContents=append(respContents,LSFile{
94+
Name:file.Name(),
95+
AbsolutePathString:filepath.Join(absolutePathString,file.Name()),
96+
IsDir:file.IsDir(),
97+
})
98+
}
99+
100+
returnLSResponse{
101+
AbsolutePathString:absolutePathString,
102+
Contents:respContents,
103+
},nil
104+
}
105+
106+
typeLSQuerystruct {
107+
// e.g. [], ["repos", "coder"],
108+
Path []string`json:"path"`
109+
// Whether the supplied path is relative to the user's home directory,
110+
// or the root directory.
111+
RelativityLSRelativity`json:"relativity"`
112+
}
113+
114+
typeLSResponsestruct {
115+
// Returned so clients can display the full path to the user, and
116+
// copy it to configure file sync
117+
// e.g. Windows: "C:\\Users\\coder"
118+
// Linux: "/home/coder"
119+
AbsolutePathStringstring`json:"absolute_path_string"`
120+
Contents []LSFile`json:"contents"`
121+
}
122+
123+
typeLSFilestruct {
124+
Namestring`json:"name"`
125+
// e.g. "C:\\Users\\coder\\hello.txt"
126+
// "/home/coder/hello.txt"
127+
AbsolutePathStringstring`json:"absolute_path_string"`
128+
IsDirbool`json:"is_dir"`
129+
}
130+
131+
typeLSRelativitystring
132+
133+
const (
134+
LSRelativityRootLSRelativity="root"
135+
LSRelativityHomeLSRelativity="home"
136+
)

‎agent/ls_internal_test.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package agent
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"runtime"
7+
"strings"
8+
"testing"
9+
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
funcTestListFilesNonExistentDirectory(t*testing.T) {
14+
t.Parallel()
15+
16+
query:=LSQuery{
17+
Path: []string{"idontexist"},
18+
Relativity:LSRelativityHome,
19+
}
20+
_,err:=listFiles(query)
21+
require.ErrorIs(t,err,os.ErrNotExist)
22+
}
23+
24+
funcTestListFilesPermissionDenied(t*testing.T) {
25+
t.Parallel()
26+
27+
ifruntime.GOOS=="windows" {
28+
t.Skip("creating an unreadable-by-user directory is non-trivial on Windows")
29+
}
30+
31+
home,err:=os.UserHomeDir()
32+
require.NoError(t,err)
33+
34+
tmpDir:=t.TempDir()
35+
36+
reposDir:=filepath.Join(tmpDir,"repos")
37+
err=os.Mkdir(reposDir,0o000)
38+
require.NoError(t,err)
39+
40+
rel,err:=filepath.Rel(home,reposDir)
41+
require.NoError(t,err)
42+
43+
query:=LSQuery{
44+
Path:pathToArray(rel),
45+
Relativity:LSRelativityHome,
46+
}
47+
_,err=listFiles(query)
48+
require.ErrorIs(t,err,os.ErrPermission)
49+
}
50+
51+
funcTestListFilesNotADirectory(t*testing.T) {
52+
t.Parallel()
53+
54+
home,err:=os.UserHomeDir()
55+
require.NoError(t,err)
56+
57+
tmpDir:=t.TempDir()
58+
59+
filePath:=filepath.Join(tmpDir,"file.txt")
60+
err=os.WriteFile(filePath, []byte("content"),0o600)
61+
require.NoError(t,err)
62+
63+
rel,err:=filepath.Rel(home,filePath)
64+
require.NoError(t,err)
65+
66+
query:=LSQuery{
67+
Path:pathToArray(rel),
68+
Relativity:LSRelativityHome,
69+
}
70+
_,err=listFiles(query)
71+
require.ErrorContains(t,err,"path is not a directory")
72+
}
73+
74+
funcTestListFilesSuccess(t*testing.T) {
75+
t.Parallel()
76+
77+
tc:= []struct {
78+
namestring
79+
baseFuncfunc(t*testing.T)string
80+
relativityLSRelativity
81+
}{
82+
{
83+
name:"home",
84+
baseFunc:func(t*testing.T)string {
85+
home,err:=os.UserHomeDir()
86+
require.NoError(t,err)
87+
returnhome
88+
},
89+
relativity:LSRelativityHome,
90+
},
91+
{
92+
name:"root",
93+
baseFunc:func(t*testing.T)string {
94+
ifruntime.GOOS=="windows" {
95+
return"C:\\"
96+
}
97+
return"/"
98+
},
99+
relativity:LSRelativityRoot,
100+
},
101+
}
102+
103+
// nolint:paralleltest // Not since Go v1.22.
104+
for_,tc:=rangetc {
105+
t.Run(tc.name,func(t*testing.T) {
106+
t.Parallel()
107+
108+
base:=tc.baseFunc(t)
109+
tmpDir:=t.TempDir()
110+
111+
reposDir:=filepath.Join(tmpDir,"repos")
112+
err:=os.Mkdir(reposDir,0o755)
113+
require.NoError(t,err)
114+
115+
downloadsDir:=filepath.Join(tmpDir,"Downloads")
116+
err=os.Mkdir(downloadsDir,0o755)
117+
require.NoError(t,err)
118+
119+
rel,err:=filepath.Rel(base,tmpDir)
120+
require.NoError(t,err)
121+
relComponents:=pathToArray(rel)
122+
123+
query:=LSQuery{
124+
Path:relComponents,
125+
Relativity:tc.relativity,
126+
}
127+
resp,err:=listFiles(query)
128+
require.NoError(t,err)
129+
130+
require.Equal(t,tmpDir,resp.AbsolutePathString)
131+
132+
varfoundRepos,foundDownloadsbool
133+
for_,file:=rangeresp.Contents {
134+
switchfile.Name {
135+
case"repos":
136+
foundRepos=true
137+
expectedPath:=filepath.Join(tmpDir,"repos")
138+
require.Equal(t,expectedPath,file.AbsolutePathString)
139+
require.True(t,file.IsDir)
140+
case"Downloads":
141+
foundDownloads=true
142+
expectedPath:=filepath.Join(tmpDir,"Downloads")
143+
require.Equal(t,expectedPath,file.AbsolutePathString)
144+
require.True(t,file.IsDir)
145+
}
146+
}
147+
require.True(t,foundRepos&&foundDownloads,"expected to find both repos and Downloads directories, got: %+v",resp.Contents)
148+
})
149+
}
150+
}
151+
152+
funcTestListFilesWindowsRoot(t*testing.T) {
153+
t.Parallel()
154+
155+
ifruntime.GOOS!="windows" {
156+
t.Skip("skipping test on non-Windows OS")
157+
}
158+
159+
query:=LSQuery{
160+
Path: []string{},
161+
Relativity:LSRelativityRoot,
162+
}
163+
resp,err:=listFiles(query)
164+
require.NoError(t,err)
165+
require.Equal(t,"C:\\",resp.AbsolutePathString)
166+
}
167+
168+
funcpathToArray(pathstring) []string {
169+
returnstrings.FieldsFunc(path,func(rrune)bool {
170+
returnr==os.PathSeparator
171+
})
172+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp