diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95c00ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +logs.log +crud-api +crud-api.exe \ No newline at end of file diff --git a/README.md b/README.md index 9dc9a5e..781d913 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,9 @@ # CRUD api -## A simple CRUD api written in Go`s standart library +## A simple CRUD api written in Go -API has implementation of "GET", "POST", "PUT", "DELETE" http methods, allowing to Create, Read, Update and Delete json objects in database. +API has implementation of "GET", "POST", "PUT", "DELETE" http methods, allowing to Create, Read, Update and Delete objects in database via json input. -The structure of a basic json object represents Go struct "RandomData" with exported fields "title" (string), "text" (string) and unexported fields "DateCreated" (time.Time), "LastUpdated" (time.Time) and ID (int64) +--- -Example of a single object stored in a json database : { - "ID": 1618064651615612586, - "DateCreated": "2021-04-10T14:24:11.615612068Z", - "LastUpdated": "2021-04-10T14:24:11.615612068Z", - "title": "Title", - "text": "text" - } +Just practicing things -This project was my first take on such thing. The goal was to create a basic working example and it looks like that I`ve achieved that goal. diff --git a/database/database.json b/database/database.json deleted file mode 100644 index 05590d1..0000000 --- a/database/database.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "ID": 1618064636424100339, - "DateCreated": "2021-04-10T14:23:56.424100065Z", - "LastUpdated": "2021-04-10T14:23:56.424100065Z", - "title": "FUMO?", - "text": "FUMO" - }, - { - "ID": 1618064651615612586, - "DateCreated": "2021-04-10T14:24:11.615612068Z", - "LastUpdated": "2021-04-10T14:24:11.615612068Z", - "title": "Title", - "text": "text" - }, - { - "ID": 1618065099475731301, - "DateCreated": "2021-04-10T14:31:39.47573104Z", - "LastUpdated": "2021-04-10T14:31:39.47573104Z", - "title": "link", - "text": "https://youtu.be/dQw4w9WgXcQ" - } -] \ No newline at end of file diff --git a/dbHandle/create.go b/dbHandle/create.go new file mode 100644 index 0000000..7661d85 --- /dev/null +++ b/dbHandle/create.go @@ -0,0 +1,49 @@ +package dbhandle + +import ( + "database/sql" + "os" + "path/filepath" + + _ "github.com/mattn/go-sqlite3" +) + +// full path to the local database that has been created by `CreateLocalDB` +var dbpath string + +// Creates a local database file in the same directory +// as executable if does not exist already. Creates a table "randomdata" with +// such structure: (id PRIMARY KEY, title TEXT, text TEXT), which +// represents underlying fields of `RandomData` +func CreateLocalDB(dbName string) (*DB, error) { + // double check if dbName is actually just a name, not a path + dbName = filepath.Base(dbName) + + executablePath, err := os.Executable() + if err != nil { + return nil, err + } + exeDir := filepath.Dir(executablePath) + + dbpath = filepath.Join(exeDir, dbName) + + // create db if does not exist + dbfile, err := os.OpenFile(dbpath, os.O_CREATE, os.ModePerm) + if err != nil { + return nil, err + } + dbfile.Close() + + // create table that suits `RandomData` struct + db, err := sql.Open(drivername, dbpath) + if err != nil { + return nil, err + } + + _, err = db.Exec("CREATE TABLE IF NOT EXISTS randomdata (id INTEGER PRIMARY KEY, title TEXT, text TEXT)") + if err != nil { + return nil, err + } + + return &DB{db}, nil +} diff --git a/dbHandle/db.go b/dbHandle/db.go new file mode 100644 index 0000000..84ae967 --- /dev/null +++ b/dbHandle/db.go @@ -0,0 +1,9 @@ +package dbhandle + +import "database/sql" + +type DB struct { + *sql.DB +} + +const drivername string = "sqlite3" diff --git a/dbHandle/handle.go b/dbHandle/handle.go new file mode 100644 index 0000000..441b15f --- /dev/null +++ b/dbHandle/handle.go @@ -0,0 +1,29 @@ +package dbhandle + +import ( + "net/http" + + randomdata "github.com/Unbewohnte/crud-api/randomData" + _ "github.com/mattn/go-sqlite3" +) + +func (db *DB) GetEverything() ([]*randomdata.RandomData, error) + +func (db *DB) GetSpecific() (*randomdata.RandomData, error) + +func (db *DB) DeleteSpecific() error + +func (db *DB) PatchSpecific() error + +func (db *DB) Create(rd randomdata.RandomData) error { + _, err := db.Exec("INSERT INTO randomdata (title, text) VALUES (?, ?)", rd.Title, rd.Text) + if err != nil { + return err + } + + return nil +} + +func (db *DB) HandleSpecificWeb(w http.ResponseWriter, r *http.Request) + +func (db *DB) HandleGlobalWeb(w http.ResponseWriter, r *http.Request) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..08cc4bb --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/Unbewohnte/crud-api + +go 1.17 + +require github.com/mattn/go-sqlite3 v1.14.8 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/logs/logs.go b/logs/logs.go new file mode 100644 index 0000000..1db05b8 --- /dev/null +++ b/logs/logs.go @@ -0,0 +1,26 @@ +package logs + +import ( + "log" + "os" + "path/filepath" +) + +// Create a logfile in the same directory as executable and set output +// of `log` package to it +func SetUp() error { + executablePath, err := os.Executable() + if err != nil { + return err + } + exeDir := filepath.Dir(executablePath) + + logfile, err := os.Create(filepath.Join(exeDir, "logs.log")) + if err != nil { + return err + } + + log.SetOutput(logfile) + + return nil +} diff --git a/main.go b/main.go index 77ec167..36e7233 100644 --- a/main.go +++ b/main.go @@ -1,370 +1,168 @@ package main import ( - "encoding/json" + "flag" "fmt" - "io" "log" "net/http" - "os" - "path/filepath" - "strconv" - "strings" "time" -) - -type RandomData struct { - // unexported for json - ID int64 - DateCreated time.Time - LastUpdated time.Time - // exported for json - Title string `json:"title"` - Text string `json:"text"` -} - -type randomDataHandler struct { - dbFilepath string -} -func InitLogs() { - var logsDir string = filepath.Join(".", "logs") - - err := os.MkdirAll(logsDir, os.ModePerm) - if err != nil { - panic(err) - } - logfile, err := os.Create(filepath.Join(logsDir, "logs.log")) - if err != nil { - panic(err) - } - log.SetOutput(logfile) -} - -func homepage(w http.ResponseWriter, r *http.Request) { - helpMessage := ` -

