From a21c17d6d9e915a4a46c0df7704c976a2aa965bc Mon Sep 17 00:00:00 2001 From: ION606 Date: Sat, 8 Feb 2025 13:05:53 -0500 Subject: [PATCH] initial code commit --- .gitignore | 5 ++ Makefile | 13 ++++ README.md | 157 ++++++++++++++++++++++++++++++++++++++++- batched-server/main.go | 111 +++++++++++++++++++++++++++++ sqlite-server/go.mod | 16 +++++ sqlite-server/main.go | 44 ++++++++++++ test/batched.sh | 53 ++++++++++++++ test/sqlite.sh | 58 +++++++++++++++ 8 files changed, 455 insertions(+), 2 deletions(-) create mode 100644 Makefile create mode 100644 batched-server/main.go create mode 100644 sqlite-server/go.mod create mode 100644 sqlite-server/main.go create mode 100644 test/batched.sh create mode 100644 test/sqlite.sh diff --git a/.gitignore b/.gitignore index 6f72f89..f67b0a6 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,8 @@ go.work.sum # env file .env + +go.sum +*.db +*.txt +*.csv diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2d2b3ba --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +.PHONY: run-batched run-sqlite setup-sqlite + +run-batched: + cd batched-server && go run main.go + +run-sqlite: setup-sqlite + cd sqlite-server && go run main.go + +setup-sqlite: + @if [ ! -f sqlite-server/go.mod ]; then \ + cd sqlite-server && go mod init sqlite-server; \ + fi + cd sqlite-server && go get modernc.org/sqlite \ No newline at end of file diff --git a/README.md b/README.md index 2c3a827..c943b77 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,155 @@ -# forms-server -An extremely lightweight server made to collect email form submissions and written in Go +# Forms Server + +An extremely lightweight server written in Go to collect email form submissions. This project provides two independent server implementations: + +1. **Batched Write Server**: A dependency-free server that stores emails in a CSV file. +2. **SQLite Server**: A server that uses SQLite for reliable and structured email storage. + +Both servers are designed to be simple, efficient, and easy to deploy. + +--- + +## Features + +- **Batched Write Server**: + - No external dependencies. + - Stores emails in a CSV file (`emails.csv`). + - Batches writes to reduce disk I/O. + - Lightweight and minimalistic. + +- **SQLite Server**: + - Uses SQLite for structured and reliable email storage. + - Prevents duplicate emails with a unique constraint. + - Easy to query and manage stored emails. + - Slightly more robust and feature-rich. + +--- + +## Getting Started + +### Prerequisites + +- Go 1.20 or higher. +- For the SQLite Server, the `modernc.org/sqlite` dependency will be installed automatically. + +--- + +### Running the Servers +1.0 Clone the repository: + ```bash + git clone https://github.com/your-username/forms-server.git + cd forms-server + ``` + +2.0 Using the Makefile: + - ```bash + make run-batched # for CSV-based + ``` + - ```bash + make run-sqlite # for SQLite-based + ``` + +2.1 **Batched Write Server**: + - Navigate to the `batched-server` directory: + ```bash + cd batched-server + ``` + - Run the server manually: + ```bash + go run main.go + ``` + - The server will start on `http://localhost:3000`. + +2.2 **SQLite Server**: + - Navigate to the `sqlite-server` directory: + ```bash + cd sqlite-server + ``` + - Install dependencies (if not already installed): + ```bash + go mod tidy + ``` + - Run the server manually: + ```bash + go run main.go + ``` + - The server will start on `http://localhost:3000`. + +--- + +### API Endpoints + +Both servers expose the following endpoints: + +- **`GET /`**: + - Returns a `200 OK` status with a message indicating the server is running. + - Example response: + ```json + "Batched Write Server is running" + ``` + +- **`POST /submit`**: + - Accepts a form submission with an `email` field. + - Example request: + ```bash + curl -X POST -d "email=user@example.com" http://localhost:3000/submit + ``` + - Example response: + ```json + {"message": "data received"} + ``` + +--- + +## Server Implementations + +### 1. Batched Write Server (CSV-based) + +- **Purpose**: A dependency-free implementation for users who want minimalism and simplicity. +- **Storage**: Emails are appended to a CSV file (`emails.csv`) in the `batched-server` directory. +- **Performance**: Batches writes to reduce disk I/O, flushing every 5 seconds or after 100 emails. +- **Format**: Each row in the CSV file contains an email and a timestamp. +- **Use Case**: Ideal for lightweight deployments where external dependencies are not desired. + +### 2. SQLite Server + +- **Purpose**: A more robust implementation using SQLite for structured storage. +- **Storage**: Emails are stored in an SQLite database (`emails.db`) in the `sqlite-server` directory. +- **Features**: + - Prevents duplicate emails with a unique constraint. + - Tracks the creation timestamp of each email. +- **Use Case**: Ideal for deployments where data integrity and querying capabilities are important. + +--- + +## Configuration + +- **Port**: Both servers run on port `3000` by default. To change the port, modify the `PORT` constant in the respective `main.go` file. +- **Storage Location**: + - Batched Write Server: Emails are stored in `batched-server/emails.csv`. + - SQLite Server: Emails are stored in `sqlite-server/emails.db`. + +--- + +## Trade-offs + +| Feature | Batched Write Server (CSV) | SQLite Server | +|------------------------|------------------------------|-----------------------------| +| **Dependencies** | None | Requires SQLite dependency | +| **Storage** | CSV file | SQLite database | +| **Data Integrity** | Basic | High (prevents duplicates) | +| **Querying** | Not supported | Supported | +| **Performance** | High (batched writes) | High (SQLite optimized) | +| **Use Case** | Minimalist, dependency-free | Robust, structured storage | + +--- + +## Contributing + +Contributions are welcome! Please open an issue or submit a pull request for any improvements or bug fixes. + +--- + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. diff --git a/batched-server/main.go b/batched-server/main.go new file mode 100644 index 0000000..f207964 --- /dev/null +++ b/batched-server/main.go @@ -0,0 +1,111 @@ +package main + +import ( + "encoding/csv" + "encoding/json" + "net/http" + "os" + "strings" + "sync" + "time" +) + +const PORT = ":3000" + +var ( + emailQueue []string + queueLock sync.Mutex +) + + +func saveEmails() { + queueLock.Lock() + defer queueLock.Unlock() + + if len(emailQueue) == 0 { + return + } + + fileExists := true + if _, err := os.Stat("emails.csv"); os.IsNotExist(err) { + fileExists = false + } + + f, err := os.OpenFile("emails.csv", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return + } + defer f.Close() + + writer := csv.NewWriter(f) + defer writer.Flush() + + // write header if file is new + if !fileExists { + if err := writer.Write([]string{"email", "timestamp"}); err != nil { + return + } + } + + // write each email with timestamp + for _, email := range emailQueue { + timestamp := time.Now().Format(time.RFC3339) + if err := writer.Write([]string{email, timestamp}); err != nil { + return + } + } + + // clear queue + emailQueue = emailQueue[:0] +} + +// background goroutine to batch writes +func init() { + go func() { + for { + time.Sleep(5 * time.Second); + queueLock.Lock(); + hasEmails := len(emailQueue) > 0; + queueLock.Unlock(); + if hasEmails { + saveEmails(); + } + } + }(); +} + + +func submitHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + email := strings.TrimSpace(r.FormValue("email")) + if email == "" { + http.Error(w, "Email required", http.StatusBadRequest) + return + } + + queueLock.Lock() + emailQueue = append(emailQueue, email) + shouldFlush := len(emailQueue) >= 100 + queueLock.Unlock() + + if shouldFlush { + saveEmails() + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"message": "data received"}) +} + + +func main() { + http.HandleFunc("/submit", submitHandler) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("Batched Write Server is running")) + }) + http.ListenAndServe(PORT, nil) +} diff --git a/sqlite-server/go.mod b/sqlite-server/go.mod new file mode 100644 index 0000000..56f594e --- /dev/null +++ b/sqlite-server/go.mod @@ -0,0 +1,16 @@ +module sqlite-server + +go 1.23.6 + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/sys v0.22.0 // indirect + modernc.org/libc v1.55.3 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/sqlite v1.34.5 // indirect +) diff --git a/sqlite-server/main.go b/sqlite-server/main.go new file mode 100644 index 0000000..cd65e62 --- /dev/null +++ b/sqlite-server/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "database/sql" + "encoding/json" + "net/http" + "strings" + + _ "modernc.org/sqlite" +) + +const PORT = ":3000" + +func main() { + db, _ := sql.Open("sqlite", "emails.db") + db.Exec(`CREATE TABLE IF NOT EXISTS emails ( + email TEXT PRIMARY KEY, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )`) + + http.HandleFunc("/submit", func(w http.ResponseWriter, r *http.Request) { + email := strings.TrimSpace(r.FormValue("email")) + if email == "" { + http.Error(w, "Email required", http.StatusBadRequest) + return + } + + _, err := db.Exec(`INSERT OR IGNORE INTO emails(email) VALUES (?)`, email) + + w.Header().Set("Content-Type", "application/json") + if err != nil { + json.NewEncoder(w).Encode(map[string]string{"error": "storage failed"}) + return + } + json.NewEncoder(w).Encode(map[string]string{"message": "data received"}) + }) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("SQLite Write Server is running")) + }) + + http.ListenAndServe(PORT, nil) +} diff --git a/test/batched.sh b/test/batched.sh new file mode 100644 index 0000000..a15e015 --- /dev/null +++ b/test/batched.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +SERVER_URL="http://localhost:3000" + +test_server_running() { + echo "Testing if the server is running..." + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$SERVER_URL/") + + if [ "$RESPONSE" -eq 200 ]; then + echo "✅ Server is running (Status: 200 OK)" + else + echo "❌ Server is NOT running (Received status: $RESPONSE)" + exit 1 + fi +} + + +test_submit_email() { + echo "Testing email submission..." + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST -d "email=test@example.com" "$SERVER_URL/submit") + + if [ "$RESPONSE" -eq 200 ]; then + echo "✅ Email submission successful (Status: 200 OK)" + else + echo "❌ Email submission failed (Received status: $RESPONSE)" + exit 1 + fi +} + + +test_csv_written() { + CSV_FILE="batched-server/emails.csv" + + if [ -f "$CSV_FILE" ]; then + if grep -q "test@example.com" "$CSV_FILE"; then + echo "✅ Email found in $CSV_FILE" + else + echo "❌ Email NOT found in $CSV_FILE" + exit 1 + fi + else + echo "❌ CSV file not found!" + exit 1 + fi +} + +# run the tests +test_server_running +test_submit_email +sleep 2 # wait for the batched write to complete +test_csv_written + +echo "✅ All tests passed successfully!" diff --git a/test/sqlite.sh b/test/sqlite.sh new file mode 100644 index 0000000..d53fb94 --- /dev/null +++ b/test/sqlite.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +SERVER_URL="http://localhost:3000" + +test_server_running() { + # testing if the server is running... + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$SERVER_URL/") + + if [ "$RESPONSE" -eq 200 ]; then + echo "✅ server is running (status: 200 ok)"; + else + echo "❌ server is not running (received status: $RESPONSE)"; + exit 1; + fi +} + +test_submit_email() { + # testing email submission... + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST -d "email=test@example.com" "$SERVER_URL/submit") + + if [ "$RESPONSE" -eq 200 ]; then + echo "✅ email submission successful (status: 200 ok)"; + else + echo "❌ email submission failed (received status: $RESPONSE)"; + exit 1; + fi +} + +test_db_entry() { + # testing if the email was written to the sqlite database... + DB_FILE="sqlite-server/emails.db" + + if [ -f "$DB_FILE" ]; then + if ! command -v sqlite3 >/dev/null 2>&1; then + echo "❌ sqlite3 is not installed. please install sqlite3 to run this test"; + exit 1; + fi; + + result=$(sqlite3 "$DB_FILE" "select email from emails where email='test@example.com';") + if [ "$result" == "test@example.com" ]; then + echo "✅ email found in $DB_FILE"; + else + echo "❌ email not found in $DB_FILE"; + exit 1; + fi; + else + echo "❌ database file $DB_FILE not found!"; + exit 1; + fi +} + +# run the tests +test_server_running; +test_submit_email; +sleep 2; # wait for the write to complete +test_db_entry; + +echo "✅ all tests passed successfully!";