diff --git a/README.md b/README.md index bcbf08a..14c967a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ A distributed management daemon designed to remotely configure system components, including: - Network connections: Manage network connections through NetworkManager's D-Bus interface +- Files: Manage files on the system - System hostname: Update the system's hostname - Authorized SSH keys: Manage the user's authorized_keys file to add, remove, or modify authorized SSH keys - System state: Restart and shutdown the system @@ -143,6 +144,7 @@ All endpoints except `/health` require authentication via an API token passed in | POST | `/hostname` | Set the hostname | | POST | `/users/{user}/keys` | Add an authorized SSH key | | DELETE | `/users/{user}/keys/{fingerprint}` | Remove an authorized SSH key | +| POST | `/system/file` | Upload a file to the system | | POST | `/system/restart` | Restart the system | | POST | `/system/shutdown` | Shutdown the system | | GET | `/cluster/members` | Get the cluster members | @@ -150,6 +152,7 @@ All endpoints except `/health` require authentication via an API token passed in | POST | `/cluster/leave` | Leave the cluster | | POST | `/cluster/event` | Send a cluster event | + ### Response Codes - 200: Success @@ -229,4 +232,20 @@ curl -X POST "http://rpi-test:8080/cluster/event" \ -d '{ "name": "restart" }' +``` + +### Upload a file + +This example will store Base64 encoded content to the target path. + +```bash +curl -X 'POST' \ + 'http://localhost:8080/system/file' \ + -H 'accept: application/json' \ + -H 'X-API-Token: 1234567890' \ + -H 'Content-Type: application/json' \ + -d '{ + "path": "/tmp/somefile", + "content": "Zm9vCg==" + }' ``` \ No newline at end of file diff --git a/api/rcond.yaml b/api/rcond.yaml index 4f62889..28dd5b8 100644 --- a/api/rcond.yaml +++ b/api/rcond.yaml @@ -494,6 +494,48 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /system/file: + post: + summary: Upload a file + description: Uploads a file to the system + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + path: + type: string + description: Path where the file will be stored + example: "/path/to/file" + content: + type: string + description: Base64 encoded content of the file + example: "SGVsbG8gV29ybGQh" + responses: + '200': + description: File uploaded successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + '400': + description: Invalid request payload + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /system/restart: post: summary: Restart system diff --git a/pkg/http/handle_system.go b/pkg/http/handle_system.go index 09b482c..ca5c7ff 100644 --- a/pkg/http/handle_system.go +++ b/pkg/http/handle_system.go @@ -1,6 +1,7 @@ package http import ( + "encoding/base64" "encoding/json" "net/http" @@ -26,3 +27,31 @@ func HandleShutdown(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "success"}) } + +func HandleFileUpload(w http.ResponseWriter, r *http.Request) { + // Parse the request body + var fileUpload struct { + Path string `json:"path"` + Content string `json:"content"` + } + if err := json.NewDecoder(r.Body).Decode(&fileUpload); err != nil { + WriteError(w, "Invalid request body", http.StatusBadRequest) + return + } + + // Decode base64 encoded content to bytes + contentBytes, err := base64.StdEncoding.DecodeString(fileUpload.Content) + if err != nil { + WriteError(w, "Failed to decode base64 content", http.StatusBadRequest) + return + } + + // Store the file + if err := system.StoreFile(fileUpload.Path, contentBytes); err != nil { + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} diff --git a/pkg/http/server.go b/pkg/http/server.go index 00a739a..828a477 100644 --- a/pkg/http/server.go +++ b/pkg/http/server.go @@ -80,6 +80,7 @@ func (s *Server) RegisterRoutes() { s.router.HandleFunc("/cluster/join", s.verifyToken(ClusterAgentHandler(s.clusterAgent, HandleClusterJoin))).Methods(http.MethodPost) s.router.HandleFunc("/cluster/leave", s.verifyToken(ClusterAgentHandler(s.clusterAgent, HandleClusterLeave))).Methods(http.MethodPost) s.router.HandleFunc("/cluster/event", s.verifyToken(ClusterAgentHandler(s.clusterAgent, HandleClusterEvent))).Methods(http.MethodPost) + s.router.HandleFunc("/system/file", s.verifyToken(HandleFileUpload)).Methods(http.MethodPost) } func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/system/file.go b/pkg/system/file.go new file mode 100644 index 0000000..424569b --- /dev/null +++ b/pkg/system/file.go @@ -0,0 +1,25 @@ +package system + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" +) + +// StoreFile stores a file on the file system at the given path. +// If the path does not exist, it will be created. +func StoreFile(path string, content []byte) error { + // Ensure the directory exists + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("failed to create directory: %v", err) + } + + // Write the file + if err := ioutil.WriteFile(path, content, 0644); err != nil { + return fmt.Errorf("failed to write file: %v", err) + } + + return nil +}