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:
|
workspace:
|
||||||
base: /go
|
base: /go
|
||||||
path: src/deadbeef.codes/steven/siteviewcounter
|
path: src/code.stevenpolley.net/steven/siteviewcounter
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
@ -22,4 +22,4 @@ steps:
|
|||||||
- name: package in docker container
|
- name: package in docker container
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
settings:
|
settings:
|
||||||
repo: registry.deadbeef.codes/siteviewcounter
|
repo: registry.stevenpolley.net/siteviewcounter
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
FROM scratch
|
FROM scratch
|
||||||
|
LABEL maintainer="himself@stevenpolley.net"
|
||||||
COPY siteviewcounter .
|
COPY siteviewcounter .
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
67
README.md
67
README.md
@ -4,31 +4,31 @@
|
|||||||
|
|
||||||
A simple view counter for a website
|
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
|
### Build Application
|
||||||
SET NAMES utf8;
|
|
||||||
SET time_zone = '+00:00';
|
|
||||||
SET foreign_key_checks = 0;
|
|
||||||
|
|
||||||
CREATE DATABASE `counter` /*!40100 DEFAULT CHARACTER SET latin1 */;
|
```bash
|
||||||
USE `counter`;
|
|
||||||
|
|
||||||
CREATE TABLE `visit` (
|
go build -a -ldflags '-w'
|
||||||
`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;
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build Container
|
### 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
|
### Example docker-compose.yml
|
||||||
|
|
||||||
@ -41,10 +41,8 @@ version: '3.7'
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
counter:
|
counter:
|
||||||
image: registry.deadbeef.codes/siteviewcounter:latest
|
image: siteviewcounter:latest
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
|
||||||
- traefik
|
|
||||||
expose:
|
expose:
|
||||||
- "8080"
|
- "8080"
|
||||||
environment:
|
environment:
|
||||||
@ -68,3 +66,34 @@ services:
|
|||||||
- TZ=America/Edmonton
|
- 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
|
// 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 {
|
type Configuration struct {
|
||||||
DSN string
|
DSN string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection represents a single connection to the database, however there may be many instances / connections
|
// 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 {
|
type Connection struct {
|
||||||
DB *sql.DB
|
DB *sql.DB
|
||||||
}
|
}
|
||||||
@ -83,3 +86,31 @@ func (conf Configuration) Connect() (*Connection, error) {
|
|||||||
|
|
||||||
return conn, nil
|
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)
|
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()
|
uniqueVisits, err = dbConn.GetUniqueVisits()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to get number of unique visits from database: %v", err)
|
log.Fatalf("failed to get number of unique visits from database: %v", err)
|
||||||
@ -78,7 +85,9 @@ func main() {
|
|||||||
// HTTP handler function
|
// HTTP handler function
|
||||||
func countHandler(w http.ResponseWriter, r *http.Request) {
|
func countHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == "GET" {
|
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.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
w.Write([]byte(strconv.Itoa(uniqueVisits)))
|
w.Write([]byte(strconv.Itoa(uniqueVisits)))
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user