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

chore: add agent endpoint for querying file system#16736

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
ethanndickson merged 11 commits intomainfromethan/agent-ls
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletionsagent/api.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,6 +41,7 @@ func (a *agent) apiHandler() http.Handler {
r.Get("/api/v0/containers", ch.ServeHTTP)
r.Get("/api/v0/listening-ports", lp.handler)
r.Get("/api/v0/netcheck", a.HandleNetcheck)
r.Post("/api/v0/list-directory", a.HandleLS)
r.Get("/debug/logs", a.HandleHTTPDebugLogs)
r.Get("/debug/magicsock", a.HandleHTTPDebugMagicsock)
r.Get("/debug/magicsock/debug-logging/{state}", a.HandleHTTPMagicsockDebugLoggingState)
Expand Down
181 changes: 181 additions & 0 deletionsagent/ls.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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"
)
207 changes: 207 additions & 0 deletionsagent/ls_internal_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
package agent

import (
"os"
"path/filepath"
"runtime"
"testing"

"github.com/stretchr/testify/require"
)

func TestListFilesNonExistentDirectory(t *testing.T) {
t.Parallel()

query := LSRequest{
Path: []string{"idontexist"},
Relativity: LSRelativityHome,
}
_, err := listFiles(query)
require.ErrorIs(t, err, os.ErrNotExist)
}

func TestListFilesPermissionDenied(t *testing.T) {
t.Parallel()

if runtime.GOOS == "windows" {
t.Skip("creating an unreadable-by-user directory is non-trivial on Windows")
}

home, err := os.UserHomeDir()
require.NoError(t, err)

tmpDir := t.TempDir()

reposDir := filepath.Join(tmpDir, "repos")
err = os.Mkdir(reposDir, 0o000)
require.NoError(t, err)

rel, err := filepath.Rel(home, reposDir)
require.NoError(t, err)

query := LSRequest{
Path: pathToArray(rel),
Relativity: LSRelativityHome,
}
_, err = listFiles(query)
require.ErrorIs(t, err, os.ErrPermission)
}

func TestListFilesNotADirectory(t *testing.T) {
t.Parallel()

home, err := os.UserHomeDir()
require.NoError(t, err)

tmpDir := t.TempDir()

filePath := filepath.Join(tmpDir, "file.txt")
err = os.WriteFile(filePath, []byte("content"), 0o600)
require.NoError(t, err)

rel, err := filepath.Rel(home, filePath)
require.NoError(t, err)

query := LSRequest{
Path: pathToArray(rel),
Relativity: LSRelativityHome,
}
_, err = listFiles(query)
require.ErrorContains(t, err, "is not a directory")
}

func TestListFilesSuccess(t *testing.T) {
t.Parallel()

tc := []struct {
name string
baseFunc func(t *testing.T) string
relativity LSRelativity
}{
{
name: "home",
baseFunc: func(t *testing.T) string {
home, err := os.UserHomeDir()
require.NoError(t, err)
return home
},
relativity: LSRelativityHome,
},
{
name: "root",
baseFunc: func(*testing.T) string {
if runtime.GOOS == "windows" {
return ""
}
return "/"
},
relativity: LSRelativityRoot,
},
}

// nolint:paralleltest // Not since Go v1.22.
for _, tc := range tc {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

base := tc.baseFunc(t)
tmpDir := t.TempDir()

reposDir := filepath.Join(tmpDir, "repos")
err := os.Mkdir(reposDir, 0o755)
require.NoError(t, err)

downloadsDir := filepath.Join(tmpDir, "Downloads")
err = os.Mkdir(downloadsDir, 0o755)
require.NoError(t, err)

textFile := filepath.Join(tmpDir, "file.txt")
err = os.WriteFile(textFile, []byte("content"), 0o600)
require.NoError(t, err)

var queryComponents []string
// We can't get an absolute path relative to empty string on Windows.
if runtime.GOOS == "windows" && base == "" {
queryComponents = pathToArray(tmpDir)
} else {
rel, err := filepath.Rel(base, tmpDir)
require.NoError(t, err)
queryComponents = pathToArray(rel)
}

query := LSRequest{
Path: queryComponents,
Relativity: tc.relativity,
}
resp, err := listFiles(query)
require.NoError(t, err)

require.Equal(t, tmpDir, resp.AbsolutePathString)
require.ElementsMatch(t, []LSFile{
{
Name: "repos",
AbsolutePathString: reposDir,
IsDir: true,
},
{
Name: "Downloads",
AbsolutePathString: downloadsDir,
IsDir: true,
},
{
Name: "file.txt",
AbsolutePathString: textFile,
IsDir: false,
},
}, resp.Contents)
})
}
}

func TestListFilesListDrives(t *testing.T) {
t.Parallel()

if runtime.GOOS != "windows" {
t.Skip("skipping test on non-Windows OS")
}

query := LSRequest{
Path: []string{},
Relativity: LSRelativityRoot,
}
resp, err := listFiles(query)
require.NoError(t, err)
require.Contains(t, resp.Contents, LSFile{
Name: "C:\\",
AbsolutePathString: "C:\\",
IsDir: true,
})

query = LSRequest{
Path: []string{"C:\\"},
Relativity: LSRelativityRoot,
}
resp, err = listFiles(query)
require.NoError(t, err)

query = LSRequest{
Path: resp.AbsolutePath,
Relativity: LSRelativityRoot,
}
resp, err = listFiles(query)
require.NoError(t, err)
// System directory should always exist
require.Contains(t, resp.Contents, LSFile{
Name: "Windows",
AbsolutePathString: "C:\\Windows",
IsDir: true,
})

query = LSRequest{
// Network drives are not supported.
Path: []string{"\\sshfs\\work"},
Relativity: LSRelativityRoot,
}
resp, err = listFiles(query)
require.ErrorContains(t, err, "drive")
}
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp