initial code commit

This commit is contained in:
2025-02-08 13:05:53 -05:00
parent d59d9990db
commit a21c17d6d9
8 changed files with 455 additions and 2 deletions
+5
View File
@@ -23,3 +23,8 @@ go.work.sum
# env file # env file
.env .env
go.sum
*.db
*.txt
*.csv
+13
View File
@@ -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
+155 -2
View File
@@ -1,2 +1,155 @@
# forms-server # Forms Server
An extremely lightweight server made to collect email form submissions and written in Go
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.
+111
View File
@@ -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)
}
+16
View File
@@ -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
)
+44
View File
@@ -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)
}
+53
View File
@@ -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!"
+58
View File
@@ -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!";