CRUD api in Go's standart library

- - ` - fmt.Fprint(w, helpMessage) -} + dbhandle "github.com/Unbewohnte/crud-api/dbHandle" + "github.com/Unbewohnte/crud-api/logs" +) -func newDatabaseHandler() *randomDataHandler { - dbDirpath := filepath.Join(".", "database") - err := os.MkdirAll(dbDirpath, os.ModePerm) - if err != nil { - panic(err) - } - dbFilepath := filepath.Join(dbDirpath, "database.json") +var ( + port *uint = flag.Uint("port", 8080, "Specifies a port on which the helping page will be served") + dbname string = "database.db" +) - dbFile, err := os.OpenFile(dbFilepath, os.O_CREATE, os.ModePerm) +func init() { + // set up logs, parse flags + err := logs.SetUp() if err != nil { panic(err) } - defer dbFile.Close() - - log.Println("Successfully created new database handler") - - return &randomDataHandler{ - dbFilepath: dbFilepath, - } -} - -func (dbHandler *randomDataHandler) writeRandomData(newData RandomData) error { - dbBytes, err := dbHandler.readDatabase() - if err != nil { - log.Println("Error reading db (writeRandomData) : ", err) - } - - var db []RandomData - - err = json.Unmarshal(dbBytes, &db) - if err != nil { - log.Println("Error unmarshalling db (writeRandomData) : ", err) - } - - db = append(db, newData) - - dbFile, err := os.OpenFile(dbHandler.dbFilepath, os.O_WRONLY, 0644) - if err != nil { - log.Println("Error opening db file (writeRandomData) : ", err) - } - defer dbFile.Close() - - jsonBytes, err := json.MarshalIndent(db, "", " ") - if err != nil { - log.Println("Error marshalling db (writeRandomData) : ", err) - } - - dbFile.Write(jsonBytes) - - return nil -} - -func (dbHandler *randomDataHandler) readDatabase() ([]byte, error) { - dbBytes, err := os.ReadFile(dbHandler.dbFilepath) - if err != nil { - log.Println("Error reading db (readDatabase) : ", err) - return nil, err - } - return dbBytes, nil -} - -func (dbHandler *randomDataHandler) removeRandomData(id int64) error { - dbBytes, err := dbHandler.readDatabase() - if err != nil { - return err - } - - var db []RandomData - err = json.Unmarshal(dbBytes, &db) - if err != nil { - return err - } - var counter int64 = 0 - for _, randomData := range db { - if id == randomData.ID { - db = append(db[:counter], db[counter+1:]...) - err = dbHandler.writeDB(db) - if err != nil { - return err - } - } - counter++ - } - return nil + flag.Parse() } -func (dbHandler *randomDataHandler) writeDB(db []RandomData) error { - jsonEncodedDB, err := json.MarshalIndent(db, "", " ") - if err != nil { - return err - } - - dbFile, err := os.OpenFile(dbHandler.dbFilepath, os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - return err - } - defer dbFile.Close() - - dbFile.Write(jsonEncodedDB) - - return nil -} - -func (dbHandler *randomDataHandler) get(w http.ResponseWriter, r *http.Request) { - dbBytes, err := dbHandler.readDatabase() - if err != nil { - log.Println("Error reading db (get) : ", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Write(dbBytes) -} - -func (dbHandler *randomDataHandler) create(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("content-type") != "application/json" { - w.WriteHeader(http.StatusUnsupportedMediaType) - w.Write([]byte(fmt.Sprintf("Got `%s` instead of `application/json`", r.Header.Get("content-type")))) - return - } - requestBody, err := io.ReadAll(r.Body) - if err != nil { - log.Println("Error reading http request (create) : ", err) - w.WriteHeader(http.StatusBadRequest) - return - } - defer r.Body.Close() - - var newRandomData RandomData - err = json.Unmarshal(requestBody, &newRandomData) - if err != nil { - log.Printf("Error unmarshalling http request (create) : %q \n", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - newRandomData.DateCreated = time.Now().UTC() - newRandomData.LastUpdated = newRandomData.DateCreated - newRandomData.ID = time.Now().UTC().UnixNano() - - err = dbHandler.writeRandomData(newRandomData) - if err != nil { - log.Println("Error writing RandomData (create): ", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - - log.Println("Successfuly added to db : ", newRandomData) -} - -func (dbHandler *randomDataHandler) getSpecificRandomData(w http.ResponseWriter, r *http.Request) { - givenID := strings.Split(r.URL.String(), "/")[2] - - dbBytes, err := dbHandler.readDatabase() - if err != nil { - log.Println("Error reading db (getSpecificRandomData) : ", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - var db []RandomData - - err = json.Unmarshal(dbBytes, &db) - if err != nil { - log.Println("Error unmarshalling database (getSpecificRandomData) : ", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - int64GivenID, _ := strconv.ParseInt(givenID, 10, 64) - for _, randomData := range db { - if int64GivenID == randomData.ID { - response, err := json.MarshalIndent(randomData, "", " ") - if err != nil { - log.Println("Error marshaling response(getSpecificRandomData) : ", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Write(response) - return - } - } - w.WriteHeader(http.StatusNotFound) -} - -func (dbHandler *randomDataHandler) updateSpecificRandomData(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("content-type") != "application/json" { - w.WriteHeader(http.StatusUnsupportedMediaType) - w.Write([]byte(fmt.Sprintf("Got `%s` instead of `application/json`", r.Header.Get("content-type")))) - return - } - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - log.Println("Error reading http request (create) : ", err) - w.WriteHeader(http.StatusBadRequest) - r.Body.Close() - return - } - defer r.Body.Close() - - var givenUpdatedRandomData RandomData - err = json.Unmarshal(requestBody, &givenUpdatedRandomData) - if err != nil { - log.Println("Error unmarshalling request body (updateSpecificRandomData) : ", err) - return - } - - givenID := strings.Split(r.URL.String(), "/")[2] - - dbBytes, err := dbHandler.readDatabase() - if err != nil { - log.Println("Error reading db (updateSpecificRandomData) : ", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - var db []RandomData - - err = json.Unmarshal(dbBytes, &db) - if err != nil { - log.Println("Error unmarshalling database (update) : ", err) - return - } - - int64GivenID, _ := strconv.ParseInt(givenID, 10, 64) - var counter int64 - for _, randomData := range db { - if int64GivenID == randomData.ID { - var updatedRandomData RandomData - - updatedRandomData = givenUpdatedRandomData - updatedRandomData.ID = randomData.ID - updatedRandomData.DateCreated = randomData.DateCreated - updatedRandomData.LastUpdated = time.Now().UTC() - - dbHandler.removeRandomData(int64GivenID) - dbHandler.writeRandomData(updatedRandomData) - - log.Printf("Successfully updated RandomData with id %v \n", updatedRandomData.ID) - return - } - counter++ - } - - w.WriteHeader(http.StatusNotFound) -} - -func (dbHandler *randomDataHandler) deleteSpecificRandomData(w http.ResponseWriter, r *http.Request) { - givenID := strings.Split(r.URL.String(), "/")[2] - - int64GivenID, _ := strconv.ParseInt(givenID, 10, 64) - - err := dbHandler.removeRandomData(int64GivenID) +func main() { + // create a local db file + db, err := dbhandle.CreateLocalDB(dbname) if err != nil { - log.Println("Error removing RandomData (deleteSpecificRandomData) : ", err) - w.WriteHeader(http.StatusInternalServerError) - return + log.Fatalf("error setting up a database: %s", err) } - w.WriteHeader(http.StatusOK) - - log.Printf("Successfully deleted RandomData with id %v \n", int64GivenID) -} - -func (dbHandler *randomDataHandler) handle(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - dbHandler.get(w, r) - case "POST": - dbHandler.create(w, r) - default: - w.WriteHeader(http.StatusMethodNotAllowed) - } -} - -func (dbHandler *randomDataHandler) handleSpecific(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - dbHandler.getSpecificRandomData(w, r) - case "PUT": - dbHandler.updateSpecificRandomData(w, r) - case "DELETE": - dbHandler.deleteSpecificRandomData(w, r) - default: - w.WriteHeader(http.StatusMethodNotAllowed) - } -} - -func init() { - InitLogs() -} - -func main() { - - databaseHandler := newDatabaseHandler() - - servemux := http.NewServeMux() - servemux.HandleFunc("/", homepage) - servemux.HandleFunc("/randomdata", databaseHandler.handle) - servemux.HandleFunc("/randomdata/", databaseHandler.handleSpecific) + mux := http.NewServeMux() + mux.HandleFunc("/", helpPage) + mux.HandleFunc("/randomdata", db.HandleGlobalWeb) + mux.HandleFunc("/randomdata/", db.HandleSpecificWeb) server := &http.Server{ - Addr: ":8000", - Handler: servemux, + Addr: fmt.Sprintf(":%d", *port), + Handler: mux, ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, } + log.Printf("Starting on port %d\n", *port) + fmt.Printf("Starting on port %d\n", *port) + log.Fatal(server.ListenAndServe()) } + +func helpPage(w http.ResponseWriter, r *http.Request) { + helpMessage := ` +

