@@ -12,11 +12,15 @@ import (
12
12
"strconv"
13
13
"syscall"
14
14
15
+ "github.com/icholy/replace"
16
+ "github.com/spf13/afero"
17
+ "golang.org/x/text/transform"
15
18
"golang.org/x/xerrors"
16
19
17
20
"cdr.dev/slog"
18
21
"github.com/coder/coder/v2/coderd/httpapi"
19
22
"github.com/coder/coder/v2/codersdk"
23
+ "github.com/coder/coder/v2/codersdk/workspacesdk"
20
24
)
21
25
22
26
type HTTPResponseCode = int
@@ -165,3 +169,105 @@ func (a *agent) writeFile(ctx context.Context, r *http.Request, path string) (HT
165
169
166
170
return 0 , nil
167
171
}
172
+
173
+ func (a * agent ) HandleEditFiles (rw http.ResponseWriter , r * http.Request ) {
174
+ ctx := r .Context ()
175
+
176
+ var req workspacesdk.FileEditRequest
177
+ if ! httpapi .Read (ctx , rw , r , & req ) {
178
+ return
179
+ }
180
+
181
+ if len (req .Files ) == 0 {
182
+ httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
183
+ Message : "must specify at least one file" ,
184
+ })
185
+ return
186
+ }
187
+
188
+ var combinedErr error
189
+ status := http .StatusOK
190
+ for _ , edit := range req .Files {
191
+ s , err := a .editFile (r .Context (), edit .Path , edit .Edits )
192
+ // Keep the highest response status, so 500 will be preferred over 400, etc.
193
+ if s > status {
194
+ status = s
195
+ }
196
+ if err != nil {
197
+ combinedErr = errors .Join (combinedErr , err )
198
+ }
199
+ }
200
+
201
+ if combinedErr != nil {
202
+ httpapi .Write (ctx , rw , status , codersdk.Response {
203
+ Message : combinedErr .Error (),
204
+ })
205
+ return
206
+ }
207
+
208
+ httpapi .Write (ctx , rw , http .StatusOK , codersdk.Response {
209
+ Message : "Successfully edited file(s)" ,
210
+ })
211
+ }
212
+
213
+ func (a * agent ) editFile (ctx context.Context , path string , edits []workspacesdk.FileEdit ) (int , error ) {
214
+ if path == "" {
215
+ return http .StatusBadRequest , xerrors .New ("\" path\" is required" )
216
+ }
217
+
218
+ if ! filepath .IsAbs (path ) {
219
+ return http .StatusBadRequest , xerrors .Errorf ("file path must be absolute: %q" , path )
220
+ }
221
+
222
+ if len (edits ) == 0 {
223
+ return http .StatusBadRequest , xerrors .New ("must specify at least one edit" )
224
+ }
225
+
226
+ f , err := a .filesystem .Open (path )
227
+ if err != nil {
228
+ status := http .StatusInternalServerError
229
+ switch {
230
+ case errors .Is (err , os .ErrNotExist ):
231
+ status = http .StatusNotFound
232
+ case errors .Is (err , os .ErrPermission ):
233
+ status = http .StatusForbidden
234
+ }
235
+ return status , err
236
+ }
237
+ defer f .Close ()
238
+
239
+ stat , err := f .Stat ()
240
+ if err != nil {
241
+ return http .StatusInternalServerError , err
242
+ }
243
+
244
+ if stat .IsDir () {
245
+ return http .StatusBadRequest , xerrors .Errorf ("open %s: not a file" , path )
246
+ }
247
+
248
+ transforms := make ([]transform.Transformer , len (edits ))
249
+ for i , edit := range edits {
250
+ transforms [i ] = replace .String (edit .Search , edit .Replace )
251
+ }
252
+
253
+ tmpfile , err := afero .TempFile (a .filesystem , "" , filepath .Base (path ))
254
+ if err != nil {
255
+ return http .StatusInternalServerError , err
256
+ }
257
+ defer tmpfile .Close ()
258
+
259
+ _ , err = io .Copy (tmpfile , replace .Chain (f , transforms ... ))
260
+ if err != nil {
261
+ if rerr := a .filesystem .Remove (tmpfile .Name ()); rerr != nil {
262
+ a .logger .Warn (ctx , "unable to clean up temp file" , slog .Error (rerr ))
263
+ }
264
+ return http .StatusInternalServerError , xerrors .Errorf ("edit %s: %w" , path , err )
265
+ }
266
+
267
+ err = a .filesystem .Rename (tmpfile .Name (), path )
268
+ if err != nil {
269
+ return http .StatusInternalServerError , err
270
+ }
271
+
272
+ return 0 , nil
273
+ }
0 commit comments