This commit is contained in:
		
							
								
								
									
										25
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					kind: pipeline
 | 
				
			||||||
 | 
					name: default
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					workspace:
 | 
				
			||||||
 | 
					  base: /go
 | 
				
			||||||
 | 
					  path: src/deadbeef.codes/steven/siteviewcounter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					steps:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- name: build server
 | 
				
			||||||
 | 
					  image: golang
 | 
				
			||||||
 | 
					  pull: always
 | 
				
			||||||
 | 
					  environment:
 | 
				
			||||||
 | 
					    GOOS: linux
 | 
				
			||||||
 | 
					    GOARCH: amd64
 | 
				
			||||||
 | 
					    CGO_ENABLED: 0
 | 
				
			||||||
 | 
					  commands:
 | 
				
			||||||
 | 
					  - go version
 | 
				
			||||||
 | 
					  - go get
 | 
				
			||||||
 | 
					  - go build -a -ldflags '-w'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- name: package in docker container
 | 
				
			||||||
 | 
					  image: plugins/docker
 | 
				
			||||||
 | 
					  settings:
 | 
				
			||||||
 | 
					    repo: registry.deadbeef.codes/siteviewcounter
 | 
				
			||||||
							
								
								
									
										7
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					FROM scratch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COPY siteviewcounter .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					EXPOSE 8080
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CMD [ "./siteviewcounter" ]
 | 
				
			||||||
							
								
								
									
										60
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								README.md
									
									
									
									
									
								
							@@ -1,2 +1,62 @@
 | 
				
			|||||||
