Jelajahi Sumber

完成基础架构代码

1、完成配置文件读取
2、完成 Redis 封装和测试
3、完成 Postgres 分装和测试
巴拉迪维 3 tahun lalu
melakukan
ea9bc2e2f7

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+.vscode
+docker/container-data/*
+!docker/container-data/.keep

+ 15 - 0
config.ini

@@ -0,0 +1,15 @@
+[app]
+port = 9090
+
+[redis]
+host = 127.0.0.1:56379
+database= 0
+username=
+password=
+
+[postgres]
+host = localhost
+port = 55432
+user = postgres
+password = 0DePm!oG_12Cz^kd_m
+database = oh_url_shortener

+ 14 - 0
core/access_log.go

@@ -0,0 +1,14 @@
+package core
+
+import (
+	"database/sql"
+	"time"
+)
+
+type AccessLog struct {
+	ID         int64          `db:"id"`
+	ShortUrl   string         `db:"short_url"`
+	AccessTime time.Time      `db:"access_time"`
+	Ip         sql.NullString `db:"ip"`
+	UserAgent  sql.NullString `db:"user_agent"`
+}

+ 12 - 0
core/short_url.go

@@ -0,0 +1,12 @@
+package core
+
+import "time"
+
+type ShortUrl struct {
+	ID        int64     `db:"id"`
+	ShortUrl  string    `db:"short_url"`
+	DestUrl   string    `db:"dest_url"`
+	Sha       string    `db:"sha"`
+	CreatedAt time.Time `db:"created_at"`
+	Valid     bool      `db:"is_valid"`
+}

+ 68 - 0
db/db_service.go

@@ -0,0 +1,68 @@
+package db
+
+import (
+	"fmt"
+	"ohurlshortener/utils"
+
+	"github.com/jmoiron/sqlx"
+	_ "github.com/lib/pq"
+)
+
+var dbService = &DatabaseService{}
+
+type DatabaseService struct {
+	Connection *sqlx.DB
+}
+
+func InitDatabaseService() (*DatabaseService, error) {
+	connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
+		utils.DatabaseConifg.Host, utils.DatabaseConifg.Port, utils.DatabaseConifg.User,
+		utils.DatabaseConifg.Password, utils.DatabaseConifg.DbName)
+	conn, err := sqlx.Connect("postgres", connStr)
+	if err != nil {
+		return dbService, err
+	}
+	conn.SetMaxOpenConns(10)
+	conn.SetMaxIdleConns(1)
+	conn.SetConnMaxLifetime(0) //always REUSE
+	dbService.Connection = conn
+	return dbService, nil
+}
+
+func NamedExec(query string, args interface{}) error {
+	_, err := dbService.Connection.NamedExec(query, args)
+	return err
+}
+
+func ExecTx(query string, args ...interface{}) error {
+	tx, err := dbService.Connection.Begin()
+	if err != nil {
+		return err
+	}
+	defer tx.Commit()
+
+	stmt, err := tx.Prepare(dbService.Connection.Rebind(query))
+	if err != nil {
+		return err
+	}
+	defer stmt.Close()
+
+	_, err = stmt.Exec(args...)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func Get(query string, dest interface{}, args ...interface{}) error {
+	return dbService.Connection.Get(dest, query, args...)
+}
+
+func Select(query string, dest interface{}, args ...interface{}) error {
+	return dbService.Connection.Select(dest, query, args...)
+}
+
+func Close() {
+	dbService.Connection.Close()
+}

+ 119 - 0
db/db_service_test.go

@@ -0,0 +1,119 @@
+package db
+
+import (
+	"database/sql"
+	"fmt"
+	"log"
+	"ohurlshortener/core"
+	"ohurlshortener/utils"
+	"testing"
+	"time"
+
+	_ "github.com/lib/pq"
+)
+
+func Test2(t *testing.T) {
+	init4Test(t)
+
+	query1 := `INSERT INTO public.access_logs
+	(short_url, access_time, ip, user_agent)
+	VALUES(:short_url,:access_time,:ip,:user_agent)`
+
+	wanted1 := core.AccessLog{
+		ShortUrl:   "https://gitee.com/barat",
+		AccessTime: time.Now(),
+		Ip: sql.NullString{
+			String: "127.0.0.1",
+			Valid:  true,
+		},
+		UserAgent: sql.NullString{
+			String: "hello world",
+			Valid:  true,
+		},
+	}
+
+	wanted2 := core.AccessLog{
+		ShortUrl:   "https://gitee.com/barat",
+		AccessTime: time.Now(),
+	}
+
+	w := []core.AccessLog{wanted1, wanted2}
+	err := NamedExec(query1, w)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	query2 := `select * from public.access_logs`
+	wanted3 := []core.AccessLog{}
+	err = Select(query2, &wanted3)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	if len(wanted3) <= 0 {
+		t.Errorf("found 0 row but expected more.")
+	}
+}
+
+func Test1(t *testing.T) {
+	init4Test(t)
+
+	wanted1 := core.ShortUrl{
+		ShortUrl:  "https://gitee.com/barat",
+		DestUrl:   "https://github.com/barats",
+		CreatedAt: time.Now(),
+		Sha:       fmt.Sprintf("a%d", time.Now().Unix()),
+		Valid:     true,
+	}
+
+	wanted2 := core.ShortUrl{
+		ShortUrl:  "https://gitee.com/barat",
+		DestUrl:   "https://github.com/barats",
+		CreatedAt: time.Now(),
+		Sha:       fmt.Sprintf("b%d", time.Now().Unix()),
+		Valid:     true,
+	}
+	query1 := `INSERT INTO public.short_urls
+	(short_url, dest_url, sha, created_at, is_valid)
+	VALUES(:short_url,:dest_url,:sha,:created_at,:is_valid)`
+	err := NamedExec(query1, []core.ShortUrl{wanted1, wanted2})
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	query2 := "select * from public.short_urls where is_valid = true and sha = $1"
+	found := core.ShortUrl{}
+	err = Get(query2, &found, wanted2.Sha)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	if found.Sha != wanted2.Sha {
+		t.Errorf("wanted %v found %v", wanted2, found)
+		return
+	}
+
+	query3 := "select * from public.short_urls where is_valid = true"
+	found2 := []core.ShortUrl{}
+	err = Select(query3, &found2)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	log.Printf("found size: %d", len(found2))
+}
+
+func init4Test(t *testing.T) {
+	_, err := utils.InitConfig("../config.ini")
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	_, err = InitDatabaseService()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+}

+ 2 - 0
docker/build_start.sh

@@ -0,0 +1,2 @@
+#!/bin/bash
+docker-compose -f services.yml --env-file vars.env up --build  --force-recreate 

+ 0 - 0
docker/container-data/.keep


+ 35 - 0
docker/services.yml

@@ -0,0 +1,35 @@
+version: '3'
+services:
+
+  postgres:
+    image: postgres:${PG_VERSION}
+    container_name: ${PG_CONTAINER_NAME}
+    hostname: postgres
+    environment:
+      - POSTGRES_USER=${PG_SUPER_USER}
+      - POSTGRES_PASSWORD=${PG_SUPER_PWD}      
+      - TZ=PRC
+      - PGTZ=PRC
+    volumes:
+      - ../structure.sql:/docker-entrypoint-initdb.d/001.sql
+      - ../docker/container-data/postgresql:/var/lib/postgresql/data      
+    ports:
+      - ${PG_LOCAL_PORT}:5432
+    networks:
+      - network_ohurlshortener
+
+  redis:
+    image: redis:${RD_VERSION}
+    container_name: ${RD_CONTAINER_NAME}
+    hostname: redis
+    ports:    
+      - ${RD_LOCAL_PORT}:6379
+    networks:
+      - network_ohurlshortener 
+      
+networks:
+  network_ohurlshortener:
+    driver: bridge
+    name: network_ohurlshortener
+    driver_opts:
+      com.docker.network.enable_ipv6: "true"

+ 2 - 0
docker/stop_prune.sh

@@ -0,0 +1,2 @@
+#!/bin/bash
+docker-compose -f services.yml  --env-file vars.env down && docker volume prune -f

+ 13 - 0
docker/vars.env

@@ -0,0 +1,13 @@
+# Environment Variables for docker-compose
+
+# Postgresql Vars
+PG_VERSION=9.6
+PG_CONTAINER_NAME=ohurlshortener_pg
+PG_LOCAL_PORT=55432
+PG_SUPER_USER=postgres
+PG_SUPER_PWD=0DePm!oG_12Cz^kd_m
+
+#Redis Vars
+RD_VERSION=6.2.6
+RD_CONTAINER_NAME=ohurlshortener_redis
+RD_LOCAL_PORT=56379

+ 11 - 0
go.mod

@@ -0,0 +1,11 @@
+module ohurlshortener
+
+go 1.16
+
+require (
+	github.com/gin-gonic/gin v1.7.7 
+	github.com/go-redis/redis/v8 v8.11.4
+	github.com/jmoiron/sqlx v1.3.4 
+	github.com/lib/pq v1.10.4 
+	gopkg.in/ini.v1 v1.66.4 
+)

+ 161 - 0
go.sum

@@ -0,0 +1,161 @@
+github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
+github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
+github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
+github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
+github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w=
+github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
+github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
+github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
+github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
+github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
+gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

+ 31 - 0
main.go

@@ -0,0 +1,31 @@
+package main
+
+import (
+	"fmt"
+	"ohurlshortener/db"
+	"ohurlshortener/redis"
+	"ohurlshortener/utils"
+
+	"github.com/gin-gonic/gin"
+)
+
+var config_file = "config.ini"
+
+func init() {
+	//Things MUST BE DONE before app starts
+	_, err := utils.InitConfig(config_file)
+	utils.ExitOnError("Config initialization failed.", err)
+
+	_, err = redis.InitRedisService()
+	utils.ExitOnError("Redis initialization failed.", err)
+
+	_, err = db.InitDatabaseService()
+	utils.ExitOnError("Database initialization failed.", err)
+}
+
+func main() {
+	gin.SetMode(gin.ReleaseMode)
+	r := gin.Default()
+	err := r.Run(fmt.Sprintf(":%d", utils.AppConfig.Port))
+	utils.ExitOnError("[ohUrlShortener] web service failed to start.", err)
+}

+ 53 - 0
redis/redis_service.go

@@ -0,0 +1,53 @@
+package redis
+
+import (
+	"context"
+	"ohurlshortener/utils"
+	"time"
+
+	"github.com/go-redis/redis/v8"
+)
+
+var (
+	redisService = &RedisService{}
+	ctx          = context.Background()
+)
+
+type RedisService struct {
+	redisClient *redis.Client
+}
+
+func InitRedisService() (*RedisService, error) {
+	redisClient := redis.NewClient(&redis.Options{
+		Addr:     utils.RedisConfig.Host,
+		DB:       utils.RedisConfig.Database,
+		Username: utils.RedisConfig.User,
+		Password: utils.RedisConfig.Password,
+	})
+
+	_, err := redisClient.Ping(ctx).Result()
+	if err != nil {
+		return nil, err
+	}
+
+	redisService.redisClient = redisClient
+
+	return redisService, nil
+}
+
+func Set(key string, value interface{}, ttl time.Duration) error {
+	return redisService.redisClient.Set(ctx, key, value, ttl).Err()
+}
+
+func Set30m(key string, value interface{}) error {
+	return Set(key, value, 30*time.Minute)
+}
+
+//Set4Ever Needs redis version 6.0 or above
+func Set4Ever(key string, value interface{}) error {
+	return Set(key, value, redis.KeepTTL)
+}
+
+func GetString(key string) (string, error) {
+	return redisService.redisClient.Get(ctx, key).Result()
+}

+ 47 - 0
redis/redis_service_test.go

@@ -0,0 +1,47 @@
+package redis
+
+import (
+	"ohurlshortener/utils"
+	"testing"
+
+	oredis "github.com/go-redis/redis/v8"
+)
+
+func init4Test(t *testing.T) {
+
+	_, err := utils.InitConfig("../config.ini")
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	_, err = InitRedisService()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+}
+
+func TestSet(t *testing.T) {
+	init4Test(t)
+	err := Set4Ever("hello", "world")
+	if err != nil {
+		t.Error(err)
+		return
+	}
+}
+
+func TestGetString(t *testing.T) {
+	init4Test(t)
+	rs, err := GetString("hello")
+	if err == oredis.Nil {
+		t.Errorf("GetString() found NOTHING.")
+		return
+	} else if err != nil {
+		t.Error(err)
+		return
+	}
+	if rs != "world" {
+		t.Errorf("GetString() wanted %s, found %s", "world", rs)
+		return
+	}
+}

+ 46 - 0
structure.sql

@@ -0,0 +1,46 @@
+-- Database Structure For ohUrlShortener
+CREATE DATABASE oh_url_shortener ENCODING 'UTF8';
+
+-- Connect to database repostats
+\c oh_url_shortener
+
+CREATE TABLE public.short_urls (
+  id serial4 NOT NULL,
+	short_url text NOT NULL,
+	dest_url varchar(200) NOT NULL,
+	sha varchar(100) NOT NULL,
+	created_at timestamp with time zone NOT NULL DEFAULT now(),
+	is_valid bool NOT NULL DEFAULT true,	
+	CONSTRAINT short_urls_pk PRIMARY KEY (id),
+	CONSTRAINT short_urls_sha_un UNIQUE (sha)
+);
+
+CREATE TABLE public.access_logs (
+	id serial4 NOT NULL,
+	short_url varchar(200) NOT NULL,
+	access_time timestamp with time zone NOT NULL DEFAULT NOW(),
+	ip varchar(32) NULL,
+	user_agent varchar(500) NULL,
+	CONSTRAINT access_logs_pk PRIMARY KEY (id)
+);
+CREATE INDEX access_logs_short_url_idx ON public.access_logs (short_url);
+CREATE INDEX access_logs_access_time_idx ON public.access_logs (access_time);
+CREATE INDEX access_logs_ip_idx ON public.access_logs (ip);
+
+
+CREATE VIEW public.url_puv AS
+SELECT 
+	l.SHORT_URL AS "Short_URL",
+	count(l.ip) AS "IP_Count",
+	count(DISTINCT(l.ip)) AS "Distinct_IP_Count"
+FROM public.access_logs l
+GROUP BY l.short_url;
+
+CREATE VIEW public.puv_by_date AS 
+SELECT
+	date(l.access_time) AS "Access_Date",
+	count(l.ip) AS "IP_Count",
+	count(DISTINCT(l.ip)) AS "Distinct_IP_Count"
+FROM public.access_logs l
+GROUP BY date(l.access_time)
+ORDER BY "Access_Date" DESC;

+ 56 - 0
utils/config.go

@@ -0,0 +1,56 @@
+package utils
+
+import (
+	"gopkg.in/ini.v1"
+)
+
+var (
+	DatabaseConifg DatabaseConfigInfo
+	AppConfig      AppConfigInfo
+	RedisConfig    RedisConfigInfo
+)
+
+type AppConfigInfo struct {
+	Port int
+}
+
+type RedisConfigInfo struct {
+	Host     string
+	User     string
+	Password string
+	Database int
+}
+
+type DatabaseConfigInfo struct {
+	Host     string
+	Port     int
+	User     string
+	Password string
+	DbName   string
+}
+
+func InitConfig(file string) (*ini.File, error) {
+
+	cfg, err := ini.Load(file)
+	if err != nil {
+		return nil, nil
+	}
+
+	section := cfg.Section("postgres")
+	DatabaseConifg.Host = section.Key("host").String()
+	DatabaseConifg.Port = section.Key("port").MustInt()
+	DatabaseConifg.User = section.Key("user").String()
+	DatabaseConifg.Password = section.Key("password").String()
+	DatabaseConifg.DbName = section.Key("database").String()
+
+	appSection := cfg.Section("app")
+	AppConfig.Port = appSection.Key("port").MustInt()
+
+	redisSection := cfg.Section("redis")
+	RedisConfig.Host = redisSection.Key("host").String()
+	RedisConfig.User = redisSection.Key("user").String()
+	RedisConfig.Password = redisSection.Key("password").String()
+	RedisConfig.Database = redisSection.Key("database").MustInt()
+
+	return cfg, err
+}

+ 19 - 0
utils/utils.go

@@ -0,0 +1,19 @@
+package utils
+
+import (
+	"log"
+	"os"
+)
+
+func ExitOnError(message string, err error) {
+	if err != nil {
+		log.Printf("[%s] - %s", message, err)
+		os.Exit(-1)
+	}
+}
+
+func PrintOnError(message string, err error) {
+	if err != nil {
+		log.Printf("[%s] - %s", message, err)
+	}
+}