mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
182 lines
4.8 KiB
Go
182 lines
4.8 KiB
Go
package agent
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/shirou/gopsutil/v4/disk"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/httpapi"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
var WindowsDriveRegex = regexp.MustCompile(`^[a-zA-Z]:\\$`)
|
|
|
|
func (*agent) HandleLS(rw http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
var query LSRequest
|
|
if !httpapi.Read(ctx, rw, r, &query) {
|
|
return
|
|
}
|
|
|
|
resp, err := listFiles(query)
|
|
if err != nil {
|
|
status := http.StatusInternalServerError
|
|
switch {
|
|
case errors.Is(err, os.ErrNotExist):
|
|
status = http.StatusNotFound
|
|
case errors.Is(err, os.ErrPermission):
|
|
status = http.StatusForbidden
|
|
default:
|
|
}
|
|
httpapi.Write(ctx, rw, status, codersdk.Response{
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, resp)
|
|
}
|
|
|
|
func listFiles(query LSRequest) (LSResponse, error) {
|
|
var fullPath []string
|
|
switch query.Relativity {
|
|
case LSRelativityHome:
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return LSResponse{}, xerrors.Errorf("failed to get user home directory: %w", err)
|
|
}
|
|
fullPath = []string{home}
|
|
case LSRelativityRoot:
|
|
if runtime.GOOS == "windows" {
|
|
if len(query.Path) == 0 {
|
|
return listDrives()
|
|
}
|
|
if !WindowsDriveRegex.MatchString(query.Path[0]) {
|
|
return LSResponse{}, xerrors.Errorf("invalid drive letter %q", query.Path[0])
|
|
}
|
|
} else {
|
|
fullPath = []string{"/"}
|
|
}
|
|
default:
|
|
return LSResponse{}, xerrors.Errorf("unsupported relativity type %q", query.Relativity)
|
|
}
|
|
|
|
fullPath = append(fullPath, query.Path...)
|
|
fullPathRelative := filepath.Join(fullPath...)
|
|
absolutePathString, err := filepath.Abs(fullPathRelative)
|
|
if err != nil {
|
|
return LSResponse{}, xerrors.Errorf("failed to get absolute path of %q: %w", fullPathRelative, err)
|
|
}
|
|
|
|
f, err := os.Open(absolutePathString)
|
|
if err != nil {
|
|
return LSResponse{}, xerrors.Errorf("failed to open directory %q: %w", absolutePathString, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
stat, err := f.Stat()
|
|
if err != nil {
|
|
return LSResponse{}, xerrors.Errorf("failed to stat directory %q: %w", absolutePathString, err)
|
|
}
|
|
|
|
if !stat.IsDir() {
|
|
return LSResponse{}, xerrors.Errorf("path %q is not a directory", absolutePathString)
|
|
}
|
|
|
|
// `contents` may be partially populated even if the operation fails midway.
|
|
contents, _ := f.ReadDir(-1)
|
|
respContents := make([]LSFile, 0, len(contents))
|
|
for _, file := range contents {
|
|
respContents = append(respContents, LSFile{
|
|
Name: file.Name(),
|
|
AbsolutePathString: filepath.Join(absolutePathString, file.Name()),
|
|
IsDir: file.IsDir(),
|
|
})
|
|
}
|
|
|
|
absolutePath := pathToArray(absolutePathString)
|
|
|
|
return LSResponse{
|
|
AbsolutePath: absolutePath,
|
|
AbsolutePathString: absolutePathString,
|
|
Contents: respContents,
|
|
}, nil
|
|
}
|
|
|
|
func listDrives() (LSResponse, error) {
|
|
partitionStats, err := disk.Partitions(true)
|
|
if err != nil {
|
|
return LSResponse{}, xerrors.Errorf("failed to get partitions: %w", err)
|
|
}
|
|
contents := make([]LSFile, 0, len(partitionStats))
|
|
for _, a := range partitionStats {
|
|
// Drive letters on Windows have a trailing separator as part of their name.
|
|
// i.e. `os.Open("C:")` does not work, but `os.Open("C:\\")` does.
|
|
name := a.Mountpoint + string(os.PathSeparator)
|
|
contents = append(contents, LSFile{
|
|
Name: name,
|
|
AbsolutePathString: name,
|
|
IsDir: true,
|
|
})
|
|
}
|
|
|
|
return LSResponse{
|
|
AbsolutePath: []string{},
|
|
AbsolutePathString: "",
|
|
Contents: contents,
|
|
}, nil
|
|
}
|
|
|
|
func pathToArray(path string) []string {
|
|
out := strings.FieldsFunc(path, func(r rune) bool {
|
|
return r == os.PathSeparator
|
|
})
|
|
// Drive letters on Windows have a trailing separator as part of their name.
|
|
// i.e. `os.Open("C:")` does not work, but `os.Open("C:\\")` does.
|
|
if runtime.GOOS == "windows" && len(out) > 0 {
|
|
out[0] += string(os.PathSeparator)
|
|
}
|
|
return out
|
|
}
|
|
|
|
type LSRequest struct {
|
|
// e.g. [], ["repos", "coder"],
|
|
Path []string `json:"path"`
|
|
// Whether the supplied path is relative to the user's home directory,
|
|
// or the root directory.
|
|
Relativity LSRelativity `json:"relativity"`
|
|
}
|
|
|
|
type LSResponse struct {
|
|
AbsolutePath []string `json:"absolute_path"`
|
|
// Returned so clients can display the full path to the user, and
|
|
// copy it to configure file sync
|
|
// e.g. Windows: "C:\\Users\\coder"
|
|
// Linux: "/home/coder"
|
|
AbsolutePathString string `json:"absolute_path_string"`
|
|
Contents []LSFile `json:"contents"`
|
|
}
|
|
|
|
type LSFile struct {
|
|
Name string `json:"name"`
|
|
// e.g. "C:\\Users\\coder\\hello.txt"
|
|
// "/home/coder/hello.txt"
|
|
AbsolutePathString string `json:"absolute_path_string"`
|
|
IsDir bool `json:"is_dir"`
|
|
}
|
|
|
|
type LSRelativity string
|
|
|
|
const (
|
|
LSRelativityRoot LSRelativity = "root"
|
|
LSRelativityHome LSRelativity = "home"
|
|
)
|