Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a0502e6

Browse files
authored
Merge branch 'main' into david-fraley/update-quickstart
2 parents 4e4d27e + f9f0ebb commit a0502e6

File tree

102 files changed

+7755
-726
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

102 files changed

+7755
-726
lines changed

agent/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ func (a *agent) apiHandler() http.Handler {
6060
r.Get("/api/v0/listening-ports", lp.handler)
6161
r.Get("/api/v0/netcheck", a.HandleNetcheck)
6262
r.Post("/api/v0/list-directory", a.HandleLS)
63+
r.Get("/api/v0/read-file", a.HandleReadFile)
64+
r.Post("/api/v0/write-file", a.HandleWriteFile)
6365
r.Get("/debug/logs", a.HandleHTTPDebugLogs)
6466
r.Get("/debug/magicsock", a.HandleHTTPDebugMagicsock)
6567
r.Get("/debug/magicsock/debug-logging/{state}", a.HandleHTTPMagicsockDebugLoggingState)

agent/files.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package agent
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"mime"
9+
"net/http"
10+
"os"
11+
"path/filepath"
12+
"strconv"
13+
"syscall"
14+
15+
"golang.org/x/xerrors"
16+
17+
"cdr.dev/slog"
18+
"github.com/coder/coder/v2/coderd/httpapi"
19+
"github.com/coder/coder/v2/codersdk"
20+
)
21+
22+
type HTTPResponseCode = int
23+
24+
func (a *agent) HandleReadFile(rw http.ResponseWriter, r *http.Request) {
25+
ctx := r.Context()
26+
27+
query := r.URL.Query()
28+
parser := httpapi.NewQueryParamParser().RequiredNotEmpty("path")
29+
path := parser.String(query, "", "path")
30+
offset := parser.PositiveInt64(query, 0, "offset")
31+
limit := parser.PositiveInt64(query, 0, "limit")
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+
})
38+
return
39+
}
40+
41+
status, err := a.streamFile(ctx, rw, path, offset, limit)
42+
if err != nil {
43+
httpapi.Write(ctx, rw, status, codersdk.Response{
44+
Message: err.Error(),
45+
})
46+
return
47+
}
48+
}
49+
50+
func (a *agent) streamFile(ctx context.Context, rw http.ResponseWriter, path string, offset, limit int64) (HTTPResponseCode, error) {
51+
if !filepath.IsAbs(path) {
52+
return http.StatusBadRequest, xerrors.Errorf("file path must be absolute: %q", path)
53+
}
54+
55+
f, err := a.filesystem.Open(path)
56+
if err != nil {
57+
status := http.StatusInternalServerError
58+
switch {
59+
case errors.Is(err, os.ErrNotExist):
60+
status = http.StatusNotFound
61+
case errors.Is(err, os.ErrPermission):
62+
status = http.StatusForbidden
63+
}
64+
return status, err
65+
}
66+
defer f.Close()
67+
68+
stat, err := f.Stat()
69+
if err != nil {
70+
return http.StatusInternalServerError, err
71+
}
72+
73+
if stat.IsDir() {
74+
return http.StatusBadRequest, xerrors.Errorf("open %s: not a file", path)
75+
}
76+
77+
size := stat.Size()
78+
if limit == 0 {
79+
limit = size
80+
}
81+
bytesRemaining := max(size-offset, 0)
82+
bytesToRead := min(bytesRemaining, limit)
83+
84+
// Relying on just the file name for the mime type for now.
85+
mimeType := mime.TypeByExtension(filepath.Ext(path))
86+
if mimeType == "" {
87+
mimeType = "application/octet-stream"
88+
}
89+
rw.Header().Set("Content-Type", mimeType)
90+
rw.Header().Set("Content-Length", strconv.FormatInt(bytesToRead, 10))
91+
rw.WriteHeader(http.StatusOK)
92+
93+
reader := io.NewSectionReader(f, offset, bytesToRead)
94+
_, err = io.Copy(rw, reader)
95+
if err != nil && !errors.Is(err, io.EOF) && ctx.Err() == nil {
96+
a.logger.Error(ctx, "workspace agent read file", slog.Error(err))
97+
}
98+
99+
return 0, nil
100+
}
101+
102+
func (a *agent) HandleWriteFile(rw http.ResponseWriter, r *http.Request) {
103+
ctx := r.Context()
104+
105+
query := r.URL.Query()
106+
parser := httpapi.NewQueryParamParser().RequiredNotEmpty("path")
107+
path := parser.String(query, "", "path")
108+
parser.ErrorExcessParams(query)
109+
if len(parser.Errors) > 0 {
110+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
111+
Message: "Query parameters have invalid values.",
112+
Validations: parser.Errors,
113+
})
114+
return
115+
}
116+
117+
status, err := a.writeFile(ctx, r, path)
118+
if err != nil {
119+
httpapi.Write(ctx, rw, status, codersdk.Response{
120+
Message: err.Error(),
121+
})
122+
return
123+
}
124+
125+
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
126+
Message: fmt.Sprintf("Successfully wrote to %q", path),
127+
})
128+
}
129+
130+
func (a *agent) writeFile(ctx context.Context, r *http.Request, path string) (HTTPResponseCode, error) {
131+
if !filepath.IsAbs(path) {
132+
return http.StatusBadRequest, xerrors.Errorf("file path must be absolute: %q", path)
133+
}
134+
135+
dir := filepath.Dir(path)
136+
err := a.filesystem.MkdirAll(dir, 0o755)
137+
if err != nil {
138+
status := http.StatusInternalServerError
139+
switch {
140+
case errors.Is(err, os.ErrPermission):
141+
status = http.StatusForbidden
142+
case errors.Is(err, syscall.ENOTDIR):
143+
status = http.StatusBadRequest
144+
}
145+
return status, err
146+
}
147+
148+
f, err := a.filesystem.Create(path)
149+
if err != nil {
150+
status := http.StatusInternalServerError
151+
switch {
152+
case errors.Is(err, os.ErrPermission):
153+
status = http.StatusForbidden
154+
case errors.Is(err, syscall.EISDIR):
155+
status = http.StatusBadRequest
156+
}
157+
return status, err
158+
}
159+
defer f.Close()
160+
161+
_, err = io.Copy(f, r.Body)
162+
if err != nil && !errors.Is(err, io.EOF) && ctx.Err() == nil {
163+
a.logger.Error(ctx, "workspace agent write file", slog.Error(err))
164+
}
165+
166+
return 0, nil
167+
}

0 commit comments

Comments
 (0)