Compare commits
12 Commits
2658b18797
...
master
Author | SHA1 | Date | |
---|---|---|---|
586a3b1cd8 | |||
cacbfa66fb | |||
bb4112e145 | |||
022327d539 | |||
ef7d5411af | |||
98f50317be | |||
15c8ac5f68 | |||
53eaf066b3 | |||
596835a0a6 | |||
214bdd0358 | |||
d432521dae | |||
86ffcb6f3b |
@ -3,7 +3,7 @@ name: default
|
||||
|
||||
workspace:
|
||||
base: /go
|
||||
path: src/deadbeef.codes/steven/siteviewcounter
|
||||
path: src/code.stevenpolley.net/steven/siteviewcounter
|
||||
|
||||
steps:
|
||||
|
||||
@ -22,4 +22,4 @@ steps:
|
||||
- name: package in docker container
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: registry.deadbeef.codes/siteviewcounter
|
||||
repo: registry.stevenpolley.net/siteviewcounter
|
||||
|
@ -1,5 +1,5 @@
|
||||
FROM scratch
|
||||
|
||||
LABEL maintainer="himself@stevenpolley.net"
|
||||
COPY siteviewcounter .
|
||||
|
||||
EXPOSE 8080
|
||||
|
69
README.md
69
README.md
@ -4,31 +4,31 @@
|
||||
|
||||
A simple view counter for a website
|
||||
|
||||
### Database initialization
|
||||
### Requirements
|
||||
|
||||
The following SQL will initialize the database for this application. No automigrate / initialization is done upon first running the application, so this must be ran by an administrator.
|
||||
* Go
|
||||
* Docker
|
||||
* Docker Compose (Optional) or Kubernetes (Optional)
|
||||
|
||||
```sql
|
||||
SET NAMES utf8;
|
||||
SET time_zone = '+00:00';
|
||||
SET foreign_key_checks = 0;
|
||||
### Build Application
|
||||
|
||||
CREATE DATABASE `counter` /*!40100 DEFAULT CHARACTER SET latin1 */;
|
||||
USE `counter`;
|
||||
```bash
|
||||
|
||||
CREATE TABLE `visit` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`ip_address` varchar(15) NOT NULL,
|
||||
`visits` int(11) NOT NULL,
|
||||
`last_visited` datetime NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
go build -a -ldflags '-w'
|
||||
|
||||
```
|
||||
|
||||
### Build Container
|
||||
|
||||
Disclaimer! If you use this, you'll need to build the container yourself. My registry is used for my internal infrastructure only and is not publicly available.
|
||||
Disclaimer! If you use this, you'll need to build the container yourself. I have a CICD pipeline setup, but my registry is used for my internal infrastructure only and is not publicly available.
|
||||
|
||||
Because this is a staticly linked binary with no external runtime dependancies, the container literally only contains the binary file, keeping it clean and low in size (6.3MB). I never did understand why people include operating systems in containers.
|
||||
|
||||
```bash
|
||||
|
||||
docker build -t siteviewconter:latest .
|
||||
|
||||
```
|
||||
|
||||
### Example docker-compose.yml
|
||||
|
||||
@ -41,10 +41,8 @@ version: '3.7'
|
||||
services:
|
||||
|
||||
counter:
|
||||
image: registry.deadbeef.codes/siteviewcounter:latest
|
||||
image: siteviewcounter:latest
|
||||
restart: always
|
||||
depends_on:
|
||||
- traefik
|
||||
expose:
|
||||
- "8080"
|
||||
environment:
|
||||
@ -67,4 +65,35 @@ services:
|
||||
- MYSQL_DATABASE=counter
|
||||
- TZ=America/Edmonton
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
### Example front end usage
|
||||
|
||||
You can pretty much implement this in your front end however you want, you just need to make a GET request to whatever endpoint the counter container is running at. This is how I use it though...
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
var counterReq = new XMLHttpRequest();
|
||||
counterReq.onreadystatechange = function() {
|
||||
console.log("counterReq ready state is " + this.readyState);
|
||||
if (this.readyState == 4) {
|
||||
console.log("counterReq status is " + this.status);
|
||||
if (this.status == 200) {
|
||||
document.getElementById("counter").innerHTML = this.responseText + " unique visitors"
|
||||
} else { // failed to load
|
||||
console.log("failed to load counter module")
|
||||
}
|
||||
}
|
||||
}
|
||||
counterReq.open("GET", "https://counter.example.com", true);
|
||||
counterReq.send();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="counter"></div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
|
@ -6,11 +6,14 @@ import (
|
||||
)
|
||||
|
||||
// Configuration holds the DSN connection string and a resource Semaphore to limit the number of active connections
|
||||
// Note: This is a copied design pattern I've used for other projects which had much more data inside a Configuration struct.
|
||||
// Examples include semaphores and persistent connection pools, which I've stripped out for this project.
|
||||
type Configuration struct {
|
||||
DSN string
|
||||
}
|
||||
|
||||
// Connection represents a single connection to the database, however there may be many instances / connections
|
||||
// Note: This is a copied design pattern I've used for other projects which had much more data inside a Connection struct.
|
||||
type Connection struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
@ -83,3 +86,31 @@ func (conf Configuration) Connect() (*Connection, error) {
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// InitializeDatabase will check if tables exist in the database, and if not then create them.
|
||||
func (conn Connection) InitializeDatabase() error {
|
||||
rows, err := conn.DB.Query(`SHOW TABLES`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SHOW TABLES query failed: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if rows.Next() { // Table already exists, leave with no error
|
||||
return nil
|
||||
}
|
||||
|
||||
// Table does not exist, create it
|
||||
_, err = conn.DB.Exec(`CREATE TABLE visit (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
ip_address varchar(15) NOT NULL,
|
||||
visits int(11) NOT NULL,
|
||||
last_visited datetime NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;`)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create table: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
7
go.mod
Normal file
7
go.mod
Normal file
@ -0,0 +1,7 @@
|
||||
module deadbeef.codes/steven/siteviewcounter
|
||||
|
||||
go 1.22
|
||||
|
||||
require github.com/go-sql-driver/mysql v1.8.1
|
||||
|
||||
require filippo.io/edwards25519 v1.1.0 // indirect
|
4
go.sum
Normal file
4
go.sum
Normal file
@ -0,0 +1,4 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
11
main.go
11
main.go
@ -57,6 +57,13 @@ func init() {
|
||||
log.Fatalf("failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
// Check if database needs to be initialized (create tables)
|
||||
// does nothing if tables exist
|
||||
err = dbConn.InitializeDatabase()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize database: %v", err)
|
||||
}
|
||||
|
||||
uniqueVisits, err = dbConn.GetUniqueVisits()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get number of unique visits from database: %v", err)
|
||||
@ -78,7 +85,9 @@ func main() {
|
||||
// HTTP handler function
|
||||
func countHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
// CORS header change required
|
||||
// CORS header change required.
|
||||
//TBD wildcard is bad because it could allow illegitmate visits to be recorded if someone was nefarious and embedded
|
||||
// front end code on a different website than your own. Need to implement environment variable to set allowed origin.
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Write([]byte(strconv.Itoa(uniqueVisits)))
|
||||
|
||||
|
Reference in New Issue
Block a user