CRUD api

+ + ` + fmt.Fprint(w, helpMessage) +} + +// func (dbHandler *randomDataHandler) updateSpecificRandomData(w http.ResponseWriter, r *http.Request) { +// if r.Header.Get("content-type") != "application/json" { +// w.WriteHeader(http.StatusUnsupportedMediaType) +// w.Write([]byte(fmt.Sprintf("Got `%s` instead of `application/json`", r.Header.Get("content-type")))) +// return +// } + +// requestBody, err := io.ReadAll(r.Body) +// if err != nil { +// log.Println("Error reading http request (create) : ", err) +// w.WriteHeader(http.StatusBadRequest) +// r.Body.Close() +// return +// } +// defer r.Body.Close() + +// var givenUpdatedRandomData RandomData +// err = json.Unmarshal(requestBody, &givenUpdatedRandomData) +// if err != nil { +// log.Println("Error unmarshalling request body (updateSpecificRandomData) : ", err) +// return +// } + +// givenID := strings.Split(r.URL.String(), "/")[2] + +// dbBytes, err := dbHandler.readDatabase() +// if err != nil { +// log.Println("Error reading db (updateSpecificRandomData) : ", err) +// w.WriteHeader(http.StatusInternalServerError) +// return +// } +// var db []RandomData + +// err = json.Unmarshal(dbBytes, &db) +// if err != nil { +// log.Println("Error unmarshalling database (update) : ", err) +// return +// } + +// int64GivenID, _ := strconv.ParseInt(givenID, 10, 64) +// var counter int64 +// for _, randomData := range db { +// if int64GivenID == randomData.ID { +// var updatedRandomData RandomData + +// updatedRandomData = givenUpdatedRandomData +// updatedRandomData.ID = randomData.ID +// updatedRandomData.DateCreated = randomData.DateCreated +// updatedRandomData.LastUpdated = time.Now().UTC() + +// dbHandler.removeRandomData(int64GivenID) +// dbHandler.writeRandomData(updatedRandomData) + +// log.Printf("Successfully updated RandomData with id %v \n", updatedRandomData.ID) +// return +// } +// counter++ +// } + +// w.WriteHeader(http.StatusNotFound) +// } + +// func (dbHandler *randomDataHandler) deleteSpecificRandomData(w http.ResponseWriter, r *http.Request) { +// givenID := strings.Split(r.URL.String(), "/")[2] + +// int64GivenID, _ := strconv.ParseInt(givenID, 10, 64) + +// err := dbHandler.removeRandomData(int64GivenID) +// if err != nil { +// log.Println("Error removing RandomData (deleteSpecificRandomData) : ", err) +// w.WriteHeader(http.StatusInternalServerError) +// return +// } + +// w.WriteHeader(http.StatusOK) + +// log.Printf("Successfully deleted RandomData with id %v \n", int64GivenID) +// } + +// func (dbHandler *randomDataHandler) handle(w http.ResponseWriter, r *http.Request) { +// switch r.Method { +// case "GET": +// dbHandler.get(w, r) +// case "POST": +// dbHandler.create(w, r) +// default: +// w.WriteHeader(http.StatusMethodNotAllowed) +// } +// } + +// func (dbHandler *randomDataHandler) handleSpecific(w http.ResponseWriter, r *http.Request) { +// switch r.Method { +// case "GET": +// dbHandler.getSpecificRandomData(w, r) +// case "PUT": +// dbHandler.updateSpecificRandomData(w, r) +// case "DELETE": +// dbHandler.deleteSpecificRandomData(w, r) +// default: +// w.WriteHeader(http.StatusMethodNotAllowed) +// } +// } diff --git a/randomData/randomData.go b/randomData/randomData.go new file mode 100644 index 0000000..ed921bf --- /dev/null +++ b/randomData/randomData.go @@ -0,0 +1,35 @@ +package randomdata + +import ( + "encoding/json" + "time" +) + +// The `bridge` between input values and a record in db +type RandomData struct { + DateCreated int64 + LastUpdated int64 + Title string `json:"title"` + Text string `json:"text"` +} + +// Create a new `RandomData` +func New(title string, text string) *RandomData { + return &RandomData{ + DateCreated: time.Now().UTC().Unix(), + LastUpdated: time.Now().UTC().Unix(), + Title: title, + Text: text, + } +} + +// Unmarshal `RandomData` from Json encoded bytes +func FromJson(jsonBytes []byte) (*RandomData, error) { + var randomData RandomData + err := json.Unmarshal(jsonBytes, &randomData) + if err != nil { + return nil, err + } + + return &randomData, nil +}