Skip to content

Commit 19cef9f

Browse files
sallyomclaude
authored andcommitted
Simple upload files (ambient-code#455)
Signed-off-by: sallyom <[email protected]> Signed-off-by: Sally O'Malley <[email protected]> Co-authored-by: Claude Sonnet 4.5 <[email protected]>
1 parent cb6c54a commit 19cef9f

File tree

19 files changed

+2788
-307
lines changed

19 files changed

+2788
-307
lines changed

components/backend/handlers/content.go

Lines changed: 80 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"time"
1414

1515
"ambient-code-backend/git"
16+
"ambient-code-backend/pathutil"
1617

1718
"github.com/gin-gonic/gin"
1819
)
@@ -61,7 +62,7 @@ func ContentGitPush(c *gin.Context) {
6162
}
6263

6364
// Basic safety: repoDir must be under StateBaseDir
64-
if !strings.HasPrefix(repoDir+string(os.PathSeparator), StateBaseDir+string(os.PathSeparator)) && repoDir != StateBaseDir {
65+
if !pathutil.IsPathWithinBase(repoDir, StateBaseDir) && repoDir != StateBaseDir {
6566
log.Printf("contentGitPush: invalid repoPath resolved=%q stateBaseDir=%q", repoDir, StateBaseDir)
6667
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid repoPath"})
6768
return
@@ -101,7 +102,7 @@ func ContentGitAbandon(c *gin.Context) {
101102
repoDir = StateBaseDir
102103
}
103104

104-
if !strings.HasPrefix(repoDir+string(os.PathSeparator), StateBaseDir+string(os.PathSeparator)) && repoDir != StateBaseDir {
105+
if !pathutil.IsPathWithinBase(repoDir, StateBaseDir) && repoDir != StateBaseDir {
105106
log.Printf("contentGitAbandon: invalid repoPath resolved=%q base=%q", repoDir, StateBaseDir)
106107
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid repoPath"})
107108
return
@@ -126,7 +127,7 @@ func ContentGitDiff(c *gin.Context) {
126127
}
127128

128129
repoDir := filepath.Clean(filepath.Join(StateBaseDir, repoPath))
129-
if !strings.HasPrefix(repoDir+string(os.PathSeparator), StateBaseDir+string(os.PathSeparator)) && repoDir != StateBaseDir {
130+
if !pathutil.IsPathWithinBase(repoDir, StateBaseDir) && repoDir != StateBaseDir {
130131
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid repoPath"})
131132
return
132133
}
@@ -159,13 +160,13 @@ func ContentGitDiff(c *gin.Context) {
159160
// ContentGitStatus handles GET /content/git-status?path=
160161
func ContentGitStatus(c *gin.Context) {
161162
path := filepath.Clean("/" + strings.TrimSpace(c.Query("path")))
162-
if path == "/" || strings.Contains(path, "..") {
163+
abs := filepath.Join(StateBaseDir, path)
164+
// Verify abs is within StateBaseDir to prevent path traversal
165+
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
163166
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
164167
return
165168
}
166169

167-
abs := filepath.Join(StateBaseDir, path)
168-
169170
// Check if directory exists
170171
if info, err := os.Stat(abs); err != nil || !info.IsDir() {
171172
c.JSON(http.StatusOK, gin.H{
@@ -224,13 +225,13 @@ func ContentGitConfigureRemote(c *gin.Context) {
224225
}
225226

226227
path := filepath.Clean("/" + body.Path)
227-
if path == "/" || strings.Contains(path, "..") {
228+
abs := filepath.Join(StateBaseDir, path)
229+
// Verify abs is within StateBaseDir to prevent path traversal
230+
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
228231
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
229232
return
230233
}
231234

232-
abs := filepath.Join(StateBaseDir, path)
233-
234235
// Check if directory exists
235236
if info, err := os.Stat(abs); err != nil || !info.IsDir() {
236237
c.JSON(http.StatusBadRequest, gin.H{"error": "directory not found"})
@@ -301,13 +302,13 @@ func ContentGitSync(c *gin.Context) {
301302
}
302303

303304
path := filepath.Clean("/" + body.Path)
304-
if path == "/" || strings.Contains(path, "..") {
305+
abs := filepath.Join(StateBaseDir, path)
306+
// Verify abs is within StateBaseDir to prevent path traversal
307+
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
305308
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
306309
return
307310
}
308311

309-
abs := filepath.Join(StateBaseDir, path)
310-
311312
// Check if git repo exists
312313
gitDir := filepath.Join(abs, ".git")
313314
if _, err := os.Stat(gitDir); err != nil {
@@ -345,12 +346,13 @@ func ContentWrite(c *gin.Context) {
345346
log.Printf("ContentWrite: path=%q contentLen=%d encoding=%q StateBaseDir=%q", req.Path, len(req.Content), req.Encoding, StateBaseDir)
346347

347348
path := filepath.Clean("/" + strings.TrimSpace(req.Path))
348-
if path == "/" || strings.Contains(path, "..") {
349-
log.Printf("ContentWrite: invalid path rejected: path=%q", path)
349+
abs := filepath.Join(StateBaseDir, path)
350+
// Verify abs is within StateBaseDir to prevent path traversal
351+
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
352+
log.Printf("ContentWrite: path traversal attempt rejected: path=%q abs=%q", path, abs)
350353
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
351354
return
352355
}
353-
abs := filepath.Join(StateBaseDir, path)
354356
log.Printf("ContentWrite: absolute path=%q", abs)
355357

356358
if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
@@ -385,12 +387,13 @@ func ContentRead(c *gin.Context) {
385387
log.Printf("ContentRead: requested path=%q StateBaseDir=%q", c.Query("path"), StateBaseDir)
386388
log.Printf("ContentRead: cleaned path=%q", path)
387389

388-
if path == "/" || strings.Contains(path, "..") {
389-
log.Printf("ContentRead: invalid path rejected: path=%q", path)
390+
abs := filepath.Join(StateBaseDir, path)
391+
// Verify abs is within StateBaseDir to prevent path traversal
392+
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
393+
log.Printf("ContentRead: path traversal attempt rejected: path=%q abs=%q", path, abs)
390394
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
391395
return
392396
}
393-
abs := filepath.Join(StateBaseDir, path)
394397
log.Printf("ContentRead: absolute path=%q", abs)
395398

396399
b, err := os.ReadFile(abs)
@@ -414,12 +417,13 @@ func ContentList(c *gin.Context) {
414417
log.Printf("ContentList: cleaned path=%q", path)
415418
log.Printf("ContentList: StateBaseDir=%q", StateBaseDir)
416419

417-
if path == "/" || strings.Contains(path, "..") {
418-
log.Printf("ContentList: invalid path rejected: path=%q", path)
420+
abs := filepath.Join(StateBaseDir, path)
421+
// Verify abs is within StateBaseDir to prevent path traversal
422+
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
423+
log.Printf("ContentList: path traversal attempt rejected: path=%q abs=%q", path, abs)
419424
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
420425
return
421426
}
422-
abs := filepath.Join(StateBaseDir, path)
423427
log.Printf("ContentList: absolute path=%q", abs)
424428

425429
info, err := os.Stat(abs)
@@ -672,7 +676,9 @@ func ContentGitMergeStatus(c *gin.Context) {
672676
path := filepath.Clean("/" + strings.TrimSpace(c.Query("path")))
673677
branch := strings.TrimSpace(c.Query("branch"))
674678

675-
if path == "/" || strings.Contains(path, "..") {
679+
abs := filepath.Join(StateBaseDir, path)
680+
// Verify abs is within StateBaseDir to prevent path traversal
681+
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
676682
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
677683
return
678684
}
@@ -681,8 +687,6 @@ func ContentGitMergeStatus(c *gin.Context) {
681687
branch = "main"
682688
}
683689

684-
abs := filepath.Join(StateBaseDir, path)
685-
686690
// Check if git repo exists
687691
gitDir := filepath.Join(abs, ".git")
688692
if _, err := os.Stat(gitDir); err != nil {
@@ -722,7 +726,9 @@ func ContentGitPull(c *gin.Context) {
722726
}
723727

724728
path := filepath.Clean("/" + body.Path)
725-
if path == "/" || strings.Contains(path, "..") {
729+
abs := filepath.Join(StateBaseDir, path)
730+
// Verify abs is within StateBaseDir to prevent path traversal
731+
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
726732
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
727733
return
728734
}
@@ -731,8 +737,6 @@ func ContentGitPull(c *gin.Context) {
731737
body.Branch = "main"
732738
}
733739

734-
abs := filepath.Join(StateBaseDir, path)
735-
736740
if err := GitPullRepo(c.Request.Context(), abs, body.Branch); err != nil {
737741
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
738742
return
@@ -757,7 +761,9 @@ func ContentGitPushToBranch(c *gin.Context) {
757761
}
758762

759763
path := filepath.Clean("/" + body.Path)
760-
if path == "/" || strings.Contains(path, "..") {
764+
abs := filepath.Join(StateBaseDir, path)
765+
// Verify abs is within StateBaseDir to prevent path traversal
766+
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
761767
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
762768
return
763769
}
@@ -770,8 +776,6 @@ func ContentGitPushToBranch(c *gin.Context) {
770776
body.Message = "Session artifacts update"
771777
}
772778

773-
abs := filepath.Join(StateBaseDir, path)
774-
775779
if err := GitPushToRepo(c.Request.Context(), abs, body.Branch, body.Message); err != nil {
776780
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
777781
return
@@ -795,7 +799,9 @@ func ContentGitCreateBranch(c *gin.Context) {
795799
}
796800

797801
path := filepath.Clean("/" + body.Path)
798-
if path == "/" || strings.Contains(path, "..") {
802+
abs := filepath.Join(StateBaseDir, path)
803+
// Verify abs is within StateBaseDir to prevent path traversal
804+
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
799805
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
800806
return
801807
}
@@ -805,8 +811,6 @@ func ContentGitCreateBranch(c *gin.Context) {
805811
return
806812
}
807813

808-
abs := filepath.Join(StateBaseDir, path)
809-
810814
if err := GitCreateBranch(c.Request.Context(), abs, body.BranchName); err != nil {
811815
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
812816
return
@@ -820,13 +824,13 @@ func ContentGitCreateBranch(c *gin.Context) {
820824
func ContentGitListBranches(c *gin.Context) {
821825
path := filepath.Clean("/" + strings.TrimSpace(c.Query("path")))
822826

823-
if path == "/" || strings.Contains(path, "..") {
827+
abs := filepath.Join(StateBaseDir, path)
828+
// Verify abs is within StateBaseDir to prevent path traversal
829+
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
824830
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
825831
return
826832
}
827833

828-
abs := filepath.Join(StateBaseDir, path)
829-
830834
branches, err := GitListRemoteBranches(c.Request.Context(), abs)
831835
if err != nil {
832836
// Log actual error for debugging, but return generic message to avoid leaking internal details
@@ -837,3 +841,43 @@ func ContentGitListBranches(c *gin.Context) {
837841

838842
c.JSON(http.StatusOK, gin.H{"branches": branches})
839843
}
844+
845+
// ContentDelete handles DELETE /content/delete when running in CONTENT_SERVICE_MODE
846+
func ContentDelete(c *gin.Context) {
847+
var req struct {
848+
Path string `json:"path"`
849+
}
850+
if err := c.ShouldBindJSON(&req); err != nil {
851+
log.Printf("ContentDelete: bind JSON failed: %v", err)
852+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
853+
return
854+
}
855+
log.Printf("ContentDelete: path=%q StateBaseDir=%q", req.Path, StateBaseDir)
856+
857+
path := filepath.Clean("/" + strings.TrimSpace(req.Path))
858+
abs := filepath.Join(StateBaseDir, path)
859+
// Verify abs is within StateBaseDir to prevent path traversal
860+
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
861+
log.Printf("ContentDelete: path traversal attempt rejected: path=%q abs=%q", path, abs)
862+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
863+
return
864+
}
865+
log.Printf("ContentDelete: absolute path=%q", abs)
866+
867+
// Check if file exists
868+
if _, err := os.Stat(abs); os.IsNotExist(err) {
869+
log.Printf("ContentDelete: file not found: %q", abs)
870+
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
871+
return
872+
}
873+
874+
// Delete the file
875+
if err := os.Remove(abs); err != nil {
876+
log.Printf("ContentDelete: delete failed for %q: %v", abs, err)
877+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete file"})
878+
return
879+
}
880+
881+
log.Printf("ContentDelete: successfully deleted %q", abs)
882+
c.JSON(http.StatusOK, gin.H{"message": "file deleted successfully"})
883+
}

0 commit comments

Comments
 (0)