@@ -11,23 +11,39 @@ import (
1111 "strings"
1212
1313 "github.com/shirou/gopsutil/v4/disk"
14+ "github.com/spf13/afero"
1415 "golang.org/x/xerrors"
1516
1617 "github.com/coder/coder/v2/coderd/httpapi"
1718 "github.com/coder/coder/v2/codersdk"
19+ "github.com/coder/coder/v2/codersdk/workspacesdk"
1820)
1921
2022var WindowsDriveRegex = regexp .MustCompile (`^[a-zA-Z]:\\$` )
2123
22- func (* agent ) HandleLS (rw http.ResponseWriter , r * http.Request ) {
24+ func (a * agent ) HandleLS (rw http.ResponseWriter , r * http.Request ) {
2325 ctx := r .Context ()
2426
25- var query LSRequest
26- if ! httpapi .Read (ctx , rw , r , & query ) {
27+ // An absolute path may be optionally provided, otherwise a path split into an
28+ // array must be provided in the body (which can be relative).
29+ query := r .URL .Query ()
30+ parser := httpapi .NewQueryParamParser ()
31+ path := parser .String (query , "" , "path" )
32+ parser .ErrorExcessParams (query )
33+ if len (parser .Errors ) > 0 {
34+ httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
35+ Message : "Query parameters have invalid values." ,
36+ Validations : parser .Errors ,
37+ })
2738 return
2839 }
2940
30- resp , err := listFiles (query )
41+ var req workspacesdk.LSRequest
42+ if ! httpapi .Read (ctx , rw , r , & req ) {
43+ return
44+ }
45+
46+ resp , err := listFiles (a .filesystem , path , req )
3147 if err != nil {
3248 status := http .StatusInternalServerError
3349 switch {
@@ -46,66 +62,74 @@ func (*agent) HandleLS(rw http.ResponseWriter, r *http.Request) {
4662 httpapi .Write (ctx , rw , http .StatusOK , resp )
4763}
4864
49- func listFiles (query LSRequest ) (LSResponse , error ) {
50- var fullPath []string
51- switch query .Relativity {
52- case LSRelativityHome :
53- home , err := os .UserHomeDir ()
54- if err != nil {
55- return LSResponse {}, xerrors .Errorf ("failed to get user home directory: %w" , err )
65+ func listFiles (fs afero.Fs , path string , query workspacesdk.LSRequest ) (workspacesdk.LSResponse , error ) {
66+ absolutePathString := path
67+ if absolutePathString != "" {
68+ if ! filepath .IsAbs (path ) {
69+ return workspacesdk.LSResponse {}, xerrors .Errorf ("path must be absolute: %q" , path )
5670 }
57- fullPath = []string {home }
58- case LSRelativityRoot :
59- if runtime .GOOS == "windows" {
60- if len (query .Path ) == 0 {
61- return listDrives ()
71+ } else {
72+ var fullPath []string
73+ switch query .Relativity {
74+ case workspacesdk .LSRelativityHome :
75+ home , err := os .UserHomeDir ()
76+ if err != nil {
77+ return workspacesdk.LSResponse {}, xerrors .Errorf ("failed to get user home directory: %w" , err )
6278 }
63- if ! WindowsDriveRegex .MatchString (query .Path [0 ]) {
64- return LSResponse {}, xerrors .Errorf ("invalid drive letter %q" , query .Path [0 ])
79+ fullPath = []string {home }
80+ case workspacesdk .LSRelativityRoot :
81+ if runtime .GOOS == "windows" {
82+ if len (query .Path ) == 0 {
83+ return listDrives ()
84+ }
85+ if ! WindowsDriveRegex .MatchString (query .Path [0 ]) {
86+ return workspacesdk.LSResponse {}, xerrors .Errorf ("invalid drive letter %q" , query .Path [0 ])
87+ }
88+ } else {
89+ fullPath = []string {"/" }
6590 }
66- } else {
67- fullPath = [] string { "/" }
91+ default :
92+ return workspacesdk. LSResponse {}, xerrors . Errorf ( "unsupported relativity type %q" , query . Relativity )
6893 }
69- default :
70- return LSResponse {}, xerrors .Errorf ("unsupported relativity type %q" , query .Relativity )
71- }
7294
73- fullPath = append (fullPath , query .Path ... )
74- fullPathRelative := filepath .Join (fullPath ... )
75- absolutePathString , err := filepath .Abs (fullPathRelative )
76- if err != nil {
77- return LSResponse {}, xerrors .Errorf ("failed to get absolute path of %q: %w" , fullPathRelative , err )
95+ fullPath = append (fullPath , query .Path ... )
96+ fullPathRelative := filepath .Join (fullPath ... )
97+ var err error
98+ absolutePathString , err = filepath .Abs (fullPathRelative )
99+ if err != nil {
100+ return workspacesdk.LSResponse {}, xerrors .Errorf ("failed to get absolute path of %q: %w" , fullPathRelative , err )
101+ }
78102 }
79103
80104 // codeql[go/path-injection] - The intent is to allow the user to navigate to any directory in their workspace.
81- f , err := os .Open (absolutePathString )
105+ f , err := fs .Open (absolutePathString )
82106 if err != nil {
83- return LSResponse {}, xerrors .Errorf ("failed to open directory %q: %w" , absolutePathString , err )
107+ return workspacesdk. LSResponse {}, xerrors .Errorf ("failed to open directory %q: %w" , absolutePathString , err )
84108 }
85109 defer f .Close ()
86110
87111 stat , err := f .Stat ()
88112 if err != nil {
89- return LSResponse {}, xerrors .Errorf ("failed to stat directory %q: %w" , absolutePathString , err )
113+ return workspacesdk. LSResponse {}, xerrors .Errorf ("failed to stat directory %q: %w" , absolutePathString , err )
90114 }
91115
92116 if ! stat .IsDir () {
93- return LSResponse {}, xerrors .Errorf ("path %q is not a directory" , absolutePathString )
117+ return workspacesdk. LSResponse {}, xerrors .Errorf ("path %q is not a directory" , absolutePathString )
94118 }
95119
96120 // `contents` may be partially populated even if the operation fails midway.
97- contents , _ := f .ReadDir (- 1 )
98- respContents := make ([]LSFile , 0 , len (contents ))
121+ contents , _ := f .Readdir (- 1 )
122+ respContents := make ([]workspacesdk. LSFile , 0 , len (contents ))
99123 for _ , file := range contents {
100- respContents = append (respContents , LSFile {
124+ respContents = append (respContents , workspacesdk. LSFile {
101125 Name : file .Name (),
102126 AbsolutePathString : filepath .Join (absolutePathString , file .Name ()),
103127 IsDir : file .IsDir (),
104128 })
105129 }
106130
107131 // Sort alphabetically: directories then files
108- slices .SortFunc (respContents , func (a , b LSFile ) int {
132+ slices .SortFunc (respContents , func (a , b workspacesdk. LSFile ) int {
109133 if a .IsDir && ! b .IsDir {
110134 return - 1
111135 }
@@ -117,35 +141,35 @@ func listFiles(query LSRequest) (LSResponse, error) {
117141
118142 absolutePath := pathToArray (absolutePathString )
119143
120- return LSResponse {
144+ return workspacesdk. LSResponse {
121145 AbsolutePath : absolutePath ,
122146 AbsolutePathString : absolutePathString ,
123147 Contents : respContents ,
124148 }, nil
125149}
126150
127- func listDrives () (LSResponse , error ) {
151+ func listDrives () (workspacesdk. LSResponse , error ) {
128152 // disk.Partitions() will return partitions even if there was a failure to
129153 // get one. Any errored partitions will not be returned.
130154 partitionStats , err := disk .Partitions (true )
131155 if err != nil && len (partitionStats ) == 0 {
132156 // Only return the error if there were no partitions returned.
133- return LSResponse {}, xerrors .Errorf ("failed to get partitions: %w" , err )
157+ return workspacesdk. LSResponse {}, xerrors .Errorf ("failed to get partitions: %w" , err )
134158 }
135159
136- contents := make ([]LSFile , 0 , len (partitionStats ))
160+ contents := make ([]workspacesdk. LSFile , 0 , len (partitionStats ))
137161 for _ , a := range partitionStats {
138162 // Drive letters on Windows have a trailing separator as part of their name.
139163 // i.e. `os.Open("C:")` does not work, but `os.Open("C:\\")` does.
140164 name := a .Mountpoint + string (os .PathSeparator )
141- contents = append (contents , LSFile {
165+ contents = append (contents , workspacesdk. LSFile {
142166 Name : name ,
143167 AbsolutePathString : name ,
144168 IsDir : true ,
145169 })
146170 }
147171
148- return LSResponse {
172+ return workspacesdk. LSResponse {
149173 AbsolutePath : []string {},
150174 AbsolutePathString : "" ,
151175 Contents : contents ,
@@ -163,36 +187,3 @@ func pathToArray(path string) []string {
163187 }
164188 return out
165189}
166-
167- type LSRequest struct {
168- // e.g. [], ["repos", "coder"],
169- Path []string `json:"path"`
170- // Whether the supplied path is relative to the user's home directory,
171- // or the root directory.
172- Relativity LSRelativity `json:"relativity"`
173- }
174-
175- type LSResponse struct {
176- AbsolutePath []string `json:"absolute_path"`
177- // Returned so clients can display the full path to the user, and
178- // copy it to configure file sync
179- // e.g. Windows: "C:\\Users\\coder"
180- // Linux: "/home/coder"
181- AbsolutePathString string `json:"absolute_path_string"`
182- Contents []LSFile `json:"contents"`
183- }
184-
185- type LSFile struct {
186- Name string `json:"name"`
187- // e.g. "C:\\Users\\coder\\hello.txt"
188- // "/home/coder/hello.txt"
189- AbsolutePathString string `json:"absolute_path_string"`
190- IsDir bool `json:"is_dir"`
191- }
192-
193- type LSRelativity string
194-
195- const (
196- LSRelativityRoot LSRelativity = "root"
197- LSRelativityHome LSRelativity = "home"
198- )
0 commit comments