commit b71f88cbf78e1ebc186cc1bddfa1fcc5d7f15992 Author: Sangbum Kim Date: Mon Aug 21 17:58:21 2017 +0900 added initial skeleton diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce8e70c --- /dev/null +++ b/.gitignore @@ -0,0 +1,127 @@ + +# Created by https://www.gitignore.io/api/intellij,go,linux,osx,windows + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +.idea +*.iml + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + + +### Go ### +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + + +### Linux ### +*~ +*.swp + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Windows ### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +build/ +bind/ +vendor/ +mecab/ +*.log +rooibos diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1c5d12e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "rooibos dev", + "type": "go", + "request": "launch", + "mode": "debug", + "remotePath": "", + "port": 2345, + "host": "127.0.0.1", + "program": "${workspaceRoot}/rooibos", + "env": {}, + "args": [ + "-C", + "dev.yml" + ], + "showLog": true + } + ] +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5fc49d3 --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ + + +# This how we want to name the binary output +BINARY=rooibos + +# These are the values we want to pass for VERSION and BUILD +# git tag 1.0.1 +# git commit -am "One more change after the tags" +VERSION=`git describe --always` +BUILD=`date +%FT%T%z` + +# Setup the -ldflags option for go build here, interpolate the variable values +LDFLAGS=-ldflags "-w -s -X amuz.es/gogs/infra/rooibos/util.version=${VERSION} -X amuz.es/gogs/infra/rooibos/util.buildDate=${BUILD} " +CGO_ENABLED=0 + + +build: + go build ${LDFLAGS} -tags=jsoniter -o ${BINARY} . + strip -x ${BINARY} + +setup: + go get -u github.com/kardianos/govendor + # go get -u github.com/gogo/protobuf/proto + # go get -u github.com/gogo/protobuf/protoc-gen-gogo + # go get -u github.com/gogo/protobuf/gogoproto + # go get -u github.com/gogo/protobuf/protoc-gen-gofast + # go get -u github.com/gogo/protobuf/protoc-gen-gogofaster + # go get -u github.com/golang/protobuf/proto + ${GOPATH}/bin/govendor fetch -v +missing + #libjpeg-turbo +generate: + go get -u github.com/gogo/protobuf/proto + go get -u github.com/gogo/protobuf/protoc-gen-gogo + go get -u github.com/gogo/protobuf/gogoproto + go get -u github.com/gogo/protobuf/protoc-gen-gofast + go get -u github.com/gogo/protobuf/protoc-gen-gogofaster + go get -u github.com/golang/protobuf/proto + protoc --gogofaster_out=. --proto_path=../../../../:. subsys/redis/app_token_data.proto + #${GOPATH}/bin/ffjson subsys/http/iface/rest.go + #${GOPATH}/bin/easyjson subsys/http/iface/rest.go + + +strip: + upx rooibos +# Installs our project: copies binaries +install: + go install ${LDFLAGS} + +# Cleans our project: deletes binaries +clean: + if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi + +.PHONY: clean install diff --git a/README.md b/README.md new file mode 100644 index 0000000..eec5db2 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +![alt tag](https://upload.wikimedia.org/wikipedia/commons/2/23/Golang.png) + +[![Build Status](https://travis-ci.org/Massad/gin-boilerplate.svg?branch=master)](https://travis-ci.org/Massad/gin-boilerplate) +[![Join the chat at https://gitter.im/Massad/gin-boilerplate](https://badges.gitter.im/Massad/gin-boilerplate.svg)](https://gitter.im/Massad/gin-boilerplate?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +Welcome to **Golang Gin boilerplate**! + +The fastest way to deploy a restful api's with [Gin Framework](https://gin-gonic.github.io/gin/) with a structured project that defaults to **PostgreSQL** database and **Redis** as the session storage. + +## Configured with + +* [go-gorp](github.com/go-gorp/gorp): Go Relational Persistence +* [RedisStore](https://github.com/gin-gonic/contrib/tree/master/sessions): Gin middleware for session management with multi-backend support (currently cookie, Redis). +* Built-in **CORS Middleware** +* Feature **PostgreSQL 9.4** JSON queries +* Unit test + +### Installation + +``` +$ go get github.com/Massad/gin-boilerplate +``` + +``` +$ cd $GOPATH/src/github.com/Massad/gin-boilerplate +``` + +``` +$ go get -t -v ./... +``` + +> Sometimes you need to get this package manually +``` +$ go get github.com/bmizerany/assert +``` + +You will find the **database.sql** in `db/database.sql` + +And you can import the postgres database using this command: +``` +$ psql -U postgres -h localhost < ./db/database.sql +``` + +## Running Your Application + +``` +$ go run *.go +``` + +## Building Your Application + +``` +$ go build -v +``` + +``` +$ ./gin-boilerplate +``` + +## Testing Your Application + +``` +$ go test -v ./tests/* +``` + + +## Import Postman Collection (API's) +You can import from this [link](https://www.getpostman.com/collections/ac0680f90961bafd5de7). If you don't have **Postman**, check this link [https://www.getpostman.com](https://www.getpostman.com/) + +## Contribution + +You are welcome to contribute to keep it up to date and always improving! + +If you have any question or need help, drop a message at [https://gitter.im/Massad/gin-boilerplate](https://gitter.im/Massad/gin-boilerplate) + +--- + +## License +(The MIT License) + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/bench.py b/bench.py new file mode 100755 index 0000000..ebf64bc --- /dev/null +++ b/bench.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +import os +import re +import time + +def report(c, lines): + rtv = [] + for str in lines: + str=re.sub("^[ \t]+",'', str) + if str.find("Latency") == 0 or str.find("Requests/sec") == 0: + token = re.split("[ \t]+", str) + value = re.sub("[^0-9\.]", '', token[1]) + if re.match("[0-9\.]+us", token[1]): + rtv.append(float(value)/1000) + elif re.match("[0-9\.]+s", token[1]): + rtv.append(float(value)*1000) + else: + rtv.append(float(value)) + print c,"\t", rtv[0],"\t",rtv[1] + +def run(cmd): + stdin, stdout, stderr = os.popen3(cmd) + return stdout.readlines() + +offset = 5 +maxConcurrency = offset * 61 +duration = 10 +for num in range(1, maxConcurrency/offset): + concurrency = num * offset + thread = 16 if concurrency > 16 else concurrency + cmd = ( +'''wrk --timeout 7 -t %d -c %d -d %d http://localhost:8080/api/v1/media/admin/2/thumb''' + % (thread, concurrency, duration)) + result =run(cmd) + report(concurrency, result) + time.sleep(5) diff --git a/enums/member_status.go b/enums/member_status.go new file mode 100644 index 0000000..8081a89 --- /dev/null +++ b/enums/member_status.go @@ -0,0 +1,80 @@ +package enums + +import ( + "bytes" + "errors" +) + +type MemberStatus int8 + +const ( + MemberStatusInvalid MemberStatus = 0 + MemberStatusDisabled MemberStatus = 1 + MemberStatusLocked MemberStatus = 2 + MemberStatusEnabled MemberStatus = 99 +) + +func MemberStatusNameOf(value string) MemberStatus { + switch value { + case "INVALID": + return MemberStatusInvalid + case "DISABLED": + return MemberStatusDisabled + case "LOCKED": + return MemberStatusLocked + case "ENABLED": + return MemberStatusEnabled + } + return MemberStatusInvalid +} + +func (status MemberStatus) Name() string { + switch status { + case MemberStatusInvalid: + return "INVALID" + case MemberStatusDisabled: + return "DISABLED" + case MemberStatusLocked: + return "LOCKED" + case MemberStatusEnabled: + return "ENABLED" + } + return "INVALID" +} + +func (status MemberStatus) String() string { + switch status { + case MemberStatusInvalid: + return "알 수 없음" + case MemberStatusDisabled: + return "사용중지" + case MemberStatusLocked: + return "사용일시중지" + case MemberStatusEnabled: + return "사용중" + } + return "알 수 없음" +} +func (status *MemberStatus) UnmarshalJSON(from []byte) error { + + quote := []byte("\"") + quoteSize := len(quote) + + if len(from) < quoteSize*2 { + return errors.New("invalid quote notation") + } + + if !bytes.HasPrefix(from, quote) || !bytes.HasSuffix(from, quote) { + return errors.New("invalid quote notation") + } + *status = MemberStatusNameOf(string(from[quoteSize:len(from)-quoteSize])) + return nil +} + +func (status MemberStatus) MarshalJSON() ([]byte, error) { + var buffer bytes.Buffer + buffer.WriteRune('"') + buffer.WriteString(status.Name()) + buffer.WriteRune('"') + return buffer.Bytes(), nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..570e8bd --- /dev/null +++ b/main.go @@ -0,0 +1,149 @@ +package main + +import ( + "os" + "os/signal" + "runtime" + "syscall" + "time" + + "amuz.es/gogs/infra/rooibos/subsys/http" + "amuz.es/gogs/infra/rooibos/subsys/periodic" + + "amuz.es/gogs/infra/rooibos/util" + "gopkg.in/alecthomas/kingpin.v2" + "sync" + "context" +) + +var ( + app = kingpin.New(util.Config.Name(), "graceful API server").Author("tom.cat") + verbose = app.Flag("verbose", "Enable verbose mode.").Short('v').Bool() + logDir = app.Flag("log-dir", "logstore directory.").Short('L').String() + configFile = app.Flag("config-file", "config file path").Default("settings.yml").HintOptions("FILENAME").Short('C').String() + logger = util.NewLogger("main") +) + +func systemStart(ctx context.Context, errorSignal chan error, waiter *sync.WaitGroup) { + defer func() { + if err := recover(); err != nil { + errorSignal <- err.(error) + } + }() + start := time.Now() + app.Version(util.Config.Version()) + if _, err := app.Parse(os.Args[1:]); err != nil { + panic(err) + } + setMaxProcs() + + if err := util.LoadConfig(*configFile); err != nil { + panic(err) + } + + util.InitLogger(*verbose, *logDir, &util.Config.Logging.Application) + showBanner(util.Config.Phase, util.Config.Version(), util.Config.BuildDate()) + // init subsystem + //db.InitDB(&util.Config.Db) + //redis.InitRedis(&util.Config.Redis) + //redis.AppTokenInitService(util.Config.Phase, util.Config.AppToken.Expires, util.Config.AppToken.RefreshExpires) + //redis.UploadFileInitService(util.Config.Phase, util.Config.AppToken.UploadRetryExpires) + periodic.Start(errorSignal, ctx, waiter) + + http.Start(util.Config.Bind, &util.Config.Logging.Access, errorSignal, ctx, waiter) + + logger.Info("bootstrapped application ", time.Since(start)) + util.NotifyDaemon(util.DaemonStarted) +} + +func systemReload() { + util.RotateLogger() +} + +func systemTeardown(exitCode *int, waiter *sync.WaitGroup) { + logger.Info("closing application") + util.NotifyDaemon(util.DaemonStopping) + waiter.Wait() + //redis.CloseRedis() + logger.Info("bye") + os.Exit(*exitCode) +} + +func main() { + var ( + exitCode = 0 + ctx, canceled = context.WithCancel(context.Background()) + waiter = &sync.WaitGroup{} + exitSignal = make(chan os.Signal, 1) + errorSignal = make(chan error, 10) + ) + systemStart(ctx, errorSignal, waiter) + defer systemTeardown(&exitCode, waiter) + signal.Notify(exitSignal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + + for { + select { + case <-ctx.Done(): + logger.Info("Service request to close this application") + return + case sysSignal := <-exitSignal: + switch sysSignal { + case syscall.SIGHUP: + systemReload() + default: + canceled() + logger.Info("SYSCALL! ", sysSignal.String()) + return + } + case err := <-errorSignal: + canceled() + logger.Error("exception raised! ", err) + exitCode = -1 + return + } + } +} + +func setMaxProcs() { + // TODO(vmarmol): Consider limiting if we have a CPU mask in effect. + // Allow as many threads as we have cores unless the user specified a value. + var numProcs int + // if *maxProcs < 1 { + numProcs = runtime.NumCPU() + // } else { + // numProcs = *maxProcs + // } + runtime.GOMAXPROCS(numProcs) + + // Check if the setting was successful. + actualNumProcs := runtime.GOMAXPROCS(0) + if actualNumProcs != numProcs { + logger.Warn("Specified max procs of %v but using %v", numProcs, actualNumProcs) + } +} + +func showBanner(phase string, version string, buildDate string) { + if util.LoggerIsStd() { + logger.Info(` + { + { } + }_{ __{ + .-{ } }-. ` + util.Config.Name() + ` + ( } { ) version: ` + version + ` + ` + "|`-.._____..-'| buildDate: " + buildDate + ` + | ;--. phase: ` + phase + ` + | (__ \ + | | ) ) + | |/ / + | / / + | ( / + \ y' + ` + "`-.._____..-'") + } else { + logger. + WithField("version", version). + WithField("buildDate", buildDate). + WithField("phase", phase). + Info("##", util.Config.Name()) + } +} diff --git a/rooibos-dev.service b/rooibos-dev.service new file mode 100644 index 0000000..b53a769 --- /dev/null +++ b/rooibos-dev.service @@ -0,0 +1,28 @@ +[Unit] +Description=Rooibos API Daemon +Documentation=https://amuz.es +After=syslog.target network-online.target +Wants=network-online.target + +[Service] +Type=notify +Restart=always +RestartSec=15 +ExecStart=/opt/eden/service/rooibos/cur-dev/rooibos -C dev.yml -L /opt/eden/service/rooibos/logs/dev +WorkingDirectory=/opt/eden/service/rooibos/cur-dev +#ExecReload=/bin/kill -s HUP $MAINPID +TimeoutStartSec=5 + + +# Disable timeout logic and wait until process is stopped +TimeoutStopSec=0 +# SIGTERM signal is used to stop Minio +KillSignal=SIGTERM +SendSIGKILL=no + +SuccessExitStatus=0 +# kill only the docker process, not all processes in the cgroup +KillMode=process + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/settings.yml b/settings.yml new file mode 100644 index 0000000..44f84c5 --- /dev/null +++ b/settings.yml @@ -0,0 +1,29 @@ +phase: development +db: + network: tcp + host: db.gear.amuz.es + driver: mysql + user: db-dev + password: "ttt4t4" + port: 3306 + db: yori-dev + args: "readTimeout=3s&parseTime=true&collation=utf8mb4_unicode_ci&strict=true&sql_notes=false&time_zone=%27UTC%27&loc=UTC" +redis: + host: db.gear.amuz.es + port: 6379 + password: fgfgdgf + db: 1 + maxretries: 7 + poolsize: 30 +logging: + application: + filename: "Stdout" + maxsizemb: 500 + maxbackup: 30 + maxday: 14 + access: + filename: "access.log" + maxsizemb: 500 + maxbackup: 30 + maxday: 14 +bind: "0.0.0.0:8082" diff --git a/subsys/db/conn.go b/subsys/db/conn.go new file mode 100644 index 0000000..c4ad290 --- /dev/null +++ b/subsys/db/conn.go @@ -0,0 +1,74 @@ +package db + +import ( + "errors" + "fmt" + + "amuz.es/gogs/infra/rooibos/util" + _ "github.com/go-sql-driver/mysql" + "github.com/jmoiron/sqlx" + errs "github.com/pkg/errors" +) + +var ( + db *sqlx.DB = nil + logger = util.NewLogger("sql") +) + +func InitDB(config *util.DbConfig) { + connectStr := fmt.Sprintf( + "%s:%s@%s(%s:%d)/%s?%s", + config.User, + config.Password, + config.Network, + config.Host, + config.Port, + config.DB, + config.Args) + logger.Debugf("connect to %s@%s(%s:%d)/%s?%s", + config.User, + config.Network, + config.Host, + config.Port, + config.DB, + config.Args) + if conn, err := sqlx.Connect(config.Driver, connectStr); err != nil { + panic(errs.Wrap(err, "failed to connect db")) + } else { + db = conn + //https://github.com/go-sql-driver/mysql/issues/257 무조건 0 + db.SetMaxIdleConns(0) + db.SetMaxOpenConns(25) + logger.Info("DB connected") + prepareSqls() + } +} + +func prepareSqls() { + memberPrepare() +} +func Begin() (tx *sqlx.Tx, err error) { + tx, err = db.Beginx() + return +} + +func prepareSql(sql string) (*sqlx.Stmt, error) { + if stmt, err := db.Preparex(sql); err != nil { + return nil, errs.Wrapf(err, "failed to prepare sql %s", sql) + } else { + return stmt, nil + } +} +func tablecheck(tablename string) error { + var fetched int + if db.Get( + &fetched, ` + SELECT COUNT(*) +FROM information_schema.tables +WHERE table_schema = DATABASE() +AND table_name = ?`, + tablename) != nil || fetched != 1 { + return errors.New(fmt.Sprintf("table %s not exist", tablename)) + } + return nil +} diff --git a/subsys/db/create.my.sql b/subsys/db/create.my.sql new file mode 100644 index 0000000..4393c45 --- /dev/null +++ b/subsys/db/create.my.sql @@ -0,0 +1,24 @@ + +BEGIN; + + CREATE TABLE IF NOT EXISTS member + ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL, + contact VARCHAR(128) NOT NULL, + name VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + status TINYINT NOT NULL, + company_seq BIGINT UNSIGNED NOT NULL, + UNIQUE INDEX uk_member_01 (company_seq,contact), + INDEX idx_member_01 (company_seq), + INDEX idx_member_02 (contact), + INDEX idx_member_03 (status) + ) + ENGINE=InnoDB + ROW_FORMAT=COMPRESSED + CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + ; + + diff --git a/subsys/db/member.go b/subsys/db/member.go new file mode 100644 index 0000000..0ad600e --- /dev/null +++ b/subsys/db/member.go @@ -0,0 +1,191 @@ +package db + +import ( + "bytes" + + "amuz.es/gogs/infra/rooibos/enums" + + "database/sql" + "strconv" + "time" + + "github.com/go-sql-driver/mysql" + "github.com/jmoiron/sqlx" +) + +var ( + memberInsertQuery *sqlx.Stmt + memberSelectByContactQuery *sqlx.Stmt + memberSelectByIdQuery *sqlx.Stmt +) + +type member struct { + In_Id uint64 `db:"id"` + In_CreatedAt time.Time `db:"created_at"` + In_UpdatedAt mysql.NullTime `db:"updated_at"` + In_Contact string `db:"contact"` + In_Name string `db:"name"` + In_Password string `db:"password"` + In_Status enums.MemberStatus `db:"status"` + In_CompanySeq uint64 `db:"company_seq"` +} + +func (member *member) Id() uint64 { return member.In_Id } +func (member *member) CreatedAt() time.Time { return member.In_CreatedAt } +func (member *member) UpdatedAt() *time.Time { + if member.In_UpdatedAt.Valid { + return &member.In_UpdatedAt.Time + } else { + return nil + } +} +func (member *member) Contact() string { return member.In_Contact } +func (member *member) Name() string { return member.In_Name } +func (member *member) Status() enums.MemberStatus { return member.In_Status } +func (member *member) CompanySeq() uint64 { return member.In_CompanySeq } +func (member *member) String() string { + var buffer bytes.Buffer + buffer.WriteString("Member(") + if member != nil { + buffer.WriteString(strconv.FormatUint(member.In_Id, 10)) + } else { + buffer.WriteString("nil") + } + buffer.WriteString(")") + return buffer.String() +} +func NewMember( + id uint64, + createdAt time.Time, + updatedAt *time.Time, + contact string, + name string, + password string, + status enums.MemberStatus, + companySeq uint64) (Member){ + var updatedAtWrapped mysql.NullTime + if updatedAt != nil{ + updatedAtWrapped=mysql.NullTime{*updatedAt, true} + } + return &member{ + id, + createdAt, + updatedAtWrapped, + contact, + name, + password, + status, + companySeq, + } +} + +type Member interface { + Id() uint64 + CreatedAt() time.Time + UpdatedAt() *time.Time + Contact() string + Name() string + Status() enums.MemberStatus + CompanySeq() uint64 + String() string +} + +func memberPrepare() { + if err := tablecheck("member"); err != nil { + panic(err) + } + if query, err := prepareSql( + `INSERT INTO member ( + created_at, contact, name, + password, status, company_seq + ) + VALUES + (NOW(), ?,?,?,?,?) + `); err != nil { + panic(err) + } else { + memberInsertQuery = query + } + + if query, err := prepareSql(` select * from member where contact=? and status=? order by id desc limit 1`); err != nil { + panic(err) + } else { + memberSelectByContactQuery = query + } + + if query, err := prepareSql(`Select * from member where id=?`); err != nil { + panic(err) + } else { + memberSelectByIdQuery = query + } + logger.Debug("Member query prepared") +} + +func MemberFindByContactWithTx(tx *sqlx.Tx, contact string, status enums.MemberStatus) (Member, error) { + obj := member{} + if err := tx.Stmtx(memberSelectByContactQuery).Get(&obj, contact, status); err != nil { + return nil, err + } + return &obj, nil +} +func MemberFindByContactOne(contact string, status enums.MemberStatus) (Member, error) { + obj := member{} + if err := memberSelectByContactQuery.Get(&obj, contact, status); err != nil { + return nil, err + } + return &obj, nil +} +func MemberFindOneWithTx(tx *sqlx.Tx, pk uint64) (Member, error) { + obj := member{} + if err := tx.Stmtx(memberSelectByIdQuery).Get(&obj, pk); err != nil { + return nil, err + } + return &obj, nil +} +func MemberFindOne(pk uint64) (Member, error) { + obj := member{} + if err := memberSelectByIdQuery.Get(&obj, pk); err != nil { + return nil, err + } + return &obj, nil +} +func MemberInsertWithTx(tx *sqlx.Tx, contact string, name string, password string, status enums.MemberStatus, companySeq uint64) (uint64, error) { + var ( + err error + result sql.Result + pk int64 + ) + if result, err = tx.Stmtx(memberInsertQuery).Exec( + contact, + name, + password, + status, + companySeq); err != nil { + return 0, err + } + + if pk, err = result.LastInsertId(); err != nil { + return 0, err + } + return (uint64)(pk), nil +} +func MemberInsert(contact string, name string, password string, status enums.MemberStatus, companySeq uint64) (uint64, error) { + var ( + err error + result sql.Result + pk int64 + ) + if result, err = memberInsertQuery.Exec( + contact, + name, + password, + status, + companySeq); err != nil { + return 0, err + } + + if pk, err = result.LastInsertId(); err != nil { + return 0, err + } + return (uint64)(pk), nil +} diff --git a/subsys/db/util.go b/subsys/db/util.go new file mode 100644 index 0000000..2be4e52 --- /dev/null +++ b/subsys/db/util.go @@ -0,0 +1,47 @@ +package db + +import ( + "database/sql/driver" + "fmt" + "strconv" +) + +// NullUint64 represents an uint64 that may be null. +// NullUint64 implements the Scanner interface so +// it can be used as a scan destination, similar to NullString. + +// NullUint64 is a sql.Scanner for unsigned ints. +type NullUint64 struct { + Uint64 uint64 + Valid bool +} + +func (n NullUint64) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.Uint64, nil +} + +// Scan implements the sql.Scanner interface. +func (n *NullUint64) Scan(src interface{}) error { + if src == nil { + n.Uint64, n.Valid = 0, false + return nil + } + n.Valid = true + s := asString(src) + var err error + n.Uint64, err = strconv.ParseUint(s, 10, 64) + return err +} + +func asString(src interface{}) string { + switch v := src.(type) { + case string: + return v + case []byte: + return string(v) + } + return fmt.Sprintf("%v", src) +} diff --git a/subsys/http/iface/metric.go b/subsys/http/iface/metric.go new file mode 100644 index 0000000..b2cdefa --- /dev/null +++ b/subsys/http/iface/metric.go @@ -0,0 +1,176 @@ +package iface + +import ( + "time" +) + + + +type KeyValuePair struct { + Key string `json:"key" binding:"required"` + Value interface{} `json:"value" binding:"required"` +} + + +type WrappedResponse struct { + Message string `json:"message"` + Info *PageInfo `json:"info,omitempty"` + Content interface{} `json:"content,omitempty"` +} + + +type PageInfo struct { + Offset uint32 `json:"offset" binding:"required"` + Size uint16 `json:"size" binding:"required"` +} + + +type Tuple struct { + Path string + Code int +} + + +type CounterMetric struct { + Inc chan Tuple `json:"-"` + internalRequestsSum uint64 + internalRequests map[string]uint64 + internalRequestCodes map[string]uint64 + RequestsSum uint64 `json:"request_sum_per_minute"` + Requests map[string]uint64 `json:"requests_per_minute"` + RequestCodes map[string]uint64 `json:"request_codes_per_minute"` +} + + +type DurationMetricReadable struct { + Min string `json:"min"` + Max string `json:"max"` + Mean string `json:"mean"` + Stdev string `json:"stdev"` + P90 string `json:"p90"` + P95 string `json:"p95"` + P99 string `json:"p99"` + + Timestamp time.Time `json:"timestamp"` +} + + +type DurationMetric struct { + Min uint64 `json:"min"` + Max uint64 `json:"max"` + Mean uint64 `json:"mean"` + Stdev uint64 `json:"stdev"` + P90 uint64 `json:"p90"` + P95 uint64 `json:"p95"` + P99 uint64 `json:"p99"` + Timestamp time.Time `json:"timestamp"` +} + + +type RequestMetricReadable struct { + Duration DurationMetricReadable `json:"duration"` + Count CounterMetric `json:"count"` +} + + +type RequestMetric struct { + Duration DurationMetric `json:"duration"` + Count CounterMetric `json:"count"` +} + + +type RuntimeMetric struct { + GoVersion string `json:"go_version"` + GoOs string `json:"go_os"` + GoArch string `json:"go_arch"` + CpuNum int `json:"cpu_num"` + GoroutineNum int `json:"goroutine_num"` + Gomaxprocs int `json:"go_maxprocs"` + CgoCallNum int64 `json:"cgo_call_num"` +} + + +type MemoryMetric struct { + MemAllocated uint64 `json:"mem_allocated"` + MemTotal uint64 `json:"mem_total"` + MemSys uint64 `json:"mem_sys"` + Lookups uint64 `json:"lookups"` + MemMallocs uint64 `json:"mem_mallocs"` + MemFrees uint64 `json:"mem_frees"` + + HeapAlloc uint64 `json:"heap_alloc"` + HeapSys uint64 `json:"heap_sys"` + HeapIdle uint64 `json:"heap_idle"` + HeapInuse uint64 `json:"heap_inuse"` + HeapReleased uint64 `json:"heap_released"` + HeapObjects uint64 `json:"heap_objects"` + + StackInuse uint64 `json:"stack_inuse"` + StackSys uint64 `json:"stack_sys"` + MSpanInuse uint64 `json:"m_span_inuse"` + MSpanSys uint64 `json:"m_span_sys"` + MCacheInuse uint64 `json:"m_cache_inuse"` + MCacheSys uint64 `json:"m_cache_sys"` + BuckHashSys uint64 `json:"buck_hash_sys"` + GCSys uint64 `json:"gc_sys"` + OtherSys uint64 `json:"other_sys"` + + NextGC uint64 `json:"next_gc"` + LastGC uint64 `json:"last_gc"` + PauseTotalNs uint64 `json:"pause_total_ns"` + PauseNs uint64 `json:"pause_ns"` + NumGC uint32 `json:"num_gc"` +} + + +type Metric struct { + Cmdline []string `json:"cmd_line"` + Uptime uint64 `json:"uptime"` + Request RequestMetric `json:"request"` + Runtime RuntimeMetric `json:"runtime"` + Memory MemoryMetric `json:"memory"` +} + + +type MemoryMetricReadable struct { + MemAllocated string `json:"mem_allocated"` + MemTotal string `json:"mem_total"` + MemSys string `json:"mem_sys"` + Lookups uint64 `json:"lookups"` + MemMallocs uint64 `json:"mem_mallocs"` + MemFrees uint64 `json:"mem_frees"` + + HeapAlloc string `json:"heap_alloc"` + HeapSys string `json:"heap_sys"` + HeapIdle string `json:"heap_idle"` + HeapInuse string `json:"heap_inuse"` + HeapReleased string `json:"heap_released"` + HeapObjects uint64 `json:"heap_objects"` + + StackInuse string `json:"stack_inuse"` + StackSys string `json:"stack_sys"` + MSpanInuse string `json:"m_span_inuse"` + MSpanSys string `json:"m_span_sys"` + MCacheInuse string `json:"m_cache_inuse"` + MCacheSys string `json:"m_cache_sys"` + BuckHashSys string `json:"buck_hash_sys"` + GCSys string `json:"gc_sys"` + OtherSys string `json:"other_sys"` + + NextGC string `json:"next_gc"` + LastGC string `json:"last_gc"` + PauseTotalNs string `json:"pause_total_ns"` + PauseNs string `json:"pause_ns"` + NumGC uint32 `json:"num_gc"` +} + + +type MetricReadable struct { + Cmdline []string `json:"cmd_line"` + Uptime string `json:"uptime"` + Request RequestMetricReadable `json:"request"` + Runtime RuntimeMetric `json:"runtime"` + Memory MemoryMetricReadable `json:"memory"` +} + +// -vars="mem:memory.mem_allocated,duration:request.duration.min,duration:request.duration.max,duration:request.duration.mean,duration:request.duration.stdev,duration:request.duration.p90,duration:request.duration.p95,duration:request.duration.p99,mem:memory.mem_allocated,mem:memory.heap_alloc,mem:memory.heap_inuse,mem:memory.stack_inuse,duration:memory.next_gc,duration:memory.last_gc,duration:memory.pause_total_ns,duration:memory.pause_ns" diff --git a/subsys/http/iface/metric_count.go b/subsys/http/iface/metric_count.go new file mode 100644 index 0000000..a85bf32 --- /dev/null +++ b/subsys/http/iface/metric_count.go @@ -0,0 +1,64 @@ +package iface + +import ( + "strconv" + "time" +) + +//turns a new initialized CounterMetric object. +func NewCounterMetric() *CounterMetric { + ca := &CounterMetric{} + ca.Inc = make(chan Tuple) + ca.internalRequestsSum = 0 + ca.internalRequests = make(map[string]uint64, 0) + ca.internalRequestCodes = make(map[string]uint64, 0) + return ca +} + +// StartTimer will call a forever loop in a goroutine to calculate +// metrics for measurements every d ticks. The parameter of this +// function should normally be 1 * time.Minute, if not it will expose +// unintuive JSON keys (requests_per_minute and +// request_sum_per_minute). +func (ca *CounterMetric) StartTimer(d time.Duration) { + timer := time.Tick(d) + go func() { + for { + select { + case tup := <-ca.Inc: + ca.internalRequestsSum++ + ca.internalRequests[tup.Path]++ + ca.internalRequestCodes[strconv.FormatInt(int64(tup.Code), 10)]++ + case <-timer: + ca.reset() + } + } + }() +} + +// GetStats to fulfill aspects.Aspect interface, it returns the data +// that will be served as JSON. +func (ca *CounterMetric) GetStats() interface{} { + return *ca +} + +// Name to fulfill aspects.Aspect interface, it will return the name +// of the JSON object that will be served. +func (ca *CounterMetric) Name() string { + return "Counter" +} + +// InRoot to fulfill aspects.Aspect interface, it will return where to +// put the JSON object into the monitoring endpoint. +func (ca *CounterMetric) InRoot() bool { + return false +} + +func (ca *CounterMetric) reset() { + ca.RequestsSum = ca.internalRequestsSum + ca.Requests = ca.internalRequests + ca.RequestCodes = ca.internalRequestCodes + ca.internalRequestsSum = 0 + ca.internalRequests = make(map[string]uint64, ca.RequestsSum) + ca.internalRequestCodes = make(map[string]uint64, len(ca.RequestCodes)) +} diff --git a/subsys/http/iface/metric_duration.go b/subsys/http/iface/metric_duration.go new file mode 100644 index 0000000..ed2c216 --- /dev/null +++ b/subsys/http/iface/metric_duration.go @@ -0,0 +1,118 @@ +package iface + +import ( + "math" + "sort" + "time" +) + +type RequestDurationAggr struct { + lastMinuteRequestTimes []float64 + Min float64 `json:"min"` + Max float64 `json:"max"` + Mean float64 `json:"mean"` + Stdev float64 `json:"stdev"` + P90 float64 `json:"p90"` + P95 float64 `json:"p95"` + P99 float64 `json:"p99"` + Timestamp time.Time `json:"timestamp"` +} + +// NewRequestDurationAggr returns a new initialized RequestDurationAggr +// object. +func NewRequestDurationAggr() *RequestDurationAggr { + rt := &RequestDurationAggr{} + rt.lastMinuteRequestTimes = make([]float64, 0) + rt.Timestamp = time.Now() + return rt +} + +// StartTimer will call a forever loop in a goroutine to calculate +// metrics for measurements every d ticks. +func (rt *RequestDurationAggr) StartTimer(d time.Duration) { + timer := time.Tick(d) + go func() { + for { + <-timer + rt.calculate() + } + }() +} + +// GetStats to fulfill Metrics.Metric interface, it returns the data +// that will be served as JSON. +func (rt *RequestDurationAggr) GetStats() interface{} { + return rt +} + +// Name to fulfill Metrics.Metric interface, it will return the name +// of the JSON object that will be served. +func (rt *RequestDurationAggr) Name() string { + return "RequestTime" +} + +// InRoot to fulfill Metrics.Metric interface, it will return where to +// put the JSON object into the monitoring endpoint. +func (rt *RequestDurationAggr) InRoot() bool { + return false +} + +func (rt *RequestDurationAggr) Add(n float64) { + rt.lastMinuteRequestTimes = append(rt.lastMinuteRequestTimes, n) +} + +func (rt *RequestDurationAggr) calculate() { + sortedSlice := rt.lastMinuteRequestTimes[:] + rt.lastMinuteRequestTimes = make([]float64, 0) + l := len(sortedSlice) + if l <= 1 { + return + } + sort.Float64s(sortedSlice) + + rt.Timestamp = time.Now() + rt.Min = sortedSlice[0] + rt.Max = sortedSlice[l - 1] + rt.Mean = mean(sortedSlice, l) + rt.Stdev = correctedStdev(sortedSlice, rt.Mean, l) + rt.P90 = p90(sortedSlice, l) + rt.P95 = p95(sortedSlice, l) + rt.P99 = p99(sortedSlice, l) +} + +func mean(orderedObservations []float64, l int) float64 { + res := 0.0 + for i := 0; i < l; i++ { + res += orderedObservations[i] + } + + return res / float64(l) +} + +func p90(orderedObservations []float64, l int) float64 { + return percentile(orderedObservations, l, 0.9) +} + +func p95(orderedObservations []float64, l int) float64 { + return percentile(orderedObservations, l, 0.95) +} + +func p99(orderedObservations []float64, l int) float64 { + return percentile(orderedObservations, l, 0.99) +} + +// percentile with argument p \in (0,1), l is the length of given orderedObservations +// It does a simple apporximation of an ordered list of observations. +// Formula: sortedSlice[0.95*length(sortedSlice)] +func percentile(orderedObservations []float64, l int, p float64) float64 { + return orderedObservations[int(p * float64(l))] +} + +func correctedStdev(observations []float64, mean float64, l int) float64 { + var omega float64 + for i := 0; i < l; i++ { + omega += math.Pow(observations[i]-mean, 2) + } + stdev := math.Sqrt(1 / (float64(l) - 1) * omega) + return stdev +} diff --git a/subsys/http/init.go b/subsys/http/init.go new file mode 100644 index 0000000..bdcc22a --- /dev/null +++ b/subsys/http/init.go @@ -0,0 +1,122 @@ +// +build go1.8 + +package http + +import ( + "time" + "net/http" + + "amuz.es/gogs/infra/rooibos/subsys/http/middleware/logging" + "amuz.es/gogs/infra/rooibos/subsys/http/middleware/recovery" + "amuz.es/gogs/infra/rooibos/util" + "github.com/gin-gonic/gin" + errs "github.com/pkg/errors" + + "amuz.es/gogs/infra/rooibos/subsys/http/route" + "github.com/mkevac/debugcharts" + "log" + "context" + "sync" +) + +var ( + logger = util.NewLogger("http") +) + +func Start(bindAddress string, accessLogConfig *util.LogConfig, + errorSignal chan error, ctx context.Context, waiter *sync.WaitGroup) { + server, err := initialize(bindAddress, accessLogConfig) + if err != nil { + errorSignal <- err + return + } + go bind(server, errorSignal, waiter) + go gracefulCloser(server, ctx, waiter) + logger.Info(bindAddress + " bound") + +} +func initialize(bindAddress string, accessLogConfig *util.LogConfig) (server *http.Server, err error) { + defer func() { + if recov := recover(); recov != nil { + err = recov.(error) + } + }() + gin.SetMode(gin.ReleaseMode) + engine := gin.New() + + initMiddleware(engine, accessLogConfig) + initRoutes(engine) + + logWriter := logger.Writer() + server = &http.Server{ + Addr: bindAddress, + Handler: engine, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 60 * time.Second, + MaxHeaderBytes: 1 << 20, + ErrorLog: log.New(logWriter, "", 0), + } + return +} +func gracefulCloser(server *http.Server, ctx context.Context, waiter *sync.WaitGroup) { + defer waiter.Done() + <-ctx.Done() + logger.Info("stopping HTTP") + if err := server.Shutdown(context.Background()); err != nil { + logger.Info("HTTP closed:", err) + } + logger.Info("HTTP shutdowned safely.") +} + +func bind(server *http.Server, errorSignal chan error, waiter *sync.WaitGroup) { + waiter.Add(1) + if err := server.ListenAndServe(); err != nil { + errorSignal <- errs.Wrap(err, "failed to listen or serve http") + } +} + +func initRoutes(engine *gin.Engine) { + suburl := "" + path := engine.Group(suburl) + // handlers + + //debug handler + debug := path.Group("/debug") + debug.GET("/metric", route.Metric) + debugcharts.GinDebugRouter(engine) + debug.GET("/pprof/", route.ProfileIndexHandler()) + debug.GET("/pprof/heap", route.ProfileHeapHandler()) + debug.GET("/pprof/goroutine", route.ProfileGoroutineHandler()) + debug.GET("/pprof/block", route.ProfileBlockHandler()) + debug.GET("/pprof/threadcreate", route.ProfileThreadCreateHandler()) + debug.GET("/pprof/cmdline", route.ProfileCmdlineHandler()) + debug.GET("/pprof/profile", route.ProfileProfileHandler()) + debug.GET("/pprof/symbol", route.ProfileSymbolHandler()) + debug.POST("/pprof/symbol", route.ProfileSymbolHandler()) + debug.GET("/pprof/trace", route.ProfileTraceHandler()) + + path.GET("/favicon.ico", func(c *gin.Context) { + c.Redirect(http.StatusMovedPermanently, "/static/favicon/images/favicon.ico") + }) + + path.GET("/manifest.json", func(c *gin.Context) { + c.Redirect(http.StatusMovedPermanently, "/static/favicon/manifest.json") + }) + + path.GET("/browserconfig.xml", func(c *gin.Context) { + c.Redirect(http.StatusMovedPermanently, "/static/favicon/browserconfig.xml") + }) + + path.GET("/", route.Index) +} + +func initMiddleware(engine *gin.Engine, accessLogConfig *util.LogConfig) { + engine.Use(logging.AccessLog(accessLogConfig)) + engine.Use(recovery.RecoveryJSON()) + + // not found handler + engine.NoRoute(recovery.RecoveryHttpError(http.StatusNotFound)) + engine.NoMethod(recovery.RecoveryHttpError(http.StatusMethodNotAllowed)) + +} diff --git a/subsys/http/middleware/logging/middleware.go b/subsys/http/middleware/logging/middleware.go new file mode 100644 index 0000000..ba410be --- /dev/null +++ b/subsys/http/middleware/logging/middleware.go @@ -0,0 +1,154 @@ +// middleware +package logging + +import ( + "bytes" + "io" + "net" + "net/http" + "strconv" + "time" + + "amuz.es/gogs/infra/rooibos/subsys/http/iface" + "amuz.es/gogs/infra/rooibos/util" + "github.com/gin-gonic/gin" +) + +const ( + dateFormat = "02/Jan/2006:15:04:05 -0700" +) + +var DurationAspect = iface.NewRequestDurationAggr() +var CounterAspect = iface.NewCounterMetric() + +type logItem struct { + host string + method string + uri string + proto string + referer string + userAgent string + requestedHost string + requestedPath string + status int + responseSize int + requestTime time.Time + duration time.Duration +} + +func AccessLog(config *util.LogConfig) gin.HandlerFunc { + _, logWriter := util.NewLogWriter(config) + DurationAspect.StartTimer(10 * time.Second) + CounterAspect.StartTimer(1 * time.Minute) + logChan := make(chan logItem) + + go sendLogging(logWriter, logChan) + return func(c *gin.Context) { + now := time.Now() + req := c.Request + + defer func() { + dur := time.Now().Sub(now) + + logItemData := logItem{ + status: c.Writer.Status(), + responseSize: c.Writer.Size(), + requestTime: now, + duration: dur, + } + + if req != nil { + logItemData.host = remoteHost(req) + logItemData.method = req.Method + logItemData.uri = req.RequestURI + logItemData.proto = req.Proto + logItemData.referer = req.Referer() + logItemData.userAgent = req.UserAgent() + logItemData.requestedHost = req.Host + logItemData.requestedPath = req.URL.Path + } else { + logItemData.host = "-" + logItemData.method = "" + logItemData.uri = "" + logItemData.proto = "" + logItemData.referer = "" + logItemData.userAgent = "" + logItemData.requestedHost = "" + logItemData.requestedPath = "" + + } + + logChan <- logItemData + }() + + c.Next() + } +} +func sendLogging(accesslog io.Writer, logChan chan logItem) { + for { + select { + case logItemData := <-logChan: + DurationAspect.Add(float64(logItemData.duration.Nanoseconds())) + CounterAspect.Inc <- iface.Tuple{ + Path: logItemData.requestedPath, + Code: logItemData.status, + } + // Logs an access event in Apache combined log format (with a minor customization with the duration). + + buf := bytes.NewBufferString("") + buf.WriteString(logItemData.host) + buf.WriteString(` - - [`) + buf.WriteString(logItemData.requestTime.Format(dateFormat)) + buf.WriteString(`] "`) + buf.WriteString(logItemData.method) + buf.WriteByte(' ') + buf.WriteString(logItemData.uri) + buf.WriteByte(' ') + buf.WriteString(logItemData.proto) + buf.WriteString(`" `) + buf.WriteString(strconv.Itoa(logItemData.status)) + buf.WriteByte(' ') + buf.WriteString(strconv.Itoa(logItemData.responseSize)) + buf.WriteString(` "`) + buf.WriteString(logItemData.referer) + buf.WriteString(`" "`) + buf.WriteString(logItemData.userAgent) + buf.WriteString(`" `) + buf.WriteString(strconv.FormatInt((logItemData.duration.Nanoseconds() / time.Millisecond.Nanoseconds()), 10)) + buf.WriteByte(' ') + buf.WriteString(logItemData.requestedHost) + buf.WriteByte('\n') + accesslog.Write(buf.Bytes()) + } + } +} + +// strip port from addresses with hostname, ipv4 or ipv6 +func stripPort(address string) string { + if h, _, err := net.SplitHostPort(address); err == nil { + return h + } + + return address +} + +// The remote address of the client. When the 'X-Forwarded-For' +// header is set, then it is used instead. +func remoteAddr(r *http.Request) string { + ff := r.Header.Get("X-Forwarded-For") + if ff != "" { + return ff + } + + return r.RemoteAddr +} + +func remoteHost(r *http.Request) string { + a := remoteAddr(r) + h := stripPort(a) + if h != "" { + return h + } + + return "-" +} diff --git a/subsys/http/middleware/recovery/middleware.go b/subsys/http/middleware/recovery/middleware.go new file mode 100644 index 0000000..2698f1e --- /dev/null +++ b/subsys/http/middleware/recovery/middleware.go @@ -0,0 +1,143 @@ +package recovery + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "net/http/httputil" + "runtime" + "strings" + + "amuz.es/gogs/infra/rooibos/subsys/http/iface" + "amuz.es/gogs/infra/rooibos/util" + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +var ( + dunno = []byte("???") + centerDot = []byte("·") + dot = []byte(".") + slash = []byte("/") + logger = util.NewLogger("recovery") + color = "danger" + preText = "루이보스 API 오류 :bomb:" + title = "API 요청을 처리하는 중에 의도하지 않은 오류가 발행하였습니다." + footer = "확인해 주세요! :hugging_face:" +) + +func recoveryInternal(stack []byte, code int, req *http.Request, err *string) { + httprequest, _ := httputil.DumpRequest(req, false) + detailInfo := map[string]interface{}{ + "method": req.Method, + "statusCode": code, + "path": req.URL.Path, + "message": err, + "request": string(httprequest), + "stack": string(stack), + } + logger.WithFields(logrus.Fields(detailInfo)).Error("[Recovery] panic recovered:\n") +} + +func recoveryError(code int, _ *gin.Context) *iface.WrappedResponse { + return &iface.WrappedResponse{ + Message: http.StatusText(code), + } +} + +func RecoveryHttpError(code int) gin.HandlerFunc { + return func(c *gin.Context) { + info := recoveryError(code, c) + util.DumpJSON(c, code, info) + } +} + +func RecoveryJSON() gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + code := http.StatusInternalServerError + stack := stack(5) + systemErrMessage := err.(error).Error() + go recoveryInternal(stack, code, c.Copy().Request, &systemErrMessage) + util.DumpJSON(c, code, &iface.WrappedResponse{ + Message: systemErrMessage, + }) + c.Abort() + } + }() + c.Next() + } +} + +// stack returns a nicely formated stack frame, skipping skip frames +func stack(skip int) []byte { + buf := new(bytes.Buffer) // the returned data + // As we loop, we open files and read them. These variables record the currently + // loaded file. + var lines [][]byte + var lastFile string + for i := skip; ; i++ { + // Skip the expected number of frames + pc, file, line, ok := runtime.Caller(i) + if !ok { + break + } + if paths := strings.SplitN(file, "src/", 2); len(paths) == 1 { + // Print this much at least. If we can't find the source, it won't show. + fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc) + } else if vendors := strings.SplitN(paths[1], "vendor/", 2); len(vendors) == 1 { + // Print this much at least. If we can't find the source, it won't show. + fmt.Fprintf(buf, "%s:%d (0x%x)\n", paths[1], line, pc) + } else { + // Print this much at least. If we can't find the source, it won't show. + fmt.Fprintf(buf, "%s:%d (0x%x)\n", vendors[1], line, pc) + } + if file != lastFile { + data, err := ioutil.ReadFile(file) + if err != nil { + continue + } + lines = bytes.Split(data, []byte{'\n'}) + lastFile = file + } + fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line)) + } + return buf.Bytes() +} + +// source returns a space-trimmed slice of the n'th line. +func source(lines [][]byte, n int) []byte { + n-- // in stack trace, lines are 1-indexed but our array is 0-indexed + if n < 0 || n >= len(lines) { + return dunno + } + return bytes.TrimSpace(lines[n]) + +} + +// function returns, if possible, the name of the function containing the PC. +func function(pc uintptr) []byte { + fn := runtime.FuncForPC(pc) + if fn == nil { + return dunno + } + name := []byte(fn.Name()) + // The name includes the path name to the package, which is unnecessary + // since the file name is already included. Plus, it has center dots. + // That is, we see + // runtime/debug.*T·ptrmethod + // and want + // *T.ptrmethod + // Also the package path might contains dot (e.g. code.google.com/...), + // so first eliminate the path prefix + if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 { + name = name[lastslash+1:] + } + if period := bytes.Index(name, dot); period >= 0 { + name = name[period+1:] + } + name = bytes.Replace(name, centerDot, dot, -1) + return name +} diff --git a/subsys/http/route/index.go b/subsys/http/route/index.go new file mode 100644 index 0000000..1d201b5 --- /dev/null +++ b/subsys/http/route/index.go @@ -0,0 +1,56 @@ +package route + +import ( + "net/http" + + "amuz.es/gogs/infra/rooibos/subsys/http/iface" + "amuz.es/gogs/infra/rooibos/util" + common_iface "amuz.es/gogs/infra/rooibos/util/iface" + "github.com/gin-gonic/gin" + "amuz.es/gogs/infra/rooibos/subsys/db" + "time" + "amuz.es/gogs/infra/rooibos/enums" +) + +func Index(c *gin.Context) { + uuidTest,_ :=common_iface.NewUUIDV4() + resp := &iface.WrappedResponse{ + Message: "Pong!", + Content: []iface.KeyValuePair{ + { + Key: "name", + Value: util.Config.Name(), + }, + { + Key: "version", + Value: util.Config.Version(), + }, + { + Key: "buildDate", + Value: util.Config.BuildDate(), + }, + { + Key: "phase", + Value: util.Config.Phase, + }, + { + Key:"uuid", + Value: uuidTest, + }, + { + Key: "model", + Value: db.NewMember( + uint64(0), + time.Now(), + nil, + "hello", + "name", + "hello", + enums.MemberStatusEnabled, + uint64(0), + ), + }, + }, + } + util.DumpJSON(c, http.StatusOK, resp) +} diff --git a/subsys/http/route/metric.go b/subsys/http/route/metric.go new file mode 100644 index 0000000..e6952b2 --- /dev/null +++ b/subsys/http/route/metric.go @@ -0,0 +1,151 @@ +package route + +import ( + "os" + "runtime" + "time" + + "amuz.es/gogs/infra/rooibos/subsys/http/iface" + "amuz.es/gogs/infra/rooibos/subsys/http/middleware/logging" + "amuz.es/gogs/infra/rooibos/util" + "github.com/gin-gonic/gin" +) + +var ( + startTime = time.Now() +) + +func Metric(c *gin.Context) { + _, readable := c.Request.URL.Query()["readable"] + if readable { + memory := serializeReadableStat() + util.DumpJSON(c, 200, memory) + } else { + memory := serializeMemoryStat() + util.DumpJSON(c, 200, memory) + } +} + +func serializeMemoryStat() *iface.Metric { + m := new(runtime.MemStats) + runtime.ReadMemStats(m) + return &iface.Metric{ + Cmdline: os.Args, + Uptime: uint64(time.Now().UnixNano() - startTime.UnixNano()), + Request:iface.RequestMetric{ + Duration:iface.DurationMetric{ + Min: uint64(logging.DurationAspect.Min), + Max: uint64(logging.DurationAspect.Max), + Mean: uint64(logging.DurationAspect.Mean), + Stdev: uint64(logging.DurationAspect.Stdev), + P90: uint64(logging.DurationAspect.P90), + P95: uint64(logging.DurationAspect.P95), + P99: uint64(logging.DurationAspect.P99), + Timestamp: logging.DurationAspect.Timestamp, + }, + Count:*logging.CounterAspect, + }, + Runtime:iface.RuntimeMetric{ + GoVersion: runtime.Version(), + GoOs: runtime.GOOS, + GoArch: runtime.GOARCH, + CpuNum: runtime.NumCPU(), + GoroutineNum: runtime.NumGoroutine(), + Gomaxprocs: runtime.GOMAXPROCS(0), + CgoCallNum: runtime.NumCgoCall(), + }, + Memory:iface.MemoryMetric{ + MemAllocated: m.Alloc, + MemTotal: m.TotalAlloc, + MemSys: m.Sys, + Lookups: m.Lookups, + MemMallocs: m.Mallocs, + MemFrees: m.Frees, + + HeapAlloc: m.HeapAlloc, + HeapSys: m.HeapSys, + HeapIdle: m.HeapIdle, + HeapInuse: m.HeapInuse, + HeapReleased: m.HeapReleased, + HeapObjects: m.HeapObjects, + + StackInuse: m.StackInuse, + StackSys: m.StackSys, + MSpanInuse: m.MSpanInuse, + MSpanSys: m.MSpanSys, + MCacheInuse: m.MCacheInuse, + MCacheSys: m.MCacheSys, + BuckHashSys: m.BuckHashSys, + GCSys: m.GCSys, + OtherSys: m.OtherSys, + + NextGC: m.NextGC, + LastGC: uint64(time.Now().UnixNano()) - m.LastGC, + PauseTotalNs: m.PauseTotalNs, + PauseNs: m.PauseNs[(m.NumGC + 255) % 256], + NumGC: m.NumGC, + }, + } +} + +// +func serializeReadableStat() *iface.MetricReadable { + m := new(runtime.MemStats) + runtime.ReadMemStats(m) + return &iface.MetricReadable{ + Uptime: util.TimeSincePro(startTime), + Request:iface.RequestMetricReadable{ + Duration:iface.DurationMetricReadable{ + Min: util.TimeSinceDuration(uint64(logging.DurationAspect.Min)), + Max: util.TimeSinceDuration(uint64(logging.DurationAspect.Max)), + Mean: util.TimeSinceDuration(uint64(logging.DurationAspect.Mean)), + Stdev: util.TimeSinceDuration(uint64(logging.DurationAspect.Stdev)), + P90: util.TimeSinceDuration(uint64(logging.DurationAspect.P90)), + P95: util.TimeSinceDuration(uint64(logging.DurationAspect.P95)), + P99: util.TimeSinceDuration(uint64(logging.DurationAspect.P99)), + Timestamp: logging.DurationAspect.Timestamp, + }, + Count:*logging.CounterAspect, + }, + Runtime:iface.RuntimeMetric{ + GoVersion: runtime.Version(), + GoOs: runtime.GOOS, + GoArch: runtime.GOARCH, + CpuNum: runtime.NumCPU(), + GoroutineNum: runtime.NumGoroutine(), + Gomaxprocs: runtime.GOMAXPROCS(0), + CgoCallNum: runtime.NumCgoCall(), + }, + Memory:iface.MemoryMetricReadable{ + MemAllocated: util.FileSize(m.Alloc), + MemTotal: util.FileSize(m.TotalAlloc), + MemSys: util.FileSize(m.Sys), + Lookups: m.Lookups, + MemMallocs: m.Mallocs, + MemFrees: m.Frees, + + HeapAlloc: util.FileSize(m.HeapAlloc), + HeapSys: util.FileSize(m.HeapSys), + HeapIdle: util.FileSize(m.HeapIdle), + HeapInuse: util.FileSize(m.HeapInuse), + HeapReleased: util.FileSize(m.HeapReleased), + HeapObjects: m.HeapObjects, + + StackInuse: util.FileSize(m.StackInuse), + StackSys: util.FileSize(m.StackSys), + MSpanInuse: util.FileSize(m.MSpanInuse), + MSpanSys: util.FileSize(m.MSpanSys), + MCacheInuse: util.FileSize(m.MCacheInuse), + MCacheSys: util.FileSize(m.MCacheSys), + BuckHashSys: util.FileSize(m.BuckHashSys), + GCSys: util.FileSize(m.GCSys), + OtherSys: util.FileSize(m.OtherSys), + + NextGC: util.FileSize(m.NextGC), + LastGC: util.TimeSinceDuration(uint64(time.Now().UnixNano()) - m.LastGC) + " ago", + PauseTotalNs: util.TimeSinceDuration(m.PauseTotalNs), + PauseNs: util.TimeSinceDuration(m.PauseNs[(m.NumGC + 255) % 256]), + NumGC: m.NumGC, + }, + } +} diff --git a/subsys/http/route/profile.go b/subsys/http/route/profile.go new file mode 100644 index 0000000..e22fbf3 --- /dev/null +++ b/subsys/http/route/profile.go @@ -0,0 +1,69 @@ +package route + +import ( + "net/http/pprof" + "github.com/gin-gonic/gin" +) + +// IndexHandler will pass the call from /debug/pprof to pprof +func ProfileIndexHandler() gin.HandlerFunc { + return func(ctx *gin.Context) { + pprof.Index(ctx.Writer, ctx.Request) + } +} + +// HeapHandler will pass the call from /debug/pprof/heap to pprof +func ProfileHeapHandler() gin.HandlerFunc { + return func(ctx *gin.Context) { + pprof.Handler("heap").ServeHTTP(ctx.Writer, ctx.Request) + } +} + +// GoroutineHandler will pass the call from /debug/pprof/goroutine to pprof +func ProfileGoroutineHandler() gin.HandlerFunc { + return func(ctx *gin.Context) { + pprof.Handler("goroutine").ServeHTTP(ctx.Writer, ctx.Request) + } +} + +// BlockHandler will pass the call from /debug/pprof/block to pprof +func ProfileBlockHandler() gin.HandlerFunc { + return func(ctx *gin.Context) { + pprof.Handler("block").ServeHTTP(ctx.Writer, ctx.Request) + } +} + +// ThreadCreateHandler will pass the call from /debug/pprof/threadcreate to pprof +func ProfileThreadCreateHandler() gin.HandlerFunc { + return func(ctx *gin.Context) { + pprof.Handler("threadcreate").ServeHTTP(ctx.Writer, ctx.Request) + } +} + +// CmdlineHandler will pass the call from /debug/pprof/cmdline to pprof +func ProfileCmdlineHandler() gin.HandlerFunc { + return func(ctx *gin.Context) { + pprof.Cmdline(ctx.Writer, ctx.Request) + } +} + +// ProfileHandler will pass the call from /debug/pprof/profile to pprof +func ProfileProfileHandler() gin.HandlerFunc { + return func(ctx *gin.Context) { + pprof.Profile(ctx.Writer, ctx.Request) + } +} + +// SymbolHandler will pass the call from /debug/pprof/symbol to pprof +func ProfileSymbolHandler() gin.HandlerFunc { + return func(ctx *gin.Context) { + pprof.Symbol(ctx.Writer, ctx.Request) + } +} + +// TraceHandler will pass the call from /debug/pprof/trace to pprof +func ProfileTraceHandler() gin.HandlerFunc { + return func(ctx *gin.Context) { + pprof.Trace(ctx.Writer, ctx.Request) + } +} diff --git a/subsys/http/route/services.go b/subsys/http/route/services.go new file mode 100644 index 0000000..fbcc09c --- /dev/null +++ b/subsys/http/route/services.go @@ -0,0 +1,27 @@ +package route + +import ( + "net/http" + "time" + + "amuz.es/gogs/infra/rooibos/util" +) + +var logger = util.NewLogger("route") + +func extractLastModified(hh http.Header) (modified *time.Time) { + if modifiedStr := extractHeader("If-Modified-Since", hh); modifiedStr == nil { + } else if modifiedTime, err := http.ParseTime(*modifiedStr); err != nil { + } else { + modified = &modifiedTime + } + return +} + +func extractHeader(key string, hh http.Header) (ret *string) { + if hdrStr := hh.Get(key); hdrStr == "" { + } else { + ret = &hdrStr + } + return +} diff --git a/subsys/periodic/init.go b/subsys/periodic/init.go new file mode 100644 index 0000000..9a40dab --- /dev/null +++ b/subsys/periodic/init.go @@ -0,0 +1,110 @@ +package periodic + +import ( + "time" + + cronLib "github.com/robfig/cron" + + "sync" + + "sync/atomic" + + "amuz.es/gogs/infra/rooibos/util" + "context" +) + +var ( + logger = util.NewLogger("periodic") + runningJobGroup = sync.WaitGroup{} + runningJobCount = int32(0) +) + +type Job struct { + started time.Time + Name string + Execution func() + isRunning int32 +} + +func (j *Job) Run() { + if ok := atomic.CompareAndSwapInt32(&j.isRunning, 0, -1); !ok { + return + } + + // ENTER CRITICAL SECTION + j.started = time.Now() + runningJobGroup.Add(1) + atomic.AddInt32(&runningJobCount, 1) + defer func() { + // EXIT CRITICAL SECTION + atomic.AddInt32(&runningJobCount, -1) + runningJobGroup.Done() + atomic.StoreInt32(&j.isRunning, 0) + logger.Debugf("Job %s Stopped in %s", j.Name, time.Now().Sub(j.started)) + }() + j.Execution() +} + +func Start(errorSignal chan error, ctx context.Context, waiter *sync.WaitGroup) { + cron, err := initialize(waiter) + if err != nil { + errorSignal <- err + return + } + go gracefulCloser(cron, ctx, waiter) + logger.Info("scheduler started") +} + +func initialize(waiter *sync.WaitGroup) (cron *cronLib.Cron, err error) { + defer func() { + if recov := recover(); recov != nil { + err = recov.(error) + } + }() + cron = cronLib.New() + waiter.Add(1) + logger.Debug("starting scheduler") + defer cron.Start() + return +} + +func gracefulCloser(cron *cronLib.Cron, ctx context.Context, waiter *sync.WaitGroup) { + <-ctx.Done() + defer waiter.Done() + logger.Info("stopping scheduler") + cron.Stop() + + waitChannel := make(chan struct{}, 1) + go func() { + runningJobGroup.Wait() + close(waitChannel) + }() + warningTicker := time.Tick(1 * time.Second) + for { + select { + case <-waitChannel: + logger.Info("Periodic Jobs shutdowned safely.") + return + case <-warningTicker: + logger.Infof( + "Waiting Periodic Jobs (remains %d job(s))", + runningJobCount, + ) + } + } +} + +func initJobs(closeSignal chan struct{}) { + // cron.AddJob("* * * * * *", &Job{ + // Name: "job 1", + // Execution: func() { + // logger.Debugf("job 1 (executing %d job(s))", runningJobCount) + // select { + // case <-closeSignal: + // logger.Debugf("job 1 quit signal received!") + // case <-time.After(time.Millisecond * 2000): + // } + // }, + // }) + // todo upload 실패한건, 오래된건 관리. +} diff --git a/subsys/redis/app_token_data.pb.go b/subsys/redis/app_token_data.pb.go new file mode 100644 index 0000000..1a62d02 --- /dev/null +++ b/subsys/redis/app_token_data.pb.go @@ -0,0 +1,458 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: subsys/redis/app_token_data.proto + +/* + Package redis is a generated protocol buffer package. + + It is generated from these files: + subsys/redis/app_token_data.proto + + It has these top-level messages: + TokenData +*/ +package redis + +import proto "github.com/gogo/protobuf/proto" +import fmt "fmt" +import math "math" +import _ "github.com/gogo/protobuf/gogoproto" +import _ "github.com/golang/protobuf/ptypes/timestamp" + +import amuz_es_gogs_infra_rooibos_util_iface "amuz.es/gogs/infra/rooibos/util/iface" +import time "time" + +import github_com_gogo_protobuf_types "github.com/gogo/protobuf/types" + +import io "io" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf +var _ = time.Kitchen + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package + +type TokenData struct { + ClientId amuz_es_gogs_infra_rooibos_util_iface.UUID `protobuf:"bytes,1,opt,name=ClientId,proto3,customtype=amuz.es/gogs/infra/rooibos/util/iface.UUID" json:"client_id"` + RefreshToken amuz_es_gogs_infra_rooibos_util_iface.UUID `protobuf:"bytes,2,opt,name=RefreshToken,proto3,customtype=amuz.es/gogs/infra/rooibos/util/iface.UUID" json:"refresh_token"` + MemberId uint64 `protobuf:"fixed64,3,opt,name=MemberId,proto3" json:"member_id"` + CreatedAt time.Time `protobuf:"bytes,4,opt,name=CreatedAt,stdtime" json:"created_at"` +} + +func (m *TokenData) Reset() { *m = TokenData{} } +func (m *TokenData) String() string { return proto.CompactTextString(m) } +func (*TokenData) ProtoMessage() {} +func (*TokenData) Descriptor() ([]byte, []int) { return fileDescriptorAppTokenData, []int{0} } + +func (m *TokenData) GetMemberId() uint64 { + if m != nil { + return m.MemberId + } + return 0 +} + +func (m *TokenData) GetCreatedAt() time.Time { + if m != nil { + return m.CreatedAt + } + return time.Time{} +} + +func init() { + proto.RegisterType((*TokenData)(nil), "redis.TokenData") +} +func (m *TokenData) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *TokenData) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + dAtA[i] = 0xa + i++ + i = encodeVarintAppTokenData(dAtA, i, uint64(m.ClientId.Size())) + n1, err := m.ClientId.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n1 + dAtA[i] = 0x12 + i++ + i = encodeVarintAppTokenData(dAtA, i, uint64(m.RefreshToken.Size())) + n2, err := m.RefreshToken.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n2 + if m.MemberId != 0 { + dAtA[i] = 0x19 + i++ + i = encodeFixed64AppTokenData(dAtA, i, uint64(m.MemberId)) + } + dAtA[i] = 0x22 + i++ + i = encodeVarintAppTokenData(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdTime(m.CreatedAt))) + n3, err := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.CreatedAt, dAtA[i:]) + if err != nil { + return 0, err + } + i += n3 + return i, nil +} + +func encodeFixed64AppTokenData(dAtA []byte, offset int, v uint64) int { + dAtA[offset] = uint8(v) + dAtA[offset+1] = uint8(v >> 8) + dAtA[offset+2] = uint8(v >> 16) + dAtA[offset+3] = uint8(v >> 24) + dAtA[offset+4] = uint8(v >> 32) + dAtA[offset+5] = uint8(v >> 40) + dAtA[offset+6] = uint8(v >> 48) + dAtA[offset+7] = uint8(v >> 56) + return offset + 8 +} +func encodeFixed32AppTokenData(dAtA []byte, offset int, v uint32) int { + dAtA[offset] = uint8(v) + dAtA[offset+1] = uint8(v >> 8) + dAtA[offset+2] = uint8(v >> 16) + dAtA[offset+3] = uint8(v >> 24) + return offset + 4 +} +func encodeVarintAppTokenData(dAtA []byte, offset int, v uint64) int { + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return offset + 1 +} +func (m *TokenData) Size() (n int) { + var l int + _ = l + l = m.ClientId.Size() + n += 1 + l + sovAppTokenData(uint64(l)) + l = m.RefreshToken.Size() + n += 1 + l + sovAppTokenData(uint64(l)) + if m.MemberId != 0 { + n += 9 + } + l = github_com_gogo_protobuf_types.SizeOfStdTime(m.CreatedAt) + n += 1 + l + sovAppTokenData(uint64(l)) + return n +} + +func sovAppTokenData(x uint64) (n int) { + for { + n++ + x >>= 7 + if x == 0 { + break + } + } + return n +} +func sozAppTokenData(x uint64) (n int) { + return sovAppTokenData(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *TokenData) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAppTokenData + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: TokenData: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: TokenData: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ClientId", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAppTokenData + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthAppTokenData + } + postIndex := iNdEx + byteLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.ClientId.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RefreshToken", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAppTokenData + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthAppTokenData + } + postIndex := iNdEx + byteLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.RefreshToken.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field MemberId", wireType) + } + m.MemberId = 0 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + iNdEx += 8 + m.MemberId = uint64(dAtA[iNdEx-8]) + m.MemberId |= uint64(dAtA[iNdEx-7]) << 8 + m.MemberId |= uint64(dAtA[iNdEx-6]) << 16 + m.MemberId |= uint64(dAtA[iNdEx-5]) << 24 + m.MemberId |= uint64(dAtA[iNdEx-4]) << 32 + m.MemberId |= uint64(dAtA[iNdEx-3]) << 40 + m.MemberId |= uint64(dAtA[iNdEx-2]) << 48 + m.MemberId |= uint64(dAtA[iNdEx-1]) << 56 + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field CreatedAt", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAppTokenData + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthAppTokenData + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.CreatedAt, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipAppTokenData(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthAppTokenData + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipAppTokenData(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowAppTokenData + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowAppTokenData + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + return iNdEx, nil + case 1: + iNdEx += 8 + return iNdEx, nil + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowAppTokenData + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + iNdEx += length + if length < 0 { + return 0, ErrInvalidLengthAppTokenData + } + return iNdEx, nil + case 3: + for { + var innerWire uint64 + var start int = iNdEx + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowAppTokenData + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + innerWire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + innerWireType := int(innerWire & 0x7) + if innerWireType == 4 { + break + } + next, err := skipAppTokenData(dAtA[start:]) + if err != nil { + return 0, err + } + iNdEx = start + next + } + return iNdEx, nil + case 4: + return iNdEx, nil + case 5: + iNdEx += 4 + return iNdEx, nil + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + } + panic("unreachable") +} + +var ( + ErrInvalidLengthAppTokenData = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowAppTokenData = fmt.Errorf("proto: integer overflow") +) + +func init() { proto.RegisterFile("subsys/redis/app_token_data.proto", fileDescriptorAppTokenData) } + +var fileDescriptorAppTokenData = []byte{ + // 336 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x90, 0xb1, 0x4e, 0xf3, 0x30, + 0x14, 0x85, 0xeb, 0xfe, 0x3f, 0x55, 0x6b, 0x5a, 0x09, 0x65, 0x40, 0x51, 0x87, 0xa4, 0x30, 0x15, + 0x24, 0x6c, 0x09, 0x1e, 0x00, 0x91, 0x76, 0xe9, 0x80, 0x84, 0xa2, 0x76, 0x62, 0x88, 0x9c, 0xe4, + 0x26, 0xb5, 0x68, 0xea, 0xc8, 0x76, 0x06, 0x78, 0x8a, 0x3e, 0x56, 0x27, 0xc4, 0xcc, 0x10, 0x50, + 0xd9, 0xfa, 0x14, 0x28, 0x8e, 0x42, 0xc5, 0xca, 0xe6, 0xeb, 0x73, 0xef, 0x77, 0x8e, 0x0e, 0x3e, + 0x53, 0x45, 0xa8, 0x9e, 0x15, 0x95, 0x10, 0x73, 0x45, 0x59, 0x9e, 0x07, 0x5a, 0x3c, 0xc1, 0x3a, + 0x88, 0x99, 0x66, 0x24, 0x97, 0x42, 0x0b, 0xeb, 0xc8, 0x68, 0xc3, 0xab, 0x94, 0xeb, 0x65, 0x11, + 0x92, 0x48, 0x64, 0x34, 0x15, 0xa9, 0xa0, 0x46, 0x0d, 0x8b, 0xc4, 0x4c, 0x66, 0x30, 0xaf, 0xfa, + 0x6a, 0xe8, 0xa6, 0x42, 0xa4, 0x2b, 0x38, 0x6c, 0x69, 0x9e, 0x81, 0xd2, 0x2c, 0xcb, 0xeb, 0x85, + 0xf3, 0xd7, 0x36, 0xee, 0xcd, 0x2b, 0xaf, 0x29, 0xd3, 0xcc, 0x7a, 0xc4, 0xdd, 0xc9, 0x8a, 0xc3, + 0x5a, 0xcf, 0x62, 0x1b, 0x8d, 0xd0, 0xb8, 0xef, 0xdd, 0x6e, 0x4b, 0xb7, 0xf5, 0x5e, 0xba, 0x97, + 0x2c, 0x2b, 0x5e, 0x08, 0xa8, 0x0a, 0xae, 0x28, 0x5f, 0x27, 0x92, 0x51, 0x29, 0x04, 0x0f, 0x85, + 0xa2, 0x85, 0xe6, 0x2b, 0xca, 0x13, 0x16, 0x01, 0x59, 0x2c, 0x66, 0xd3, 0x7d, 0xe9, 0xf6, 0x22, + 0x43, 0x09, 0x78, 0xec, 0xff, 0x00, 0xad, 0x14, 0xf7, 0x7d, 0x48, 0x24, 0xa8, 0xa5, 0x31, 0xb4, + 0xdb, 0xc6, 0x60, 0xf2, 0x27, 0x83, 0x81, 0xac, 0x49, 0x75, 0x4f, 0xfe, 0x2f, 0xb0, 0x75, 0x81, + 0xbb, 0xf7, 0x90, 0x85, 0x20, 0x67, 0xb1, 0xfd, 0x6f, 0x84, 0xc6, 0x1d, 0x6f, 0x50, 0x65, 0xca, + 0xcc, 0x9f, 0xc9, 0xd4, 0xc8, 0xd6, 0x03, 0xee, 0x4d, 0x24, 0x30, 0x0d, 0xf1, 0x9d, 0xb6, 0xff, + 0x8f, 0xd0, 0xf8, 0xf8, 0x7a, 0x48, 0xea, 0xce, 0x48, 0xd3, 0x19, 0x99, 0x37, 0x9d, 0x79, 0xa7, + 0x55, 0xd8, 0x7d, 0xe9, 0xe2, 0xa8, 0x3e, 0x0a, 0x98, 0xde, 0x7c, 0xb8, 0xc8, 0x3f, 0x40, 0xbc, + 0x93, 0xed, 0xce, 0x41, 0x6f, 0x3b, 0x07, 0x7d, 0xee, 0x1c, 0xb4, 0xf9, 0x72, 0x5a, 0x61, 0xc7, + 0x80, 0x6e, 0xbe, 0x03, 0x00, 0x00, 0xff, 0xff, 0x88, 0x6c, 0x3e, 0xdb, 0xe5, 0x01, 0x00, 0x00, +} diff --git a/subsys/redis/app_token_data.proto b/subsys/redis/app_token_data.proto new file mode 100644 index 0000000..e696528 --- /dev/null +++ b/subsys/redis/app_token_data.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; +package redis; +import "github.com/gogo/protobuf/gogoproto/gogo.proto"; +import "google/protobuf/timestamp.proto"; + +message TokenData { + bytes ClientId =1 [ + (gogoproto.customtype) = "amuz.es/gogs/infra/rooibos/util/iface.UUID", + (gogoproto.jsontag) = "client_id", + (gogoproto.nullable) = false + ]; + bytes RefreshToken =2 [ + (gogoproto.customtype) = "amuz.es/gogs/infra/rooibos/util/iface.UUID", + (gogoproto.jsontag) = "refresh_token", + (gogoproto.nullable) = false + ]; + fixed64 MemberId =3 [ + (gogoproto.jsontag) = "member_id" + ]; + + google.protobuf.Timestamp CreatedAt =4 [ + (gogoproto.jsontag) = "created_at", + (gogoproto.stdtime) = true, + (gogoproto.nullable) = false + ]; + +} diff --git a/subsys/redis/app_token_service.go b/subsys/redis/app_token_service.go new file mode 100644 index 0000000..f94130d --- /dev/null +++ b/subsys/redis/app_token_service.go @@ -0,0 +1,275 @@ +package redis + +import ( + "errors" + "fmt" + "strconv" + + "amuz.es/gogs/infra/rooibos/util/iface" + "github.com/garyburd/redigo/redis" +) + +var ( + tokenStoreBaseKey []byte + refreshTokenStoreBaseKey []byte + sessionStoreBaseKey []byte + tokenExpireTime int64 + refreshTokenExpireTime int64 +) + +func AppTokenInitService(phase string, tokenExpireTimeData, refreshTokenExpireTimeData int64) { + tokenStoreBaseKey = []byte(fmt.Sprint(phase, "/RB", "/TK/")) + refreshTokenStoreBaseKey = []byte(fmt.Sprint(phase, "/RB", "/RK/")) + sessionStoreBaseKey = []byte(fmt.Sprint(phase, "/RB", "/SE/")) + + tokenExpireTime = tokenExpireTimeData / 1000 + refreshTokenExpireTime = refreshTokenExpireTimeData / 1000 +} + +func AppTokenCreateSession(clientId iface.UUID, memberId uint64) (out *SessionData, err error) { + var client resourceConn + if client, err = getConn(); err != nil { + return + } + defer putConn(client) + + var ( + refreshToken, _ = iface.NewUUIDV4() + token, _ = iface.NewUUIDV4() + + tokenStoreKey = append(tokenStoreBaseKey, token.String()...) + refreshTokenStoreKey = append(refreshTokenStoreBaseKey, refreshToken.String()...) + sessionStoreKey = strconv.AppendUint(sessionStoreBaseKey, memberId, 10) + + refreshTokenData []byte + tokenData []byte + sessionData []byte + + newSession = &SessionData{ + Token: token, + RefreshToken: refreshToken, + } + ) + + if refreshTokenData, err = (&RefreshTokenData{ + ClientId: clientId, + MemberId: memberId, + }).Marshal(); err != nil { + return + } + + if tokenData, err = (&TokenData{ + ClientId: clientId, + MemberId: memberId, + RefreshToken: refreshToken, + }).Marshal(); err != nil { + return + } + if sessionData, err = newSession.Marshal(); err != nil { + return + } + var prevSession = new(SessionData) + + var tx = func() (err error) { + if _, err = client.Do("WATCH", sessionStoreKey); err != nil { + return + } + + if fetchErr := extractDataFromRedis(prevSession, sessionStoreKey, client); fetchErr != nil { + prevSession = nil + } + client.Send("MULTI") + + if prevSession != nil { + previousTokenStoreKey := append(tokenStoreBaseKey, prevSession.Token.String()...) + previousRefreshTokenStoreKey := append(refreshTokenStoreBaseKey, prevSession.RefreshToken.String()...) + client.Send("DEL", previousTokenStoreKey, previousRefreshTokenStoreKey) + } + client.Send("SETEX", tokenStoreKey, tokenExpireTime, tokenData) + client.Send("SETEX", refreshTokenStoreKey, refreshTokenExpireTime, refreshTokenData) + client.Send("SETEX", sessionStoreKey, refreshTokenExpireTime, sessionData) + + if _, err = redis.Values(client.Do("EXEC")); err != nil { + return newTxError(err) + } + + out = newSession + return + } + + err = retries(tx) + return +} + +func AppTokenFindMemberIdByToken(clientId iface.UUID, token iface.UUID) (memberId uint64, err error) { + var client resourceConn + if client, err = getConn(); err != nil { + return + } + defer putConn(client) + var ( + tokenStoreKey = append(tokenStoreBaseKey, token.String()...) + matchedTokenData = new(TokenData) + ) + + if err = extractDataFromRedis(matchedTokenData, tokenStoreKey, client); err != nil { + return + } + + if clientId.Equal(matchedTokenData.ClientId) { + memberId = matchedTokenData.MemberId + } else { + client.Send("DEL", tokenStoreKey) + err = errors.New("matched token is invalid") + } + return + +} + +func AppTokenFindMemberIdByRefreshToken(clientId iface.UUID, refreshToken iface.UUID) (memberId uint64, err error) { + var client resourceConn + if client, err = getConn(); err != nil { + return + } + defer putConn(client) + var ( + refreshTokenStoreKey = append(refreshTokenStoreBaseKey, refreshToken.String()...) + matchedRefreshTokenData = new(RefreshTokenData) + ) + + if err = extractDataFromRedis(matchedRefreshTokenData, refreshTokenStoreKey, client); err != nil { + return + } + + if clientId.Equal(matchedRefreshTokenData.ClientId) { + memberId = matchedRefreshTokenData.MemberId + } else { + sessionStoreKey := strconv.AppendUint(sessionStoreBaseKey, matchedRefreshTokenData.MemberId, 10) + sessionData := new(SessionData) //newSessionData + if errFetchSession := extractDataFromRedis(sessionData, sessionStoreKey, client); errFetchSession != nil { + logger.Info("failed to AppTokenFindMemberIdByRefreshToken", errFetchSession) + client.Send("DEL", refreshTokenStoreKey, sessionStoreKey) + } else { + tokenStoreKey := append(tokenStoreBaseKey, sessionData.Token.String()...) + client.Send("DEL", refreshTokenStoreKey, sessionStoreKey, tokenStoreKey) + } + err = errors.New("matched token is invalid") + } + return + +} +func AppTokenRefreshSessionByRefreshToken(clientId iface.UUID, refreshToken iface.UUID) (out *SessionData, err error) { + var client resourceConn + if client, err = getConn(); err != nil { + return + } + defer putConn(client) + + var ( + refreshTokenStoreKey = append(refreshTokenStoreBaseKey, refreshToken.String()...) + prevRefreshTokenData = new(RefreshTokenData) //refreshTokenData + prevSessionData = new(SessionData) //previousSessionData + prevTokenData = new(TokenData) //previousTokenData + + memberId uint64 + sessionStoreKey []byte + prevTokenStoreKey []byte + prevAnotherRefreshTokenStoreKey []byte + ) + + if fetchErr := extractDataFromRedis(prevRefreshTokenData, refreshTokenStoreKey, client); fetchErr != nil { + err = errors.New("matched refreshToken not found") + return + } else { + memberId = prevRefreshTokenData.MemberId + sessionStoreKey = strconv.AppendUint(sessionStoreBaseKey, memberId, 10) + } + + var tx = func() (err error) { + if _, err = client.Do("WATCH", sessionStoreKey); err != nil { + return + } + + if err = extractDataFromRedis(prevSessionData, sessionStoreKey, client); err != nil { + client.Send("MULTI") + client.Send("DEL", refreshTokenStoreKey) + if _, err = redis.Values(client.Do("EXEC")); err != nil { + logger.Info("failed to AppTokenRefreshSessionByRefreshToken", err) + } + err = errors.New("matched sessionData not found") + return + } else { + prevTokenStoreKey = append(tokenStoreBaseKey, prevSessionData.Token.String()...) + prevAnotherRefreshTokenStoreKey = append(refreshTokenStoreBaseKey, prevSessionData.RefreshToken.String()...) + } + + if fetchErr := extractDataFromRedis(prevTokenData, prevTokenStoreKey, client); fetchErr != nil { + prevTokenData = nil + } + + if prevRefreshTokenData.ClientId.Equal(clientId) && + prevSessionData.RefreshToken.Equal(refreshToken) && + (prevTokenData == nil || refreshToken.Equal(prevTokenData.RefreshToken)) { + // 검증성공! + var ( + newToken, _ = iface.NewUUIDV4() //token + newTokenStoreKey = append(tokenStoreBaseKey, newToken.String()...) //tokenData + + newTokenData []byte //tokenData + newSessionData []byte //newSessionData + newSession = &SessionData{ //newSessionData + Token: newToken, + RefreshToken: refreshToken, + } + ) + //newSession + if newTokenData, err = (&TokenData{ + ClientId: clientId, + MemberId: memberId, + RefreshToken: refreshToken, + }).Marshal(); err != nil { + return + } + + if newSessionData, err = newSession.Marshal(); err != nil { + return + } + + client.Send("MULTI") + + if prevTokenData != nil { + client.Send("DEL", prevTokenStoreKey) + } + + client.Send("SETEX", newTokenStoreKey, tokenExpireTime, newTokenData) + client.Send("SETEX", sessionStoreKey, refreshTokenExpireTime, newSessionData) + client.Send("EXPIRE", refreshTokenStoreKey, refreshTokenExpireTime) + + if _, err = redis.Values(client.Do("EXEC")); err != nil { + return + } + + out = newSession + return + } else { + // 부정사용의혹! + + client.Send("MULTI") + // 부정사용의혹! + if prevTokenData != nil { + client.Send("DEL", refreshTokenStoreKey, sessionStoreKey, prevTokenStoreKey, prevAnotherRefreshTokenStoreKey) + } else { + client.Send("DEL", refreshTokenStoreKey, sessionStoreKey, prevAnotherRefreshTokenStoreKey) + } + if _, err = redis.Values(client.Do("EXEC")); err != nil { + logger.Info("failed to AppTokenRefreshSessionByRefreshToken", err) + } + + err = errors.New("cannot refresh token") + } + return + } + + err = retries(tx) + return +} diff --git a/subsys/redis/conn.go b/subsys/redis/conn.go new file mode 100644 index 0000000..4424196 --- /dev/null +++ b/subsys/redis/conn.go @@ -0,0 +1,65 @@ +package redis + +import ( + "fmt" + "time" + + "amuz.es/gogs/infra/rooibos/util" + "github.com/garyburd/redigo/redis" + "github.com/youtube/vitess/go/pools" + "golang.org/x/net/context" +) + +var ( + pool *pools.ResourcePool = nil + logger = util.NewLogger("redis") +) + +type resourceConn struct { + redis.Conn +} + +func (r resourceConn) Close() { + r.Conn.Close() +} + +func InitRedis(config *util.RedisConfig) { + connectStr := fmt.Sprintf("%s:%d", + config.Host, + config.Port) + logger.Debugf("connect to redis://%s:%d/%d", + config.Host, + config.Port, + config.DB) + + pool = pools.NewResourcePool(func() (pools.Resource, error) { + c, err := redis.Dial("tcp", connectStr, + redis.DialDatabase(config.DB), + redis.DialPassword(config.Password), + ) + return resourceConn{c}, err + }, 10, 10, time.Minute) + + logger.Info("Redis connected") +} + +func getConn() (conn resourceConn, err error) { + ctx := context.TODO() + var resource pools.Resource + if resource, err = pool.Get(ctx); err == nil { + res := resource.(resourceConn) + conn = res + } + return +} + +func putConn(conn resourceConn) { + pool.Put(conn) +} + +func CloseRedis() { + if pool != nil { + pool.Close() + logger.Info("Redis closed") + } +} diff --git a/subsys/redis/redis_tx.go b/subsys/redis/redis_tx.go new file mode 100644 index 0000000..cfa45f7 --- /dev/null +++ b/subsys/redis/redis_tx.go @@ -0,0 +1,46 @@ +package redis + +import ( + "math/rand" + "time" + + retry "github.com/giantswarm/retry-go" + errs "github.com/pkg/errors" +) + +type txError error + +func newTxError(err error) error { + return newTxErrorWithMessage("key had changed during this tx", err) +} + +func newTxErrorWithMessage(message string, err error) error { + var wrapped error + if err == nil { + wrapped = errs.New(message) + } else { + wrapped = errs.Wrap(err, message) + } + return wrapped +} + +func retries(tx func() error) error { + return retry.Do(tx, + retry.RetryChecker(func(err error) (ret bool) { + switch err.(type) { + case txError: + ret = true + } + return + }), + retry.Timeout(400*time.Millisecond), + retry.AfterRetry(func(err error) { + if err == nil { + return + } + sleeps := 37 + rand.Intn(51) + time.Sleep(time.Duration(sleeps) * time.Millisecond) + }), + retry.MaxTries(5), + ) +} diff --git a/subsys/redis/serialize.go b/subsys/redis/serialize.go new file mode 100644 index 0000000..1fab4bd --- /dev/null +++ b/subsys/redis/serialize.go @@ -0,0 +1,16 @@ +package redis + +import ( + "amuz.es/gogs/infra/rooibos/util/iface" + "github.com/garyburd/redigo/redis" +) + +func extractDataFromRedis(target iface.Serializable, key []byte, fetcher resourceConn) error { + if resp, err := redis.Bytes(fetcher.Do("GET", key)); err != nil { + return err + } else if err = target.Unmarshal(resp); err != nil { + return err + } else { + return nil + } +} diff --git a/util/config.go b/util/config.go new file mode 100644 index 0000000..ebda2cb --- /dev/null +++ b/util/config.go @@ -0,0 +1,81 @@ +package util + +import ( + "io/ioutil" + + "gopkg.in/yaml.v2" +) + +type LogConfig struct { + FileName string + MaxSizeMb int + MaxBackup int + MaxDay int +} + +type DbConfig struct { + Network string + Driver string + Host string + User string + Password string + Port uint16 + DB string + Args string +} + +type RedisConfig struct { + Host string + Port uint16 + Password string + DB int + MaxRetries int + PoolSize int +} + +type config struct { + Phase string + Bind string + Db DbConfig + Redis RedisConfig + Logging struct { + Application LogConfig + Access LogConfig + } +} + +func (c config) Name() string { + return name +} + +func (c config) Version() string { + return version +} + +func (c config) BuildDate() string { + return buildDate +} + +const name = "Rooibos" + +var ( + buildDate string + version string + Config = config{} +) + +func LoadConfig(path string) error { + var ( + source []byte + err error + ) + if source, err = ioutil.ReadFile(path); err != nil { + return err + } + + if err = yaml.Unmarshal(source, &Config); err != nil { + return err + } + + return nil +} diff --git a/util/daemon.go b/util/daemon.go new file mode 100644 index 0000000..55a7311 --- /dev/null +++ b/util/daemon.go @@ -0,0 +1,16 @@ +package util + +import "github.com/coreos/go-systemd/daemon" + +type notifyType string + +const ( + DaemonStarted notifyType = "READY=1" + DaemonStopping notifyType = "STOPPING=1" +) + +var sockPath string + +func NotifyDaemon(status notifyType) { + daemon.SdNotify(false, string(status)) +} diff --git a/util/iface/bytes16.go b/util/iface/bytes16.go new file mode 100644 index 0000000..018df3b --- /dev/null +++ b/util/iface/bytes16.go @@ -0,0 +1,101 @@ +package iface + +import ( + "bytes" + "database/sql/driver" + "encoding/hex" + "errors" + "fmt" +) + +func (b Bytes16) Marshal() ([]byte, error) { + return b[:], nil +} + +func (b Bytes16) MarshalTo(buf []byte) (n int, err error) { + copy(buf, b[:]) + return len(b), nil +} +func (b *Bytes16) Unmarshal(buf []byte) error { + if len(buf) != Bytes16Length { + return fmt.Errorf("invalid bytes16 (got %d bytes)", len(buf)) + } + copy(b[:], buf) + return nil +} + +func (b *Bytes16) UnmarshalHex(buf []byte) (err error) { + hexLen := hex.DecodedLen(len(buf)) + if hexLen != Bytes16Length { + return fmt.Errorf("invalid bytes16 (got %d bytes)", len(buf)) + + } + _, err = hex.Decode(b[:], buf) + return +} + +func (b Bytes16) MarshalHex() ([]byte, error) { + hexLen := hex.EncodedLen(Bytes16Length) + hexBuf := make([]byte,hexLen) + if encodedLen := hex.Encode(hexBuf, b[:]); encodedLen != hexLen { + return nil, fmt.Errorf("invalid bytes16 length: %d", len(hexBuf)) + } + return hexBuf, nil +} + +func (b Bytes16) MarshalJSON() (encoded []byte, err error) { + encoded, err = b.MarshalHex() + if err != nil { + return + } + var buffer bytes.Buffer + buffer.WriteRune('"') + buffer.Write(encoded) + buffer.WriteRune('"') + return buffer.Bytes(), nil +} +func (b *Bytes16) UnmarshalJSON(from []byte) error { + + quote := []byte("\"") + quoteSize := len(quote) + + if len(from) < quoteSize*2 { + return errors.New("invalid quote notation") + } + + if !bytes.HasPrefix(from, quote) || !bytes.HasSuffix(from, quote) { + return errors.New("invalid quote notation") + } + return b.UnmarshalHex(from[quoteSize:len(from)-quoteSize]) +} + +func (b Bytes16) Compare(other Bytes16) int { + return bytes.Compare(b[:], other[:]) +} + +func (b Bytes16) Equal(other Bytes16) bool { + return b.Compare(other) == 0 +} + +func (b *Bytes16) Size() int { + if b == nil { + return 0 + } + return Bytes16Length +} + +// Scan implements the Scanner interface. +func (b *Bytes16) Scan(src interface{}) error { + switch src.(type) { + case string: + b.UnmarshalHex([]byte(src.(string))) + default: + return errors.New("Incompatible type for UUID") + } + return nil +} + +// Value implements the driver Valuer interface. +func (b Bytes16) Value() (driver.Value, error) { + return b.MarshalHex() +} diff --git a/util/iface/bytes32.go b/util/iface/bytes32.go new file mode 100644 index 0000000..fece118 --- /dev/null +++ b/util/iface/bytes32.go @@ -0,0 +1,101 @@ +package iface + +import ( + "bytes" + "database/sql/driver" + "encoding/hex" + "errors" + "fmt" +) + +func (b Bytes32) Marshal() ([]byte, error) { + return b[:], nil +} + +func (b Bytes32) MarshalTo(buf []byte) (n int, err error) { + copy(buf, b[:]) + return len(b), nil +} +func (b *Bytes32) Unmarshal(buf []byte) error { + if len(buf) != Bytes32Length { + return fmt.Errorf("invalid bytes32 (got %d bytes)", len(buf)) + } + copy(b[:], buf) + return nil +} + +func (b *Bytes32) UnmarshalHex(buf []byte) (err error) { + hexLen := hex.DecodedLen(len(buf)) + if hexLen != Bytes32Length { + return fmt.Errorf("invalid bytes32 (got %d bytes)", len(buf)) + + } + _, err = hex.Decode(b[:], buf) + return +} + +func (b Bytes32) MarshalHex() ([]byte, error) { + hexLen := hex.EncodedLen(Bytes32Length) + hexBuf := make([]byte,hexLen) + if encodedLen := hex.Encode(hexBuf, b[:]); encodedLen != hexLen { + return nil, fmt.Errorf("invalid bytes32 length: %d", len(hexBuf)) + } + return hexBuf, nil +} + +func (b Bytes32) MarshalJSON() (encoded []byte, err error) { + encoded, err = b.MarshalHex() + if err != nil { + return + } + var buffer bytes.Buffer + buffer.WriteRune('"') + buffer.Write(encoded) + buffer.WriteRune('"') + return buffer.Bytes(), nil +} +func (b *Bytes32) UnmarshalJSON(from []byte) error { + + quote:=[]byte("\"") + quoteSize:=len(quote) + + if len(from) < quoteSize*2{ + return errors.New("invalid quote notation") + } + + if !bytes.HasPrefix(from,quote) ||!bytes.HasSuffix(from,quote){ + return errors.New("invalid quote notation") + } + return b.UnmarshalHex(from[quoteSize:len(from)-quoteSize]) +} + +func (b Bytes32) Compare(other Bytes32) int { + return bytes.Compare(b[:], other[:]) +} + +func (b Bytes32) Equal(other Bytes32) bool { + return b.Compare(other) == 0 +} + +func (b *Bytes32) Size() int { + if b == nil { + return 0 + } + return Bytes32Length +} + +// Scan implements the Scanner interface. +func (b *Bytes32) Scan(src interface{}) error { + switch src.(type) { + case string: + b.UnmarshalHex([]byte(src.(string))) + default: + return errors.New("Incompatible type for UUID") + } + return nil +} + +// Value implements the driver Valuer interface. +func (b Bytes32) Value() (driver.Value, error) { + return b.MarshalHex() +} diff --git a/util/iface/bytes64.go b/util/iface/bytes64.go new file mode 100644 index 0000000..58ee6af --- /dev/null +++ b/util/iface/bytes64.go @@ -0,0 +1,100 @@ +package iface + +import ( + "bytes" + "database/sql/driver" + "encoding/hex" + "errors" + "fmt" +) + +func (b Bytes64) Marshal() ([]byte, error) { + return b[:], nil +} + +func (b Bytes64) MarshalTo(buf []byte) (n int, err error) { + copy(buf, b[:]) + return len(b), nil +} +func (b *Bytes64) Unmarshal(buf []byte) error { + if len(buf) != Bytes64Length { + return fmt.Errorf("invalid bytes64 (got %d bytes)", len(buf)) + } + copy(b[:], buf) + return nil +} + +func (b *Bytes64) UnmarshalHex(buf []byte) (err error) { + hexLen := hex.DecodedLen(len(buf)) + if hexLen != Bytes64Length { + return fmt.Errorf("invalid bytes64 (got %d bytes)", len(buf)) + + } + _, err = hex.Decode(b[:], buf) + return +} + +func (b Bytes64) MarshalHex() ([]byte, error) { + hexLen := hex.EncodedLen(Bytes64Length) + hexBuf := make([]byte,hexLen) + if encodedLen := hex.Encode(hexBuf, b[:]); encodedLen != hexLen { + return nil, fmt.Errorf("invalid bytes64 length: %d", len(hexBuf)) + } + return hexBuf, nil +} + +func (b Bytes64) MarshalJSON()(encoded []byte, err error) { + encoded, err = b.MarshalHex() + if err != nil { + return + } + var buffer bytes.Buffer + buffer.WriteRune('"') + buffer.Write(encoded) + buffer.WriteRune('"') + return buffer.Bytes(), nil +} +func (b *Bytes64) UnmarshalJSON(from []byte) error { + quote:=[]byte("\"") + quoteSize:=len(quote) + + if len(from) < quoteSize*2{ + return errors.New("invalid quote notation") + } + + if !bytes.HasPrefix(from,quote) ||!bytes.HasSuffix(from,quote){ + return errors.New("invalid quote notation") + } + return b.UnmarshalHex(from[quoteSize:len(from)-quoteSize]) +} + +func (b Bytes64) Compare(other Bytes64) int { + return bytes.Compare(b[:], other[:]) +} + +func (b Bytes64) Equal(other Bytes64) bool { + return b.Compare(other) == 0 +} + +func (b *Bytes64) Size() int { + if b == nil { + return 0 + } + return Bytes64Length +} + +// Scan implements the Scanner interface. +func (b *Bytes64) Scan(src interface{}) error { + switch src.(type) { + case string: + b.UnmarshalHex([]byte(src.(string))) + default: + return errors.New("Incompatible type for UUID") + } + return nil +} + +// Value implements the driver Valuer interface. +func (b Bytes64) Value() (driver.Value, error) { + return b.MarshalHex() +} diff --git a/util/iface/types.go b/util/iface/types.go new file mode 100644 index 0000000..911fc51 --- /dev/null +++ b/util/iface/types.go @@ -0,0 +1,33 @@ +package iface + +import ( + "amuz.es/gogs/infra/rooibos/util" + "github.com/google/uuid" +) + +const Bytes16Length = 16 +const Bytes32Length = 32 +const Bytes64Length = 64 +const UUIDLength = 16 + +type Bytes16 [Bytes16Length]byte +type Bytes32 [Bytes32Length]byte +type Bytes64 [Bytes64Length]byte +type UUID struct { + uuid.UUID +} + +var logger = util.NewLogger("common_iface") + +type Serializable interface { + Marshal() ([]byte, error) + Unmarshal(buf []byte) error + String() string +} + + + +type JSONSerializable interface { + UnmarshalJSON(b []byte) error + MarshalJSON() ([]byte, error) +} \ No newline at end of file diff --git a/util/iface/uuid.go b/util/iface/uuid.go new file mode 100644 index 0000000..9dd5a59 --- /dev/null +++ b/util/iface/uuid.go @@ -0,0 +1,115 @@ +package iface + +import ( + "bytes" + "database/sql/driver" + "encoding/hex" + "errors" + "github.com/google/uuid" + "fmt" +) + +func (u UUID) Marshal() ([]byte, error) { + return u.UUID[:], nil +} + +func (u UUID) MarshalTo(buf []byte) (n int, err error) { + copy(buf, u.UUID[:]) + return len(u.UUID), nil +} + +func (u *UUID) Unmarshal(buf []byte) error { + if len(buf) != UUIDLength { + return fmt.Errorf("invalid bytes16 (got %d bytes)", len(buf)) + } + copy(u.UUID[:], buf) + return nil +} + +func (u *UUID) UnmarshalHex(buf []byte) (err error) { + hexLen := hex.DecodedLen(len(buf)) + if hexLen != UUIDLength { + return fmt.Errorf("invalid UUID (got %d bytes)", len(buf)) + + } + _, err = hex.Decode(u.UUID[:], buf) + return +} + +func (u *UUID) MarshalHex() ([]byte, error) { + hexLen := hex.EncodedLen(UUIDLength) + hexBuf := make([]byte,hexLen) + if encodedLen := hex.Encode(hexBuf, u.UUID[:]); encodedLen != hexLen { + return nil, fmt.Errorf("invalid bytes16 length: %d", len(hexBuf)) + } + return hexBuf, nil +} + +func (u UUID) MarshalJSON() (encoded []byte, err error) { + encoded, err = u.MarshalHex() + if err != nil { + return + } + var buffer bytes.Buffer + buffer.WriteRune('"') + buffer.Write(encoded) + buffer.WriteRune('"') + return buffer.Bytes(), nil +} +func (u *UUID) UnmarshalJSON(from []byte) error { + quote := []byte("\"") + quoteSize := len(quote) + + if len(from) < quoteSize*2 { + return errors.New("invalid quote notation") + } + + if !bytes.HasPrefix(from, quote) || !bytes.HasSuffix(from, quote) { + return errors.New("invalid quote notation") + } + return u.UnmarshalHex(from[quoteSize:len(from)-quoteSize]) +} + +func (u UUID) Compare(other UUID) int { + return bytes.Compare(u.UUID[:], other.UUID[:]) +} + +func (u UUID) Equal(other UUID) bool { + return u.Compare(other) == 0 +} + +func (u *UUID) Size() int { + if u == nil { + return 0 + } + return UUIDLength +} + +func NewUUIDV4() (u UUID, err error) { + var ui uuid.UUID + if ui, err = uuid.NewRandom(); err == nil { + u.UUID = ui + } + return +} + +// Scan implements the Scanner interface. +func (u *UUID) Scan(src interface{}) error { + switch src.(type) { + case string: + u.UnmarshalHex([]byte(src.(string))) + default: + return errors.New("Incompatible type for UUID") + } + return nil +} + +// Value implements the driver Valuer interface. +func (u UUID) Value() (driver.Value, error) { + return u.MarshalHex() +} + +// Value implements the driver Valuer interface. +func (u UUID) Random() { + u.UUID = uuid.New() +} diff --git a/util/json.go b/util/json.go new file mode 100644 index 0000000..ea679a9 --- /dev/null +++ b/util/json.go @@ -0,0 +1,81 @@ +package util + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" +) + +var jsonContentType = []string{"application/json; charset=utf-8"} + +func writeContentType(w http.ResponseWriter, value []string) { + header := w.Header() + if val := header["Content-Type"]; len(val) == 0 { + header["Content-Type"] = value + } +} + +func DumpJSONHeaderOnly(c *gin.Context, code int) { + c.Status(code) + + // Encode + header := c.Writer.Header() + if val := header["Content-Type"]; len(val) == 0 { + header["Content-Type"] = jsonContentType + } +} + +func DumpJSON(c *gin.Context, code int, serializable interface{}) { + c.Status(code) + + // Encode + header := c.Writer.Header() + if val := header["Content-Type"]; len(val) == 0 { + header["Content-Type"] = jsonContentType + } + + if enc := json.NewEncoder(c.Writer); enc == nil { + panic(errors.New("empty json encoder")) + } else if err := enc.Encode(serializable); err != nil { + panic(err) + } +} + +func LoadJSON(data []byte, deserializable interface{}) (err error) { + if reader := bytes.NewReader(data); reader == nil { + err = errors.New("empty json data") + } else { + err = LoadJSONReader(reader, deserializable) + } + return + +} + +func LoadJSONReader(r io.Reader, deserializable interface{}) (err error) { + decoder := json.NewDecoder(r) + + return decoder.Decode(deserializable) +} + +func BindJSON(c *gin.Context, deserializable interface{}) (err error) { + defer func() { + if err != nil { + c.AbortWithError(400, err).SetType(gin.ErrorTypeBind) + } + }() + if err = LoadJSONReader(c.Request.Body, deserializable); err != nil { + return + } + + // validate(obj) + + if binding.Validator == nil { + return nil + } + return binding.Validator.ValidateStruct(deserializable) +} diff --git a/util/logger.go b/util/logger.go new file mode 100644 index 0000000..f1d20bb --- /dev/null +++ b/util/logger.go @@ -0,0 +1,73 @@ +package util + +import ( + "io" + "os" + "path" + + "github.com/sirupsen/logrus" + prefixed "github.com/x-cray/logrus-prefixed-formatter" + lumberjack "gopkg.in/natefinch/lumberjack.v2" +) + +var ( + logger = logrus.StandardLogger() + rotaters []*lumberjack.Logger + logDir string + formatter = prefixed.TextFormatter{} +) + +func init() { + logger.Formatter = &formatter +} +func LoggerIsStd() bool { + return !formatter.DisableColors +} +func InitLogger(verbose bool, logDirArg string, config *LogConfig) { + logDir = logDirArg + colorSupport, writer := NewLogWriter(config) + formatter.DisableColors = !colorSupport + logger.Out = writer + if verbose { + logger.Level = logrus.DebugLevel + } else { + logger.Level = logrus.InfoLevel + } +} + +func NewLogger(prefix string) *logrus.Entry { + return logrus.NewEntry(logger).WithField("prefix", prefix) +} +func NewLogWriter(config *LogConfig) (bool, io.Writer) { + switch config.FileName { + case "Stdout": + return true, os.Stdout + case "Stderr": + return true, os.Stderr + default: + logpath := config.FileName + if logDir != "" { + logpath = path.Join(logDir, config.FileName) + } + logger.Info(" Attention!! log writes to ", config.FileName) + rotater := &lumberjack.Logger{ + Filename: logpath, + MaxSize: config.MaxSizeMb, // megabytes + MaxBackups: config.MaxBackup, + MaxAge: config.MaxDay, //days + } + rotaters = append(rotaters, rotater) + return false, rotater + } +} + +func RotateLogger() { + if len(rotaters) == 0 { + return + } + logger.Info("rotating logger") + for _, rotater := range rotaters { + rotater.Rotate() + } + logger.Info("rotated") +} diff --git a/util/parse.go b/util/parse.go new file mode 100644 index 0000000..10bc984 --- /dev/null +++ b/util/parse.go @@ -0,0 +1,138 @@ +package util + +import ( + "strconv" +) + +func mustParseInt(value string, bits int) int64 { + if parsed, err := strconv.ParseInt(value, 10, bits); err != nil { + panic(err) + } else { + return parsed + } +} + +func mustParseUint(value string, bits int) uint64 { + if parsed, err := strconv.ParseUint(value, 10, bits); err != nil { + panic(err) + } else { + return parsed + } +} + +func ParseUint8(value string) (ret *uint8) { + + if parsed, err := strconv.ParseUint(value, 10, 8); err == nil { + ret = new(uint8) + *ret = uint8(parsed) + } + return +} +func ParseUint16(value string) (ret *uint16) { + + if parsed, err := strconv.ParseUint(value, 10, 16); err == nil { + ret = new(uint16) + *ret = uint16(parsed) + } + return +} +func ParseUint32(value string) (ret *uint32) { + + if parsed, err := strconv.ParseUint(value, 10, 32); err == nil { + ret = new(uint32) + *ret = uint32(parsed) + } + return +} +func ParseUint64(value string) (ret *uint64) { + + if parsed, err := strconv.ParseUint(value, 10, 64); err == nil { + ret = new(uint64) + *ret = uint64(parsed) + } + return +} +func ParseUint(value string) (ret *uint) { + + if parsed, err := strconv.ParseUint(value, 10, 0); err == nil { + ret = new(uint) + *ret = uint(parsed) + } + return +} +func ParseInt8(value string) (ret *int8) { + + if parsed, err := strconv.ParseInt(value, 10, 8); err == nil { + ret = new(int8) + *ret = int8(parsed) + } + return +} +func ParseInt16(value string) (ret *int16) { + + if parsed, err := strconv.ParseInt(value, 10, 16); err == nil { + ret = new(int16) + *ret = int16(parsed) + } + return +} +func ParseInt32(value string) (ret *int32) { + + if parsed, err := strconv.ParseInt(value, 10, 32); err == nil { + ret = new(int32) + *ret = int32(parsed) + } + return +} +func ParseInt64(value string) (ret *int64) { + + if parsed, err := strconv.ParseInt(value, 10, 64); err == nil { + ret = new(int64) + *ret = int64(parsed) + } + return +} +func ParseInt(value string) (ret *int) { + if parsed, err := strconv.ParseInt(value, 10, 0); err == nil { + ret = new(int) + *ret = int(parsed) + } + return +} + +func ParseUint8Must(value string) uint8 { + return uint8(mustParseUint(value, 8)) +} +func ParseUint16Must(value string) uint16 { + return uint16(mustParseUint(value, 16)) +} +func ParseUint32Must(value string) uint32 { + return uint32(mustParseUint(value, 32)) +} +func ParseUint64Must(value string) uint64 { + return mustParseUint(value, 64) +} + +func ParseUintMust(value string) uint { + return uint(mustParseUint(value, 0)) +} + +func ParseInt8Must(value string) int8 { + return int8(mustParseInt(value, 8)) +} + +func ParseInt16Must(value string) int16 { + return int16(mustParseInt(value, 16)) +} + +func ParseInt32Must(value string) int32 { + return int32(mustParseInt(value, 32)) +} + +func ParseInt64Must(value string) int64 { + return mustParseInt(value, 64) +} + +func ParseIntMust(value string) int { + return int(mustParseInt(value, 0)) +} diff --git a/util/tool.go b/util/tool.go new file mode 100644 index 0000000..0d139e2 --- /dev/null +++ b/util/tool.go @@ -0,0 +1,313 @@ +package util + +import ( + "bytes" + "crypto/rand" + "fmt" + "image" + "math" + "reflect" + "strings" + "time" + "unsafe" +) + +func Int64ToBytes(s int64) []byte { + return []byte{ + byte(s >> 0), + byte(s >> 8), + byte(s >> 16), + byte(s >> 24), + byte(s >> 32), + byte(s >> 40), + byte(s >> 48), + byte(s >> 56), + } +} + +func BytesToInt64(s []byte) int64 { + return int64(0) | + (int64(s[0]) << 0) | + (int64(s[1]) << 8) | + (int64(s[2]) << 16) | + (int64(s[3]) << 24) | + (int64(s[4]) << 32) | + (int64(s[5]) << 40) | + (int64(s[6]) << 48) | + (int64(s[7]) << 56) +} + +func BytesToString(b []byte) string { + bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + sh := reflect.StringHeader{ + Data: bh.Data, + Len: bh.Len} + return *(*string)(unsafe.Pointer(&sh)) +} + +func StringToBytes(s string) []byte { + sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) + bh := reflect.SliceHeader{ + Data: sh.Data, + Len: sh.Len, + Cap: 0} + return *(*[]byte)(unsafe.Pointer(&bh)) +} + +// GetRandomString generate random string by specify chars. +func GetRandomString(n int, alphabets ...byte) string { + const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + var bytes = make([]byte, 0, n) + rand.Read(bytes) + for _, b := range bytes { + if len(alphabets) == 0 { + bytes = append(bytes, alphanum[b%byte(len(alphanum))]) + } else { + bytes = append(bytes, alphabets[b%byte(len(alphabets))]) + } + } + return string(bytes) +} + +// Seconds-based time units +var ( + NanoSecond uint64 = 1 + MicroSecond = 1000 * NanoSecond + MilliSecond = 1000 * MicroSecond + Second = 1000 * MilliSecond + Minute = 60 * Second + Hour = 60 * Minute + Day = 24 * Hour + Week = 7 * Day + Month = 30 * Day + Year = 12 * Month +) + +func computeTimeDiff(buff *bytes.Buffer, diff uint64) uint64 { + switch { + case diff < 1*NanoSecond: + diff = 0 + case diff < 2*NanoSecond: + diff = 0 + buff.WriteString("001ns") + case diff < 1*MicroSecond: + buff.WriteString(fmt.Sprintf("%03dns", diff)) + diff -= diff / NanoSecond * NanoSecond + case diff < 2*MicroSecond: + diff -= 1 * MilliSecond + buff.WriteString("001us") + case diff < 1*MilliSecond: + buff.WriteString(fmt.Sprintf("%03dus", diff/MicroSecond)) + diff -= diff / MicroSecond * MicroSecond + case diff < 2*MilliSecond: + diff -= 1 * Second + buff.WriteString("001ms") + case diff < 1*Second: + buff.WriteString(fmt.Sprintf("%03dms", diff/MilliSecond)) + diff -= diff / MilliSecond * MilliSecond + case diff < 2*Second: + diff -= 1 * Minute + buff.WriteString("01s") + case diff < 1*Minute: + buff.WriteString(fmt.Sprintf("%02ds", diff/Second)) + diff -= diff / Second * Second + case diff < 2*Minute: + diff -= 1 * Minute + buff.WriteString("01m") + case diff < 1*Hour: + buff.WriteString(fmt.Sprintf("%02dm", diff/Minute)) + diff -= diff / Minute * Minute + case diff < 2*Hour: + diff -= 1 * Hour + buff.WriteString("01h") + case diff < 1*Day: + buff.WriteString(fmt.Sprintf("%02dh", diff/Hour)) + diff -= diff / Hour * Hour + case diff < 2*Day: + diff -= 1 * Day + buff.WriteString("01d") + case diff < 1*Week: + buff.WriteString(fmt.Sprintf("%02dd", diff/Day)) + diff -= diff / Day * Day + case diff < 2*Week: + diff -= 1 * Week + buff.WriteString("1w") + case diff < 1*Month: + buff.WriteString(fmt.Sprintf("%01dw", diff/Week)) + diff -= diff / Week * Week + + case diff < 2*Month: + diff -= 1 * Month + buff.WriteString("01M") + case diff < 1*Year: + buff.WriteString(fmt.Sprintf("%02dM", diff/Month)) + diff -= diff / Month * Month + + case diff < 2*Year: + diff -= 1 * Year + buff.WriteString("01y") + default: + buff.WriteString(fmt.Sprintf("%02dy", diff/Year)) + diff = 0 + } + return diff +} + +// TimeSincePro calculates the time interval and generate full user-friendly string. +func TimeSincePro(then time.Time) string { + now := time.Now() + diff := uint64(now.UnixNano() - then.UnixNano()) + + if then.After(now) { + return "future" + } + + return TimeSinceDuration(diff) +} + +// TimeSincePro calculates the time interval and generate full user-friendly string. +func TimeSinceDuration(then uint64) string { + var ( + timeStr bytes.Buffer + diff = then + ) + + for { + diff = computeTimeDiff(&timeStr, diff) + if diff == 0 { + break + } else { + timeStr.WriteRune(' ') + } + } + if timeStr.Len() == 0 { + return "instant!" + + } else { + return timeStr.String() + } +} + +const ( + Byte = 1 + KByte = Byte * 1024 + MByte = KByte * 1024 + GByte = MByte * 1024 + TByte = GByte * 1024 + PByte = TByte * 1024 + EByte = PByte * 1024 +) + +var bytesSizeTable = map[string]uint64{ + "b": Byte, + "kb": KByte, + "mb": MByte, + "gb": GByte, + "tb": TByte, + "pb": PByte, + "eb": EByte, +} + +func logn(n, b float64) float64 { + return math.Log(n) / math.Log(b) +} + +func humanateBytes(s uint64, base float64, sizes []string) string { + if s < 10 { + return fmt.Sprintf("%dB", s) + } + e := math.Floor(logn(float64(s), base)) + suffix := sizes[int(e)] + val := float64(s) / math.Pow(base, math.Floor(e)) + f := "%.0f" + if val < 10 { + f = "%.1f" + } + + return fmt.Sprintf(f+"%s", val, suffix) +} + +// FileSize calculates the file size and generate user-friendly string. +func FileSize(s uint64) string { + sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"} + return humanateBytes(s, 1024, sizes) +} + +// Subtract deals with subtraction of all types of number. +func Subtract(left interface{}, right interface{}) interface{} { + var rleft, rright int64 + var fleft, fright float64 + var isInt bool = true + switch left.(type) { + case int: + rleft = int64(left.(int)) + case int8: + rleft = int64(left.(int8)) + case int16: + rleft = int64(left.(int16)) + case int32: + rleft = int64(left.(int32)) + case int64: + rleft = left.(int64) + case float32: + fleft = float64(left.(float32)) + isInt = false + case float64: + fleft = left.(float64) + isInt = false + } + + switch right.(type) { + case int: + rright = int64(right.(int)) + case int8: + rright = int64(right.(int8)) + case int16: + rright = int64(right.(int16)) + case int32: + rright = int64(right.(int32)) + case int64: + rright = right.(int64) + case float32: + fright = float64(left.(float32)) + isInt = false + case float64: + fleft = left.(float64) + isInt = false + } + + if isInt { + return rleft - rright + } else { + return fleft + float64(rleft) - (fright + float64(rright)) + } +} + +// EllipsisString returns a truncated short string, +// it appends '...' in the end of the length of string is too large. +func EllipsisString(str string, length int) string { + if len(str) < length { + return str + } + return str[:length-3] + "..." +} + +func AspectRatio(srcRect image.Point, toResize uint64) image.Point { + w, h := int(toResize), getRatioSize(int(toResize), srcRect.Y, srcRect.X) + if srcRect.X < srcRect.Y { + w, h = getRatioSize(int(toResize), srcRect.X, srcRect.Y), int(toResize) + } + return image.Point{w, h} +} +func getRatioSize(a, b, c int) int { + d := a * b / c + return (d + 1) & -1 +} + +func Basename(s string) string { + n := strings.LastIndexByte(s, '.') + if n >= 0 { + return s[:n] + } + return s +}