# siteviewcounter
 | 
					# siteviewcounter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[](https://drone.deadbeef.codes/steven/siteviewcounter)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A simple view counter for a website
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Database initialization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```sql 
 | 
				
			||||||
 | 
					SET NAMES utf8;
 | 
				
			||||||
 | 
					SET time_zone = '+00:00';
 | 
				
			||||||
 | 
					SET foreign_key_checks = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE DATABASE `counter` /*!40100 DEFAULT CHARACTER SET latin1 */;
 | 
				
			||||||
 | 
					USE `counter`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Example docker-compose.yml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```yaml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					version: '3.7'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					services:
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					  counter:
 | 
				
			||||||
 | 
					    image: registry.deadbeef.codes/siteviewcounter:latest
 | 
				
			||||||
 | 
					    restart: always
 | 
				
			||||||
 | 
					    depends_on:
 | 
				
			||||||
 | 
					      - traefik
 | 
				
			||||||
 | 
					    expose:
 | 
				
			||||||
 | 
					      - "8080"
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      - dbname=counter
 | 
				
			||||||
 | 
					      - dbhostname=counter-db
 | 
				
			||||||
 | 
					      - dbusername=root
 | 
				
			||||||
 | 
					      - dbpassword=CHANGEME
 | 
				
			||||||
 | 
					      - timezone=America/Edmonton
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					  counter-db:
 | 
				
			||||||
 | 
					    image: mariadb:10
 | 
				
			||||||
 | 
					    restart: always
 | 
				
			||||||
 | 
					    expose:
 | 
				
			||||||
 | 
					      - "3306"
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - /data/counter-db:/var/lib/mysql
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      - MYSQL_RANDOM_ROOT_PASSWORD=yes
 | 
				
			||||||
 | 
					      - MYSQL_DATABASE=counter
 | 
				
			||||||
 | 
					      - TZ=America/Edmonton
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
							
								
								
									
										640
									
								
								countersql/database.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										640
									
								
								countersql/database.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,640 @@
 | 
				
			|||||||
 | 
					package countersql
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"database/sql"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Configuration holds the DSN connection string and a resource Semaphore to limit the number of active connections
 | 
				
			||||||
 | 
					type Configuration struct {
 | 
				
			||||||
 | 
						DSN string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Connection represents a single connection to the database, however there may be many instances / connections
 | 
				
			||||||
 | 
					type Connection struct {
 | 
				
			||||||
 | 
						DB *sql.DB
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HasIPVisited returns true only if the IP address is present in the database
 | 
				
			||||||
 | 
					func (conn Connection) HasIPVisited(ipAddress string) (bool, error) {
 | 
				
			||||||
 | 
						rows, err := conn.DB.Query(`SELECT id FROM visit WHERE ip_address = ? LIMIT 1;`, ipAddress)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return false, fmt.Errorf("SELECT query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !rows.Next() {
 | 
				
			||||||
 | 
							return false, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return true, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetUniqueVisits counts the number of entires in the visits table, representing one unique source IP address per row
 | 
				
			||||||
 | 
					func (conn Connection) GetUniqueVisits() (int, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rows, err := conn.DB.Query(`SELECT COUNT(*) FROM visit`)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, fmt.Errorf("SELECT query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !rows.Next() {
 | 
				
			||||||
 | 
							return 0, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var uniqueVists int
 | 
				
			||||||
 | 
						if err := rows.Scan(&uniqueVists); err != nil {
 | 
				
			||||||
 | 
							return 0, fmt.Errorf("failed to scan database row: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return uniqueVists, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IncrementVisitor accepts an IP address and updates the row matching that IP address
 | 
				
			||||||
 | 
					// It does not check if the row matching the IP address supplied exists or not
 | 
				
			||||||
 | 
					func (conn Connection) IncrementVisitor(ipAddress string) error {
 | 
				
			||||||
 | 
						_, err := conn.DB.Exec(`UPDATE visit SET visits = visits + 1, last_visited = NOW() WHERE ip_address = ?`, ipAddress)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("UPDATE query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AddVisitor accepts an IP address and inserts a new row into the database as this represents a new unique visitor
 | 
				
			||||||
 | 
					func (conn Connection) AddVisitor(ipAddress string) error {
 | 
				
			||||||
 | 
						_, err := conn.DB.Exec(`INSERT INTO visit (ip_address, visits, last_visited) VALUES (?, '0', NOW())`, ipAddress)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("INSERT query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ************************************
 | 
				
			||||||
 | 
					// Task Database Functions
 | 
				
			||||||
 | 
					// ************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetTasks returns a slice of all tasks in the database.  It requires
 | 
				
			||||||
 | 
					// a boolean parameter.  If true, it only returns active tasks.  If false
 | 
				
			||||||
 | 
					// it will return all tasks.  A task is active if the time now is past
 | 
				
			||||||
 | 
					// the planned_start date.
 | 
				
			||||||
 | 
					func (conn Connection) GetTasks(activeTasksOnly bool) ([]Task, error) {
 | 
				
			||||||
 | 
						var rows *sql.Rows
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						if activeTasksOnly {
 | 
				
			||||||
 | 
							rows, err = conn.DB.Query(`SELECT
 | 
				
			||||||
 | 
														tasks.id, tasks.name, tasks.weight, tasks.timecost, tasks.planned_start, tasks.planned_end, tasks.actual_start, tasks.actual_end, tasks.expire_action, tasks.close_code, lists.id, lists.name
 | 
				
			||||||
 | 
														FROM tasks
 | 
				
			||||||
 | 
														INNER JOIN lists
 | 
				
			||||||
 | 
														ON tasks.list_id = lists.id
 | 
				
			||||||
 | 
														WHERE tasks.close_code IS NULL AND tasks.planned_start < ?
 | 
				
			||||||
 | 
														ORDER BY tasks.actual_start DESC, tasks.weight DESC`, time.Now().Add(-time.Hour*6))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							rows, err = conn.DB.Query(`SELECT
 | 
				
			||||||
 | 
														tasks.id, tasks.name, tasks.weight, tasks.timecost, tasks.planned_start, tasks.planned_end, tasks.actual_start, tasks.actual_end, tasks.expire_action, tasks.close_code, lists.id, lists.name
 | 
				
			||||||
 | 
														FROM tasks
 | 
				
			||||||
 | 
														INNER JOIN lists
 | 
				
			||||||
 | 
														ON tasks.list_id = lists.id
 | 
				
			||||||
 | 
														ORDER BY tasks.actual_start DESC, tasks.weight DESC`)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("SELECT query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !rows.Next() {
 | 
				
			||||||
 | 
							return nil, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var tasks []Task
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							task := Task{}
 | 
				
			||||||
 | 
							if err := rows.Scan(&task.ID, &task.Name, &task.Weight, &task.TimeCost, &task.PlannedStart, &task.PlannedEnd, &task.ActualStart, &task.ActualEnd, &task.ExpireAction, &task.CloseCode, &task.List.ID, &task.List.Name); err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to scan database row: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							tasks = append(tasks, task)
 | 
				
			||||||
 | 
							if !rows.Next() {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return tasks, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetTasksByListID returns a slice of all tasks on a specific list in the database
 | 
				
			||||||
 | 
					func (conn Connection) GetTasksByListID(listID int, activeTasksOnly bool) ([]Task, error) {
 | 
				
			||||||
 | 
						activeTasksQuery := ""
 | 
				
			||||||
 | 
						if activeTasksOnly {
 | 
				
			||||||
 | 
							activeTasksQuery = " AND tasks.close_code IS NULL"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						rows, err := conn.DB.Query(`SELECT
 | 
				
			||||||
 | 
													tasks.id, tasks.name, tasks.weight, tasks.timecost, tasks.planned_start, tasks.planned_end, tasks.actual_start, tasks.actual_end, tasks.expire_action, tasks.close_code, lists.id, lists.name
 | 
				
			||||||
 | 
													FROM tasks
 | 
				
			||||||
 | 
													INNER JOIN lists ON tasks.list_id = list.id
 | 
				
			||||||
 | 
													WHERE lists.id = ? `+activeTasksQuery, listID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("SELECT query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !rows.Next() {
 | 
				
			||||||
 | 
							return nil, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var tasks []Task
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							task := Task{}
 | 
				
			||||||
 | 
							if err := rows.Scan(&task.ID, &task.Name, &task.Weight, &task.TimeCost, &task.PlannedStart, &task.PlannedEnd, &task.ActualStart, &task.ActualEnd, &task.ExpireAction, &task.CloseCode, &task.List.ID, &task.List.Name); err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to scan database row: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							tasks = append(tasks, task)
 | 
				
			||||||
 | 
							if !rows.Next() {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return tasks, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetTaskByID returns a task matching the ID provided
 | 
				
			||||||
 | 
					func (conn Connection) GetTaskByID(taskID int) (*Task, error) {
 | 
				
			||||||
 | 
						rows, err := conn.DB.Query(`SELECT
 | 
				
			||||||
 | 
													tasks.id, tasks.name, tasks.weight, tasks.timecost, tasks.planned_start, tasks.planned_end, tasks.actual_start, tasks.actual_end, tasks.expire_action, tasks.close_code, lists.id, lists.name
 | 
				
			||||||
 | 
													FROM tasks
 | 
				
			||||||
 | 
													INNER JOIN lists ON tasks.list_id = list.id
 | 
				
			||||||
 | 
													WHERE tasks.id = ? `, taskID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("SELECT query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !rows.Next() {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("no results returned for taskID '%d'", taskID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						task := Task{}
 | 
				
			||||||
 | 
						if err := rows.Scan(&task.ID, &task.Name, &task.Weight, &task.TimeCost, &task.PlannedStart, &task.PlannedEnd, &task.ActualStart, &task.ActualEnd, &task.ExpireAction, &task.CloseCode, &task.List.ID, &task.List.Name); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to scan database row: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &task, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewTask will insert a row into the database, given a pre-instantiated task
 | 
				
			||||||
 | 
					func (conn Connection) NewTask(task *Task) error {
 | 
				
			||||||
 | 
						_, err := conn.DB.Exec(`INSERT INTO tasks
 | 
				
			||||||
 | 
												(list_id, name, weight, timecost, planned_start, planned_end, actual_start, actual_end, expire_action, close_code)
 | 
				
			||||||
 | 
												VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, ?, NULL)`, task.List.ID, task.Name, task.Weight, task.TimeCost, task.PlannedStart, task.PlannedEnd, task.ExpireAction)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("INSERT query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeleteTaskByID will remove a row from the database, given a list ID
 | 
				
			||||||
 | 
					func (conn Connection) DeleteTaskByID(taskID int) error {
 | 
				
			||||||
 | 
						_, err := conn.DB.Exec("DELETE FROM tasks WHERE tasks.id = ?", taskID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("Delete query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// StartTask will set the actual_start to now, given a task ID.  This marks the task as active and started
 | 
				
			||||||
 | 
					func (conn Connection) StartTask(taskID int) error {
 | 
				
			||||||
 | 
						_, err := conn.DB.Exec("UPDATE tasks SET actual_start = now() WHERE tasks.id = ?", taskID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("Delete query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdateTask will update a task row in the database, given a pre-instantiated task
 | 
				
			||||||
 | 
					func (conn Connection) UpdateTask(task *Task) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						_, err = conn.DB.Exec(`UPDATE tasks SET
 | 
				
			||||||
 | 
												list_id = ?,
 | 
				
			||||||
 | 
												name = ?,
 | 
				
			||||||
 | 
												weight = ?,
 | 
				
			||||||
 | 
												timecost = ?,
 | 
				
			||||||
 | 
												planned_start = ?,
 | 
				
			||||||
 | 
												planned_end = ?,
 | 
				
			||||||
 | 
												actual_start = ?,
 | 
				
			||||||
 | 
												actual_end = ?,
 | 
				
			||||||
 | 
												expire_action = ?,
 | 
				
			||||||
 | 
												close_code = ?
 | 
				
			||||||
 | 
												WHERE id = ?`,
 | 
				
			||||||
 | 
							task.List.ID, task.Name, task.Weight, task.TimeCost, task.PlannedStart, task.PlannedEnd, task.ActualStart, task.ActualEnd, task.ExpireAction, task.CloseCode, task.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("UPDATE task query failed for %d: %v", task.ID, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// StopTask will stop a task by setting the close code
 | 
				
			||||||
 | 
					// closeCode can be: completed, cancelled or reset
 | 
				
			||||||
 | 
					// completed is self explanatory.
 | 
				
			||||||
 | 
					// cancelled indicates the task itself is being canelled / skipped
 | 
				
			||||||
 | 
					// reset is not an official close code, but indicates that actual_start should be cleared (timer reset)
 | 
				
			||||||
 | 
					func (conn Connection) StopTask(taskID int, closeCode string) error {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						if closeCode == "reset" {
 | 
				
			||||||
 | 
							_, err = conn.DB.Exec("UPDATE tasks SET actual_start = NULL WHERE tasks.id = ?", taskID)
 | 
				
			||||||
 | 
						} else if closeCode == "cancelled" {
 | 
				
			||||||
 | 
							_, err = conn.DB.Exec("UPDATE tasks SET actual_end = now(), close_code = 'cancelled' WHERE tasks.id = ?", taskID)
 | 
				
			||||||
 | 
						} else if closeCode == "completed" {
 | 
				
			||||||
 | 
							_, err = conn.DB.Exec("UPDATE tasks SET actual_end = now(), close_code = 'completed' WHERE tasks.id = ?", taskID)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							err = fmt.Errorf("invalid close code: '%s' - must be reset, cancelled or completed", closeCode)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("StopTask query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ************************************
 | 
				
			||||||
 | 
					// List Database Functions
 | 
				
			||||||
 | 
					// ************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetLists returns a slice of all lists in the database.
 | 
				
			||||||
 | 
					// If joinLocations is true, it will grab the locations as well
 | 
				
			||||||
 | 
					func (conn Connection) GetLists(getListLocations bool) ([]List, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rows, err := conn.DB.Query(`SELECT
 | 
				
			||||||
 | 
													lists.id, lists.name
 | 
				
			||||||
 | 
													FROM lists`)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("SELECT query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !rows.Next() {
 | 
				
			||||||
 | 
							return nil, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var lists []List
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							list := List{}
 | 
				
			||||||
 | 
							if err := rows.Scan(&list.ID, &list.Name); err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to scan database row: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							lists = append(lists, list)
 | 
				
			||||||
 | 
							if !rows.Next() {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if getListLocations {
 | 
				
			||||||
 | 
							wg := sync.WaitGroup{}
 | 
				
			||||||
 | 
							m := sync.Mutex{}
 | 
				
			||||||
 | 
							for i := range lists {
 | 
				
			||||||
 | 
								wg.Add(1)
 | 
				
			||||||
 | 
								go func(i int) {
 | 
				
			||||||
 | 
									defer wg.Done()
 | 
				
			||||||
 | 
									rows, err := conn.DB.Query(`SELECT
 | 
				
			||||||
 | 
																listlocations.location_id, locations.name
 | 
				
			||||||
 | 
																FROM listlocations
 | 
				
			||||||
 | 
																INNER JOIN locations
 | 
				
			||||||
 | 
																ON listlocations.location_id = locations.id
 | 
				
			||||||
 | 
																WHERE list_id = ?`, lists[i].ID)
 | 
				
			||||||
 | 
									defer rows.Close()
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										log.Printf("SELECT listlocations query failed: %v", err)
 | 
				
			||||||
 | 
										return
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if !rows.Next() {
 | 
				
			||||||
 | 
										return
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									var locations []Location
 | 
				
			||||||
 | 
									for {
 | 
				
			||||||
 | 
										var location Location
 | 
				
			||||||
 | 
										if err := rows.Scan(&location.ID, &location.Name); err != nil {
 | 
				
			||||||
 | 
											log.Printf("failed to scan database row: %v", err)
 | 
				
			||||||
 | 
											continue
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										locations = append(locations, location)
 | 
				
			||||||
 | 
										if !rows.Next() {
 | 
				
			||||||
 | 
											break
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									m.Lock()
 | 
				
			||||||
 | 
									lists[i].Locations = locations
 | 
				
			||||||
 | 
									m.Unlock()
 | 
				
			||||||
 | 
								}(i)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							wg.Wait() // wait
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return lists, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetListByID will return a single list in the database with the given ID
 | 
				
			||||||
 | 
					func (conn Connection) GetListByID(listID int, getListLocations bool) (*List, error) {
 | 
				
			||||||
 | 
						rows, err := conn.DB.Query(`SELECT
 | 
				
			||||||
 | 
													lists.name
 | 
				
			||||||
 | 
													FROM lists
 | 
				
			||||||
 | 
													WHERE lists.id = ?
 | 
				
			||||||
 | 
													LIMIT 1`, listID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("SELECT query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !rows.Next() {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("no results returned for listID '%d'", listID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						list := List{}
 | 
				
			||||||
 | 
						if err := rows.Scan(&list.Name); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to scan database row: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						list.ID = listID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if getListLocations {
 | 
				
			||||||
 | 
							rows, err := conn.DB.Query(`SELECT
 | 
				
			||||||
 | 
														listlocations.location_id
 | 
				
			||||||
 | 
														FROM listlocations
 | 
				
			||||||
 | 
														WHERE list_id = ?`, list.ID)
 | 
				
			||||||
 | 
							defer rows.Close()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("SELECT listlocations query failed: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !rows.Next() {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("no results returned for listID '%d'", listID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var locations []Location
 | 
				
			||||||
 | 
							for {
 | 
				
			||||||
 | 
								var location Location
 | 
				
			||||||
 | 
								if err := rows.Scan(&location.ID); err != nil {
 | 
				
			||||||
 | 
									log.Printf("failed to scan database row: %v", err)
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								locations = append(locations, location)
 | 
				
			||||||
 | 
								if !rows.Next() {
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							list.Locations = locations
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &list, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewList will insert a row into the database, given a pre-instantiated list
 | 
				
			||||||
 | 
					func (conn Connection) NewList(list *List) error {
 | 
				
			||||||
 | 
						result, err := conn.DB.Exec(`INSERT
 | 
				
			||||||
 | 
												INTO lists
 | 
				
			||||||
 | 
												(name)
 | 
				
			||||||
 | 
												VALUES (?)`, list.Name)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("INSERT list query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						lastInsertID, _ := result.LastInsertId()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						query := `	INSERT
 | 
				
			||||||
 | 
									INTO listlocations
 | 
				
			||||||
 | 
									(list_id, location_id)
 | 
				
			||||||
 | 
									VALUES `
 | 
				
			||||||
 | 
						// TBD: Handle list location mapping inserts here, loop through and build query, then execute it.
 | 
				
			||||||
 | 
						values := []interface{}{}
 | 
				
			||||||
 | 
						for _, listLocation := range list.Locations {
 | 
				
			||||||
 | 
							values = append(values, lastInsertID, listLocation.ID)
 | 
				
			||||||
 | 
							// the rest of this loop is magic lifted from https://play.golang.org/p/YqNJKybpwWB
 | 
				
			||||||
 | 
							numFields := 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							query += `(`
 | 
				
			||||||
 | 
							for j := 0; j < numFields; j++ {
 | 
				
			||||||
 | 
								query += `?,`
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							query = query[:len(query)-1] + `),`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						query = query[:len(query)-1] // this is also part of the above mentioned magic
 | 
				
			||||||
 | 
						_, err = conn.DB.Exec(query, values...)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("INSERT listlocations query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdateList will update a list row in the database, given a pre-instantiated list
 | 
				
			||||||
 | 
					func (conn Connection) UpdateList(list *List) error {
 | 
				
			||||||
 | 
						wg := sync.WaitGroup{}
 | 
				
			||||||
 | 
						wg.Add(2)
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							defer wg.Done()
 | 
				
			||||||
 | 
							_, err = conn.DB.Exec(`UPDATE lists
 | 
				
			||||||
 | 
												(name)
 | 
				
			||||||
 | 
												VALUES (?)`, list.Name)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								err = fmt.Errorf("INSERT list query failed: %v", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle list locations...  Have fun
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							defer wg.Done()
 | 
				
			||||||
 | 
							var rows *sql.Rows
 | 
				
			||||||
 | 
							rows, err = conn.DB.Query(`SELECT
 | 
				
			||||||
 | 
														listlocations.location_id
 | 
				
			||||||
 | 
														WHERE list_id = ?`, list.ID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								err = fmt.Errorf("SELECT listlocations query failed: %v", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var locationIDsToRemove, locationIDsToAdd, dbIDs []int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if rows.Next() { // If this is false, the list has no assigned locations in the database currently
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// We loop through databaseRows and determine locations that need to be removed
 | 
				
			||||||
 | 
								for {
 | 
				
			||||||
 | 
									var currDBID int
 | 
				
			||||||
 | 
									if err := rows.Scan(&currDBID); err != nil {
 | 
				
			||||||
 | 
										err = fmt.Errorf("failed to scan listlocations database row: %v", err)
 | 
				
			||||||
 | 
										return
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									matchFound := false
 | 
				
			||||||
 | 
									for _, currNewLocation := range list.Locations {
 | 
				
			||||||
 | 
										if currNewLocation.ID == currDBID { // If we find a match, we know this is a location that both exists in the database, and is set on the new object, so we wouldn't take action
 | 
				
			||||||
 | 
											matchFound = true
 | 
				
			||||||
 | 
											continue
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if !matchFound { // If we don't find a match, we know that this row in the database should be removed because it's no longer set on the new listObject
 | 
				
			||||||
 | 
										locationIDsToRemove = append(locationIDsToRemove, currDBID)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									// We collect the locationIDs while looping through the database rows so we can re-use them below
 | 
				
			||||||
 | 
									dbIDs = append(dbIDs, currDBID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if !rows.Next() {
 | 
				
			||||||
 | 
										break
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// We loop through the newly set locations and determine any that need to be added to the database
 | 
				
			||||||
 | 
								for _, currNewLocation := range list.Locations {
 | 
				
			||||||
 | 
									matchFound := false
 | 
				
			||||||
 | 
									for _, currDBID := range dbIDs {
 | 
				
			||||||
 | 
										if currNewLocation.ID == currDBID { // If we find a match, we know the location is set on the new object and already exists in the database, so we take no action
 | 
				
			||||||
 | 
											matchFound = true
 | 
				
			||||||
 | 
											continue
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if !matchFound { // If we don't find a match, we know the location is set on the new object, but doesn't exist in the database, so we need to add it
 | 
				
			||||||
 | 
										locationIDsToAdd = append(locationIDsToAdd, currNewLocation.ID)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else { // If there are no rows in the database then we know we just need to all all locations set on this new list object and put them in the database
 | 
				
			||||||
 | 
								for _, currNewLocation := range list.Locations {
 | 
				
			||||||
 | 
									locationIDsToAdd = append(locationIDsToAdd, currNewLocation.ID)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// We now have populated locationIDsToAdd and locationIDsToRemove, so we just need to build and execute the queries
 | 
				
			||||||
 | 
							wg.Add(2)
 | 
				
			||||||
 | 
							go func() {
 | 
				
			||||||
 | 
								defer wg.Done()
 | 
				
			||||||
 | 
								query := `	INSERT
 | 
				
			||||||
 | 
									INTO listlocations
 | 
				
			||||||
 | 
									(list_id, location_id)
 | 
				
			||||||
 | 
									VALUES `
 | 
				
			||||||
 | 
								// TBD: Handle list location mapping inserts here, loop through and build query, then execute it.
 | 
				
			||||||
 | 
								values := []interface{}{}
 | 
				
			||||||
 | 
								for _, locationID := range locationIDsToAdd {
 | 
				
			||||||
 | 
									values = append(values, list.ID, locationID)
 | 
				
			||||||
 | 
									// the rest of this loop is magic lifted from https://play.golang.org/p/YqNJKybpwWB
 | 
				
			||||||
 | 
									numFields := 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									query += `(`
 | 
				
			||||||
 | 
									for j := 0; j < numFields; j++ {
 | 
				
			||||||
 | 
										query += `?,`
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									query = query[:len(query)-1] + `),`
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								query = query[:len(query)-1] // this is also part of the above mentioned magic
 | 
				
			||||||
 | 
								_, err = conn.DB.Exec(query, values...)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									err = fmt.Errorf("INSERT listlocations query failed: %v", err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							go func() {
 | 
				
			||||||
 | 
								defer wg.Done()
 | 
				
			||||||
 | 
								query := `	DELETE
 | 
				
			||||||
 | 
											FROM listlocations
 | 
				
			||||||
 | 
											WHERE `
 | 
				
			||||||
 | 
								values := []interface{}{}
 | 
				
			||||||
 | 
								for _, locationID := range locationIDsToRemove {
 | 
				
			||||||
 | 
									values = append(values, list.ID, locationID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									query += `(list_id = ? AND location_id = ?) OR`
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								query = query[:len(query)-3]
 | 
				
			||||||
 | 
								_, err = conn.DB.Exec(query, values...)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									err = fmt.Errorf("DELETE listlocations query failed: %v", err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}()
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						wg.Wait()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("a goroutine in UpdateList failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeleteListByID will remove a row from the database, given a list ID
 | 
				
			||||||
 | 
					func (conn Connection) DeleteListByID(listID int) error {
 | 
				
			||||||
 | 
						_, err := conn.DB.Exec("DELETE FROM lists WHERE lists.id = ?", listID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("Delete query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ************************************
 | 
				
			||||||
 | 
					// Location Database Functions
 | 
				
			||||||
 | 
					// ************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetLocations returns a slice of all locations in the database
 | 
				
			||||||
 | 
					func (conn Connection) GetLocations() ([]Location, error) {
 | 
				
			||||||
 | 
						rows, err := conn.DB.Query(`SELECT
 | 
				
			||||||
 | 
													locations.id, locations.name, locations.mac_address
 | 
				
			||||||
 | 
													FROM locations`)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("SELECT query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !rows.Next() {
 | 
				
			||||||
 | 
							return nil, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var locations []Location
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							location := Location{}
 | 
				
			||||||
 | 
							if err := rows.Scan(&location.ID, &location.Name, &location.MACAddress); err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to scan database row: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							locations = append(locations, location)
 | 
				
			||||||
 | 
							if !rows.Next() {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return locations, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeleteLocationByID will remove a row from the database, given a list ID
 | 
				
			||||||
 | 
					func (conn Connection) DeleteLocationByID(locationID int) error {
 | 
				
			||||||
 | 
						_, err := conn.DB.Exec(`DELETE
 | 
				
			||||||
 | 
												FROM locations
 | 
				
			||||||
 | 
												WHERE locations.id = ?`, locationID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("Delete query failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Connect will open a TCP connection to the database with the given DSN configuration
 | 
				
			||||||
 | 
					func (conf Configuration) Connect() (*Connection, error) {
 | 
				
			||||||
 | 
						conn := &Connection{}
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						conn.DB, err = sql.Open("mysql", conf.DSN)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to open db: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return conn, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										115
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,115 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"deadbeef.codes/steven/siteviewcounter/countersql"
 | 
				
			||||||
 | 
						"github.com/go-sql-driver/mysql"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						database     countersql.Configuration
 | 
				
			||||||
 | 
						uniqueVisits int
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Application Startup
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						envVars := make(map[string]string)
 | 
				
			||||||
 | 
						envVars["dbusername"] = os.Getenv("dbusername")
 | 
				
			||||||
 | 
						envVars["dbpassword"] = os.Getenv("dbpassword")
 | 
				
			||||||
 | 
						envVars["dbhostname"] = os.Getenv("dbhostname")
 | 
				
			||||||
 | 
						envVars["dbname"] = os.Getenv("dbname")
 | 
				
			||||||
 | 
						envVars["timezone"] = os.Getenv("timezone")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for key, value := range envVars {
 | 
				
			||||||
 | 
							if value == "" {
 | 
				
			||||||
 | 
								log.Fatalf("shell environment variable %s is not set", key)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Database Config
 | 
				
			||||||
 | 
						dbConfig := mysql.Config{}
 | 
				
			||||||
 | 
						dbConfig.User = envVars["dbusername"]
 | 
				
			||||||
 | 
						dbConfig.Passwd = envVars["dbpassword"]
 | 
				
			||||||
 | 
						dbConfig.Addr = envVars["dbhostname"]
 | 
				
			||||||
 | 
						dbConfig.DBName = envVars["dbname"]
 | 
				
			||||||
 | 
						dbConfig.Net = "tcp"
 | 
				
			||||||
 | 
						dbConfig.ParseTime = true
 | 
				
			||||||
 | 
						dbConfig.AllowNativePasswords = true
 | 
				
			||||||
 | 
						database = countersql.Configuration{}
 | 
				
			||||||
 | 
						database.DSN = dbConfig.FormatDSN()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test database online at startup and get count of visits
 | 
				
			||||||
 | 
						dbConn, err := database.Connect()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("failed to connect to database: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						uniqueVisits, err = dbConn.GetUniqueVisits()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("failed to get number of unique visits from database: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						dbConn.DB.Close()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HTTP Routing
 | 
				
			||||||
 | 
					func main() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// API Handlers
 | 
				
			||||||
 | 
						http.HandleFunc("/", countHandler)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Print("Service listening on :8080")
 | 
				
			||||||
 | 
						log.Fatal(http.ListenAndServe(":8080", nil))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func countHandler(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						if r.Method == "GET" {
 | 
				
			||||||
 | 
							w.Header().Set("Access-Control-Allow-Origin", "*")
 | 
				
			||||||
 | 
							w.Write([]byte(strconv.Itoa(uniqueVisits)))
 | 
				
			||||||
 | 
							dbConn, err := database.Connect()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Printf("failed to connect to database: %v", err)
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusFailedDependency)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							defer dbConn.DB.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var ipAddress string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(r.Header.Get("X-Forwarded-For")) > 0 {
 | 
				
			||||||
 | 
								ipAddress = r.Header.Get("X-Forwarded-For")
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ipAddress = r.RemoteAddr
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ipAddress = strings.Split(ipAddress, ":")[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							returnVisitor, err := dbConn.HasIPVisited(ipAddress)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Printf("failed to determine if this is a return visitor, no data is being logged: %v", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if returnVisitor {
 | 
				
			||||||
 | 
								err = dbConn.IncrementVisitor(ipAddress)
 | 
				
			||||||
 | 
								log.Printf("return visitor from %s", ipAddress)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								err = dbConn.AddVisitor(ipAddress)
 | 
				
			||||||
 | 
								uniqueVisits++
 | 
				
			||||||
 | 
								log.Printf("new visitor from %s", ipAddress)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Printf("failed to add/update visit record in database: %v", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusMethodNotAllowed)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user