328 lines
7.1 KiB
Go
328 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
|
|
"github.com/julienschmidt/httprouter"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
const configPath = "config.json"
|
|
|
|
var db *sql.DB
|
|
|
|
type config struct {
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
}
|
|
|
|
type note struct {
|
|
Id int64 `json:"id"`
|
|
Title string `json:"title"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
func httpError(err error, statusCode int, message string, w http.ResponseWriter) bool {
|
|
if err != nil {
|
|
log.Println(message, err)
|
|
writeError(w, statusCode, message)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, statusCode int, message string) {
|
|
type errorResponse struct {
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
response := errorResponse{Message: message}
|
|
json, err := json.Marshal(response)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
w.Write(json)
|
|
}
|
|
|
|
func getNotes(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
|
|
res, err := db.Query(`SELECT Id, Title, Content FROM Notes`)
|
|
if httpError(err, http.StatusInternalServerError, "Failed to load notes", w) {
|
|
return
|
|
}
|
|
|
|
defer res.Close()
|
|
|
|
var notes []note = []note{}
|
|
|
|
for res.Next() {
|
|
if httpError(res.Err(), http.StatusInternalServerError, "Failed to load notes", w) {
|
|
return
|
|
}
|
|
|
|
var note note
|
|
res.Scan(¬e.Id, ¬e.Title, ¬e.Content)
|
|
notes = append(notes, note)
|
|
}
|
|
|
|
json, err := json.Marshal(notes)
|
|
if httpError(err, http.StatusInternalServerError, "Failed to create JSON", w) {
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(json)
|
|
}
|
|
|
|
func postNotes(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
|
|
data, err := io.ReadAll(req.Body)
|
|
if httpError(err, http.StatusBadRequest, "Failed to read data", w) {
|
|
return
|
|
}
|
|
|
|
var note note
|
|
err = json.Unmarshal(data, ¬e)
|
|
if httpError(err, http.StatusBadRequest, "Invalid request", w) {
|
|
return
|
|
}
|
|
|
|
if len(note.Title) == 0 {
|
|
writeError(w, http.StatusBadRequest, "Missing note title")
|
|
return
|
|
}
|
|
|
|
stmt, err := db.Prepare(`INSERT INTO Notes(Title, Content) VALUES(?, ?) RETURNING Id`)
|
|
if httpError(err, http.StatusInternalServerError, "Failed to store note", w) {
|
|
return
|
|
}
|
|
|
|
defer stmt.Close()
|
|
|
|
res := stmt.QueryRow(note.Title, note.Content)
|
|
err = res.Scan(¬e.Id)
|
|
if httpError(err, http.StatusInternalServerError, "Failed to store note", w) {
|
|
return
|
|
}
|
|
|
|
json, err := json.Marshal(note)
|
|
if httpError(err, http.StatusInternalServerError, "Failed to create JSON", w) {
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
w.Write(json)
|
|
}
|
|
|
|
func getNote(w http.ResponseWriter, req *http.Request, p httprouter.Params) {
|
|
id, err := strconv.ParseInt(p.ByName("id"), 10, 64)
|
|
if httpError(err, http.StatusBadRequest, "Invalid id", w) {
|
|
return
|
|
}
|
|
|
|
stmt, err := db.Prepare(`SELECT Title, Content FROM Notes WHERE Id = ?`)
|
|
if httpError(err, http.StatusInternalServerError, "Failed to get note", w) {
|
|
return
|
|
}
|
|
|
|
defer stmt.Close()
|
|
|
|
res := stmt.QueryRow(id)
|
|
|
|
var note note = note{Id: id}
|
|
|
|
err = res.Scan(¬e.Title, ¬e.Content)
|
|
if err == sql.ErrNoRows {
|
|
writeError(w, http.StatusNotFound, "Note doesn't exist")
|
|
return
|
|
}
|
|
|
|
if httpError(err, http.StatusInternalServerError, "Failed to get note", w) {
|
|
return
|
|
}
|
|
|
|
json, err := json.Marshal(note)
|
|
if httpError(err, http.StatusInternalServerError, "Failed to create JSON", w) {
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(json)
|
|
}
|
|
|
|
func putNote(w http.ResponseWriter, req *http.Request, p httprouter.Params) {
|
|
id, err := strconv.ParseInt(p.ByName("id"), 10, 64)
|
|
if httpError(err, http.StatusBadRequest, "Invalid id", w) {
|
|
return
|
|
}
|
|
|
|
data, err := io.ReadAll(req.Body)
|
|
if httpError(err, http.StatusBadRequest, "Failed to read data", w) {
|
|
return
|
|
}
|
|
|
|
var note note
|
|
err = json.Unmarshal(data, ¬e)
|
|
if httpError(err, http.StatusBadRequest, "Invalid request", w) {
|
|
return
|
|
}
|
|
|
|
if len(note.Title) == 0 {
|
|
writeError(w, http.StatusBadRequest, "Missing note title")
|
|
return
|
|
}
|
|
|
|
stmt, err := db.Prepare(`UPDATE Notes SET Title = ?, Content = ? WHERE Id = ?`)
|
|
if httpError(err, http.StatusInternalServerError, "Failed to update note", w) {
|
|
return
|
|
}
|
|
|
|
defer stmt.Close()
|
|
|
|
res, err := stmt.Exec(note.Title, note.Content, id)
|
|
if httpError(err, http.StatusInternalServerError, "Failed to update note", w) {
|
|
return
|
|
}
|
|
|
|
updated, err := res.RowsAffected()
|
|
if httpError(err, http.StatusInternalServerError, "Failed to update note", w) {
|
|
return
|
|
}
|
|
|
|
if updated == 0 {
|
|
writeError(w, http.StatusNotFound, "Note doesn't exist")
|
|
return
|
|
}
|
|
|
|
note.Id = id
|
|
json, err := json.Marshal(note)
|
|
if httpError(err, http.StatusInternalServerError, "Failed to create JSON", w) {
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(json)
|
|
}
|
|
|
|
func deleteNote(w http.ResponseWriter, req *http.Request, p httprouter.Params) {
|
|
id, err := strconv.ParseInt(p.ByName("id"), 10, 64)
|
|
if httpError(err, http.StatusBadRequest, "Invalid id", w) {
|
|
return
|
|
}
|
|
|
|
stmt, err := db.Prepare(`DELETE FROM Notes WHERE Id = ?`)
|
|
if httpError(err, http.StatusInternalServerError, "Failed to delete note", w) {
|
|
return
|
|
}
|
|
|
|
defer stmt.Close()
|
|
|
|
res, err := stmt.Exec(id)
|
|
if httpError(err, http.StatusInternalServerError, "Failed to delete note", w) {
|
|
return
|
|
}
|
|
|
|
deleted, err := res.RowsAffected()
|
|
if httpError(err, http.StatusInternalServerError, "Failed to delete note", w) {
|
|
return
|
|
}
|
|
|
|
if deleted == 0 {
|
|
writeError(w, http.StatusNotFound, "Note doesn't exist")
|
|
return
|
|
}
|
|
}
|
|
|
|
func fileExists(path string) (bool, error) {
|
|
_, err := os.Stat(path)
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
|
|
return false, err
|
|
}
|
|
|
|
func loadConfig(path string) (config, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return config{}, err
|
|
}
|
|
|
|
var cfg config = config{}
|
|
err = json.Unmarshal(raw, &cfg)
|
|
if err != nil {
|
|
return config{}, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func main() {
|
|
log.Println("Loading config")
|
|
|
|
exists, err := fileExists(configPath)
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
if !exists {
|
|
// Config etc
|
|
json, err := json.MarshalIndent(config{
|
|
Host: "localhost",
|
|
Port: 8080,
|
|
}, "", "\t")
|
|
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
os.WriteFile(configPath, json, 0o664)
|
|
log.Println("Created config.json, edit accordingly")
|
|
return
|
|
}
|
|
|
|
config, err := loadConfig(configPath)
|
|
if err != nil {
|
|
log.Fatalln(config)
|
|
}
|
|
|
|
log.Println("Starting NoteServer")
|
|
|
|
log.Println("Loading database")
|
|
|
|
db, err = sql.Open("sqlite3", "./notes.sqlite")
|
|
if err != nil {
|
|
log.Fatalln("Failed to load db:", err)
|
|
}
|
|
|
|
log.Println("Creating missing tables")
|
|
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS Notes(Id INTEGER PRIMARY KEY AUTOINCREMENT, Title TEXT, Content TEXT)`)
|
|
if err != nil {
|
|
log.Fatalln("Failed to create table:", err)
|
|
}
|
|
|
|
log.Println("Listening")
|
|
|
|
router := httprouter.New()
|
|
router.GET("/api/notes", getNotes)
|
|
router.POST("/api/notes", postNotes)
|
|
router.GET("/api/notes/:id", getNote)
|
|
router.PUT("/api/notes/:id", putNote)
|
|
router.DELETE("/api/notes/:id", deleteNote)
|
|
|
|
http.ListenAndServe(":8080", router) // TODO: configure host, port, TLS
|
|
}
|