document deployment
This commit is contained in:
30
server/README.md
Normal file
30
server/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# Palace Server
|
||||
|
||||
Minimal Go API server with SQLite for Palace collaborative features.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd server
|
||||
go mod tidy
|
||||
go run main.go
|
||||
```
|
||||
|
||||
Server runs on port 3001 by default (set PORT env var to change).
|
||||
|
||||
## Resource Usage
|
||||
|
||||
- Single binary (~10MB)
|
||||
- SQLite database file
|
||||
- Minimal memory footprint
|
||||
- No external dependencies in production
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `GET /api/spaces` - List all spaces
|
||||
- `GET /api/spaces/{id}` - Get space by ID
|
||||
- `PUT /api/spaces/{id}` - Save/update space
|
||||
- `DELETE /api/spaces/{id}` - Delete space
|
||||
- `POST /api/spaces/{id}/notes` - Add note to space
|
||||
- `PATCH /api/spaces/{spaceId}/notes/{noteId}` - Update note
|
||||
- `DELETE /api/spaces/{spaceId}/notes/{noteId}` - Delete note
|
||||
8
server/go.mod
Normal file
8
server/go.mod
Normal file
@ -0,0 +1,8 @@
|
||||
module palace-server
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
)
|
||||
266
server/main.go
Normal file
266
server/main.go
Normal file
@ -0,0 +1,266 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type Note struct {
|
||||
ID string `json:"id"`
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type Space struct {
|
||||
ID string `json:"id"`
|
||||
Notes []Note `json:"notes"`
|
||||
}
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
func main() {
|
||||
dbPath := os.Getenv("DB_PATH")
|
||||
if dbPath == "" {
|
||||
dbPath = "./palace.db"
|
||||
}
|
||||
|
||||
var err error
|
||||
db, err = sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err := initDB(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
|
||||
r.HandleFunc("/api/spaces", getSpaces).Methods("GET")
|
||||
r.HandleFunc("/api/spaces/{id}", getSpace).Methods("GET")
|
||||
r.HandleFunc("/api/spaces/{id}", saveSpace).Methods("PUT")
|
||||
r.HandleFunc("/api/spaces/{id}", deleteSpace).Methods("DELETE")
|
||||
r.HandleFunc("/api/spaces/{id}/notes", addNote).Methods("POST")
|
||||
r.HandleFunc("/api/spaces/{spaceId}/notes/{noteId}", updateNote).Methods("PATCH")
|
||||
r.HandleFunc("/api/spaces/{spaceId}/notes/{noteId}", deleteNote).Methods("DELETE")
|
||||
|
||||
r.Use(corsMiddleware)
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "3001"
|
||||
}
|
||||
|
||||
log.Printf("Server starting on port %s", port)
|
||||
log.Fatal(http.ListenAndServe(":"+port, r))
|
||||
}
|
||||
|
||||
func initDB() error {
|
||||
query := `
|
||||
CREATE TABLE IF NOT EXISTS spaces (
|
||||
id TEXT PRIMARY KEY,
|
||||
notes TEXT DEFAULT '[]'
|
||||
);`
|
||||
_, err := db.Exec(query)
|
||||
return err
|
||||
}
|
||||
|
||||
func corsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func getSpaces(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := db.Query("SELECT id FROM spaces")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var spaces []Space
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
continue
|
||||
}
|
||||
spaces = append(spaces, Space{ID: id, Notes: []Note{}})
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(spaces)
|
||||
}
|
||||
|
||||
func getSpace(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
var notesJSON string
|
||||
err := db.QueryRow("SELECT notes FROM spaces WHERE id = ?", id).Scan(¬esJSON)
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "Space not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var notes []Note
|
||||
if err := json.Unmarshal([]byte(notesJSON), ¬es); err != nil {
|
||||
notes = []Note{}
|
||||
}
|
||||
|
||||
space := Space{ID: id, Notes: notes}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(space)
|
||||
}
|
||||
|
||||
func saveSpace(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
var space Space
|
||||
if err := json.NewDecoder(r.Body).Decode(&space); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
notesJSON, _ := json.Marshal(space.Notes)
|
||||
|
||||
_, err := db.Exec("INSERT OR REPLACE INTO spaces (id, notes) VALUES (?, ?)", id, string(notesJSON))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func deleteSpace(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
_, err := db.Exec("DELETE FROM spaces WHERE id = ?", id)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func addNote(w http.ResponseWriter, r *http.Request) {
|
||||
spaceId := mux.Vars(r)["id"]
|
||||
|
||||
var note Note
|
||||
if err := json.NewDecoder(r.Body).Decode(¬e); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var notesJSON string
|
||||
err := db.QueryRow("SELECT notes FROM spaces WHERE id = ?", spaceId).Scan(¬esJSON)
|
||||
if err != nil {
|
||||
http.Error(w, "Space not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var notes []Note
|
||||
json.Unmarshal([]byte(notesJSON), ¬es)
|
||||
notes = append(notes, note)
|
||||
|
||||
newNotesJSON, _ := json.Marshal(notes)
|
||||
_, err = db.Exec("UPDATE spaces SET notes = ? WHERE id = ?", string(newNotesJSON), spaceId)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func updateNote(w http.ResponseWriter, r *http.Request) {
|
||||
spaceId := mux.Vars(r)["spaceId"]
|
||||
noteId := mux.Vars(r)["noteId"]
|
||||
|
||||
var updates Note
|
||||
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var notesJSON string
|
||||
err := db.QueryRow("SELECT notes FROM spaces WHERE id = ?", spaceId).Scan(¬esJSON)
|
||||
if err != nil {
|
||||
http.Error(w, "Space not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var notes []Note
|
||||
json.Unmarshal([]byte(notesJSON), ¬es)
|
||||
|
||||
for i, note := range notes {
|
||||
if note.ID == noteId {
|
||||
if updates.X != 0 { notes[i].X = updates.X }
|
||||
if updates.Y != 0 { notes[i].Y = updates.Y }
|
||||
if updates.Content != "" { notes[i].Content = updates.Content }
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
newNotesJSON, _ := json.Marshal(notes)
|
||||
_, err = db.Exec("UPDATE spaces SET notes = ? WHERE id = ?", string(newNotesJSON), spaceId)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func deleteNote(w http.ResponseWriter, r *http.Request) {
|
||||
spaceId := mux.Vars(r)["spaceId"]
|
||||
noteId := mux.Vars(r)["noteId"]
|
||||
|
||||
var notesJSON string
|
||||
err := db.QueryRow("SELECT notes FROM spaces WHERE id = ?", spaceId).Scan(¬esJSON)
|
||||
if err != nil {
|
||||
http.Error(w, "Space not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var notes []Note
|
||||
json.Unmarshal([]byte(notesJSON), ¬es)
|
||||
|
||||
for i, note := range notes {
|
||||
if note.ID == noteId {
|
||||
notes = append(notes[:i], notes[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
newNotesJSON, _ := json.Marshal(notes)
|
||||
_, err = db.Exec("UPDATE spaces SET notes = ? WHERE id = ?", string(newNotesJSON), spaceId)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
Reference in New Issue
Block a user