From 5c5db96f8fcad22dffd2d3d9f1a45b2d46129f87 Mon Sep 17 00:00:00 2001 From: Mason Payne Date: Tue, 4 Feb 2025 01:58:33 -0700 Subject: [PATCH] separate into two files, add file_command capabilities --- index.html | 348 ++++++++++++++++++++++++++++++++++++++ main.go | 485 +++++++++++++++++------------------------------------ 2 files changed, 500 insertions(+), 333 deletions(-) create mode 100644 index.html diff --git a/index.html b/index.html new file mode 100644 index 0000000..bab85e1 --- /dev/null +++ b/index.html @@ -0,0 +1,348 @@ + + + + + ChatGPT Clone - Conversations + + + + + + + + + +
+
+
Conversations
+ +
+
+
+
+ +
+
+ + +
+
+

Conversation

+
+
+
+
+
+ + +
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/main.go b/main.go index 96e1bfc..9f3ae00 100644 --- a/main.go +++ b/main.go @@ -2,12 +2,17 @@ package main import ( "bytes" + "encoding/base64" "encoding/json" + "errors" "html/template" "io" + "io/fs" "log" "net/http" "os" + "path/filepath" + "strings" "time" ) @@ -40,6 +45,8 @@ type ChatCompletionResponse struct { var ( openaiAPIKey string indexTmpl *template.Template + workDir string // Path to the workspace directory. + absWorkDir string // Absolute workspace path. ) func init() { @@ -47,16 +54,42 @@ func init() { if openaiAPIKey == "" { log.Fatal("Environment variable OPENAI_API_KEY is not set") } - // Parse the HTML template with responsive layout updates. - indexTmpl = template.Must(template.New("index").Parse(indexHTML)) + workDir = os.Getenv("WORK_DIR") + if workDir == "" { + workDir = "./workspace" + } + var err error + absWorkDir, err = filepath.Abs(workDir) + if err != nil { + log.Fatalf("Error getting absolute path for workspace: %v", err) + } + if err := os.MkdirAll(absWorkDir, os.ModePerm); err != nil { + log.Fatalf("Error creating workspace directory: %v", err) + } + + // Load the HTML template from an external file. + tmplBytes, err := os.ReadFile("index.html") + if err != nil { + log.Fatalf("Error reading index.html: %v", err) + } + indexTmpl, err = template.New("index").Parse(string(tmplBytes)) + if err != nil { + log.Fatalf("Error parsing index.html template: %v", err) + } } func main() { http.HandleFunc("/", indexHandler) http.HandleFunc("/chat", chatHandler) http.HandleFunc("/title", titleHandler) + + // File system endpoints (sandboxed within absWorkDir) + http.HandleFunc("/files", filesListHandler) // GET: list files. + http.HandleFunc("/file", fileHandler) // GET for read; POST for write. + addr := ":8080" log.Println("Server starting on", addr) + log.Println("Workspace directory:", absWorkDir) log.Fatal(http.ListenAndServe(addr, nil)) } @@ -141,7 +174,7 @@ func titleHandler(w http.ResponseWriter, r *http.Request) { titleRequestMessages := []Message{ { Role: "system", - Content: "You are an AI that generates conversation titles. Based on the following conversation, please provide a short, descriptive title of no more than five words with no extra commentary.", + Content: "You are an AI that generates conversation titles. Based on the conversation, please provide a short, descriptive title (five words maximum) with no extra commentary.", }, } titleRequestMessages = append(titleRequestMessages, clientRequest.Messages...) @@ -193,333 +226,119 @@ func titleHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(responsePayload) } -const indexHTML = ` - - - - - ChatGPT Clone - Conversations - - - - - - - - - -
-
-
Conversations
- -
-
-
- -
- - - -
- -
- - -
-
-

Conversation

-
-
-
-
-
- - -
-
-
-
-
- - - - - - - - - -` +// +// File system endpoints +// + +// filesListHandler lists all files (recursively) in the workspace. +func filesListHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Only GET allowed", http.StatusMethodNotAllowed) + return + } + var files []string + err := filepath.WalkDir(absWorkDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() { + rel, err := filepath.Rel(absWorkDir, path) + if err != nil { + return err + } + files = append(files, rel) + } + return nil + }) + if err != nil { + http.Error(w, "Error listing files: "+err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"files": files}) +} + +// fileHandler supports file read (GET) and write (POST) operations. +func fileHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + handleFileRead(w, r) + case http.MethodPost: + handleFileWrite(w, r) + default: + http.Error(w, "Only GET and POST allowed", http.StatusMethodNotAllowed) + } +} + +func handleFileRead(w http.ResponseWriter, r *http.Request) { + filename := r.URL.Query().Get("filename") + if filename == "" { + http.Error(w, "Missing filename", http.StatusBadRequest) + return + } + fullPath, err := secureFilePath(filename) + if err != nil { + http.Error(w, "Invalid filename: "+err.Error(), http.StatusBadRequest) + return + } + data, err := os.ReadFile(fullPath) + if err != nil { + http.Error(w, "Error reading file: "+err.Error(), http.StatusInternalServerError) + return + } + encoded := base64.StdEncoding.EncodeToString(data) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"content_b64": encoded}) +} + +func handleFileWrite(w http.ResponseWriter, r *http.Request) { + var payload struct { + Filename string `json:"filename"` + ContentB64 string `json:"content_b64"` + } + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) + return + } + if payload.Filename == "" || payload.ContentB64 == "" { + http.Error(w, "Filename and content_b64 are required", http.StatusBadRequest) + return + } + decoded, err := base64.StdEncoding.DecodeString(payload.ContentB64) + if err != nil { + http.Error(w, "Error decoding Base64 content: "+err.Error(), http.StatusBadRequest) + return + } + fullPath, err := secureFilePath(payload.Filename) + if err != nil { + http.Error(w, "Invalid filename: "+err.Error(), http.StatusBadRequest) + return + } + dir := filepath.Dir(fullPath) + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + http.Error(w, "Error creating directory: "+err.Error(), http.StatusInternalServerError) + return + } + if err := os.WriteFile(fullPath, decoded, 0644); err != nil { + http.Error(w, "Error writing file: "+err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +func secureFilePath(relPath string) (string, error) { + relPath = filepath.ToSlash(relPath) + if strings.Contains(relPath, "..") { + return "", errors.New("relative paths outside the workspace are not allowed") + } + fullPath := filepath.Join(absWorkDir, relPath) + absFullPath, err := filepath.Abs(fullPath) + if err != nil { + return "", err + } + if !strings.HasPrefix(absFullPath, absWorkDir) { + return "", errors.New("attempt to access files outside the workspace") + } + return absFullPath, nil +}