2018-08-16 06:22:20 +00:00
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
2018-06-30 00:57:05 +00:00
package core
import (
"fmt"
"github.com/go-yaml/yaml"
2018-09-12 04:14:22 +00:00
"github.com/hunterlong/statup/core/notifier"
2018-06-30 00:57:05 +00:00
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
2018-09-05 10:54:57 +00:00
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/postgres"
_ "github.com/jinzhu/gorm/dialects/sqlite"
2018-06-30 00:57:05 +00:00
"os"
"time"
)
var (
2018-10-07 05:04:06 +00:00
// DbSession stores the Statup database session
2018-09-06 05:28:35 +00:00
DbSession * gorm . DB
2018-11-19 07:13:53 +00:00
DbModels [ ] interface { }
2018-06-30 00:57:05 +00:00
)
2018-11-19 07:13:53 +00:00
func init ( ) {
DbModels = [ ] interface { } { & types . Service { } , & types . User { } , & types . Hit { } , & types . Failure { } , & types . Message { } , & types . Checkin { } , & types . CheckinHit { } , & notifier . Notification { } }
}
2018-10-07 05:04:06 +00:00
// DbConfig stores the config.yml file for the statup configuration
2018-09-27 03:20:02 +00:00
type DbConfig types . DbConfig
2018-09-05 10:54:57 +00:00
2018-09-10 22:16:23 +00:00
// failuresDB returns the 'failures' database column
func failuresDB ( ) * gorm . DB {
return DbSession . Model ( & types . Failure { } )
}
// hitsDB returns the 'hits' database column
2018-09-05 10:54:57 +00:00
func hitsDB ( ) * gorm . DB {
2018-09-10 09:01:04 +00:00
return DbSession . Model ( & types . Hit { } )
2018-09-05 10:54:57 +00:00
}
2018-09-10 22:16:23 +00:00
// servicesDB returns the 'services' database column
2018-09-05 10:54:57 +00:00
func servicesDB ( ) * gorm . DB {
2018-09-10 09:01:04 +00:00
return DbSession . Model ( & types . Service { } )
2018-09-05 10:54:57 +00:00
}
2018-09-10 22:16:23 +00:00
// coreDB returns the single column 'core'
2018-09-05 10:54:57 +00:00
func coreDB ( ) * gorm . DB {
2018-09-10 09:01:04 +00:00
return DbSession . Table ( "core" ) . Model ( & CoreApp )
2018-09-05 10:54:57 +00:00
}
2018-09-10 22:16:23 +00:00
// usersDB returns the 'users' database column
2018-09-05 10:54:57 +00:00
func usersDB ( ) * gorm . DB {
2018-09-10 09:01:04 +00:00
return DbSession . Model ( & types . User { } )
2018-09-05 10:54:57 +00:00
}
2018-10-07 22:38:56 +00:00
// checkinDB returns the Checkin records for a service
2018-09-05 10:54:57 +00:00
func checkinDB ( ) * gorm . DB {
2018-11-25 03:56:09 +00:00
return DbSession . Model ( & types . Checkin { } )
2018-11-21 03:39:40 +00:00
}
// checkinHitsDB returns the Checkin Hits records for a service
func checkinHitsDB ( ) * gorm . DB {
return DbSession . Model ( & types . CheckinHit { } )
2018-09-05 10:54:57 +00:00
}
2018-11-07 05:06:44 +00:00
// messagesDb returns the Checkin records for a service
func messagesDb ( ) * gorm . DB {
return DbSession . Model ( & types . Message { } )
}
2018-09-25 07:03:49 +00:00
// HitsBetween returns the gorm database query for a collection of service hits between a time range
2018-10-03 03:48:40 +00:00
func ( s * Service ) HitsBetween ( t1 , t2 time . Time , group string , column string ) * gorm . DB {
selector := Dbtimestamp ( group , column )
2018-11-25 10:18:21 +00:00
if CoreApp . DbConnection == "postgres" {
2018-11-19 07:13:53 +00:00
timeQuery := fmt . Sprintf ( "service = %v AND created_at BETWEEN '%v.000000' AND '%v.000000'" , s . Id , t1 . UTC ( ) . Format ( types . POSTGRES_TIME ) , t2 . UTC ( ) . Format ( types . POSTGRES_TIME ) )
return DbSession . Model ( & types . Hit { } ) . Select ( selector ) . Where ( timeQuery )
} else {
return DbSession . Model ( & types . Hit { } ) . Select ( selector ) . Where ( "service = ? AND created_at BETWEEN ? AND ?" , s . Id , t1 . UTC ( ) . Format ( types . TIME_DAY ) , t2 . UTC ( ) . Format ( types . TIME_DAY ) )
}
2018-09-25 04:19:59 +00:00
}
2018-10-06 06:38:33 +00:00
// CloseDB will close the database connection if available
2018-09-25 04:19:59 +00:00
func CloseDB ( ) {
if DbSession != nil {
DbSession . DB ( ) . Close ( )
}
}
2018-11-19 07:13:53 +00:00
// AfterFind for Core will set the timezone
func ( c * Core ) AfterFind ( ) ( err error ) {
c . CreatedAt = utils . Timezoner ( c . CreatedAt , CoreApp . Timezone )
c . UpdatedAt = utils . Timezoner ( c . UpdatedAt , CoreApp . Timezone )
return
}
2018-10-06 06:38:33 +00:00
// AfterFind for Service will set the timezone
2018-09-17 22:13:42 +00:00
func ( s * Service ) AfterFind ( ) ( err error ) {
s . CreatedAt = utils . Timezoner ( s . CreatedAt , CoreApp . Timezone )
2018-11-13 23:38:25 +00:00
s . UpdatedAt = utils . Timezoner ( s . UpdatedAt , CoreApp . Timezone )
2018-09-17 22:13:42 +00:00
return
}
2018-10-06 06:38:33 +00:00
// AfterFind for Hit will set the timezone
2018-11-13 23:38:25 +00:00
func ( h * Hit ) AfterFind ( ) ( err error ) {
h . CreatedAt = utils . Timezoner ( h . CreatedAt , CoreApp . Timezone )
return
}
2018-09-17 22:13:42 +00:00
2018-10-07 04:48:33 +00:00
// AfterFind for failure will set the timezone
func ( f * failure ) AfterFind ( ) ( err error ) {
2018-09-17 22:13:42 +00:00
f . CreatedAt = utils . Timezoner ( f . CreatedAt , CoreApp . Timezone )
return
}
2018-10-06 06:38:33 +00:00
// AfterFind for USer will set the timezone
2018-11-16 16:42:49 +00:00
func ( u * User ) AfterFind ( ) ( err error ) {
2018-09-17 22:13:42 +00:00
u . CreatedAt = utils . Timezoner ( u . CreatedAt , CoreApp . Timezone )
2018-11-19 07:13:53 +00:00
u . UpdatedAt = utils . Timezoner ( u . UpdatedAt , CoreApp . Timezone )
2018-09-17 22:13:42 +00:00
return
}
2018-10-07 22:38:56 +00:00
// AfterFind for Checkin will set the timezone
func ( c * Checkin ) AfterFind ( ) ( err error ) {
2018-10-07 05:04:06 +00:00
c . CreatedAt = utils . Timezoner ( c . CreatedAt , CoreApp . Timezone )
2018-11-19 07:13:53 +00:00
c . UpdatedAt = utils . Timezoner ( c . UpdatedAt , CoreApp . Timezone )
2018-10-04 08:18:55 +00:00
return
}
2018-10-07 04:48:33 +00:00
// AfterFind for checkinHit will set the timezone
2018-11-21 03:39:40 +00:00
func ( c * CheckinHit ) AfterFind ( ) ( err error ) {
2018-10-07 05:04:06 +00:00
c . CreatedAt = utils . Timezoner ( c . CreatedAt , CoreApp . Timezone )
2018-10-04 08:18:55 +00:00
return
}
2018-11-17 17:14:14 +00:00
// AfterFind for Message will set the timezone
func ( u * Message ) AfterFind ( ) ( err error ) {
u . CreatedAt = utils . Timezoner ( u . CreatedAt , CoreApp . Timezone )
2018-11-19 07:13:53 +00:00
u . UpdatedAt = utils . Timezoner ( u . UpdatedAt , CoreApp . Timezone )
u . StartOn = utils . Timezoner ( u . StartOn , CoreApp . Timezone )
u . EndOn = utils . Timezoner ( u . EndOn , CoreApp . Timezone )
2018-11-17 17:14:14 +00:00
return
}
2018-10-06 06:38:33 +00:00
// BeforeCreate for Hit will set CreatedAt to UTC
2018-10-07 05:04:06 +00:00
func ( h * Hit ) BeforeCreate ( ) ( err error ) {
if h . CreatedAt . IsZero ( ) {
h . CreatedAt = time . Now ( ) . UTC ( )
2018-09-25 04:19:59 +00:00
}
2018-09-17 22:13:42 +00:00
return
}
2018-10-07 04:48:33 +00:00
// BeforeCreate for failure will set CreatedAt to UTC
2018-10-07 05:04:06 +00:00
func ( f * failure ) BeforeCreate ( ) ( err error ) {
if f . CreatedAt . IsZero ( ) {
f . CreatedAt = time . Now ( ) . UTC ( )
2018-10-05 04:28:38 +00:00
}
2018-09-17 22:13:42 +00:00
return
}
2018-11-16 16:42:49 +00:00
// BeforeCreate for User will set CreatedAt to UTC
func ( u * User ) BeforeCreate ( ) ( err error ) {
2018-10-05 04:28:38 +00:00
if u . CreatedAt . IsZero ( ) {
u . CreatedAt = time . Now ( ) . UTC ( )
2018-11-19 07:13:53 +00:00
u . UpdatedAt = time . Now ( ) . UTC ( )
2018-10-05 04:28:38 +00:00
}
2018-09-17 22:13:42 +00:00
return
}
2018-11-17 17:14:14 +00:00
// BeforeCreate for Message will set CreatedAt to UTC
func ( u * Message ) BeforeCreate ( ) ( err error ) {
if u . CreatedAt . IsZero ( ) {
u . CreatedAt = time . Now ( ) . UTC ( )
2018-11-19 07:13:53 +00:00
u . UpdatedAt = time . Now ( ) . UTC ( )
2018-11-17 17:14:14 +00:00
}
return
}
2018-10-06 06:38:33 +00:00
// BeforeCreate for Service will set CreatedAt to UTC
2018-10-07 05:04:06 +00:00
func ( s * Service ) BeforeCreate ( ) ( err error ) {
if s . CreatedAt . IsZero ( ) {
s . CreatedAt = time . Now ( ) . UTC ( )
2018-11-13 23:38:25 +00:00
s . UpdatedAt = time . Now ( ) . UTC ( )
2018-10-05 04:28:38 +00:00
}
2018-09-17 22:13:42 +00:00
return
}
2018-10-07 22:38:56 +00:00
// BeforeCreate for Checkin will set CreatedAt to UTC
func ( c * Checkin ) BeforeCreate ( ) ( err error ) {
2018-10-07 05:04:06 +00:00
if c . CreatedAt . IsZero ( ) {
c . CreatedAt = time . Now ( ) . UTC ( )
2018-11-13 23:38:25 +00:00
c . UpdatedAt = time . Now ( ) . UTC ( )
2018-10-05 04:28:38 +00:00
}
2018-10-04 08:18:55 +00:00
return
}
2018-10-07 04:48:33 +00:00
// BeforeCreate for checkinHit will set CreatedAt to UTC
2018-11-21 03:39:40 +00:00
func ( c * CheckinHit ) BeforeCreate ( ) ( err error ) {
2018-10-07 05:04:06 +00:00
if c . CreatedAt . IsZero ( ) {
c . CreatedAt = time . Now ( ) . UTC ( )
2018-10-05 04:28:38 +00:00
}
2018-10-04 08:18:55 +00:00
return
}
2018-09-06 05:28:35 +00:00
// InsertCore create the single row for the Core settings in Statup
2018-09-05 10:54:57 +00:00
func ( db * DbConfig ) InsertCore ( ) ( * Core , error ) {
CoreApp = & Core { Core : & types . Core {
Name : db . Project ,
Description : db . Description ,
Config : "config.yml" ,
ApiKey : utils . NewSHA1Hash ( 9 ) ,
ApiSecret : utils . NewSHA1Hash ( 16 ) ,
Domain : db . Domain ,
MigrationId : time . Now ( ) . Unix ( ) ,
} }
CoreApp . DbConnection = db . DbConn
query := coreDB ( ) . Create ( & CoreApp )
return CoreApp , query . Error
}
2018-09-06 05:28:35 +00:00
// Connect will attempt to connect to the sqlite, postgres, or mysql database
2018-09-05 10:54:57 +00:00
func ( db * DbConfig ) Connect ( retry bool , location string ) error {
if DbSession != nil {
2018-10-11 16:53:13 +00:00
return nil
2018-09-05 10:54:57 +00:00
}
2018-09-06 05:28:35 +00:00
var conn , dbType string
2018-10-11 16:53:13 +00:00
var err error
2018-09-06 05:28:35 +00:00
dbType = Configs . DbConn
2018-10-11 16:53:13 +00:00
if Configs . DbPort == 0 {
Configs . DbPort = DefaultPort ( dbType )
}
2018-09-06 05:28:35 +00:00
switch dbType {
2018-09-05 10:54:57 +00:00
case "sqlite" :
2018-10-11 16:53:13 +00:00
conn = location + "/statup.db"
2018-09-06 05:28:35 +00:00
dbType = "sqlite3"
2018-09-05 10:54:57 +00:00
case "mysql" :
host := fmt . Sprintf ( "%v:%v" , Configs . DbHost , Configs . DbPort )
2018-09-16 07:48:34 +00:00
conn = fmt . Sprintf ( "%v:%v@tcp(%v)/%v?charset=utf8&parseTime=True&loc=UTC" , Configs . DbUser , Configs . DbPass , host , Configs . DbData )
2018-09-05 10:54:57 +00:00
case "postgres" :
2018-11-19 07:13:53 +00:00
conn = fmt . Sprintf ( "host=%v port=%v user=%v dbname=%v password=%v timezone=UTC sslmode=disable" , Configs . DbHost , Configs . DbPort , Configs . DbUser , Configs . DbData , Configs . DbPass )
2018-09-05 10:54:57 +00:00
case "mssql" :
host := fmt . Sprintf ( "%v:%v" , Configs . DbHost , Configs . DbPort )
2018-09-06 05:28:35 +00:00
conn = fmt . Sprintf ( "sqlserver://%v:%v@%v?database=%v" , Configs . DbUser , Configs . DbPass , host , Configs . DbData )
}
2018-10-11 16:53:13 +00:00
dbSession , err := gorm . Open ( dbType , conn )
2018-09-06 05:28:35 +00:00
if err != nil {
if retry {
2018-09-28 03:36:23 +00:00
utils . Log ( 1 , fmt . Sprintf ( "Database connection to '%v' is not available, trying again in 5 seconds..." , conn ) )
2018-09-06 05:28:35 +00:00
return db . waitForDb ( )
} else {
return err
2018-06-30 00:57:05 +00:00
}
}
2018-10-11 16:53:13 +00:00
err = dbSession . DB ( ) . Ping ( )
2018-07-06 03:01:03 +00:00
if err == nil {
2018-10-11 16:53:13 +00:00
DbSession = dbSession
2018-11-16 02:52:51 +00:00
utils . Log ( 1 , fmt . Sprintf ( "Database %v connection '%v@%v:%v' at %v was successful." , dbType , Configs . DbUser , Configs . DbHost , Configs . DbPort , Configs . DbData ) )
2018-07-06 03:01:03 +00:00
}
2018-06-30 00:57:05 +00:00
return err
}
2018-10-06 06:38:33 +00:00
// waitForDb will sleep for 5 seconds and try to connect to the database again
2018-09-05 10:54:57 +00:00
func ( db * DbConfig ) waitForDb ( ) error {
2018-07-20 05:26:41 +00:00
time . Sleep ( 5 * time . Second )
2018-09-05 10:54:57 +00:00
return db . Connect ( true , utils . Directory )
2018-07-20 05:26:41 +00:00
}
2018-09-06 05:28:35 +00:00
// DatabaseMaintence will automatically delete old records from 'failures' and 'hits'
// this function is currently set to delete records 7+ days old every 60 minutes
2018-06-30 00:57:05 +00:00
func DatabaseMaintence ( ) {
2018-07-28 16:44:52 +00:00
for range time . Tick ( 60 * time . Minute ) {
2018-09-27 03:20:02 +00:00
utils . Log ( 1 , "Checking for database records older than 3 months..." )
since := time . Now ( ) . AddDate ( 0 , - 3 , 0 ) . UTC ( )
2018-07-28 16:44:52 +00:00
DeleteAllSince ( "failures" , since )
DeleteAllSince ( "hits" , since )
}
2018-06-30 00:57:05 +00:00
}
2018-09-06 05:28:35 +00:00
// DeleteAllSince will delete a specific table's records based on a time.
2018-06-30 00:57:05 +00:00
func DeleteAllSince ( table string , date time . Time ) {
sql := fmt . Sprintf ( "DELETE FROM %v WHERE created_at < '%v';" , table , date . Format ( "2006-01-02" ) )
2018-11-21 08:45:55 +00:00
db := DbSession . Exec ( sql )
2018-09-05 10:54:57 +00:00
if db . Error != nil {
utils . Log ( 2 , db . Error )
2018-06-30 00:57:05 +00:00
}
}
2018-09-06 05:28:35 +00:00
// Update will save the config.yml file
2018-10-07 05:04:06 +00:00
func ( db * DbConfig ) Update ( ) error {
2018-08-30 04:49:44 +00:00
var err error
config , err := os . Create ( utils . Directory + "/config.yml" )
if err != nil {
utils . Log ( 4 , err )
return err
}
2018-10-07 05:04:06 +00:00
data , err := yaml . Marshal ( db )
2018-08-30 04:49:44 +00:00
if err != nil {
utils . Log ( 3 , err )
return err
}
config . WriteString ( string ( data ) )
config . Close ( )
return err
}
2018-09-06 05:28:35 +00:00
// Save will initially create the config.yml file
2018-10-07 05:04:06 +00:00
func ( db * DbConfig ) Save ( ) ( * DbConfig , error ) {
2018-06-30 00:57:05 +00:00
var err error
2018-08-20 03:48:28 +00:00
config , err := os . Create ( utils . Directory + "/config.yml" )
2018-06-30 00:57:05 +00:00
if err != nil {
2018-07-01 10:24:35 +00:00
utils . Log ( 4 , err )
2018-09-05 10:54:57 +00:00
return nil , err
2018-06-30 00:57:05 +00:00
}
2018-10-07 05:04:06 +00:00
db . ApiKey = utils . NewSHA1Hash ( 16 )
db . ApiSecret = utils . NewSHA1Hash ( 16 )
data , err := yaml . Marshal ( db )
2018-06-30 00:57:05 +00:00
if err != nil {
2018-07-01 10:24:35 +00:00
utils . Log ( 3 , err )
2018-09-05 10:54:57 +00:00
return nil , err
2018-06-30 00:57:05 +00:00
}
config . WriteString ( string ( data ) )
2018-09-05 10:54:57 +00:00
defer config . Close ( )
2018-10-07 05:04:06 +00:00
return db , err
2018-09-05 10:54:57 +00:00
}
2018-06-30 00:57:05 +00:00
2018-09-06 05:28:35 +00:00
// CreateCore will initialize the global variable 'CoreApp". This global variable contains most of Statup app.
2018-09-05 10:54:57 +00:00
func ( c * DbConfig ) CreateCore ( ) * Core {
2018-07-17 09:18:20 +00:00
newCore := & types . Core {
2018-06-30 00:57:05 +00:00
Name : c . Project ,
Description : c . Description ,
Config : "config.yml" ,
2018-09-05 10:54:57 +00:00
ApiKey : c . ApiKey ,
ApiSecret : c . ApiSecret ,
2018-06-30 00:57:05 +00:00
Domain : c . Domain ,
2018-07-06 03:01:03 +00:00
MigrationId : time . Now ( ) . Unix ( ) ,
2018-06-30 00:57:05 +00:00
}
2018-09-05 10:54:57 +00:00
db := coreDB ( ) . Create ( & newCore )
if db . Error == nil {
2018-07-17 09:18:20 +00:00
CoreApp = & Core { Core : newCore }
2018-07-01 10:24:35 +00:00
}
2018-09-05 10:54:57 +00:00
CoreApp , err := SelectCore ( )
2018-07-12 04:49:18 +00:00
if err != nil {
utils . Log ( 4 , err )
}
2018-09-05 10:54:57 +00:00
return CoreApp
2018-06-30 00:57:05 +00:00
}
2018-09-06 05:28:35 +00:00
// DropDatabase will DROP each table Statup created
2018-09-05 10:54:57 +00:00
func ( db * DbConfig ) DropDatabase ( ) error {
utils . Log ( 1 , "Dropping Database Tables..." )
2018-11-19 07:13:53 +00:00
err := DbSession . DropTableIfExists ( "checkins" )
err = DbSession . DropTableIfExists ( "checkin_hits" )
2018-09-12 04:14:22 +00:00
err = DbSession . DropTableIfExists ( "notifications" )
2018-09-05 10:54:57 +00:00
err = DbSession . DropTableIfExists ( "core" )
err = DbSession . DropTableIfExists ( "failures" )
err = DbSession . DropTableIfExists ( "hits" )
err = DbSession . DropTableIfExists ( "services" )
err = DbSession . DropTableIfExists ( "users" )
2018-11-07 05:06:44 +00:00
err = DbSession . DropTableIfExists ( "messages" )
2018-09-05 10:54:57 +00:00
return err . Error
}
2018-09-06 05:28:35 +00:00
// CreateDatabase will CREATE TABLES for each of the Statup elements
2018-09-05 10:54:57 +00:00
func ( db * DbConfig ) CreateDatabase ( ) error {
2018-11-19 07:13:53 +00:00
var err error
2018-07-12 04:49:18 +00:00
utils . Log ( 1 , "Creating Database Tables..." )
2018-11-19 07:13:53 +00:00
for _ , table := range DbModels {
if err := DbSession . CreateTable ( table ) ; err . Error != nil {
return err . Error
}
}
if err := DbSession . Table ( "core" ) . CreateTable ( & types . Core { } ) ; err . Error != nil {
return err . Error
}
2018-09-05 10:54:57 +00:00
utils . Log ( 1 , "Statup Database Created" )
2018-11-19 07:13:53 +00:00
return err
2018-09-05 10:54:57 +00:00
}
2018-09-06 05:28:35 +00:00
// MigrateDatabase will migrate the database structure to current version.
// This function will NOT remove previous records, tables or columns from the database.
// If this function has an issue, it will ROLLBACK to the previous state.
2018-09-05 10:54:57 +00:00
func ( db * DbConfig ) MigrateDatabase ( ) error {
utils . Log ( 1 , "Migrating Database Tables..." )
2018-09-06 05:28:35 +00:00
tx := DbSession . Begin ( )
defer func ( ) {
if r := recover ( ) ; r != nil {
tx . Rollback ( )
}
} ( )
if tx . Error != nil {
return tx . Error
}
2018-11-19 07:13:53 +00:00
for _ , table := range DbModels {
tx = tx . AutoMigrate ( table )
}
if err := tx . Table ( "core" ) . AutoMigrate ( & types . Core { } ) ; err . Error != nil {
2018-09-06 05:28:35 +00:00
tx . Rollback ( )
utils . Log ( 3 , fmt . Sprintf ( "Statup Database could not be migrated: %v" , tx . Error ) )
return tx . Error
}
2018-09-05 10:54:57 +00:00
utils . Log ( 1 , "Statup Database Migrated" )
2018-09-06 05:28:35 +00:00
return tx . Commit ( ) . Error
2018-06-30 00:57:05 +00:00
}