├── staticcheck.conf ├── etc └── 17monipdb.datx ├── .linthub.yml ├── ipipfree.ipdb ├── .gitattributes ├── service ├── message │ ├── proto │ │ ├── hb.proto │ │ ├── cli_pipe.proto │ │ ├── authorize_key.proto │ │ ├── cli.proto │ │ └── performance.proto │ ├── model │ │ ├── msg_test.go │ │ ├── performance_test.go │ │ ├── msg.go │ │ ├── hb.pb.go │ │ └── cli_pipe.pb.go │ ├── deal │ │ ├── cycle.go │ │ ├── hb.go │ │ └── message.go │ ├── pipe │ │ ├── connect.go │ │ ├── cli.go │ │ ├── pipeline.go │ │ └── cli_session.go │ └── store │ │ └── agent.go ├── models │ ├── trojan_users.go │ ├── cover.go │ ├── login_history.go │ ├── agent.go │ ├── api_log.go │ ├── user.go │ ├── time.go │ ├── file.go │ ├── request.go │ ├── article.go │ └── response.go ├── action │ ├── version.go │ ├── album.go │ ├── agent │ │ ├── ws.go │ │ └── agent.go │ ├── 2fa.go │ ├── action.go │ ├── deployer.go │ ├── wrapper.go │ ├── xterm.go │ └── user.go ├── middleware │ ├── crossite.go │ ├── session.go │ └── restlog.go └── gin.go ├── setenv ├── Dockerfile ├── modules ├── dbmodels │ ├── draft.go │ ├── share_lock.go │ ├── node.go │ ├── uuid.go │ ├── agent_perform.go │ ├── image_meta.go │ ├── face_label.go │ ├── face.go │ ├── varify.go │ ├── calendar.go │ ├── project.go │ ├── trojan_users.go │ ├── cover.go │ ├── login_history.go │ ├── agent.go │ ├── user.go │ ├── file.go │ └── article.go ├── lock │ ├── lock.go │ └── db_lock.go ├── db │ ├── draft.go │ ├── cover.go │ ├── trojan_users.go │ ├── image_meta.go │ ├── login_history.go │ ├── calendar.go │ ├── user.go │ ├── agent.go │ └── file.go ├── dns │ ├── dns.go │ ├── dns_test.go │ └── alidns.go ├── rlog │ ├── rlog_test.go │ ├── num_hook.go │ ├── es.go │ └── rlog.go ├── ipip │ └── init.go ├── logger │ └── log.go ├── cache │ ├── cache.go │ ├── redis_test.go │ ├── redis.go │ └── bolt.go ├── viewcnt │ └── view.go ├── storage │ ├── s3_oss.go │ ├── aliyun_oss.go │ ├── storage.go │ └── qcloud_cos.go ├── cron │ ├── file.go │ └── task.go ├── session │ └── session.go ├── bleve │ └── bleve.go ├── orm │ ├── database.go │ └── context_logger.go ├── imgtools │ └── imgtools.go └── configuration │ └── config.go ├── utils ├── lunar_test.go ├── ics_test.go ├── ics.go ├── utils_test.go ├── lunar.go ├── math_lexer.go ├── recover.go └── utils.go ├── README.md ├── studio.service ├── .travis.yml ├── .drone.yml ├── g └── global.go ├── .gitignore ├── go.mod ├── main.go └── control /staticcheck.conf: -------------------------------------------------------------------------------- 1 | checks = ["all", "-S1023"] -------------------------------------------------------------------------------- /etc/17monipdb.datx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duguying/studio/HEAD/etc/17monipdb.datx -------------------------------------------------------------------------------- /.linthub.yml: -------------------------------------------------------------------------------- 1 | go: 2 | lint: true 3 | shell: 4 | lint: false 5 | css: 6 | lint: false 7 | js: 8 | lint: false -------------------------------------------------------------------------------- /ipipfree.ipdb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:b82b874152c798dda407ffe7544e1f5ec67efa1f5c334efc0d3893b8053b4be1 3 | size 3649897 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /static/deps/* linguist-vendored 2 | *.tpl linguist-vendored 3 | /vendor/* linguist-vendored 4 | ipipfree.ipdb filter=lfs diff=lfs merge=lfs -text 5 | -------------------------------------------------------------------------------- /service/message/proto/hb.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package model; 3 | option go_package = "../model"; 4 | 5 | // cmd 0 6 | message HeartBeat { 7 | uint64 timestamp = 1; 8 | } -------------------------------------------------------------------------------- /setenv: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p /root/studio 4 | mkdir -p /data 5 | mv /tmp/studio /root/studio/ 6 | mv /tmp/ipipfree.ipdb /data/ 7 | chmod +x /root/studio/studio 8 | rm /tmp/setenv 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM git.duguying.net/duguying/studio-base:latest 2 | 3 | ADD "dockerdist" "/tmp" 4 | RUN "/tmp/setenv" 5 | WORKDIR "/root/studio" 6 | CMD ["-c", "/data/studio.ini"] 7 | ENTRYPOINT ["/root/studio/studio"] 8 | -------------------------------------------------------------------------------- /modules/dbmodels/draft.go: -------------------------------------------------------------------------------- 1 | package dbmodels 2 | 3 | import "time" 4 | 5 | type Draft struct { 6 | UUID 7 | 8 | ArticleID uint `gorm:"index"` 9 | Content string `gorm:"type:longtext;index:,class:FULLTEXT"` 10 | 11 | CreatedAt time.Time 12 | } 13 | -------------------------------------------------------------------------------- /utils/lunar_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestLunar(t *testing.T) { 9 | lunar := NewLunar("1990-08-16", false) 10 | fmt.Println("==>", lunar) 11 | 12 | fmt.Println(LunarToSolar(lunar)) 13 | } 14 | -------------------------------------------------------------------------------- /service/message/proto/cli_pipe.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package model; 3 | option go_package = "../model"; 4 | 5 | // cmd 3 6 | message CliPipe { 7 | string session = 1; 8 | uint32 pid = 2; 9 | bytes data = 3; 10 | uint32 data_len = 4; 11 | } -------------------------------------------------------------------------------- /modules/dbmodels/share_lock.go: -------------------------------------------------------------------------------- 1 | package dbmodels 2 | 3 | import "time" 4 | 5 | type ShareLock struct { 6 | LockKey string `gorm:"unique;type:varchar(50)"` 7 | LockOwner string `gorm:"index;type:varchar(50)"` 8 | UpdateTime time.Time `gorm:"index"` 9 | } 10 | -------------------------------------------------------------------------------- /service/models/trojan_users.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type TrojanUsers struct { 4 | ID uint `json:"id"` 5 | Username string `json:"username"` 6 | Quota int64 `json:"quota"` 7 | Download int64 `json:"download"` 8 | Upload int64 `json:"upload"` 9 | } 10 | -------------------------------------------------------------------------------- /modules/dbmodels/node.go: -------------------------------------------------------------------------------- 1 | package dbmodels 2 | 3 | import "time" 4 | 5 | // Node 服务节点 6 | type Node struct { 7 | UUID 8 | 9 | NodeID string `json:"node_id"` 10 | AccessIPPort string `json:"access_ipport"` 11 | UpdatedAt time.Time `json:"updated_at"` 12 | CreatedAt time.Time `json:"created_at"` 13 | } 14 | -------------------------------------------------------------------------------- /modules/dbmodels/uuid.go: -------------------------------------------------------------------------------- 1 | package dbmodels 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type UUID struct { 9 | ID string `gorm:"type:varchar(40);primary_key;" sql:"comment:'UUID'"` 10 | } 11 | 12 | func (b *UUID) BeforeCreate(tx *gorm.DB) error { 13 | b.ID = uuid.New().String() 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /modules/lock/lock.go: -------------------------------------------------------------------------------- 1 | // Package lock 锁 2 | package lock 3 | 4 | import "time" 5 | 6 | // Lock 锁 7 | type Lock interface { 8 | GetLock() bool 9 | ReleaseLock() bool 10 | LockKey() string 11 | GetIsLockExtend() bool 12 | SetIsLockExtend(isLockExtend bool) 13 | SetLockExtendIntervalSecond(lockExtendIntervalSecond time.Duration) 14 | } 15 | -------------------------------------------------------------------------------- /service/message/proto/authorize_key.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package model; 3 | option go_package = "../model"; 4 | 5 | // cmd 2 6 | message AuthorizeKey { 7 | enum KeyCmd { 8 | LIST = 0; 9 | SET = 1; 10 | DELETE = 2; 11 | } 12 | KeyCmd command = 1; 13 | repeated string public_keys = 2; 14 | } 15 | -------------------------------------------------------------------------------- /utils/ics_test.go: -------------------------------------------------------------------------------- 1 | // Package utils 包注释 2 | package utils 3 | 4 | import ( 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestGenerateICS(t *testing.T) { 10 | GenerateICS( 11 | "uuid", 12 | time.Now(), time.Now(), time.Hour, 13 | "总结标题", 14 | "南山区大新路艺华花园", 15 | "这是一个生日聚会", 16 | "https://duguying.net", 17 | "糖糖", 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /modules/db/draft.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "duguying/studio/modules/dbmodels" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | // AddDraft 添加草稿 10 | func AddDraft(tx *gorm.DB, articleID uint, content string) error { 11 | return tx.Model(dbmodels.Draft{}).Create(&dbmodels.Draft{ 12 | ArticleID: articleID, 13 | Content: content, 14 | }).Error 15 | } 16 | -------------------------------------------------------------------------------- /modules/dbmodels/agent_perform.go: -------------------------------------------------------------------------------- 1 | package dbmodels 2 | 3 | import "github.com/gogather/json" 4 | 5 | type AgentPerform struct { 6 | Id uint `json:"id"` 7 | Timestamp uint64 `json:"timestamp"` 8 | ClientId string `json:"client_id"` 9 | } 10 | 11 | func (ap *AgentPerform) String() string { 12 | c, _ := json.Marshal(ap) 13 | return string(c) 14 | } 15 | -------------------------------------------------------------------------------- /modules/dbmodels/image_meta.go: -------------------------------------------------------------------------------- 1 | package dbmodels 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/datatypes" 7 | ) 8 | 9 | type ImageMeta struct { 10 | UUID 11 | 12 | FileID string `json:"file_id" gorm:"index"` 13 | Meta datatypes.JSON `json:"meta"` 14 | Metas datatypes.JSON `json:"metas"` 15 | 16 | CreatedAt time.Time `json:"created_at"` 17 | } 18 | -------------------------------------------------------------------------------- /service/message/model/msg_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestMsg(t *testing.T) { 9 | m := Msg{ 10 | Type: 0, 11 | Cmd: 0, 12 | ClientId: "", 13 | Data: nil, 14 | } 15 | 16 | assert.Equal(t, m.String(), `{"client_id":"","cmd":0,"data":"[]","type":0}`) 17 | } 18 | -------------------------------------------------------------------------------- /modules/dbmodels/face_label.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020. All rights reserved. 2 | // This file is part of duguying project 3 | // I am coding in Tencent 4 | // Created by rainesli on 2020/5/8. 5 | 6 | package dbmodels 7 | 8 | import "time" 9 | 10 | type FaceLabel struct { 11 | UUID 12 | 13 | Label string `json:"label"` 14 | CreatedAt time.Time `json:"created_at"` 15 | } 16 | -------------------------------------------------------------------------------- /modules/db/cover.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "duguying/studio/modules/dbmodels" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | // ListCover 列出封面 10 | func ListCover(tx *gorm.DB) (covers []*dbmodels.Cover, err error) { 11 | err = tx.Model(dbmodels.Cover{}).Order("created_at desc").Find(&covers).Error 12 | if err != nil { 13 | return nil, err 14 | } 15 | return covers, nil 16 | } 17 | -------------------------------------------------------------------------------- /service/message/deal/cycle.go: -------------------------------------------------------------------------------- 1 | package deal 2 | 3 | import ( 4 | "duguying/studio/service/message/pipe" 5 | "log" 6 | ) 7 | 8 | func Start() { 9 | go func() { 10 | for { 11 | select { 12 | case msg := <-pipe.In: 13 | err := DealWithMessage(msg) 14 | if err != nil { 15 | log.Println("[ws] pipe deal with message error:", err) 16 | } 17 | } 18 | } 19 | }() 20 | } 21 | -------------------------------------------------------------------------------- /service/message/proto/cli.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package model; 3 | option go_package = "../model"; 4 | 5 | // cmd 4 6 | message CliCmd { 7 | enum Cmd { 8 | OPEN = 0; 9 | CLOSE = 1; 10 | RESIZE = 2; 11 | } 12 | 13 | Cmd cmd = 1; 14 | string session = 2; 15 | string request_id = 3; 16 | uint32 pid = 4; 17 | uint32 width = 5; 18 | uint32 height = 6; 19 | } -------------------------------------------------------------------------------- /service/models/cover.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Cover struct { 6 | ID string `json:"id"` 7 | FileID string `json:"file_id"` 8 | DayOfWeek string `json:"day_of_week"` 9 | SchedulerType int `json:"scheduler_type"` 10 | URL string `json:"url"` 11 | Title string `json:"title"` 12 | CreatedAt time.Time `json:"created_at"` 13 | } 14 | -------------------------------------------------------------------------------- /modules/db/trojan_users.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "duguying/studio/modules/dbmodels" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | // ListAllTrojanUsers 列举所有trojan帐号 10 | func ListAllTrojanUsers(tx *gorm.DB) (list []*dbmodels.TrojanUsers, err error) { 11 | list = []*dbmodels.TrojanUsers{} 12 | err = tx.Model(dbmodels.TrojanUsers{}).Find(&list).Error 13 | if err != nil { 14 | return nil, err 15 | } 16 | return list, nil 17 | } 18 | -------------------------------------------------------------------------------- /service/models/login_history.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type LoginHistory struct { 6 | ID string `json:"id"` 7 | UserID uint `json:"user_id"` 8 | SessionID string `json:"session_id"` 9 | IP string `json:"ip"` 10 | Area string `json:"area"` 11 | Expired bool `json:"expired"` 12 | LoginAt *time.Time `json:"login_at"` 13 | UserAgent string `json:"user_agent"` 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Studio [![Build Status](https://travis-ci.org/duguying/studio.svg?branch=refactor)](https://travis-ci.org/duguying/studio) 2 | 3 | my studio service, blog, album, ops, etc. 4 | 5 | ## About frontend 6 | 7 | this project does not contain ui, to installing ui please refer to [feblog](https://github.com/duguying/feblog). 8 | 9 | ## Build ## 10 | 11 | ```shell 12 | ./control.sh build 13 | ``` 14 | 15 | ## License ## 16 | 17 | MIT License 18 | -------------------------------------------------------------------------------- /studio.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Studio (Studio Service) 3 | After=syslog.target 4 | After=network.target 5 | After=mysqld.service 6 | #After=postgresql.service 7 | #After=memcached.service 8 | After=redis.service 9 | 10 | [Service] 11 | Type=simple 12 | User=app 13 | Group=app 14 | WorkingDirectory=/home/app/studio 15 | ExecStart=/home/app/studio/studio 16 | Restart=always 17 | Environment=USER=app HOME=/home/app 18 | 19 | [Install] 20 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - tip 4 | go_import_path: duguying/studio 5 | script: 6 | - "./control build" 7 | - "./control pack" 8 | deploy: 9 | provider: releases 10 | api_key: 11 | secure: EnG9EYcN7kOLUszyd5risNMMz9kmTCbpw1faoPfJ07FpTR1vgtekTCfoZaz6OuQc2457sd+KtY0veRBaaaRsXs81yqr4FNfo1y6i3TtHF3K8XCpDvvPNUkCtuDAuWyidaOt1m8jxwZIjJs3NK3G8kiTOW2Tk4isv7RGpcq5yhxM= 12 | file_glob: true 13 | file: dist/* 14 | on: 15 | repo: duguying/studio 16 | tags: true 17 | -------------------------------------------------------------------------------- /modules/dbmodels/face.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020. All rights reserved. 2 | // This file is part of duguying project 3 | // I am coding in Tencent 4 | // Created by rainesli on 2020/5/8. 5 | 6 | package dbmodels 7 | 8 | import "time" 9 | 10 | type Face struct { 11 | UUID 12 | 13 | FileId uint `json:"file_id"` 14 | FaceDescriptor string `json:"face_descriptor" gorm:"type:longtext"` 15 | LabelId uint `json:"label_id"` 16 | CreatedAt time.Time `json:"created_at"` 17 | } 18 | -------------------------------------------------------------------------------- /modules/dns/dns.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019. All rights reserved. 2 | // This file is part of duguying project 3 | // I am coding in Tencent 4 | // Created by rainesli on 2020/4/19. 5 | 6 | package dns 7 | 8 | const ( 9 | ARecord = "A" 10 | AAAARecord = "AAAA" 11 | CnameRecord = "CNAME" 12 | TxtRecord = "TXT" 13 | ) 14 | 15 | type Dns interface { 16 | AddDomainRecord(domainName string, recordType string, record string, value string) (recordId string, err error) 17 | DeleteDomainRecord(recordId string) (err error) 18 | } 19 | -------------------------------------------------------------------------------- /modules/dbmodels/varify.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2017/11/2. 4 | 5 | package dbmodels 6 | 7 | import ( 8 | "github.com/gogather/json" 9 | "time" 10 | ) 11 | 12 | type Varify struct { 13 | Id uint `json:"id"` 14 | Username string `json:"username"` 15 | Code string `json:"code"` 16 | Overdue time.Time `json:"overdue"` 17 | } 18 | 19 | func (v *Varify) String() string { 20 | c, _ := json.Marshal(v) 21 | return string(c) 22 | } 23 | -------------------------------------------------------------------------------- /modules/rlog/rlog_test.go: -------------------------------------------------------------------------------- 1 | package rlog 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/google/uuid" 9 | ) 10 | 11 | func TestRLog(t *testing.T) { 12 | id := uuid.New().String() 13 | rl, err := NewEsAdaptor("http://jump.duguying.net:19200", "test") 14 | if err != nil { 15 | fmt.Println(err) 16 | } 17 | entity := map[string]interface{}{ 18 | "name": "rex", 19 | "age": 32, 20 | "phone": 123456, 21 | "uuid": id, 22 | } 23 | line, _ := json.Marshal(entity) 24 | rl.Report(string(line)) 25 | } 26 | -------------------------------------------------------------------------------- /modules/ipip/init.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of duguying project 3 | // Created by duguying on 2018/6/20. 4 | 5 | package ipip 6 | 7 | import ( 8 | "github.com/ipipdotnet/ipdb-go" 9 | "log" 10 | ) 11 | 12 | var ( 13 | db *ipdb.City 14 | ) 15 | 16 | func InitIPIP(path string) { 17 | var err error 18 | db, err = ipdb.NewCity(path) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | } 23 | 24 | func GetLocation(ip string) (location *ipdb.CityInfo, err error) { 25 | return db.FindInfo(ip,"CN") 26 | } 27 | -------------------------------------------------------------------------------- /modules/dbmodels/calendar.go: -------------------------------------------------------------------------------- 1 | // Package dbmodels 包注释 2 | package dbmodels 3 | 4 | import "time" 5 | 6 | type Calendar struct { 7 | UUID 8 | 9 | Date time.Time `json:"date" gorm:"index"` 10 | Period time.Duration `json:"period" gorm:"index"` 11 | Summary string `json:"summary" gorm:"type:text"` 12 | Address string `json:"address" gorm:"type:text"` 13 | Description string `json:"description" gorm:"type:text"` 14 | Link string `json:"link"` 15 | Attendee string `json:"attendee"` 16 | } 17 | -------------------------------------------------------------------------------- /modules/db/image_meta.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "duguying/studio/modules/dbmodels" 5 | 6 | "gorm.io/datatypes" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // AddImageMeta 添加图片 meta 信息 11 | func AddImageMeta(tx *gorm.DB, fileID string, meta, metas string) (added *dbmodels.ImageMeta, err error) { 12 | added = &dbmodels.ImageMeta{ 13 | FileID: fileID, 14 | Meta: datatypes.JSON(meta), 15 | Metas: datatypes.JSON(metas), 16 | } 17 | err = tx.Model(dbmodels.ImageMeta{}).Create(added).Error 18 | if err != nil { 19 | return nil, err 20 | } 21 | return added, nil 22 | } 23 | -------------------------------------------------------------------------------- /service/action/version.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "duguying/studio/g" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | ) 9 | 10 | func Version(c *gin.Context) { 11 | c.JSON(http.StatusOK, map[string]interface{}{ 12 | "version": g.Version, 13 | "git_version": g.GitVersion, 14 | "build_time": g.BuildTime, 15 | }) 16 | return 17 | } 18 | 19 | func PageTest(c *gin.Context) { 20 | fmt.Println("hi") 21 | c.HTML(http.StatusOK, "test", gin.H{}) 22 | } 23 | 24 | func PageTest1(c *gin.Context) { 25 | fmt.Println("hi") 26 | c.HTML(http.StatusOK, "about/index", gin.H{}) 27 | } 28 | -------------------------------------------------------------------------------- /service/models/agent.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Agent struct { 6 | ID uint `json:"id"` 7 | Online uint `json:"online"` 8 | ClientID string `json:"client_id"` 9 | OS string `json:"os"` 10 | Arch string `json:"arch"` 11 | Hostname string `json:"hostname"` 12 | IP string `json:"ip"` 13 | Area string `json:"area"` 14 | IPIns string `json:"ip_ins"` 15 | Status uint `json:"status"` 16 | OnlineTime time.Time `json:"online_time"` 17 | OfflineTime time.Time `json:"offline_time"` 18 | } 19 | -------------------------------------------------------------------------------- /modules/logger/log.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/gogather/logger" 10 | ) 11 | 12 | var gl *logger.GroupLogger 13 | 14 | func InitLogger(dir string, expire time.Duration, level int) { 15 | logSlice := []string{} 16 | gl = logger.NewGroupLogger(dir, "studio", expire, logSlice, log.Ldate|log.Ltime|log.Lshortfile, level) 17 | } 18 | 19 | func L(group string) *logger.Logger { 20 | return gl.L(group) 21 | } 22 | 23 | func GinLogger(logPath string) (io.Writer, error) { 24 | return os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 25 | } 26 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: default 3 | 4 | steps: 5 | - name: build 6 | image: golang:1.18.3 7 | commands: 8 | - mkdir -p $(go env GOPATH)/src/duguying 9 | - ln -s $(pwd) $(go env GOPATH)/src/duguying/studio 10 | - ./control ptag 11 | - ./control build 12 | - ./control prebuild 13 | - ./control dtag 14 | 15 | - name: docker 16 | image: plugins/docker 17 | settings: 18 | repo: git.duguying.net/duguying/studio 19 | registry: git.duguying.net 20 | username: 21 | from_secret: git_docker_username 22 | password: 23 | from_secret: git_docker_password 24 | when: 25 | event: 26 | - tag -------------------------------------------------------------------------------- /modules/dbmodels/project.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2017/11/2. 4 | 5 | package dbmodels 6 | 7 | import ( 8 | "github.com/gogather/json" 9 | "time" 10 | ) 11 | 12 | type Project struct { 13 | Id uint `json:"id"` 14 | Name string `json:"name"` 15 | IconUrl string `json:"icon_url"` 16 | Author string `json:"author"` 17 | Description string `json:"description"` 18 | Time time.Time `json:"time"` 19 | } 20 | 21 | func (p *Project) String() string { 22 | c, _ := json.Marshal(p) 23 | return string(c) 24 | } 25 | -------------------------------------------------------------------------------- /g/global.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2017/11/2. 4 | 5 | package g 6 | 7 | import ( 8 | "duguying/studio/modules/cache" 9 | "duguying/studio/modules/configuration" 10 | 11 | "github.com/blevesearch/bleve/v2" 12 | "github.com/sirupsen/logrus" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | var ( 17 | Config *configuration.Config 18 | Db *gorm.DB 19 | GfwDb *gorm.DB 20 | Cache cache.Cache 21 | Index bleve.Index 22 | P2pAddr string 23 | LogEntry *logrus.Entry 24 | 25 | InstallMode bool = false 26 | 27 | Version = "0.0" 28 | GitVersion = "00000000" 29 | BuildTime = "2000-01-01T00:00:00+0800" 30 | ) 31 | -------------------------------------------------------------------------------- /modules/dbmodels/trojan_users.go: -------------------------------------------------------------------------------- 1 | package dbmodels 2 | 3 | import "duguying/studio/service/models" 4 | 5 | type TrojanUsers struct { 6 | ID uint `json:"id"` 7 | Username string `json:"username" gorm:"unique"` 8 | Password string `json:"password"` 9 | Quota int64 `json:"quota"` 10 | Download int64 `json:"download"` 11 | Upload int64 `json:"upload"` 12 | } 13 | 14 | func (tu *TrojanUsers) TableName() string { 15 | return "users" 16 | } 17 | 18 | func (tu *TrojanUsers) ToModel() *models.TrojanUsers { 19 | return &models.TrojanUsers{ 20 | ID: tu.ID, 21 | Username: tu.Username, 22 | Quota: tu.Quota, 23 | Download: tu.Download, 24 | Upload: tu.Upload, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /service/message/pipe/connect.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017. All rights reserved. 2 | // This file is part of im project 3 | // Created by duguying on 2017/9/29. 4 | 5 | package pipe 6 | 7 | import ( 8 | "github.com/gogather/safemap" 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | var conns *safemap.SafeMap 13 | 14 | func GetConnMap() *safemap.SafeMap { 15 | return conns 16 | } 17 | 18 | func AddConnect(connId string, conn *websocket.Conn) { 19 | conns.Put(connId, conn) 20 | } 21 | 22 | func RemoveConnect(connId string) { 23 | conns.Remove(connId) 24 | } 25 | 26 | func GetConnect(connId string) (*websocket.Conn, bool) { 27 | connect, exist := conns.Get(connId) 28 | return connect.(*websocket.Conn), exist 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # bin 2 | *.exe 3 | blog 4 | studio 5 | 6 | # swap 7 | *~ 8 | 9 | # tmp 10 | /tmp/ 11 | install.lock 12 | 13 | # deps 14 | # /static/ueditor/ 15 | /static/upload/ 16 | 17 | # debug 18 | /debug/ 19 | 20 | # config 21 | /custom/app.conf 22 | 23 | # gopm 24 | /.vendor/ 25 | 26 | # log 27 | output 28 | *.log 29 | 30 | # idea 31 | .idea/ 32 | *.iml 33 | 34 | # mac 35 | .DS_Store 36 | 37 | # release 38 | release/ 39 | 40 | # nohup 41 | nohup.out 42 | 43 | # var 44 | var/ 45 | 46 | release.zip 47 | 48 | # ini config file 49 | *.ini 50 | 51 | dist/ 52 | *.zip 53 | 54 | *.db 55 | *.lock 56 | 57 | /store/ 58 | 59 | .vscode/ 60 | 61 | dockerdist/ 62 | 63 | bleve/ 64 | 65 | .tags 66 | 67 | cache/ 68 | log/ 69 | gen/ 70 | -------------------------------------------------------------------------------- /modules/dbmodels/cover.go: -------------------------------------------------------------------------------- 1 | package dbmodels 2 | 3 | import ( 4 | "duguying/studio/service/models" 5 | "time" 6 | ) 7 | 8 | const ( 9 | SchedulerTypeDefault = 0 10 | SchedulerTypeDay = 1 11 | ) 12 | 13 | type Cover struct { 14 | UUID 15 | 16 | FileID string `json:"file_id"` 17 | DayOfWeek string `json:"day_of_week"` 18 | SchedulerType int `json:"scheduler_type"` 19 | Title string `json:"title"` 20 | CreatedAt time.Time `json:"created_at"` 21 | } 22 | 23 | func (c *Cover) ToModel() *models.Cover { 24 | return &models.Cover{ 25 | ID: c.ID, 26 | FileID: c.FileID, 27 | DayOfWeek: c.DayOfWeek, 28 | SchedulerType: c.SchedulerType, 29 | Title: c.Title, 30 | CreatedAt: c.CreatedAt, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /service/models/api_log.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // APILog api日志 9 | type APILog struct { 10 | ID uint `json:"id,omitempty"` 11 | Method string `json:"method"` 12 | URI string `json:"uri"` 13 | Query string `json:"query"` 14 | Body string `json:"body"` 15 | Ok bool `json:"ok"` 16 | Response string `json:"response"` 17 | ClientIP string `json:"client_ip"` 18 | RequestID string `json:"request_id"` 19 | Cost string `json:"cost"` 20 | CreatedAt time.Time `json:"created_at"` 21 | } 22 | 23 | func (al *APILog) ToMap() map[string]interface{} { 24 | obj := map[string]interface{}{} 25 | c, _ := json.Marshal(al) 26 | _ = json.Unmarshal(c, &obj) 27 | return obj 28 | } 29 | -------------------------------------------------------------------------------- /modules/cache/cache.go: -------------------------------------------------------------------------------- 1 | // Package cache 缓存 2 | package cache 3 | 4 | import ( 5 | "time" 6 | ) 7 | 8 | type CacheRedisOption struct { 9 | Timeout int 10 | DB int 11 | Addr string 12 | Password string 13 | PoolSize int 14 | } 15 | 16 | type CacheOption struct { 17 | Type string 18 | Redis *CacheRedisOption 19 | BoltPath string 20 | } 21 | 22 | // Cache 缓存接口 23 | type Cache interface { 24 | SetTTL(key string, value string, ttl time.Duration) error 25 | Set(key string, value string) error 26 | Get(key string) (string, error) 27 | Delete(key string) error 28 | } 29 | 30 | // Init 初始化 31 | func Init(option *CacheOption) Cache { 32 | var cacheCli Cache 33 | if option.Type == "redis" { 34 | cacheCli = NewRedisCache(option.Redis) 35 | } else { 36 | cacheCli = NewBoltCache(option.BoltPath) 37 | } 38 | return cacheCli 39 | } 40 | -------------------------------------------------------------------------------- /modules/viewcnt/view.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020. All rights reserved. 2 | // This file is part of duguying project 3 | // I am coding in Tencent 4 | // Created by rainesli on 2020/3/16. 5 | 6 | package viewcnt 7 | 8 | import ( 9 | "github.com/gogather/safemap" 10 | ) 11 | 12 | var ( 13 | viewCntMap = safemap.New() 14 | ) 15 | 16 | func ViewHit(ident string) { 17 | val, ok := viewCntMap.Get(ident) 18 | if !ok { 19 | viewCntMap.Put(ident, int(1)) 20 | } else { 21 | cnt := val.(int) 22 | viewCntMap.Put(ident, cnt+1) 23 | } 24 | } 25 | 26 | func GetViewCnt(ident string) (cnt int) { 27 | val, ok := viewCntMap.Get(ident) 28 | if ok { 29 | return val.(int) 30 | } else { 31 | return 0 32 | } 33 | } 34 | 35 | func ResetViewCnt(ident string) { 36 | viewCntMap.Remove(ident) 37 | } 38 | 39 | func GetMap() *safemap.SafeMap { 40 | return viewCntMap 41 | } 42 | -------------------------------------------------------------------------------- /modules/dns/dns_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2018/5/18. 4 | 5 | package dns 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestAddDomainRecord(t *testing.T) { 15 | client, err := NewAliDns("bw4tirpW2iUODxRI", "uqoJmlNeeUnoBfPJda6OaSj1pLpTPD") 16 | if err != nil { 17 | log.Println("client failed, err:", err.Error()) 18 | return 19 | } 20 | recordId, err := client.AddDomainRecord("duguying.net", ARecord, "rpi", "127.0.0.1") 21 | if err != nil { 22 | log.Println("add failed, err:", err.Error()) 23 | return 24 | } 25 | fmt.Println("success, recordId:", recordId) 26 | 27 | time.Sleep(time.Minute * 10) 28 | 29 | err = client.DeleteDomainRecord(recordId) 30 | if err != nil { 31 | log.Println("delete failed, err:", err.Error()) 32 | return 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /service/message/pipe/cli.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of duguying project 3 | // Created by duguying on 2018/6/13. 4 | 5 | package pipe 6 | 7 | import ( 8 | "fmt" 9 | "github.com/gogather/d2" 10 | ) 11 | 12 | var ( 13 | d2map = d2.NewD2() 14 | ) 15 | 16 | type ChanPair struct { 17 | ChanIn chan []byte 18 | ChanOut chan []byte 19 | } 20 | 21 | func NewCliChanPair() (pair *ChanPair) { 22 | return &ChanPair{ 23 | ChanIn: make(chan []byte, 10000), 24 | ChanOut: make(chan []byte, 10000), 25 | } 26 | } 27 | 28 | func SetCliChanPair(session string, pid uint32, pair *ChanPair) { 29 | d2map.Add(session, fmt.Sprintf("%d", pid), pair) 30 | } 31 | 32 | func GetCliChanPair(session string, pid uint32) (pair *ChanPair, exist bool) { 33 | val, exist := d2map.Get(session, fmt.Sprintf("%d", pid)) 34 | if exist { 35 | pair = val.(*ChanPair) 36 | } 37 | return pair, exist 38 | } 39 | -------------------------------------------------------------------------------- /modules/storage/s3_oss.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | type S3Oss struct { 4 | } 5 | 6 | func (s *S3Oss) List(remotePrefix string) (list []*FileInfo, err error) { 7 | //TODO implement me 8 | panic("implement me") 9 | } 10 | 11 | func (s *S3Oss) GetFileInfo(remotePath string) (info *FileInfo, err error) { 12 | //TODO implement me 13 | panic("implement me") 14 | } 15 | 16 | func (s *S3Oss) AddFile(localPath string, remotePath string) (err error) { 17 | //TODO implement me 18 | panic("implement me") 19 | } 20 | 21 | func (s *S3Oss) RenameFile(remotePath string, newRemotePath string) (err error) { 22 | //TODO implement me 23 | panic("implement me") 24 | } 25 | 26 | func (s *S3Oss) RemoveFile(remotePath string) (err error) { 27 | //TODO implement me 28 | panic("implement me") 29 | } 30 | 31 | func (s *S3Oss) FetchFile(remotePath string, localPath string) (err error) { 32 | //TODO implement me 33 | panic("implement me") 34 | } 35 | 36 | -------------------------------------------------------------------------------- /service/message/store/agent.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of duguying project 3 | // Created by duguying on 2018/6/29. 4 | 5 | package store 6 | 7 | import ( 8 | "duguying/studio/g" 9 | "duguying/studio/modules/db" 10 | "duguying/studio/service/message/model" 11 | "log" 12 | 13 | "github.com/golang/protobuf/proto" 14 | ) 15 | 16 | func PutPerf(clientId string, timestamp uint64, value []byte) error { 17 | perf := &model.PerformanceMonitor{} 18 | err := proto.Unmarshal(value, perf) 19 | if err != nil { 20 | log.Println("proto unmarshal failed, err:", err.Error()) 21 | } else { 22 | ips := []string{} 23 | for _, network := range perf.Nets { 24 | ips = append(ips, network.Ip) 25 | } 26 | err = db.PutPerf(g.Db, clientId, perf.Os, perf.Arch, perf.Hostname, ips) 27 | if err != nil { 28 | log.Println("put agent failed, err:", err.Error()) 29 | } 30 | } 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /modules/dbmodels/login_history.go: -------------------------------------------------------------------------------- 1 | package dbmodels 2 | 3 | import ( 4 | "duguying/studio/modules/ipip" 5 | "duguying/studio/service/models" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | type LoginHistory struct { 11 | UUID 12 | 13 | UserID uint `json:"user_id"` 14 | SessionID string `json:"session_id"` 15 | IP string `json:"ip"` 16 | LoginAt *time.Time `json:"login_at"` 17 | UserAgent string `json:"user_agent"` 18 | } 19 | 20 | func (lh *LoginHistory) ToModel() *models.LoginHistory { 21 | area := "" 22 | loc, err := ipip.GetLocation(lh.IP) 23 | if err != nil { 24 | area = "未知" 25 | } else { 26 | area = fmt.Sprintf("%s,%s,%s", loc.CountryName, loc.RegionName, loc.CityName) 27 | } 28 | 29 | return &models.LoginHistory{ 30 | ID: lh.ID, 31 | UserID: lh.UserID, 32 | SessionID: lh.SessionID, 33 | IP: lh.IP, 34 | Area: area, 35 | LoginAt: lh.LoginAt, 36 | UserAgent: lh.UserAgent, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /service/models/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019. All rights reserved. 2 | // This file is part of duguying project 3 | // I am coding in Tencent 4 | // Created by rainesli on 2019/8/27. 5 | 6 | // Package models 接口模型 7 | package models 8 | 9 | import "time" 10 | 11 | type UserInfo struct { 12 | ID uint `json:"id"` 13 | Username string `json:"username"` 14 | Email string `json:"email"` 15 | Avatar string `json:"avatar"` 16 | Access string `json:"access"` 17 | CreatedAt time.Time `json:"created_at"` 18 | } 19 | 20 | type LoginArgs struct { 21 | Username string `json:"username"` 22 | Password string `json:"password"` 23 | } 24 | 25 | type RegisterArgs struct { 26 | Username string `json:"username"` 27 | Password string `json:"password"` 28 | Email string `json:"email"` 29 | Phone string `json:"phone"` 30 | } 31 | 32 | type ChangePasswordRequest struct { 33 | Username string `json:"username"` 34 | OldPassword string `json:"old_password"` 35 | NewPassword string `json:"new_password"` 36 | } 37 | -------------------------------------------------------------------------------- /service/message/deal/hb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of duguying project 3 | // Created by duguying on 2018/6/7. 4 | 5 | package deal 6 | 7 | import ( 8 | "duguying/studio/service/message/model" 9 | "duguying/studio/service/message/pipe" 10 | "github.com/golang/protobuf/proto" 11 | "github.com/gorilla/websocket" 12 | "log" 13 | "time" 14 | ) 15 | 16 | func InitHb() { 17 | go func() { 18 | for { 19 | sendHb() 20 | time.Sleep(time.Second * 30) 21 | } 22 | }() 23 | } 24 | 25 | func sendHb() { 26 | cm := pipe.GetConMap() 27 | if cm == nil { 28 | return 29 | } 30 | 31 | hbp := &model.HeartBeat{ 32 | Timestamp: uint64(time.Now().Unix()), 33 | } 34 | packet, err := proto.Marshal(hbp) 35 | if err != nil { 36 | log.Println("marshal proto failed, err:", err.Error()) 37 | return 38 | } 39 | 40 | for clientId, _ := range cm.M { 41 | pipe.SendMsg(clientId, model.Msg{ 42 | Type: websocket.BinaryMessage, 43 | Cmd: model.CMD_HB, 44 | Data: packet, 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /utils/ics.go: -------------------------------------------------------------------------------- 1 | // Package utils 包注释 2 | package utils 3 | 4 | import ( 5 | "fmt" 6 | "time" 7 | 8 | ics "github.com/arran4/golang-ical" 9 | ) 10 | 11 | // GenerateICS 生成日历事件 12 | func GenerateICS(id string, date, end time.Time, period time.Duration, 13 | summary, address, description, link, attendee string) string { 14 | cal := ics.NewCalendar() 15 | cal.SetMethod(ics.MethodRequest) 16 | event := cal.AddEvent(fmt.Sprintf("%s@ics.duguying.net", id)) 17 | event.SetCreatedTime(time.Now()) 18 | event.SetDtStampTime(date) 19 | event.SetModifiedAt(time.Now()) 20 | event.SetStartAt(date) 21 | event.SetEndAt(date.Add(period)) 22 | event.SetSummary(summary) 23 | event.SetLocation(address) 24 | event.SetDescription(description) 25 | if link != "" { 26 | event.SetURL(link) 27 | } 28 | event.SetOrganizer("studio@ics.duguying.net", ics.WithCN("This Machine")) 29 | event.AddAttendee(attendee, 30 | ics.CalendarUserTypeIndividual, ics.ParticipationStatusNeedsAction, 31 | ics.ParticipationRoleReqParticipant, ics.WithRSVP(true), 32 | ) 33 | return cal.Serialize() 34 | } 35 | -------------------------------------------------------------------------------- /service/message/model/performance_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of duguying project 3 | // Created by duguying on 2018/6/7. 4 | 5 | package model 6 | 7 | import ( 8 | "fmt" 9 | "github.com/golang/protobuf/proto" 10 | "log" 11 | "testing" 12 | ) 13 | 14 | func TestPerformanceMonitor(t *testing.T) { 15 | perf := &PerformanceMonitor{ 16 | Mem: &PerformanceMonitor_Memory{ 17 | TotalMem: 1024, 18 | UsedMem: 824, 19 | FreeMem: 200, 20 | ActualUsed: 100, 21 | ActualFree: 200, 22 | TotalSwap: 100, 23 | UsedSwap: 20, 24 | FreeSwap: 80, 25 | }, 26 | } 27 | data, err := proto.Marshal(perf) 28 | if err != nil { 29 | log.Fatal("marshaling error: ", err) 30 | } 31 | fmt.Println(data) 32 | newTest := &PerformanceMonitor{} 33 | err = proto.Unmarshal(data, newTest) 34 | if err != nil { 35 | log.Fatal("unmarshaling error: ", err) 36 | } 37 | // Now test and newTest contain the same data. 38 | if perf.GetMem() != newTest.GetMem() { 39 | log.Fatalf("data mismatch %q != %q", perf.GetMem(), newTest.GetMem()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gogather/blackfriday/v2" 8 | "github.com/unknwon/com" 9 | ) 10 | 11 | func TestGenUUID(t *testing.T) { 12 | test := "/art/1+1=2" 13 | fmt.Println(com.UrlEncode(test)) 14 | } 15 | 16 | func markdownFull(input []byte) []byte { 17 | // set up the HTML renderer 18 | renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{ 19 | Flags: blackfriday.CommonHTMLFlags, 20 | Extensions: blackfriday.CommonExtensions | blackfriday.LaTeXMath, 21 | }) 22 | options := blackfriday.Options{ 23 | Extensions: blackfriday.CommonExtensions | blackfriday.LaTeXMath, 24 | } 25 | return blackfriday.Markdown(input, renderer, options) 26 | } 27 | 28 | func TestParseMath(t *testing.T) { 29 | content := `asdfa$放一$串中文就移位了sdf$$123$$dfgdf$$skdfjhkds$$ sdfs$$ 30 | 31 | test 32 | 33 | $$ 34 | a=b+c 35 | $$ 36 | 37 | ` + 38 | "```go\n" + 39 | "var a = 1;\n" + 40 | "```" 41 | 42 | content = string(markdownFull([]byte(content))) 43 | 44 | // out := ParseMath(content) 45 | fmt.Println(content) 46 | } 47 | -------------------------------------------------------------------------------- /service/middleware/crossite.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "duguying/studio/g" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func ServerMark() gin.HandlerFunc { 12 | return func(c *gin.Context) { 13 | c.Writer.Header().Set("Server", fmt.Sprintf("duguying.net - %s", g.GitVersion)) 14 | c.Next() 15 | } 16 | } 17 | 18 | func CrossSite() gin.HandlerFunc { 19 | return func(c *gin.Context) { 20 | origin := c.Request.Header.Get("Origin") 21 | c.Writer.Header().Set("Vary", "Origin") 22 | c.Writer.Header().Set("Access-Control-Allow-Origin", origin) 23 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 24 | c.Writer.Header().Set("Access-Control-Max-Age", "600") 25 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE") 26 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, X-CSRF-TOKEN, X-Token") 27 | 28 | if c.Request.Method == "OPTIONS" { 29 | c.Status(http.StatusNoContent) 30 | c.Abort() 31 | } else { 32 | c.Next() 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /service/models/time.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019. All rights reserved. 2 | // This file is part of sparta-admin project 3 | // I am coding in Tencent 4 | // Created by rainesli on 2019/3/18. 5 | 6 | package models 7 | 8 | import ( 9 | "github.com/json-iterator/go" 10 | "time" 11 | "unsafe" 12 | ) 13 | 14 | func RegisterTimeAsLayoutCodec(layout string) { 15 | jsoniter.RegisterTypeEncoder("time.Time", &timeAsString{layout: layout}) 16 | jsoniter.RegisterTypeDecoder("time.Time", &timeAsString{layout: layout}) 17 | } 18 | 19 | type timeAsString struct { 20 | layout string 21 | } 22 | 23 | func (codec *timeAsString) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) { 24 | tm, err := time.Parse(codec.layout, iter.ReadString()) 25 | if err != nil { 26 | return 27 | } 28 | *((*time.Time)(ptr)) = tm 29 | } 30 | 31 | func (codec *timeAsString) IsEmpty(ptr unsafe.Pointer) bool { 32 | ts := *((*time.Time)(ptr)) 33 | return ts.UnixNano() == 0 34 | } 35 | 36 | func (codec *timeAsString) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) { 37 | ts := *((*time.Time)(ptr)) 38 | op := ts.Format(codec.layout) 39 | stream.WriteString(op) 40 | } 41 | -------------------------------------------------------------------------------- /service/message/pipe/pipeline.go: -------------------------------------------------------------------------------- 1 | package pipe 2 | 3 | import ( 4 | "duguying/studio/service/message/model" 5 | "github.com/gogather/safemap" 6 | "log" 7 | ) 8 | 9 | type ClientPipe struct { 10 | clientId string 11 | out chan model.Msg 12 | } 13 | 14 | var In chan model.Msg 15 | var pm *safemap.SafeMap // [clientId] -> ClientPipe 16 | var cm *safemap.SafeMap // [clientId] -> connId 17 | 18 | func InitPipeline() { 19 | In = make(chan model.Msg, 100) 20 | pm = safemap.New() 21 | cm = safemap.New() 22 | conns = safemap.New() 23 | } 24 | 25 | func AddUserPipe(clientId string, out chan model.Msg, connId string) { 26 | log.Printf("注册设备 ID:%s\n", clientId) 27 | pm.Put(clientId, &ClientPipe{ 28 | clientId: clientId, 29 | out: out, 30 | }) 31 | cm.Put(clientId, connId) 32 | } 33 | 34 | func RemoveUserPipe(clientId string) { 35 | pm.Remove(clientId) 36 | cm.Remove(clientId) 37 | } 38 | 39 | func SendMsg(clientId string, msg model.Msg) (success bool) { 40 | iCli, exist := pm.Get(clientId) 41 | if !exist { 42 | return false 43 | } 44 | cli := iCli.(*ClientPipe) 45 | cli.out <- msg 46 | return true 47 | } 48 | 49 | func GetConMap() *safemap.SafeMap { 50 | return cm 51 | } 52 | -------------------------------------------------------------------------------- /modules/db/login_history.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "duguying/studio/g" 5 | "duguying/studio/modules/dbmodels" 6 | "duguying/studio/modules/session" 7 | 8 | "gorm.io/gorm" 9 | ) 10 | 11 | // AddLoginHistory 添加登陆历史 12 | func AddLoginHistory(tx *gorm.DB, sessionID string, entity *session.Entity) error { 13 | hist := &dbmodels.LoginHistory{ 14 | UserID: entity.UserID, 15 | SessionID: sessionID, 16 | IP: entity.IP, 17 | LoginAt: &entity.LoginAt, 18 | UserAgent: entity.UserAgent, 19 | } 20 | return tx.Model(dbmodels.LoginHistory{}).Create(hist).Error 21 | } 22 | 23 | // PageLoginHistoryByUserID 按用户列举登陆历史 24 | func PageLoginHistoryByUserID(tx *gorm.DB, userID uint, page uint, pageSize uint) (list []*dbmodels.LoginHistory, total int64, err error) { 25 | total = 0 26 | list = []*dbmodels.LoginHistory{} 27 | 28 | err = tx.Model(dbmodels.LoginHistory{}).Where("user_id=?", userID).Count(&total).Error 29 | if err != nil { 30 | return nil, 0, err 31 | } 32 | 33 | err = g.Db.Model(dbmodels.LoginHistory{}).Where("user_id=?", userID).Order("login_at desc"). 34 | Offset(int((page - 1) * pageSize)).Limit(int(pageSize)).Find(&list).Error 35 | if err != nil { 36 | return nil, 0, err 37 | } 38 | return list, total, nil 39 | } 40 | -------------------------------------------------------------------------------- /modules/cron/file.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "duguying/studio/g" 5 | "duguying/studio/modules/db" 6 | "duguying/studio/modules/imgtools" 7 | "duguying/studio/utils" 8 | "log" 9 | "time" 10 | ) 11 | 12 | func scanFile() error { 13 | files, err := db.ListAllMediaFile(g.Db, 0) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | for _, file := range files { 19 | time.Sleep(time.Second) 20 | 21 | localPath := utils.GetFileLocalPath(file.Path) 22 | if file.MediaWidth <= 0 || file.MediaHeight <= 0 { 23 | width, height, err := imgtools.GetImgSize(localPath) 24 | if err != nil { 25 | log.Println("获取文件尺寸失败, err:", err.Error(), "localPath:", localPath) 26 | continue 27 | } 28 | err = db.UpdateFileMediaSize(g.Db, file.ID, int(width), int(height)) 29 | if err != nil { 30 | log.Println("更新文件尺寸失败, err:", err.Error()) 31 | continue 32 | } 33 | } 34 | 35 | if file.Thumbnail == "" { 36 | thumbKey, err := imgtools.MakeThumbnail(localPath, 220) 37 | if err != nil { 38 | log.Println("制作缩略图失败, err:", err.Error()) 39 | continue 40 | } 41 | err = db.UpdateFileThumbneil(g.Db, file.ID, thumbKey) 42 | if err != nil { 43 | log.Println("更新缩略图失败, err:", err.Error()) 44 | continue 45 | } 46 | } 47 | } 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /modules/cache/redis_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017. All rights reserved. 2 | // This file is part of ofs project 3 | // Created by duguying on 2017/11/29. 4 | 5 | package cache 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | "sort" 11 | "testing" 12 | "time" 13 | 14 | "gopkg.in/redis.v5" 15 | ) 16 | 17 | var ( 18 | redisCli *redis.Client 19 | ) 20 | 21 | func initRedis() { 22 | readTimeout := 4 23 | db := 2 24 | redisCli = redis.NewClient(&redis.Options{ 25 | Addr: "127.0.0.1:6379", 26 | Password: "", 27 | DB: db, 28 | PoolSize: 10000, 29 | ReadTimeout: time.Duration(time.Second * time.Duration(readTimeout)), 30 | }) 31 | err := redisCli.Ping().Err() 32 | 33 | if err != nil { 34 | log.Println("[system]", err.Error()) 35 | } else { 36 | log.Println("[system]", "redis connect success") 37 | } 38 | } 39 | 40 | func TestSetTTL(t *testing.T) { 41 | initRedis() 42 | 43 | //err := SetMapField("hi", "hello", "1244") 44 | //fmt.Println(err) 45 | // 46 | //fmt.Println(GetMap("hi")) 47 | //DelMapField("hi","hello") 48 | 49 | redisCli.Set("hi", "hello", 0) 50 | fmt.Println(redisCli.Get("hi")) 51 | } 52 | 53 | func TestGet(t *testing.T) { 54 | args := []string{"casdf", "badfadf", "basd", "a"} 55 | fmt.Println(args) 56 | 57 | sort.Strings(args) 58 | fmt.Println(args) 59 | } 60 | -------------------------------------------------------------------------------- /modules/dbmodels/agent.go: -------------------------------------------------------------------------------- 1 | package dbmodels 2 | 3 | import ( 4 | "duguying/studio/service/models" 5 | "time" 6 | 7 | "github.com/gogather/json" 8 | ) 9 | 10 | type Agent struct { 11 | ID uint `json:"id"` 12 | Online uint `json:"online" gorm:"index"` // 1 online, 0 offline 13 | ClientID string `json:"client_id" gorm:"unique;not null"` 14 | Os string `json:"os" gorm:"index"` 15 | Arch string `json:"arch" gorm:"index"` 16 | Hostname string `json:"hostname" gorm:"index"` 17 | IP string `json:"ip" gorm:"index"` 18 | IPIns string `json:"ip_ins" gorm:"index:,class:FULLTEXT"` // json 19 | Status uint `json:"status" gorm:"index"` 20 | OnlineTime time.Time `json:"online_time"` 21 | OfflineTime time.Time `json:"offline_time"` 22 | } 23 | 24 | func (a *Agent) String() string { 25 | c, _ := json.Marshal(a) 26 | return string(c) 27 | } 28 | 29 | func (a *Agent) ToModel() *models.Agent { 30 | return &models.Agent{ 31 | ID: a.ID, 32 | Online: a.Online, 33 | ClientID: a.ClientID, 34 | OS: a.Os, 35 | Arch: a.Arch, 36 | Hostname: a.Hostname, 37 | IP: a.IP, 38 | IPIns: a.IPIns, 39 | Status: a.Status, 40 | OnlineTime: a.OnlineTime, 41 | OfflineTime: a.OfflineTime, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /service/message/pipe/cli_session.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of duguying project 3 | // Created by duguying on 2018/6/13. 4 | 5 | package pipe 6 | 7 | import ( 8 | "github.com/gogather/d2" 9 | "github.com/gorilla/websocket" 10 | "fmt" 11 | ) 12 | 13 | var ( 14 | cliSession = d2.NewD2() 15 | conSession = d2.NewD2() 16 | ) 17 | 18 | func SetCliPid(clientId string, reqId string, pid uint32) { 19 | cliSession.Add(clientId, reqId, pid) 20 | } 21 | 22 | func GetCliPid(clientId string, reqId string) (pid uint32, exist bool) { 23 | val, exist := cliSession.Get(clientId, reqId) 24 | if exist { 25 | pid = val.(uint32) 26 | } 27 | return pid, exist 28 | } 29 | 30 | func DelCliPid(clientId string, reqId string) { 31 | cliSession.RemoveKey(clientId, reqId) 32 | } 33 | 34 | // -------- 35 | 36 | func SetPidCon(clientId string, pid uint32, conn *websocket.Conn) { 37 | conSession.Add(clientId, fmt.Sprintf("%d",pid), conn) 38 | } 39 | 40 | func GetPidCon(clientId string, pid uint32) (conn *websocket.Conn, exist bool) { 41 | val, exist := conSession.Get(clientId, fmt.Sprintf("%d",pid)) 42 | if exist { 43 | conn = val.(*websocket.Conn) 44 | } 45 | return conn, exist 46 | } 47 | 48 | func DelPidCon(clientId string, pid uint32) { 49 | conSession.RemoveKey(clientId, fmt.Sprintf("%d", pid)) 50 | } 51 | -------------------------------------------------------------------------------- /modules/storage/aliyun_oss.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019. All rights reserved. 2 | // This file is part of duguying project 3 | // I am coding in Tencent 4 | // Created by rainesli on 2020/4/11. 5 | 6 | package storage 7 | 8 | type AliyunOss struct { 9 | ak string 10 | sk string 11 | bucket string 12 | } 13 | 14 | func NewAliyunOss(ak string, sk string, bucket string) (storage *AliyunOss, err error) { 15 | storage = &AliyunOss{ 16 | ak: ak, 17 | sk: sk, 18 | bucket: bucket, 19 | } 20 | return storage, nil 21 | } 22 | 23 | func (AliyunOss) List(remotePrefix string) (list []*FileInfo, err error) { 24 | panic("implement me") 25 | } 26 | 27 | func (AliyunOss) GetFileInfo(remotePath string) (info *FileInfo, err error) { 28 | panic("implement me") 29 | } 30 | 31 | func (AliyunOss) IsExist(remotePath string) (exist bool, err error) { 32 | panic("implement me") 33 | } 34 | 35 | func (AliyunOss) PutFile(localPath string, remotePath string) (err error) { 36 | panic("implement me") 37 | } 38 | 39 | func (AliyunOss) RenameFile(remotePath string, newRemotePath string) (err error) { 40 | panic("implement me") 41 | } 42 | 43 | func (AliyunOss) RemoveFile(remotePath string) (err error) { 44 | panic("implement me") 45 | } 46 | 47 | func (AliyunOss) FetchFile(remotePath string, localPath string) (err error) { 48 | panic("implement me") 49 | } 50 | -------------------------------------------------------------------------------- /utils/lunar.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/nosixtools/solarlunar" 10 | ) 11 | 12 | // Lunar 农历 13 | type Lunar struct { 14 | Year int 15 | Month int 16 | Day int 17 | Leap bool 18 | } 19 | 20 | // NewLunar 创建农历日期 21 | func NewLunar(date string, leap bool) Lunar { 22 | segs := strings.Split(date, "-") 23 | year, month, day := 0, 0, 0 24 | if len(segs) >= 3 { 25 | dayI64, _ := strconv.ParseInt(segs[2], 10, 32) 26 | day = int(dayI64) 27 | } 28 | if len(segs) >= 2 { 29 | monthI64, _ := strconv.ParseInt(segs[1], 10, 32) 30 | month = int(monthI64) 31 | } 32 | if len(segs) >= 1 { 33 | yearI64, _ := strconv.ParseInt(segs[0], 10, 32) 34 | year = int(yearI64) 35 | } 36 | return Lunar{ 37 | Year: year, 38 | Month: month, 39 | Day: day, 40 | Leap: leap, 41 | } 42 | } 43 | 44 | func (l Lunar) String() string { 45 | return fmt.Sprintf("%04d-%02d-%02d", l.Year, l.Month, l.Day) 46 | } 47 | 48 | // SolarToLunar 阳历转农历 49 | func SolarToLunar(date time.Time) Lunar { 50 | lunarDate, leap := solarlunar.SolarToLuanr(date.Format("2006-01-02")) 51 | return NewLunar(lunarDate, leap) 52 | } 53 | 54 | // LunarToSolar 农历转阳历 55 | func LunarToSolar(date Lunar) time.Time { 56 | solarDate := solarlunar.LunarToSolar(date.String(), date.Leap) 57 | solar, _ := time.Parse("2006-01-02", solarDate) 58 | return solar 59 | } 60 | -------------------------------------------------------------------------------- /modules/db/calendar.go: -------------------------------------------------------------------------------- 1 | // Package db 包注释 2 | package db 3 | 4 | import ( 5 | "duguying/studio/modules/dbmodels" 6 | "time" 7 | 8 | "gorm.io/gorm" 9 | ) 10 | 11 | // AddCalendar 添加日志事件 12 | func AddCalendar(tx *gorm.DB, date time.Time, 13 | summary, address, description, link, attendee string) (added *dbmodels.Calendar, err error) { 14 | added = &dbmodels.Calendar{ 15 | Date: date, 16 | Summary: summary, 17 | Address: address, 18 | Description: description, 19 | Link: link, 20 | Attendee: attendee, 21 | } 22 | err = tx.Model(dbmodels.Calendar{}).Create(added).Error 23 | if err != nil { 24 | return nil, err 25 | } 26 | return added, nil 27 | } 28 | 29 | // ListAllCalendarIds 列举所有未发送的日历事件表 30 | func ListAllCalendarIds(tx *gorm.DB) (ids []string, err error) { 31 | list := []*dbmodels.Calendar{} 32 | err = tx.Model(dbmodels.Calendar{}).Select("id").Where("send_at is NULL").Find(&list).Error 33 | if err != nil { 34 | return nil, err 35 | } 36 | ids = []string{} 37 | for _, calendar := range list { 38 | ids = append(ids, calendar.ID) 39 | } 40 | return ids, nil 41 | } 42 | 43 | // GetCalendarByID 按ID获取日历 44 | func GetCalendarByID(tx *gorm.DB, id string) (calendar *dbmodels.Calendar, err error) { 45 | calendar = &dbmodels.Calendar{} 46 | err = tx.Model(dbmodels.Calendar{}).Where("id=?", id).First(calendar).Error 47 | if err != nil { 48 | return nil, err 49 | } 50 | return calendar, nil 51 | } 52 | -------------------------------------------------------------------------------- /modules/dns/alidns.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2018/5/18. 4 | 5 | package dns 6 | 7 | import ( 8 | "fmt" 9 | "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns" 10 | ) 11 | 12 | type AliDns struct { 13 | ak string 14 | sk string 15 | client *alidns.Client 16 | } 17 | 18 | func NewAliDns(ak string, sk string) (cli *AliDns, err error) { 19 | client, err := alidns.NewClient() 20 | if err != nil { 21 | return nil, err 22 | } 23 | return &AliDns{ 24 | ak: ak, 25 | sk: sk, 26 | client: client, 27 | }, nil 28 | } 29 | 30 | func (ad *AliDns) AddDomainRecord(domainName string, recordType string, record string, value string) (recordId string, err error) { 31 | response, err := ad.client.AddDomainRecord(&alidns.AddDomainRecordRequest{ 32 | RR: record, 33 | Type: recordType, 34 | Value: value, 35 | DomainName: domainName, 36 | }) 37 | if err != nil { 38 | return "", err 39 | } 40 | if !response.IsSuccess() { 41 | return "", fmt.Errorf("添加失败") 42 | } 43 | return response.RecordId, nil 44 | } 45 | 46 | func (ad *AliDns) DeleteDomainRecord(recordId string) (err error) { 47 | response, err := ad.client.DeleteDomainRecord(&alidns.DeleteDomainRecordRequest{ 48 | RecordId: recordId, 49 | }) 50 | if err != nil { 51 | return err 52 | } 53 | if !response.IsSuccess() { 54 | return fmt.Errorf("删除失败") 55 | } 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /modules/rlog/num_hook.go: -------------------------------------------------------------------------------- 1 | package rlog 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type lineNumberHook struct { 13 | callerShortPath bool 14 | } 15 | 16 | // Levels 返回所有级别 17 | func (lnh *lineNumberHook) Levels() []logrus.Level { 18 | return logrus.AllLevels 19 | } 20 | 21 | // Fire 触发 22 | func (lnh *lineNumberHook) Fire(entry *logrus.Entry) error { 23 | _, file, line, ok := runtime.Caller(8) 24 | if ok { 25 | cl := file 26 | if lnh.callerShortPath { 27 | cl = lnh.shortenPath(file) 28 | } 29 | entry.Data["file"] = fmt.Sprintf("%s:%d", cl, line) 30 | } 31 | return nil 32 | } 33 | 34 | // SetShortPath 设置短路径 35 | func (lnh *lineNumberHook) SetShortPath(short bool) { 36 | lnh.callerShortPath = short 37 | } 38 | 39 | func (lnh *lineNumberHook) shortenPath(path string) (shortPath string) { 40 | hasRoot := strings.HasPrefix(path, "/") 41 | path = strings.TrimPrefix(path, "/") 42 | sep := fmt.Sprintf("%c", filepath.Separator) 43 | segs := strings.Split(path, sep) 44 | if len(segs) <= 1 { 45 | return path 46 | } 47 | length := len(segs) 48 | shortSegs := make([]string, length) 49 | last := segs[length-1] 50 | for i, seg := range segs { 51 | if i == length-1 { 52 | continue 53 | } 54 | shortSegs[i] = fmt.Sprintf("%c", seg[0]) 55 | } 56 | shortSegs[length-1] = last 57 | shortPath = strings.Join(shortSegs, sep) 58 | if hasRoot { 59 | shortPath = sep + shortPath 60 | } 61 | return shortPath 62 | } 63 | -------------------------------------------------------------------------------- /modules/rlog/es.go: -------------------------------------------------------------------------------- 1 | // Package rlog 日志适配器 2 | package rlog 3 | 4 | import ( 5 | "context" 6 | "strings" 7 | 8 | "github.com/elastic/go-elasticsearch/v7" 9 | "github.com/elastic/go-elasticsearch/v7/esapi" 10 | ) 11 | 12 | type EsAdaptor struct { 13 | client *elasticsearch.Client 14 | addr string 15 | index string 16 | } 17 | 18 | func NewEsAdaptor(addr string, index string) (adaptor *EsAdaptor, err error) { 19 | ea := &EsAdaptor{ 20 | addr: addr, 21 | index: index, 22 | } 23 | err = ea.init() 24 | if err != nil { 25 | return nil, err 26 | } 27 | return ea, nil 28 | } 29 | 30 | func (ea *EsAdaptor) init() error { 31 | // 初始化 ES 32 | esConf := elasticsearch.Config{ 33 | Addresses: []string{ea.addr}, 34 | } 35 | es, err := elasticsearch.NewClient(esConf) 36 | if err != nil { 37 | return err 38 | } 39 | es.Info() 40 | ea.client = es 41 | return nil 42 | } 43 | 44 | func (ea *EsAdaptor) Close() { 45 | 46 | } 47 | 48 | func (ea *EsAdaptor) Report(line string) error { 49 | req := esapi.IndexRequest{ 50 | Index: ea.index, 51 | Body: strings.NewReader(line), 52 | Refresh: "true", 53 | } 54 | 55 | resp, err := req.Do(context.Background(), ea.client) 56 | if err != nil { 57 | // log.Printf("ESIndexRequestErr: %s", err.Error()) 58 | return err 59 | } 60 | 61 | defer resp.Body.Close() 62 | if resp.IsError() { 63 | return err 64 | // log.Printf("ESIndexRequestErr: %s", resp.String()) 65 | } else { 66 | return nil 67 | // log.Printf("ESIndexRequestOk: %s", resp.String()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /service/message/model/msg.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of duguying project 3 | // Created by duguying on 2018/6/7. 4 | 5 | package model 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | 11 | "github.com/gogather/json" 12 | "github.com/gorilla/websocket" 13 | ) 14 | 15 | const ( 16 | CMD_HB = 0 17 | CMD_PERF = 1 18 | CMD_KEY = 2 19 | CMD_CLI_PIPE = 3 20 | CMD_CLI_CMD = 4 21 | 22 | TERM_PIPE = 0x00 23 | TERM_SIZE = 0x01 24 | TERM_PING = 0x02 25 | TERM_PONG = 0x03 26 | ) 27 | 28 | type Msg struct { 29 | Type int `json:"type"` 30 | Cmd int `json:"cmd"` 31 | ClientId string `json:"client_id"` 32 | Data []byte `json:"data"` 33 | } 34 | 35 | func (m *Msg) String() string { 36 | ds := map[string]interface{}{ 37 | "type": m.Type, 38 | "cmd": m.Cmd, 39 | "client_id": m.ClientId, 40 | } 41 | if m.Type == websocket.TextMessage { 42 | ds["data"] = fmt.Sprintf("%s", string(m.Data)) 43 | } else { 44 | ds["data"] = fmt.Sprintf("%v", m.Data) 45 | } 46 | c, err := json.Marshal(ds) 47 | if err != nil { 48 | log.Println("json marshal failed, err:", err.Error()) 49 | } 50 | return string(c) 51 | } 52 | 53 | func (m *Msg) Info() string { 54 | ds := map[string]interface{}{ 55 | "type": m.Type, 56 | "cmd": m.Cmd, 57 | "client_id": m.ClientId, 58 | "_data_len": len(m.Data), 59 | } 60 | c, err := json.Marshal(ds) 61 | if err != nil { 62 | log.Println("json marshal failed, err:", err.Error()) 63 | } 64 | return string(c) 65 | } 66 | -------------------------------------------------------------------------------- /modules/dbmodels/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2017/11/2. 4 | 5 | package dbmodels 6 | 7 | import ( 8 | "duguying/studio/g" 9 | "duguying/studio/service/models" 10 | "duguying/studio/utils" 11 | "time" 12 | 13 | "github.com/gogather/json" 14 | ) 15 | 16 | const ( 17 | RoleUser = 0 18 | RoleAdmin = 1 19 | ) 20 | 21 | var role = []string{"user", "admin"} 22 | 23 | type User struct { 24 | ID uint `json:"id"` 25 | Username string `json:"username" gorm:"index"` 26 | Password string `json:"password"` 27 | Salt string `json:"salt"` 28 | Email string `json:"email"` 29 | Role int `json:"role" gorm:"default:0"` 30 | TfaSecret string `json:"tfa_secret"` // 2FA secret base 32 31 | AvatarFileID string `json:"vatar_file_id" gorm:"comment:'图像文件ID';index"` 32 | AvatarFileKey string `json:"avatar_file_key" gorm:"comment:'图像文件路径'"` 33 | CreatedAt time.Time `json:"created_at"` 34 | } 35 | 36 | func (u *User) String() string { 37 | c, _ := json.Marshal(u) 38 | return string(c) 39 | } 40 | 41 | func (u *User) ToInfo() *models.UserInfo { 42 | host := g.Config.Get("system", "host", "http://duguying.net") 43 | avatar := host + "/logo.png" 44 | if u.AvatarFileKey != "" { 45 | avatar = utils.GetFileURL(u.AvatarFileKey) 46 | } 47 | return &models.UserInfo{ 48 | ID: u.ID, 49 | Username: u.Username, 50 | Email: u.Email, 51 | Avatar: avatar, 52 | Access: role[u.Role], 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /utils/math_lexer.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | ) 7 | 8 | type Token int 9 | 10 | const ( 11 | EOF = iota 12 | 13 | MATH // $$ 14 | ) 15 | 16 | var tokens = []string{ 17 | EOF: "EOF", 18 | MATH: "$$", 19 | } 20 | 21 | func (t Token) String() string { 22 | return tokens[t] 23 | } 24 | 25 | type Lexer struct { 26 | prev rune 27 | start int 28 | pos int 29 | reader *bufio.Reader 30 | } 31 | 32 | func NewLexer(reader io.Reader) *Lexer { 33 | return &Lexer{ 34 | reader: bufio.NewReader(reader), 35 | } 36 | } 37 | 38 | func (l *Lexer) Lex() (int, int, Token) { 39 | // keep looping until we return a token 40 | defer func() { 41 | l.start = l.pos 42 | }() 43 | out := "" 44 | for { 45 | // … 46 | // update the column to the position of the newly read in rune 47 | 48 | r, _, err := l.reader.ReadRune() 49 | if err != nil { 50 | if err == io.EOF { 51 | return l.start, l.pos, EOF 52 | } 53 | 54 | // at this point there isn't much we can do, and the compiler 55 | // should just return the raw error to the user 56 | panic(err) 57 | } 58 | 59 | switch r { 60 | case '$': 61 | { 62 | if l.prev == 0 { 63 | out = out + string(r) 64 | l.prev = r 65 | l.pos++ 66 | continue 67 | } else { 68 | if l.prev == '$' { 69 | l.prev = r 70 | l.pos++ 71 | return l.start, l.pos - 2, MATH 72 | } else { 73 | l.prev = r 74 | l.pos++ 75 | } 76 | } 77 | } 78 | default: 79 | out = out + string(r) 80 | l.prev = r 81 | l.pos++ 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /modules/session/session.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2018/5/18. 4 | 5 | // Package session 会话管理 6 | package session 7 | 8 | import ( 9 | "duguying/studio/g" 10 | "duguying/studio/modules/cache" 11 | "duguying/studio/utils" 12 | "time" 13 | 14 | "github.com/gogather/json" 15 | ) 16 | 17 | type Entity struct { 18 | UserID uint `json:"user_id"` 19 | IP string `json:"ip"` 20 | LoginAt time.Time `json:"login_at"` 21 | UserAgent string `json:"user_agent"` 22 | } 23 | 24 | func (se *Entity) String() string { 25 | c, _ := json.Marshal(se) 26 | return string(c) 27 | } 28 | 29 | func SessionID() string { 30 | guid := utils.GenUUID() 31 | return guid 32 | } 33 | 34 | func SessionSet(sessionID string, ttl time.Duration, entity *Entity) { 35 | g.Cache.SetTTL(cache.SESS+sessionID, entity.String(), ttl) 36 | } 37 | 38 | // SessionPut 设置 session ,不设置 ttl 39 | func SessionPut(sessionID string, entity *Entity) { 40 | g.Cache.Set(cache.SESS+sessionID, entity.String()) 41 | } 42 | 43 | func SessionDel(sessionID string) { 44 | g.Cache.Delete(cache.SESS + sessionID) 45 | } 46 | 47 | func SessionGet(sessionID string) (entity *Entity) { 48 | value, err := g.Cache.Get(cache.SESS + sessionID) 49 | if err != nil { 50 | // log.Println("get session from cache failed, err:", err.Error()) 51 | return nil 52 | } else { 53 | entity = &Entity{} 54 | err = json.Unmarshal([]byte(value), entity) 55 | if err != nil { 56 | // log.Println("unmarshal session entity failed, err:", err.Error()) 57 | return nil 58 | } else { 59 | return entity 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /service/models/file.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type File struct { 6 | ID string `json:"id"` 7 | Filename string `json:"filename"` 8 | Path string `json:"path"` 9 | Store int64 `json:"store"` 10 | Mime string `json:"mime"` 11 | Size uint64 `json:"size"` 12 | FileType int64 `json:"file_type"` 13 | Md5 string `json:"md5"` 14 | Recognized int64 `json:"recognized"` 15 | LocalExist bool `json:"local_exist"` 16 | ArticleRefCount int `json:"article_ref_count"` 17 | CoverRefCount int `json:"cover_ref_count"` 18 | COS bool `json:"cos"` 19 | UserID uint `json:"user_id"` 20 | MediaWidth uint64 `json:"media_width"` 21 | MediaHeight uint64 `json:"media_height"` 22 | CreatedAt time.Time `json:"created_at"` 23 | } 24 | 25 | type MediaFile struct { 26 | ID string `json:"id"` 27 | Filename string `json:"filename"` 28 | URL string `json:"url"` 29 | Mime string `json:"mime"` 30 | Size uint64 `json:"size"` 31 | FileType string `json:"file_type" ` 32 | Md5 string `json:"md5"` 33 | UserID uint `json:"user_id"` 34 | Width uint64 `json:"width"` 35 | Height uint64 `json:"height"` 36 | ThumbnailURL string `json:"thumbnail"` 37 | CreatedAt time.Time `json:"created_at"` 38 | } 39 | 40 | const ( 41 | FileType = "file" 42 | DirType = "dir" 43 | ) 44 | 45 | // FsItem 文件元素,文件或目录 46 | type FsItem struct { 47 | Type string `json:"type"` 48 | Name string `json:"name"` 49 | } 50 | -------------------------------------------------------------------------------- /service/action/album.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "duguying/studio/g" 5 | "duguying/studio/modules/db" 6 | "duguying/studio/service/models" 7 | ) 8 | 9 | func ListAlbumFiles(c *CustomContext) (interface{}, error) { 10 | userID := c.UserID() 11 | files, err := db.ListAllMediaFile(g.Db, userID) 12 | if err != nil { 13 | return nil, err 14 | } 15 | apiFiles := []*models.MediaFile{} 16 | for _, file := range files { 17 | apiFiles = append(apiFiles, file.ToMediaFile()) 18 | } 19 | return models.ListMediaFileResponse{ 20 | Ok: true, 21 | List: apiFiles, 22 | }, nil 23 | } 24 | 25 | func MediaDetail(c *CustomContext) (interface{}, error) { 26 | req := models.StringGetter{} 27 | err := c.BindQuery(&req) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | file, err := db.GetFile(g.Db, req.ID) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return models.MediaDetailResponse{ 38 | Ok: true, 39 | Data: file.ToMediaFile(), 40 | }, nil 41 | } 42 | 43 | // ListCover 列举博客封面 44 | // @Router /admin/cover/list [get] 45 | // @Tags 文章 46 | // @Description 列举博客封面 47 | // @Success 200 {object} models.CoverListResponse 48 | func ListCover(c *CustomContext) (interface{}, error) { 49 | covers, err := db.ListCover(g.Db) 50 | if err != nil { 51 | return nil, err 52 | } 53 | coverApis := []*models.Cover{} 54 | for _, cover := range covers { 55 | c := cover.ToModel() 56 | file, err := db.GetFile(g.Db, c.FileID) 57 | if err != nil { 58 | continue 59 | } 60 | c.URL = file.ToMediaFile().URL 61 | coverApis = append(coverApis, c) 62 | } 63 | return &models.CoverListResponse{ 64 | Ok: true, 65 | List: coverApis, 66 | }, nil 67 | } 68 | -------------------------------------------------------------------------------- /modules/cache/redis.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "gopkg.in/redis.v5" 8 | ) 9 | 10 | type RedisCache struct { 11 | cli *redis.Client 12 | } 13 | 14 | func NewRedisCache(cacheOption *CacheRedisOption) *RedisCache { 15 | 16 | redisCli := redis.NewClient(&redis.Options{ 17 | Addr: cacheOption.Addr, 18 | Password: cacheOption.Password, 19 | DB: cacheOption.DB, 20 | PoolSize: cacheOption.PoolSize, 21 | ReadTimeout: time.Duration(time.Second * time.Duration(cacheOption.Timeout)), 22 | }) 23 | err := redisCli.Ping().Err() 24 | 25 | if err != nil { 26 | log.Println("[system]", err.Error()) 27 | } else { 28 | log.Println("[system]", "redis connect success") 29 | } 30 | return &RedisCache{ 31 | cli: redisCli, 32 | } 33 | } 34 | 35 | const PREFIX = "blog:" 36 | const SESS = "session:" 37 | 38 | func (rc *RedisCache) Set(key, value string) error { 39 | return rc.SetTTL(key, value, 0) 40 | } 41 | 42 | func (rc *RedisCache) SetTTL(key, value string, ttl time.Duration) error { 43 | return rc.cli.Set(PREFIX+key, value, ttl).Err() 44 | } 45 | 46 | func (rc *RedisCache) Get(key string) (string, error) { 47 | return rc.cli.Get(PREFIX + key).Result() 48 | } 49 | 50 | func (rc *RedisCache) Delete(key string) error { 51 | return rc.cli.Del(PREFIX + key).Err() 52 | } 53 | 54 | func (rc *RedisCache) SetMapField(key, field string, value interface{}) error { 55 | return rc.cli.HSet(PREFIX+key, field, value).Err() 56 | } 57 | 58 | func (rc *RedisCache) DelMapField(key, field string) error { 59 | return rc.cli.HDel(PREFIX+key, field).Err() 60 | } 61 | 62 | func (rc *RedisCache) GetMap(key string) (map[string]string, error) { 63 | return rc.cli.HGetAll(PREFIX + key).Result() 64 | } 65 | -------------------------------------------------------------------------------- /service/middleware/session.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2018/5/18. 4 | 5 | // Package middleware 中间件 6 | package middleware 7 | 8 | import ( 9 | "duguying/studio/g" 10 | "duguying/studio/modules/session" 11 | "duguying/studio/service/models" 12 | "log" 13 | "net/http" 14 | 15 | "github.com/gin-gonic/gin" 16 | ) 17 | 18 | func SessionValidate(forbidAnonymous bool) func(c *gin.Context) { 19 | return func(c *gin.Context) { 20 | sid, err := c.Cookie("sid") 21 | if err != nil { 22 | sid = c.GetHeader("X-Token") 23 | } 24 | // websocket 连接,鉴权从 query 取 token 25 | if c.GetHeader("Upgrade") == "websocket" { 26 | sid, _ = c.GetQuery("token") 27 | } 28 | c.Set("sid", sid) 29 | sessionDomain := g.Config.Get("session", "domain", ".duguying.net") 30 | entity := session.SessionGet(sid) 31 | log.Println("sid:", sid, "entity:", entity) 32 | if entity == nil { 33 | c.SetCookie("sid", "", 0, "/", sessionDomain, true, false) 34 | if forbidAnonymous { 35 | c.JSON(http.StatusUnauthorized, models.CommonResponse{ 36 | Ok: false, 37 | Msg: "login first", 38 | }) 39 | c.Abort() 40 | return 41 | } else { 42 | c.Next() 43 | return 44 | } 45 | } else { 46 | log.Printf("the entity is: %s\n", entity.String()) 47 | } 48 | if entity.UserID <= 0 { 49 | c.SetCookie("sid", "", 0, "/", sessionDomain, true, false) 50 | session.SessionDel(sid) 51 | if forbidAnonymous { 52 | c.JSON(http.StatusUnauthorized, models.CommonResponse{ 53 | Ok: false, 54 | Msg: "invalid user", 55 | }) 56 | c.Abort() 57 | return 58 | } else { 59 | c.Next() 60 | return 61 | } 62 | } else { 63 | c.Set("user_id", int64(entity.UserID)) 64 | } 65 | c.Next() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /modules/storage/storage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019. All rights reserved. 2 | // This file is part of duguying project 3 | // I am coding in Tencent 4 | // Created by rainesli on 2020/4/11. 5 | 6 | package storage 7 | 8 | import ( 9 | "duguying/studio/g" 10 | "fmt" 11 | 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type FileInfo struct { 16 | Path string `json:"path"` 17 | Size int64 `json:"size"` 18 | Mime string `json:"mime"` 19 | } 20 | 21 | type Storage interface { 22 | List(remotePrefix string) (list []*FileInfo, err error) 23 | GetFileInfo(remotePath string) (info *FileInfo, err error) 24 | IsExist(remotePath string) (exist bool, err error) 25 | PutFile(localPath string, remotePath string) (err error) 26 | RenameFile(remotePath string, newRemotePath string) (err error) 27 | RemoveFile(remotePath string) (err error) 28 | FetchFile(remotePath string, localPath string) (err error) 29 | } 30 | 31 | var ( 32 | AliyunCosType = "aliyun" 33 | QcloudCosType = "qcloud" 34 | 35 | DefaultCosType = QcloudCosType 36 | ) 37 | 38 | // NewCos 创建 cos 实例 39 | func NewCos(l *logrus.Entry, cosType string) (Storage, error) { 40 | switch cosType { 41 | case AliyunCosType: 42 | { 43 | ak := g.Config.Get("aliyun-cos", "ak", "") 44 | sk := g.Config.Get("aliyun-cos", "sk", "") 45 | bucket := g.Config.Get("aliyun-cos", "bucket", "") 46 | return NewAliyunOss(ak, sk, bucket) 47 | } 48 | case QcloudCosType: 49 | { 50 | sid := g.Config.Get("qcloud-cos", "sid", "") 51 | skey := g.Config.Get("qcloud-cos", "skey", "") 52 | bucket := g.Config.Get("qcloud-cos", "bucket", "") 53 | region := g.Config.Get("qcloud-cos", "region", "") 54 | protocol := g.Config.Get("qcloud-cos", "protocol", "http") 55 | return NewQcloudOss(l, sid, skey, bucket, region, protocol) 56 | } 57 | default: 58 | { 59 | return nil, fmt.Errorf("不支持的云存储类型") 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /service/models/request.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019. All rights reserved. 2 | // This file is part of duguying project 3 | // I am coding in Tencent 4 | // Created by rainesli on 2019/8/29. 5 | 6 | package models 7 | 8 | type CommonPagerRequest struct { 9 | Page uint `json:"page" form:"page"` 10 | Size uint `json:"size" form:"size"` 11 | Status int `json:"status" form:"status"` 12 | } 13 | 14 | // type CommonGetterRequest struct { 15 | // Id uint `json:"id" form:"id"` 16 | // } 17 | 18 | type MonthlyPagerRequest struct { 19 | Page uint `json:"page" form:"page"` 20 | Size uint `json:"size" form:"size"` 21 | Year uint `json:"year" form:"year"` 22 | Month uint `json:"month" form:"month"` 23 | } 24 | 25 | type TopGetterRequest struct { 26 | Top uint `json:"top" form:"top"` 27 | } 28 | 29 | type ArticleUriGetterRequest struct { 30 | Uri string `json:"uri" form:"uri"` 31 | Id uint `json:"id" form:"id"` 32 | } 33 | 34 | type ArticlePublishRequest struct { 35 | Id uint `json:"id" form:"id"` 36 | Publish bool `json:"publish" form:"publish"` 37 | } 38 | 39 | type TagPagerRequest struct { 40 | Page uint `json:"page" form:"page"` 41 | Size uint `json:"size" form:"size"` 42 | Tag string `json:"tag" form:"tag"` 43 | } 44 | 45 | type SearchPagerRequest struct { 46 | Page uint `json:"page" form:"page"` 47 | Size uint `json:"size" form:"size"` 48 | Keyword string `json:"keyword" form:"keyword"` 49 | } 50 | 51 | type IntGetter struct { 52 | ID uint `json:"id" form:"id" binding:"required"` 53 | } 54 | 55 | type StringGetter struct { 56 | ID string `json:"id" form:"id" binding:"required"` 57 | } 58 | 59 | type UserIDGetter struct { 60 | UserID uint `json:"user_id" form:"user_id" binding:"required"` 61 | } 62 | 63 | type FileSyncRequest struct { 64 | FileID string `json:"file_id" form:"file_id"` 65 | CosType string `json:"cos_type" form:"cos_type"` 66 | } 67 | -------------------------------------------------------------------------------- /modules/db/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2018/5/18. 4 | 5 | package db 6 | 7 | import ( 8 | "duguying/studio/modules/dbmodels" 9 | 10 | "github.com/gogather/com" 11 | "github.com/google/uuid" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | func RegisterUser(tx *gorm.DB, username string, password string, email string) (user *dbmodels.User, err error) { 16 | salt := com.RandString(7) 17 | passwordEncrypt := com.Md5(password + salt) 18 | tfaSecret := com.RandString(10) 19 | user = &dbmodels.User{ 20 | Username: username, 21 | Password: passwordEncrypt, 22 | Salt: salt, 23 | Email: email, 24 | TfaSecret: tfaSecret, 25 | } 26 | err = tx.Table("users").Create(user).Error 27 | if err != nil { 28 | return nil, err 29 | } 30 | return user, nil 31 | } 32 | 33 | // UserChangePassword 修改密码 34 | func UserChangePassword(tx *gorm.DB, username string, newPassword string) (err error) { 35 | newSalt := com.Md5(uuid.New().String()) 36 | newPasswd := com.Md5(newPassword + newSalt) 37 | 38 | return tx.Model(dbmodels.User{}).Where("username=?", username).Updates(map[string]interface{}{ 39 | "salt": newSalt, 40 | "password": newPasswd, 41 | }).Error 42 | } 43 | 44 | func GetUser(tx *gorm.DB, username string) (user *dbmodels.User, err error) { 45 | user = &dbmodels.User{} 46 | err = tx.Table("users").Where("username=?", username).First(user).Error 47 | if err != nil { 48 | return nil, err 49 | } 50 | return user, nil 51 | } 52 | 53 | func GetUserByID(tx *gorm.DB, uid uint) (user *dbmodels.User, err error) { 54 | user = &dbmodels.User{} 55 | err = tx.Table("users").Where("id=?", uid).First(user).Error 56 | if err != nil { 57 | return nil, err 58 | } 59 | return user, nil 60 | } 61 | 62 | func CheckUsername(tx *gorm.DB, username string) (valid bool, err error) { 63 | count := int64(0) 64 | err = tx.Table("users").Where("username=?", username).Count(&count).Error 65 | if err != nil { 66 | return false, err 67 | } else { 68 | return count <= 0, nil 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /service/message/deal/message.go: -------------------------------------------------------------------------------- 1 | package deal 2 | 3 | import ( 4 | "duguying/studio/g" 5 | "duguying/studio/service/message/model" 6 | "duguying/studio/service/message/pipe" 7 | "duguying/studio/service/message/store" 8 | "log" 9 | "time" 10 | 11 | "github.com/golang/protobuf/proto" 12 | ) 13 | 14 | func DealWithMessage(rcvMsgPack model.Msg) (err error) { 15 | data := rcvMsgPack.Data 16 | switch rcvMsgPack.Cmd { 17 | case model.CMD_PERF: 18 | { 19 | err := store.PutPerf(rcvMsgPack.ClientId, uint64(time.Now().Unix()), data) 20 | if err != nil { 21 | log.Println("boltdb store data failed, err:", err.Error()) 22 | } 23 | return nil 24 | } 25 | case model.CMD_CLI_PIPE: 26 | { 27 | pipeData := model.CliPipe{} 28 | err := proto.Unmarshal(data, &pipeData) 29 | if err != nil { 30 | log.Println("parse pipe data failed, err:", err.Error()) 31 | return err 32 | } 33 | session := pipeData.Session 34 | pid := pipeData.Pid 35 | pair, exist := pipe.GetCliChanPair(session, pid) 36 | if exist { 37 | //log.Println("cli ---> xterm:", pipeData.Data) 38 | g.LogEntry.WithField("slice", "agentsnt").Printf("agent sent out: %d, equal expect: %v\n", 39 | pipeData.DataLen, pipeData.DataLen == uint32(len(pipeData.Data))) 40 | pair.ChanIn <- append([]byte{model.TERM_PIPE}, pipeData.Data...) 41 | } 42 | return nil 43 | } 44 | case model.CMD_CLI_CMD: 45 | { 46 | pcmd := model.CliCmd{} 47 | err := proto.Unmarshal(data, &pcmd) 48 | if err != nil { 49 | log.Println("parse pipe cmd data failed, err:", err.Error()) 50 | return err 51 | } 52 | if pcmd.Cmd == model.CliCmd_OPEN { 53 | pipe.SetCliPid(pcmd.Session, pcmd.RequestId, pcmd.Pid) 54 | } else if pcmd.Cmd == model.CliCmd_CLOSE { 55 | // 1. close ws 56 | ws, exist := pipe.GetPidCon(pcmd.Session, pcmd.Pid) 57 | if exist { 58 | ws.Close() 59 | pipe.DelPidCon(pcmd.Session, pcmd.Pid) 60 | } 61 | 62 | // 2. clear key 63 | pipe.DelCliPid(pcmd.Session, pcmd.RequestId) 64 | } 65 | return nil 66 | } 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /modules/bleve/bleve.go: -------------------------------------------------------------------------------- 1 | // Package bleve description 2 | package bleve 3 | 4 | import ( 5 | "duguying/studio/g" 6 | "duguying/studio/modules/cron" 7 | "log" 8 | "path/filepath" 9 | 10 | "github.com/blevesearch/bleve/v2" 11 | _ "github.com/blevesearch/bleve/v2/analysis/analyzer/custom" 12 | "github.com/gogather/com" 13 | _ "github.com/gogather/gojieba-bleve/v2" 14 | ) 15 | 16 | // IndexInstance 打开索引实例 17 | func IndexInstance(path string) (index bleve.Index, err error) { 18 | dictDir := g.Config.Get("bleve", "gojieba-dict", "bleve/gojieba") 19 | dictPath := filepath.Join(dictDir, "jieba.dict.utf8") 20 | hmmPath := filepath.Join(dictDir, "hmm_model.utf8") 21 | userDictPath := filepath.Join(dictDir, "user.dict.utf8") 22 | idfPath := filepath.Join(dictDir, "idf.utf8") 23 | stopWordsPath := filepath.Join(dictDir, "stop_words.utf8") 24 | indexMapping := bleve.NewIndexMapping() 25 | err = indexMapping.AddCustomTokenizer("gojieba", 26 | map[string]interface{}{ 27 | "dictpath": dictPath, 28 | "hmmpath": hmmPath, 29 | "userdictpath": userDictPath, 30 | "idf": idfPath, 31 | "stop_words": stopWordsPath, 32 | "type": "gojieba", 33 | }, 34 | ) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | err = indexMapping.AddCustomAnalyzer("gojieba", 40 | map[string]interface{}{ 41 | "type": "gojieba", 42 | "tokenizer": "gojieba", 43 | }, 44 | ) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | indexMapping.DefaultAnalyzer = "gojieba" 50 | 51 | exist := com.PathExist(path) 52 | if exist { 53 | index, err = bleve.Open(path) 54 | if err != nil { 55 | return nil, err 56 | } 57 | } else { 58 | // mapping := bleve.NewIndexMapping() 59 | index, err = bleve.New(path, indexMapping) 60 | if err != nil { 61 | return nil, err 62 | } 63 | } 64 | return index, nil 65 | } 66 | 67 | func Init() { 68 | var err error 69 | g.Index, err = IndexInstance("bleve/article") 70 | if err != nil { 71 | log.Fatalln("open bleve index failed, err:", err.Error()) 72 | return 73 | } 74 | 75 | cron.FlushArticleBleve() 76 | } 77 | -------------------------------------------------------------------------------- /utils/recover.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "runtime" 8 | ) 9 | 10 | var ( 11 | dunno = []byte("???") 12 | centerDot = []byte("·") 13 | dot = []byte(".") 14 | slash = []byte("/") 15 | ) 16 | 17 | // Stack 获取调用栈 18 | func Stack(skip int) []byte { 19 | buf := new(bytes.Buffer) // the returned data 20 | // As we loop, we open files and read them. These variables record the currently 21 | // loaded file. 22 | var lines [][]byte 23 | var lastFile string 24 | for i := skip; ; i++ { // Skip the expected number of frames 25 | pc, file, line, ok := runtime.Caller(i) 26 | if !ok { 27 | break 28 | } 29 | // Print this much at least. If we can't find the source, it won't show. 30 | fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc) 31 | if file != lastFile { 32 | data, err := ioutil.ReadFile(file) 33 | if err != nil { 34 | continue 35 | } 36 | lines = bytes.Split(data, []byte{'\n'}) 37 | lastFile = file 38 | } 39 | fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line)) 40 | } 41 | return buf.Bytes() 42 | } 43 | 44 | func source(lines [][]byte, n int) []byte { 45 | n-- // in stack trace, lines are 1-indexed but our array is 0-indexed 46 | if n < 0 || n >= len(lines) { 47 | return dunno 48 | } 49 | return bytes.TrimSpace(lines[n]) 50 | } 51 | 52 | // function returns, if possible, the name of the function containing the PC. 53 | func function(pc uintptr) []byte { 54 | fn := runtime.FuncForPC(pc) 55 | if fn == nil { 56 | return dunno 57 | } 58 | name := []byte(fn.Name()) 59 | // The name includes the path name to the package, which is unnecessary 60 | // since the file name is already included. Plus, it has center dots. 61 | // That is, we see 62 | // runtime/debug.*T·ptrmethod 63 | // and want 64 | // *T.ptrmethod 65 | // Also the package path might contains dot (e.g. code.google.com/...), 66 | // so first eliminate the path prefix 67 | if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 { 68 | name = name[lastslash+1:] 69 | } 70 | if period := bytes.Index(name, dot); period >= 0 { 71 | name = name[period+1:] 72 | } 73 | name = bytes.Replace(name, centerDot, dot, -1) 74 | return name 75 | } 76 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module duguying/studio 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 7 | github.com/aliyun/alibaba-cloud-sdk-go v1.61.1214 8 | github.com/arran4/golang-ical v0.0.0-20210807024147-770fa87aff1d 9 | github.com/blevesearch/bleve/v2 v2.3.2 10 | github.com/boltdb/bolt v1.3.1 11 | github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 12 | github.com/dogenzaka/rotator v0.0.0-20141104034428-97947bef5b93 13 | github.com/elastic/go-elasticsearch/v7 v7.17.1 14 | github.com/getsentry/raven-go v0.2.0 15 | github.com/gin-contrib/pprof v1.3.0 16 | github.com/gin-contrib/sentry v0.0.0-20191119142041-ff0e9556d1b7 17 | github.com/gin-gonic/gin v1.8.1 18 | github.com/go-errors/errors v1.4.2 19 | github.com/go-sql-driver/mysql v1.7.1 // indirect 20 | github.com/gogather/blackfriday/v2 v2.2.7 21 | github.com/gogather/cleaner v0.0.0-20190625151327-c9276b274332 // indirect 22 | github.com/gogather/com v1.0.0 23 | github.com/gogather/cron v1.1.0 24 | github.com/gogather/d2 v0.0.0-20170930025040-da424aa0003a 25 | github.com/gogather/gojieba-bleve/v2 v2.0.3 26 | github.com/gogather/json v0.0.0-20181103101242-a200f6ba6445 27 | github.com/gogather/logger v0.0.0-20200203043640-cf0bf9aa076e 28 | github.com/gogather/safemap v0.0.0-20170930074128-e4dc79c94fb6 29 | github.com/golang/protobuf v1.5.2 30 | github.com/google/go-querystring v1.1.0 // indirect 31 | github.com/google/uuid v1.3.0 32 | github.com/gorilla/websocket v1.4.2 33 | github.com/ipipdotnet/ipdb-go v1.3.1 34 | github.com/json-iterator/go v1.1.12 35 | github.com/martinlindhe/base36 v1.1.0 36 | github.com/microcosm-cc/bluemonday v1.0.15 37 | github.com/mitchellh/mapstructure v1.5.0 // indirect 38 | github.com/mozillazg/go-httpheader v0.3.1 // indirect 39 | github.com/nosixtools/solarlunar v0.0.0-20200711032723-669c9e27ecc5 40 | github.com/sirupsen/logrus v1.9.0 41 | github.com/stretchr/testify v1.8.2 42 | github.com/swaggo/gin-swagger v1.3.1 43 | github.com/swaggo/swag v1.8.5 44 | github.com/tencentyun/cos-go-sdk-v5 v0.7.38 45 | github.com/unknwon/com v1.0.1 46 | go.uber.org/automaxprocs v1.5.1 47 | golang.org/x/tools v0.11.0 // indirect 48 | google.golang.org/protobuf v1.28.0 49 | gopkg.in/ini.v1 v1.62.0 50 | gopkg.in/redis.v5 v5.2.9 51 | gorm.io/datatypes v1.2.0 52 | gorm.io/driver/mysql v1.5.1 53 | gorm.io/driver/sqlite v1.5.0 54 | gorm.io/gorm v1.25.2 55 | gorm.io/hints v1.1.2 // indirect 56 | gorm.io/plugin/dbresolver v1.4.1 // indirect 57 | rsc.io/qr v0.2.0 58 | ) 59 | -------------------------------------------------------------------------------- /service/gin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2017/11/2. 4 | 5 | // Package service 服务包 6 | package service 7 | 8 | import ( 9 | _ "duguying/studio/docs" 10 | "duguying/studio/g" 11 | "duguying/studio/modules/logger" 12 | "duguying/studio/service/action" 13 | "duguying/studio/service/action/agent" 14 | "duguying/studio/service/message/deal" 15 | "duguying/studio/service/message/pipe" 16 | "duguying/studio/service/middleware" 17 | "duguying/studio/service/models" 18 | "fmt" 19 | "path/filepath" 20 | 21 | "github.com/getsentry/raven-go" 22 | "github.com/gin-contrib/pprof" 23 | "github.com/gin-contrib/sentry" 24 | "github.com/gin-gonic/gin" 25 | ginSwagger "github.com/swaggo/gin-swagger" 26 | "github.com/swaggo/gin-swagger/swaggerFiles" 27 | ) 28 | 29 | func Run(logDir string) { 30 | models.RegisterTimeAsLayoutCodec("2006-01-02 15:04:05") 31 | gin.SetMode(g.Config.Get("system", "mode", gin.ReleaseMode)) 32 | gin.DefaultWriter, _ = logger.GinLogger(filepath.Join(logDir, "gin.log")) 33 | 34 | initWsMessage() 35 | 36 | router := gin.Default() 37 | router.Use(middleware.ServerMark()) 38 | router.Use(middleware.CrossSite()) 39 | router.Use(sentry.Recovery(raven.DefaultClient, false)) 40 | 41 | router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 42 | router.Any("/version", action.Version) 43 | 44 | // v1 api 45 | apiV1 := router.Group("/api/v1", middleware.RestLog()) 46 | { 47 | // needn't auth 48 | action.SetupFeAPI(apiV1) 49 | 50 | // auth require 51 | auth := apiV1.Group("/admin", middleware.SessionValidate(true)) 52 | action.SetupAdminAPI(auth) 53 | 54 | // agent connection point 55 | agt := apiV1.Group("/agent", middleware.SessionValidate(false)) 56 | action.SetupAgentAPI(agt) 57 | } 58 | 59 | // 兼容旧版 60 | api := router.Group("/api") 61 | { 62 | // 旧版 agent 连接点 63 | agt := api.Group("/agent") 64 | { 65 | agt.Any("/ws", agent.Ws) 66 | } 67 | 68 | // 静态站点部署器 69 | deployer := api.Group("/deploy", action.CheckToken) 70 | { 71 | deployer.POST("/upload", action.PackageUpload) 72 | deployer.POST("/archive", action.APIWrapper(action.UploadFile)) 73 | } 74 | } 75 | 76 | router.Static("/static/upload", g.Config.Get("upload", "dir", "upload")) 77 | 78 | // print http port 79 | addr := g.Config.Get("system", "listen", "127.0.0.1:9080") 80 | fmt.Printf("listen: %s\n", addr) 81 | 82 | pprof.Register(router) 83 | err := router.Run(addr) 84 | if err != nil { 85 | fmt.Println("run gin server failed, err:" + err.Error()) 86 | } 87 | } 88 | 89 | func initWsMessage() { 90 | pipe.InitPipeline() 91 | deal.Start() 92 | deal.InitHb() 93 | } 94 | -------------------------------------------------------------------------------- /service/action/agent/ws.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of duguying project 3 | // Created by duguying on 2018/6/7. 4 | 5 | package agent 6 | 7 | import ( 8 | "duguying/studio/g" 9 | "duguying/studio/modules/db" 10 | "duguying/studio/service/message/model" 11 | "duguying/studio/service/message/pipe" 12 | "log" 13 | "net/http" 14 | 15 | "github.com/gin-gonic/gin" 16 | "github.com/gogather/com" 17 | "github.com/gorilla/websocket" 18 | ) 19 | 20 | func Ws(c *gin.Context) { 21 | clientId := c.Query("client_id") 22 | 23 | if clientId == "" { 24 | c.JSON(http.StatusOK, gin.H{ 25 | "ok": false, 26 | "err": "client_id is required", 27 | }) 28 | return 29 | } 30 | 31 | log.Println("ws connect with client_id:", clientId) 32 | 33 | var upgrader = websocket.Upgrader{} 34 | upgrader.CheckOrigin = func(r *http.Request) bool { 35 | return true 36 | } 37 | conn, err := upgrader.Upgrade(c.Writer, c.Request, c.Writer.Header()) 38 | if err != nil { 39 | // 已经 upgrade 为 websocket,不能再按 http 写入 40 | log.Println("upgrade:", err) 41 | return 42 | } 43 | 44 | // client ip 45 | ip := c.ClientIP() 46 | 47 | // store connects 48 | connId := com.CreateGUID() 49 | pipe.AddConnect(connId, conn) 50 | 51 | defer conn.Close() 52 | out := make(chan model.Msg, 100) 53 | 54 | // register in and out channel 55 | pipe.AddUserPipe(clientId, out, connId) 56 | 57 | // store agent info 58 | _, err = db.CreateOrUpdateAgent(g.Db, clientId, ip) 59 | if err != nil { 60 | log.Println("put agent failed, err:", err.Error()) 61 | } 62 | 63 | // read from client, put into in channel 64 | go func(con *websocket.Conn) { 65 | for { 66 | var err error 67 | 68 | mt, msgData, err := con.ReadMessage() 69 | if err != nil { 70 | log.Println("read:", err) 71 | break 72 | } 73 | 74 | msg := model.Msg{ 75 | Type: mt, 76 | Cmd: int(msgData[0]), 77 | ClientId: clientId, 78 | Data: msgData[1:], 79 | } 80 | 81 | if g.Config.Get("ws", "log", "enable") == "enable" { 82 | log.Printf("recv: %s\n", msg.Info()) 83 | } 84 | 85 | pipe.In <- msg 86 | } 87 | }(conn) 88 | 89 | // write into client, get from out channel 90 | for { 91 | var err error 92 | var msg model.Msg 93 | 94 | msg = <-out 95 | //log.Println("send message:", msg.String()) 96 | 97 | err = conn.WriteMessage(msg.Type, append([]byte{byte(msg.Cmd)}, msg.Data...)) 98 | if err != nil { 99 | log.Println("即时消息发送到客户端:", err) 100 | break 101 | } 102 | } 103 | 104 | // exit websocket finally, and remove client pipeline 105 | pipe.RemoveUserPipe(clientId) 106 | pipe.RemoveConnect(connId) 107 | 108 | // update agent info 109 | err = db.UpdateAgentOffline(g.Db, clientId) 110 | if err != nil { 111 | log.Println("update agent offline failed, err:", err.Error()) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /modules/orm/database.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2017/11/2. 4 | 5 | // Package orm ORM初始化包 6 | package orm 7 | 8 | import ( 9 | "duguying/studio/g" 10 | "duguying/studio/modules/dbmodels" 11 | "fmt" 12 | "log" 13 | "time" 14 | 15 | "github.com/gogather/d2" 16 | "gorm.io/driver/mysql" 17 | "gorm.io/driver/sqlite" 18 | "gorm.io/gorm" 19 | "gorm.io/gorm/logger" 20 | ) 21 | 22 | var ( 23 | cache = d2.NewD2() 24 | ) 25 | 26 | func InitDatabase() { 27 | initDatabase() 28 | } 29 | 30 | func initDatabase() { 31 | if g.Config.SectionExist("database") { 32 | dbType := g.Config.Get("database", "type", "sqlite") 33 | if dbType == "mysql" { 34 | initMysql() 35 | } else { 36 | initSqlite() 37 | } 38 | 39 | if g.Config.Get("database", "log", "enable") == "enable" { 40 | // g.Db.LogMode(true) 41 | } 42 | 43 | initOrm() 44 | } else { 45 | g.InstallMode = true 46 | } 47 | } 48 | 49 | func initMysql() { 50 | newLogger := New( 51 | Config{ 52 | SlowThreshold: time.Second, // Slow SQL threshold 53 | LogLevel: logger.Info, // Log level 54 | Colorful: false, // Disable color 55 | }, 56 | ) 57 | 58 | var err error 59 | host := g.Config.Get("database", "host", "127.0.0.1") 60 | port := g.Config.GetInt64("database", "port", 3306) 61 | username := g.Config.Get("database", "username", "user") 62 | password := g.Config.Get("database", "password", "password") 63 | dbname := g.Config.Get("database", "name", "blog") 64 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", username, password, host, port, dbname) 65 | 66 | g.Db, err = gorm.Open(mysql.New(mysql.Config{ 67 | DSN: dsn, 68 | DefaultStringSize: 256, // default size for string fields 69 | }), &gorm.Config{Logger: newLogger}) 70 | if err != nil { 71 | log.Fatalf("数据库连接失败 err:%v\n", err) 72 | } 73 | 74 | // SetMaxIdleConns sets the maximum number of connections in the idle connection pool. 75 | db, _ := g.Db.DB() 76 | db.SetMaxIdleConns(10) 77 | 78 | // SetMaxOpenConns sets the maximum number of open connections to the database. 79 | db.SetMaxOpenConns(100) 80 | 81 | // SetConnMaxLifetime sets the maximum amount of time a connection may be reused. 82 | db.SetConnMaxLifetime(time.Hour) 83 | } 84 | 85 | func initSqlite() { 86 | var err error 87 | path := g.Config.Get("database", "path", "blog.db") 88 | g.Db, err = gorm.Open(sqlite.Open(path), &gorm.Config{}) 89 | if err != nil { 90 | log.Printf("数据库连接失败 err:%v\n", err) 91 | } 92 | } 93 | 94 | func initOrm() { 95 | g.Db.AutoMigrate( 96 | &dbmodels.Article{}, 97 | &dbmodels.Draft{}, 98 | &dbmodels.User{}, 99 | &dbmodels.File{}, 100 | &dbmodels.Agent{}, 101 | &dbmodels.AgentPerform{}, 102 | &dbmodels.Face{}, 103 | &dbmodels.FaceLabel{}, 104 | &dbmodels.LoginHistory{}, 105 | &dbmodels.ImageMeta{}, 106 | &dbmodels.Cover{}, 107 | &dbmodels.Calendar{}, 108 | &dbmodels.Node{}, 109 | &dbmodels.ShareLock{}, 110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /modules/imgtools/imgtools.go: -------------------------------------------------------------------------------- 1 | // Package imgtools 图片处理工具库 2 | package imgtools 3 | 4 | import ( 5 | "duguying/studio/utils" 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/gogather/com" 15 | ) 16 | 17 | // ConvertImgToWebp 图片转码到webp 18 | func ConvertImgToWebp(inpath string, outpath string, scaleWidth int64) (size int64, err error) { 19 | args := []string{"-limit", "memory", "100mb", "-limit", "map", "100mb"} 20 | if scaleWidth > 0 { 21 | args = append(args, "-resize", fmt.Sprintf("%dx", scaleWidth)) 22 | } 23 | args = append(args, inpath, "-auto-orient", outpath) 24 | cmd := exec.Command("convert", args...) 25 | err = cmd.Run() 26 | if err != nil { 27 | return 0, err 28 | } 29 | return getFileSize(outpath) 30 | } 31 | 32 | func getFileSize(path string) (size int64, err error) { 33 | info, err := os.Stat(path) 34 | if err != nil { 35 | return 0, err 36 | } 37 | return info.Size(), nil 38 | } 39 | 40 | // GetImgSize 获取图片尺寸 41 | func GetImgSize(path string) (width, height int64, err error) { 42 | // identify -ping -format '%w %h' /Users/rainesli/Desktop/F335F72D-3E57-4DE2-AE4F-947103583079.heic 43 | cmd := exec.Command("identify", "-ping", "-format", "%w %h", path) 44 | output, err := cmd.Output() 45 | if err != nil { 46 | return 0, 0, err 47 | } 48 | segs := strings.Split(string(output), " ") 49 | if len(segs) < 2 { 50 | return 0, 0, fmt.Errorf("invalid output") 51 | } 52 | width, err = strconv.ParseInt(segs[0], 10, 32) 53 | if err != nil { 54 | return 0, 0, err 55 | } 56 | height, err = strconv.ParseInt(segs[1], 10, 32) 57 | if err != nil { 58 | return 0, 0, err 59 | } 60 | 61 | return width, height, nil 62 | } 63 | 64 | // ExtractImgMeta 获取图片meta信息 65 | func ExtractImgMeta(path string) (meta, metas string, err error) { 66 | cmd := exec.Command("convert", path, "json:") 67 | output, err := cmd.Output() 68 | if err != nil { 69 | return "", "", err 70 | } 71 | info := []interface{}{} 72 | err = json.Unmarshal(output, &info) 73 | if err != nil { 74 | return "", "", err 75 | } 76 | metas = string(output) 77 | 78 | if len(info) > 0 { 79 | metaRaw, err := json.Marshal(info[0]) 80 | if err != nil { 81 | return "", "", err 82 | } 83 | meta = string(metaRaw) 84 | } 85 | 86 | return meta, metas, nil 87 | } 88 | 89 | // MakeThumbnail 制作缩略图 90 | func MakeThumbnail(path string, maxHeight int) (thumbKey string, err error) { 91 | if maxHeight <= 0 { 92 | return "", fmt.Errorf("invalid maxHeight") 93 | } 94 | 95 | args := []string{"-resize", fmt.Sprintf("x%d", maxHeight)} 96 | thumbKey = filepath.Join("img", "cache", fmt.Sprintf("%s.webp", utils.GenUUID())) 97 | thumbPath := utils.GetFileLocalPath(thumbKey) 98 | cacheDir := filepath.Dir(thumbPath) 99 | 100 | if !com.PathExist(cacheDir) { 101 | os.MkdirAll(cacheDir, 0644) 102 | } 103 | 104 | args = append(args, path, thumbPath) 105 | cmd := exec.Command("convert", args...) 106 | err = cmd.Run() 107 | if err != nil { 108 | return "", err 109 | } 110 | return thumbKey, nil 111 | } 112 | -------------------------------------------------------------------------------- /service/action/2fa.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "duguying/studio/g" 5 | "duguying/studio/modules/db" 6 | "encoding/base32" 7 | "fmt" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | 12 | "github.com/dgryski/dgoogauth" 13 | "github.com/gin-gonic/gin" 14 | "github.com/gogather/json" 15 | "rsc.io/qr" 16 | ) 17 | 18 | func QrGoogleAuth(c *gin.Context) { 19 | uidStr := c.DefaultQuery("uid", "") 20 | 21 | uid, err := strconv.ParseUint(uidStr, 10, 32) 22 | if err != nil { 23 | c.JSON(http.StatusOK, gin.H{ 24 | "ok": false, 25 | "err": err.Error(), 26 | }) 27 | return 28 | } 29 | 30 | user, err := db.GetUserByID(g.Db, uint(uid)) 31 | if err != nil { 32 | c.JSON(http.StatusOK, gin.H{ 33 | "ok": false, 34 | "err": err.Error(), 35 | }) 36 | return 37 | } 38 | 39 | secretBase32 := base32.StdEncoding.EncodeToString([]byte(user.TfaSecret)) 40 | account := fmt.Sprintf("%s@duguying.net", user.Username) 41 | issuer := "duguying.net" 42 | 43 | URL, err := url.Parse("otpauth://totp") 44 | if err != nil { 45 | c.JSON(http.StatusOK, gin.H{ 46 | "ok": false, 47 | "err": err.Error(), 48 | }) 49 | return 50 | } 51 | 52 | URL.Path += "/" + url.PathEscape(issuer) + ":" + url.PathEscape(account) 53 | 54 | params := url.Values{} 55 | params.Add("secret", secretBase32) 56 | params.Add("issuer", issuer) 57 | 58 | URL.RawQuery = params.Encode() 59 | fmt.Printf("URL is %s\n", URL.String()) 60 | 61 | code, err := qr.Encode(URL.String(), qr.Q) 62 | if err != nil { 63 | c.JSON(http.StatusOK, gin.H{ 64 | "ok": false, 65 | "err": err.Error(), 66 | }) 67 | return 68 | } 69 | 70 | c.Data(http.StatusOK, "image/png", code.PNG()) 71 | } 72 | 73 | type TfaAuthRequest struct { 74 | UID uint `json:"uid"` 75 | Token string `json:"token"` 76 | } 77 | 78 | func (tar *TfaAuthRequest) String() string { 79 | c, _ := json.Marshal(tar) 80 | return string(c) 81 | } 82 | 83 | func TfaAuth(c *gin.Context) { 84 | tar := TfaAuthRequest{} 85 | err := c.BindJSON(&tar) 86 | if err != nil { 87 | c.JSON(http.StatusOK, gin.H{ 88 | "ok": false, 89 | "err": err.Error(), 90 | }) 91 | return 92 | } 93 | 94 | user, err := db.GetUserByID(g.Db, tar.UID) 95 | if err != nil { 96 | c.JSON(http.StatusOK, gin.H{ 97 | "ok": false, 98 | "err": err.Error(), 99 | }) 100 | return 101 | } 102 | 103 | secretBase32 := base32.StdEncoding.EncodeToString([]byte(user.TfaSecret)) 104 | otpc := &dgoogauth.OTPConfig{ 105 | Secret: secretBase32, 106 | WindowSize: 1, 107 | HotpCounter: 0, 108 | UTC: true, 109 | } 110 | 111 | val, err := otpc.Authenticate(tar.Token) 112 | if err != nil { 113 | c.JSON(http.StatusOK, gin.H{ 114 | "ok": false, 115 | "err": err.Error(), 116 | }) 117 | return 118 | } 119 | 120 | if !val { 121 | c.JSON(http.StatusOK, gin.H{ 122 | "ok": false, 123 | "err": "Sorry, Not Authenticated", 124 | }) 125 | return 126 | } else { 127 | c.JSON(http.StatusOK, gin.H{ 128 | "ok": true, 129 | "err": "Authenticated!", 130 | }) 131 | return 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /modules/cron/task.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020. All rights reserved. 2 | // This file is part of duguying project 3 | // I am coding in Tencent 4 | // Created by rainesli on 2020/3/16. 5 | 6 | package cron 7 | 8 | import ( 9 | "duguying/studio/g" 10 | "duguying/studio/modules/db" 11 | "duguying/studio/modules/viewcnt" 12 | "fmt" 13 | "log" 14 | "time" 15 | 16 | "github.com/gogather/cron" 17 | ) 18 | 19 | func Init() { 20 | task := cron.New() 21 | 22 | spec := g.Config.Get("cron", "flust-view-count", fmt.Sprintf("@every 5m")) 23 | t1, err := task.AddFunc(spec, flushViewCnt) 24 | if err != nil { 25 | log.Println("create cron task failed, err:", err.Error()) 26 | } else { 27 | log.Println("create cron task success, task id:", t1) 28 | } 29 | 30 | // spec2 := g.Config.Get("calendar", "birth-check", fmt.Sprintf("@daily")) 31 | // t2, err := task.AddFunc(spec2, calendarCheck) 32 | // if err != nil { 33 | // log.Println("create cron task failed, err:", err.Error()) 34 | // } else { 35 | // log.Println("create cron task success, task id:", t2) 36 | // } 37 | 38 | spec3 := g.Config.Get("bleve", "cron", "@every 2h") 39 | t3, err := task.AddFunc(spec3, FlushArticleBleve) 40 | if err != nil { 41 | log.Println("create cron task failed, err:", err.Error()) 42 | } else { 43 | log.Println("create cron task success, task id:", t3) 44 | } 45 | 46 | task.Start() 47 | 48 | go func() { 49 | for { 50 | scanFile() 51 | time.Sleep(time.Minute) 52 | } 53 | }() 54 | } 55 | 56 | func flushViewCnt() { 57 | vcm := viewcnt.GetMap() 58 | log.Println("vcm:", vcm.M) 59 | for ident, val := range vcm.M { 60 | err := db.UpdateArticleViewCount(g.Db, ident, val.(int)) 61 | if err != nil { 62 | log.Println("update article view count failed, err:", err.Error()) 63 | } else { 64 | viewcnt.ResetViewCnt(ident) 65 | } 66 | } 67 | } 68 | 69 | // func calendarCheck() { 70 | // list, err := db.ListAllCalendarIds(g.Db) 71 | // if err != nil { 72 | // log.Println("列举日历事件失败, err:", err.Error()) 73 | // return 74 | // } 75 | 76 | // beforeDay := g.Config.GetInt64("calendar", "before-day", 7) 77 | 78 | // for _, id := range list { 79 | // cal, err := db.GetCalendarByID(g.Db, id) 80 | // if err != nil { 81 | // log.Println("获取日历详情失败, err:", err.Error()) 82 | // continue 83 | // } 84 | // if cal.Start.Add(-time.Hour * 24 * time.Duration(beforeDay)).Before(time.Now()) { 85 | // utils.GenerateICS( 86 | // cal.ID, 87 | // cal.Start, cal.End, cal.Stamp, 88 | // cal.Summary, cal.Address, cal.Description, 89 | // cal.Link, cal.Attendee, 90 | // ) 91 | // } 92 | // } 93 | // } 94 | 95 | func FlushArticleBleve() { 96 | articles, err := db.ListAllArticle(g.Db) 97 | if err != nil { 98 | log.Println("list all article failed, err:", err.Error()) 99 | return 100 | } 101 | 102 | for _, article := range articles { 103 | err = g.Index.Index(fmt.Sprintf("%d", article.ID), article.ToArticleIndex()) 104 | if err != nil { 105 | log.Printf("index article [%s] failed, err: %s\n", article.URI, err.Error()) 106 | continue 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /modules/configuration/config.go: -------------------------------------------------------------------------------- 1 | // Package configuration 配置模块 2 | package configuration 3 | 4 | import ( 5 | "fmt" 6 | "strconv" 7 | "sync" 8 | 9 | "github.com/gogather/com" 10 | "gopkg.in/ini.v1" 11 | ) 12 | 13 | type Config struct { 14 | config *ini.File 15 | path string 16 | writeLck *sync.Mutex 17 | } 18 | 19 | func NewConfig(path string) *Config { 20 | var cfg *ini.File 21 | cfgExist := com.FileExist(path) 22 | if cfgExist { 23 | cfg, _ = ini.Load(path) 24 | } else { 25 | cfg = ini.Empty() 26 | } 27 | 28 | dusCfg := &Config{ 29 | config: cfg, 30 | path: path, 31 | writeLck: &sync.Mutex{}, 32 | } 33 | 34 | if !cfgExist { 35 | dusCfg.initWithDefault() 36 | } 37 | 38 | return dusCfg 39 | } 40 | 41 | func (dc *Config) initWithDefault() (err error) { 42 | return nil 43 | } 44 | 45 | func (dc *Config) write(path string, content string) error { 46 | defer dc.writeLck.Unlock() 47 | dc.writeLck.Lock() 48 | return com.WriteFile(path, content) 49 | } 50 | 51 | // GetSectionAsMap 将配置区加载为map 52 | func (dc *Config) GetSectionAsMap(section string) map[string]string { 53 | sect, err := dc.config.GetSection(section) 54 | if err != nil { 55 | sect, _ = dc.config.NewSection(section) 56 | dc.writeLck.Lock() 57 | dc.config.SaveTo(dc.path) 58 | dc.writeLck.Unlock() 59 | } 60 | 61 | result := map[string]string{} 62 | keys := sect.Keys() 63 | for _, key := range keys { 64 | result[key.Name()] = key.Value() 65 | } 66 | return result 67 | } 68 | 69 | // GetSection 获取配置区 70 | func (dc *Config) GetSection(section string) *ini.Section { 71 | sect, err := dc.config.GetSection(section) 72 | if err != nil { 73 | sect, _ = dc.config.NewSection(section) 74 | } 75 | return sect 76 | } 77 | 78 | // Get 获取配置项,section,key为配置区与键,value为默认值,当配置项不存在时返回value默认值,且初始化该值到配置文件 79 | func (dc *Config) Get(section, key string, value string) string { 80 | sect, err := dc.config.GetSection(section) 81 | if err != nil { 82 | sect, _ = dc.config.NewSection(section) 83 | } 84 | 85 | val, err := sect.GetKey(key) 86 | if err != nil { 87 | sect.NewKey(key, value) 88 | dc.writeLck.Lock() 89 | dc.config.SaveTo(dc.path) 90 | dc.writeLck.Unlock() 91 | return value 92 | } 93 | 94 | return val.String() 95 | } 96 | 97 | // Set 运行时主动设置配置项值,并存入配置文件 98 | func (dc *Config) Set(section, key string, value string) { 99 | sect, err := dc.config.GetSection(section) 100 | if err != nil { 101 | sect, _ = dc.config.NewSection(section) 102 | } 103 | 104 | sect.NewKey(key, value) 105 | dc.writeLck.Lock() 106 | dc.config.SaveTo(dc.path) 107 | dc.writeLck.Unlock() 108 | } 109 | 110 | // SectionExist 配置区是否存在 111 | func (dc *Config) SectionExist(section string) bool { 112 | _, err := dc.config.GetSection(section) 113 | if err != nil { 114 | return false 115 | } else { 116 | return true 117 | } 118 | } 119 | 120 | // GetInt64 获取配置项值为int64类型 121 | func (dc *Config) GetInt64(section, key string, value int64) int64 { 122 | intStr := dc.Get(section, key, fmt.Sprintf("%d", value)) 123 | 124 | i, err := strconv.ParseInt(intStr, 10, 64) 125 | if err != nil { 126 | return value 127 | } else { 128 | return i 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /service/models/article.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019. All rights reserved. 2 | // This file is part of duguying project 3 | // I am coding in Tencent 4 | // Created by rainesli on 2019/8/29. 5 | 6 | package models 7 | 8 | import ( 9 | "time" 10 | 11 | "github.com/gogather/json" 12 | ) 13 | 14 | type Article struct { 15 | ID uint `json:"id"` 16 | Title string `json:"title"` 17 | URI string `json:"uri"` 18 | Keywords []string `json:"keywords"` 19 | Abstract string `json:"abstract"` 20 | Content string `json:"content"` 21 | Type int `json:"type"` 22 | Draft bool `json:"draft"` 23 | } 24 | 25 | func (aar *Article) String() string { 26 | c, _ := json.Marshal(aar) 27 | return string(c) 28 | } 29 | 30 | type ArticleShowContent struct { 31 | ID uint `json:"id"` 32 | Title string `json:"title"` 33 | URI string `json:"uri"` 34 | Author string `json:"author"` 35 | Tags []string `json:"tags"` 36 | CreatedAt time.Time `json:"created_at"` 37 | ViewCount uint `json:"view_count"` 38 | Content string `json:"content"` 39 | } 40 | 41 | func (ac *ArticleShowContent) String() string { 42 | c, _ := json.Marshal(ac) 43 | return string(c) 44 | } 45 | 46 | type ArticleContent struct { 47 | ID uint `json:"id"` 48 | Title string `json:"title"` 49 | URI string `json:"uri"` 50 | Author string `json:"author"` 51 | Tags []string `json:"tags"` 52 | Type int `json:"type"` 53 | Status int `json:"status"` 54 | CreatedAt time.Time `json:"created_at"` 55 | ViewCount uint `json:"view_count"` 56 | Content string `json:"content"` 57 | } 58 | 59 | func (asc *ArticleContent) String() string { 60 | c, _ := json.Marshal(asc) 61 | return string(c) 62 | } 63 | 64 | type ArticleTitle struct { 65 | ID uint `json:"id"` 66 | Title string `json:"title"` 67 | URI string `json:"uri"` 68 | Author string `json:"author"` 69 | CreatedAt time.Time `json:"created_at"` 70 | ViewCount uint `json:"view_count"` 71 | } 72 | 73 | type ArticleAdminTitle struct { 74 | ID uint `json:"id"` 75 | Title string `json:"title"` 76 | URI string `json:"uri"` 77 | Author string `json:"author"` 78 | CreatedAt time.Time `json:"created_at"` 79 | ViewCount uint `json:"view_count"` 80 | Status int `json:"status"` 81 | StatusName string `json:"status_name"` 82 | } 83 | 84 | func (at *ArticleTitle) String() string { 85 | c, _ := json.Marshal(at) 86 | return string(c) 87 | } 88 | 89 | type ArticleSearchAbstract struct { 90 | ID uint `json:"id"` 91 | Title string `json:"title"` 92 | URI string `json:"uri"` 93 | Tags []string `json:"tags"` 94 | Author string `json:"author"` 95 | Keywords string `json:"keywords"` 96 | Content string `json:"content"` 97 | CreatedAt *time.Time `json:"created_at"` 98 | } 99 | 100 | func (asa *ArticleSearchAbstract) String() string { 101 | c, _ := json.Marshal(asa) 102 | return string(c) 103 | } 104 | 105 | type ArchInfo struct { 106 | Date string `json:"date"` 107 | Number uint `json:"number"` 108 | Year uint `json:"year"` 109 | Month uint `json:"month"` 110 | } 111 | 112 | func (ai *ArchInfo) String() string { 113 | c, _ := json.Marshal(ai) 114 | return string(c) 115 | } 116 | -------------------------------------------------------------------------------- /modules/storage/qcloud_cos.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019. All rights reserved. 2 | // This file is part of duguying project 3 | // I am coding in Tencent 4 | // Created by rainesli on 2020/4/11. 5 | 6 | package storage 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | "net/http" 12 | "net/url" 13 | "time" 14 | 15 | "github.com/sirupsen/logrus" 16 | "github.com/tencentyun/cos-go-sdk-v5" 17 | ) 18 | 19 | type QcloudCos struct { 20 | sid string 21 | skey string 22 | bucket string 23 | client *cos.Client 24 | l *logrus.Entry 25 | ctx context.Context 26 | } 27 | 28 | func NewQcloudOss(l *logrus.Entry, sid string, skey string, bucket, region, protocol string) (storage *QcloudCos, err error) { 29 | storage = &QcloudCos{ 30 | sid: sid, 31 | skey: skey, 32 | bucket: bucket, 33 | l: l, 34 | ctx: l.Context, 35 | } 36 | 37 | u, err := url.Parse(fmt.Sprintf("%s://%s.cos.%s.myqcloud.com", protocol, bucket, region)) 38 | if err != nil { 39 | return nil, err 40 | } 41 | b := &cos.BaseURL{BucketURL: u} 42 | storage.client = cos.NewClient(b, &http.Client{ 43 | //设置超时时间 44 | Timeout: 100 * time.Second, 45 | Transport: &cos.AuthorizationTransport{ 46 | //如实填写账号和密钥,也可以设置为环境变量 47 | SecretID: storage.sid, 48 | SecretKey: storage.skey, 49 | }, 50 | }) 51 | 52 | return storage, nil 53 | } 54 | 55 | // List 列举文件 56 | func (q QcloudCos) List(remotePrefix string) (list []*FileInfo, err error) { 57 | opt := &cos.BucketGetOptions{ 58 | Prefix: remotePrefix, 59 | MaxKeys: 3, 60 | } 61 | 62 | v, _, err := q.client.Bucket.Get(context.Background(), opt) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | list = []*FileInfo{} 68 | for _, c := range v.Contents { 69 | list = append(list, &FileInfo{ 70 | Path: c.Key, 71 | Size: c.Size, 72 | }) 73 | } 74 | 75 | return list, nil 76 | } 77 | 78 | // GetFileInfo 获取文件信息 79 | func (q QcloudCos) GetFileInfo(remotePath string) (info *FileInfo, err error) { 80 | list, err := q.List(remotePath) 81 | if err != nil { 82 | return nil, err 83 | } 84 | if len(list) > 0 { 85 | info = list[0] 86 | } 87 | return info, nil 88 | } 89 | 90 | func (q QcloudCos) IsExist(remotePath string) (exist bool, err error) { 91 | return q.client.Object.IsExist(q.ctx, remotePath) 92 | } 93 | 94 | func (q QcloudCos) PutFile(localPath string, remotePath string) (err error) { 95 | _, err = q.client.Object.PutFromFile(q.ctx, remotePath, localPath, nil) 96 | if err != nil { 97 | return err 98 | } 99 | return nil 100 | } 101 | 102 | func (q QcloudCos) copyFile(remotePath string, newRemotePath string) (err error) { 103 | _, _, err = q.client.Object.Copy(q.ctx, newRemotePath, remotePath, nil) 104 | if err != nil { 105 | return err 106 | } 107 | return nil 108 | } 109 | 110 | func (q QcloudCos) RenameFile(remotePath string, newRemotePath string) (err error) { 111 | err = q.copyFile(remotePath, newRemotePath) 112 | if err != nil { 113 | return err 114 | } 115 | return q.RemoveFile(remotePath) 116 | } 117 | 118 | func (q QcloudCos) RemoveFile(remotePath string) (err error) { 119 | _, err = q.client.Object.Delete(q.ctx, remotePath, nil) 120 | if err != nil { 121 | return err 122 | } 123 | return nil 124 | } 125 | 126 | func (q QcloudCos) FetchFile(remotePath string, localPath string) (err error) { 127 | _, err = q.client.Object.GetToFile(q.ctx, remotePath, localPath, nil) 128 | if err != nil { 129 | return err 130 | } 131 | return nil 132 | } 133 | -------------------------------------------------------------------------------- /service/action/agent/agent.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2018/5/18. 4 | 5 | package agent 6 | 7 | import ( 8 | "duguying/studio/g" 9 | "duguying/studio/modules/db" 10 | "duguying/studio/modules/dns" 11 | "duguying/studio/modules/ipip" 12 | "duguying/studio/service/models" 13 | "net/http" 14 | "time" 15 | 16 | "github.com/gin-gonic/gin" 17 | ) 18 | 19 | type AgentInfo struct { 20 | Ips []string `json:"ips"` 21 | CpuNum int `json:"cpu_num"` 22 | } 23 | 24 | func Report(c *gin.Context) { 25 | auth := c.GetHeader("Authorization") 26 | authToken := g.Config.Get("dns", "agent-auth", "a466e30d7571e6e720cb4a01ce446752") 27 | if auth != authToken { 28 | c.JSON(http.StatusUnauthorized, gin.H{ 29 | "ok": false, 30 | "err": "auth failed", 31 | }) 32 | } 33 | 34 | ai := &AgentInfo{} 35 | err := c.BindJSON(ai) 36 | if err != nil { 37 | c.JSON(http.StatusOK, gin.H{ 38 | "ok": false, 39 | "err": err.Error(), 40 | }) 41 | return 42 | } 43 | 44 | ak := g.Config.Get("dns", "ak", "") 45 | sk := g.Config.Get("dns", "sk", "") 46 | rootDomain := g.Config.Get("dns", "root", "duguying.net") 47 | rpiRecord := g.Config.Get("dns", "rr", "rpi") 48 | alidns, err := dns.NewAliDns(ak, sk) 49 | if err != nil { 50 | c.JSON(http.StatusOK, gin.H{ 51 | "ok": false, 52 | "err": err.Error(), 53 | }) 54 | return 55 | } 56 | 57 | _, err = alidns.AddDomainRecord(rootDomain, rpiRecord, "A", c.ClientIP()) 58 | if err != nil { 59 | c.JSON(http.StatusOK, gin.H{ 60 | "ok": false, 61 | "err": err.Error(), 62 | }) 63 | return 64 | } 65 | 66 | c.JSON(http.StatusOK, gin.H{ 67 | "ok": true, 68 | }) 69 | } 70 | 71 | type AgentDetail struct { 72 | Id uint `json:"id"` 73 | Online uint `json:"online"` // 1 online, 0 offline 74 | ClientId string `json:"client_id" gorm:"unique;not null"` 75 | Os string `json:"os"` 76 | Arch string `json:"arch"` 77 | Hostname string `json:"hostname"` 78 | Ip string `json:"ip"` 79 | IpIns []string `json:"ip_ins"` // json 80 | Status uint `json:"status"` 81 | OnlineTime time.Time `json:"online_time"` 82 | OfflineTime time.Time `json:"offline_time"` 83 | } 84 | 85 | func List(c *gin.Context) { 86 | agents, err := db.ListAllAvailableAgents(g.Db) 87 | if err != nil { 88 | c.JSON(http.StatusOK, gin.H{ 89 | "ok": false, 90 | "err": err.Error(), 91 | }) 92 | return 93 | } 94 | 95 | apiAgents := []*models.Agent{} 96 | for _, agent := range agents { 97 | apiAgent := agent.ToModel() 98 | loc, err := ipip.GetLocation(apiAgent.IP) 99 | if err == nil { 100 | apiAgent.Area = loc.CityName 101 | } 102 | apiAgents = append(apiAgents, apiAgent) 103 | } 104 | 105 | c.JSON(http.StatusOK, gin.H{ 106 | "ok": true, 107 | "list": apiAgents, 108 | }) 109 | return 110 | } 111 | 112 | // RemoveAgent 列表中移除 agent 113 | func RemoveAgent(c *gin.Context) { 114 | getter := models.IntGetter{} 115 | err := c.BindQuery(&getter) 116 | if err != nil { 117 | c.JSON(http.StatusOK, gin.H{ 118 | "ok": false, 119 | "err": err.Error(), 120 | }) 121 | return 122 | } 123 | 124 | err = db.ForbidAgent(g.Db, getter.ID) 125 | if err != nil { 126 | c.JSON(http.StatusOK, gin.H{ 127 | "ok": false, 128 | "err": err.Error(), 129 | }) 130 | return 131 | } 132 | 133 | c.JSON(http.StatusOK, gin.H{ 134 | "ok": true, 135 | }) 136 | return 137 | } 138 | -------------------------------------------------------------------------------- /service/action/action.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "duguying/studio/service/action/agent" 5 | "duguying/studio/service/middleware" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func SetupFeAPI(api *gin.RouterGroup) { 11 | api.GET("/get_article", GetArticleShow) // 获取文章详情 12 | api.GET("/article/view_count", ArticleViewCount) // 文章浏览统计 13 | api.GET("/list", ListArticleWithContent) // 列出文章 14 | api.GET("/list_tag", ListArticleWithContentByTag) // 按Tag列出文章 15 | api.GET("/list_archive_monthly", ListArticleWithContentMonthly) // 按月归档文章内容列表 16 | api.GET("/list_title", APIWrapper(ListArticleTitle)) // 列出文章标题 17 | api.GET("/search_article", SearchArticle) // 搜索文章 18 | api.GET("/hot_article", HotArticleTitle) // 热门文章列表 19 | api.GET("/month_archive", MonthArchive) // 文章按月归档列表 20 | api.POST("/user_register", UserRegister) // 用户注册 21 | api.GET("/user_simple_info", APIWrapper(UserSimpleInfo)) // 用户信息 22 | api.POST("/user_login", APIWrapper(UserLogin)) // 用户登陆 23 | api.GET("/username_check", UsernameCheck) // 用户名检查 24 | api.POST("/2fa", TfaAuth) // 2FA校验 25 | api.GET("/sitemap", SiteMap) // 列出所有文章URI 26 | api.POST("/save_error_logger", APIWrapper(SaveErrorLogger)) // 前端错误日志 27 | api.GET("/cover/list", APIWrapper(ListCover)) // 获取博客封面 28 | } 29 | 30 | func SetupAdminAPI(api *gin.RouterGroup) { 31 | api.GET("/user_info", APIWrapper(UserInfo)) // 用户信息 32 | api.POST("/user_logout", APIWrapper(UserLogout)) // 用户登出 33 | api.PUT("/change_password", APIWrapper(ChangePassword)) // 修改密码 34 | api.GET("/login_history", APIWrapper(ListUserLoginHistory)) // 列举用户登录历史 35 | api.GET("/message/count", APIWrapper(UserMessageCount)) // 用户消息计数 36 | api.POST("/put", APIWrapper(PutFile)) // 上传文件 37 | api.POST("/upload", APIWrapper(UploadFile)) // 上传归档文件 38 | api.Any("/xterm", ConnectXTerm) // 连接xterm 39 | 40 | api.POST("/article", APIWrapper(AddArticle)) // 添加文章 41 | api.PUT("/article", APIWrapper(UpdateArticle)) // 修改文章 42 | api.PUT("/article/publish", APIWrapper(PublishArticle)) // 发布草稿 43 | api.DELETE("/article", APIWrapper(DeleteArticle)) // 删除文章 44 | api.GET("/article/list_title", APIWrapper(ListAdminArticleTitle)) // 列出文章列表 45 | api.GET("/article", APIWrapper(AdminGetArticle)) // 获取文章 46 | api.GET("/article/current_md5", APIWrapper(GetArticleCurrentMD5)) // 获取文章当前内容MD5 47 | 48 | api.GET("/2faqr", QrGoogleAuth) // 获取2FA二维码 49 | api.POST("/upload/image", APIWrapper(UploadImage)) // form 表单上传图片 50 | api.DELETE("/file/delete", APIWrapper(DeleteFile)) // 删除文件 51 | api.GET("/file/list", APIWrapper(PageFile)) // 文件列表 52 | api.GET("/file/ls", APIWrapper(FileLs)) 53 | api.PUT("/file/sync_cos", APIWrapper(FileSyncToCos)) // 文件同步到 cos 54 | api.GET("/album/list", APIWrapper(ListAlbumFiles)) // 相册图片列表 55 | api.GET("/album/media/detail", APIWrapper(MediaDetail)) // 媒体文件详情 56 | } 57 | 58 | func SetupAgentAPI(api *gin.RouterGroup) { 59 | api.GET("/list", middleware.SessionValidate(true), agent.List) // agent列表 60 | api.DELETE("/remove", middleware.SessionValidate(true), agent.RemoveAgent) // agent删除(禁用) 61 | api.Any("/ws", agent.Ws) // agent连接点 62 | } 63 | -------------------------------------------------------------------------------- /modules/db/agent.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "duguying/studio/modules/dbmodels" 5 | "time" 6 | 7 | "github.com/gogather/json" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | const ( 12 | AgentStatusAllow = 0 13 | AgentStatusForbbid = 1 14 | 15 | AgentOffline = 0 16 | AgentOnline = 1 17 | ) 18 | 19 | // CreateOrUpdateAgent 创建或更新 agent 20 | func CreateOrUpdateAgent(tx *gorm.DB, clientID string, IP string) (agent *dbmodels.Agent, err error) { 21 | existAgent := &dbmodels.Agent{} 22 | err = tx.Table("agents").Where("client_id=?", clientID).First(existAgent).Error 23 | if err != nil { 24 | // not exist, create 25 | agent = &dbmodels.Agent{ 26 | ClientID: clientID, 27 | IP: IP, 28 | Online: AgentOnline, 29 | Status: AgentStatusAllow, 30 | OnlineTime: time.Now(), 31 | OfflineTime: time.Now(), 32 | } 33 | err = tx.Table("agents").Create(agent).Error 34 | if err != nil { 35 | return nil, err 36 | } 37 | } else { 38 | // exist, update 39 | err = tx.Table("agents").Where("client_id=?", clientID).Updates(map[string]interface{}{ 40 | "online": AgentOnline, 41 | "status": AgentStatusAllow, 42 | "ip": IP, 43 | }).Error 44 | if err != nil { 45 | return nil, err 46 | } 47 | } 48 | 49 | return agent, nil 50 | } 51 | 52 | func PutPerf(tx *gorm.DB, clientID string, os string, arch string, hostname string, IPIns []string) (err error) { 53 | ipInBytes, _ := json.Marshal(IPIns) 54 | 55 | err = tx.Table("agents").Where("client_id=?", clientID).Updates(map[string]interface{}{ 56 | "online": AgentOnline, 57 | "os": os, 58 | "arch": arch, 59 | "hostname": hostname, 60 | "ip_ins": string(ipInBytes), 61 | }).Error 62 | if err != nil { 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // GetAgent 通过 id 获取 agent 70 | func GetAgent(tx *gorm.DB, id uint) (agent *dbmodels.Agent, err error) { 71 | agent = &dbmodels.Agent{} 72 | err = tx.Table("agents").Where("id=?", id).First(agent).Error 73 | if err != nil { 74 | return nil, err 75 | } else { 76 | return agent, nil 77 | } 78 | } 79 | 80 | // GetAgentByClientID 通过 clientID 获取 agent 81 | func GetAgentByClientID(tx *gorm.DB, clientID string) (agent *dbmodels.Agent, err error) { 82 | agent = &dbmodels.Agent{} 83 | err = tx.Table("agents").Where("client_id=?", clientID).First(agent).Error 84 | if err != nil { 85 | return nil, err 86 | } else { 87 | return agent, nil 88 | } 89 | } 90 | 91 | // ListAllAvailableAgents 列出所有可用 agent 92 | func ListAllAvailableAgents(tx *gorm.DB) (agents []*dbmodels.Agent, err error) { 93 | agents = []*dbmodels.Agent{} 94 | err = tx.Table("agents").Where("status=?", AgentStatusAllow).Find(&agents).Error 95 | if err != nil { 96 | return nil, err 97 | } else { 98 | return agents, nil 99 | } 100 | } 101 | 102 | // ForbidAgent 禁用 agent 103 | func ForbidAgent(tx *gorm.DB, id uint) (err error) { 104 | agent := &dbmodels.Agent{} 105 | err = tx.Table("agents").Where("id=?", id).First(agent).Error 106 | if err != nil { 107 | return err 108 | } 109 | 110 | err = tx.Table("agents").Where("id=?", id).Update("status", AgentStatusForbbid).Error 111 | if err != nil { 112 | return err 113 | } 114 | 115 | return nil 116 | } 117 | 118 | func UpdateAgentOffline(tx *gorm.DB, clientID string) (err error) { 119 | err = tx.Table("agents").Where("client_id=?", clientID).Updates(map[string]interface{}{ 120 | "online": AgentOffline, 121 | "offline_time": time.Now(), 122 | }).Error 123 | if err != nil { 124 | return err 125 | } 126 | 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /service/message/proto/performance.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package model; 3 | option go_package = "../model"; 4 | 5 | // cmd 1 6 | message PerformanceMonitor { 7 | message Memory { 8 | uint64 total_mem = 1; 9 | uint64 used_mem = 2; 10 | uint64 free_mem = 3; 11 | uint64 actual_used = 4; 12 | uint64 actual_free = 5; 13 | uint64 used_swap = 6; 14 | uint64 free_swap = 7; 15 | uint64 total_swap = 8; 16 | } 17 | 18 | message Cpu { 19 | uint64 user = 1; 20 | uint64 nice = 2; 21 | uint64 sys = 3; 22 | uint64 idle = 4; 23 | uint64 wait = 5; 24 | uint64 irq = 6; 25 | uint64 soft_irq = 7; 26 | uint64 stolen = 8; 27 | } 28 | 29 | message Load { 30 | double one = 1; 31 | double five = 2; 32 | double fifteen = 3; 33 | } 34 | 35 | message FileSystem { 36 | string dir_name = 1; 37 | string dev_name = 2; 38 | string type_name = 3; 39 | string sys_type_name = 4; 40 | string options = 5; 41 | uint32 flags = 6; 42 | 43 | uint64 total = 7; 44 | uint64 used = 8; 45 | uint64 free = 9; 46 | uint64 avail = 10; 47 | uint64 files = 11; 48 | uint64 free_files = 12; 49 | } 50 | 51 | message ProcTime { 52 | uint64 start_time = 1; 53 | uint64 user = 2; 54 | uint64 sys = 3; 55 | uint64 total = 4; 56 | } 57 | 58 | message Process { 59 | int32 pid = 1; 60 | repeated string args = 2; 61 | string exe_name = 3; 62 | string exe_cwd = 4; 63 | string exe_root = 5; 64 | 65 | ProcTime cpu_proc_time = 6; 66 | uint64 cpu_last_time = 7; 67 | double cpu_percent = 8; 68 | 69 | uint64 mem_size = 9; 70 | uint64 mem_resident = 10; 71 | uint64 mem_share = 11; 72 | uint64 mem_minor_faults = 12; 73 | uint64 mem_major_faults = 13; 74 | uint64 mem_page_faults = 14; 75 | 76 | string stat_name = 15; 77 | int32 stat_state = 16; 78 | int32 stat_ppid = 17; 79 | int32 stat_tty = 18; 80 | int32 stat_priority = 19; 81 | int32 stat_nice = 20; 82 | int32 stat_processor = 21; 83 | } 84 | 85 | message NetWork { 86 | string name = 1; 87 | string ip = 2; 88 | double speed = 3; 89 | double out_recv_pkg_err_rate = 4; //外网收包错误率 90 | double out_send_pkg_err_rate = 5; //外网发包错误率 91 | uint64 recv_byte = 6; //接收的字节数 92 | uint64 recv_pkg = 7; //接收正确的包数 93 | uint64 recv_err = 8; //接收错误的包数 94 | uint64 send_byte = 9; //发送的字节数 95 | uint64 send_pkg = 10; //发送正确的包数 96 | uint64 send_err = 11; //发送错误的包数 97 | 98 | double recv_byte_avg = 12; //一个周期平均每秒接收字节数 99 | double send_byte_avg = 13; //一个周期平均每秒发送字节数 100 | double recv_err_rate = 14; //一个周期收包错误率 101 | double send_err_rate = 15; //一个周期发包错误率 102 | double recv_pkg_avg = 16; //一个周期平均每秒收包数 103 | double send_pkg_avg = 17; //一个周期平均每秒发包数 104 | } 105 | 106 | uint64 timestamp = 3; 107 | Memory mem = 1; 108 | Cpu cpu = 2; 109 | Load load = 4; 110 | double uptime = 5; 111 | repeated Cpu cpulist = 6; 112 | repeated FileSystem file_system_list = 7; 113 | repeated Process process_list = 8; 114 | repeated NetWork nets = 9; 115 | string os = 10; // 系统名称 116 | string hostname = 11; // 主机名称 117 | string arch = 12; // 架构 118 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "duguying/studio/docs" 6 | "duguying/studio/g" 7 | "duguying/studio/modules/bleve" 8 | "duguying/studio/modules/cache" 9 | "duguying/studio/modules/configuration" 10 | "duguying/studio/modules/cron" 11 | "duguying/studio/modules/ipip" 12 | "duguying/studio/modules/logger" 13 | "duguying/studio/modules/orm" 14 | "duguying/studio/modules/rlog" 15 | "duguying/studio/service" 16 | "flag" 17 | "fmt" 18 | "os" 19 | "path/filepath" 20 | "strconv" 21 | "time" 22 | 23 | _ "go.uber.org/automaxprocs" 24 | ) 25 | 26 | var ( 27 | configPath string = "studio.ini" 28 | logDir string = "log" 29 | ) 30 | 31 | // @title Studio管理平台API文档 32 | // @version 1.0 33 | // @description This is a Studio Api server. 34 | // @termsOfService http://swagger.io/terms/ 35 | 36 | // @contact.name API Support 37 | // @contact.url http://duguying.net/ 38 | // @contact.email rainesli@tencent.com 39 | 40 | // @license.name Apache 2.0 41 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 42 | 43 | // @BasePath /api/v1 44 | func main() { 45 | versionFlag() 46 | 47 | // 初始化 config 48 | g.Config = configuration.NewConfig(configPath) 49 | 50 | // 初始化 logger 51 | initLogger() 52 | 53 | // 初始化 ipip 54 | initIPIP() 55 | 56 | // 初始化 redis 57 | g.Cache = cache.Init(getCacheOption()) 58 | 59 | // 初始化 database 60 | orm.InitDatabase() 61 | 62 | // 初始化 swagger 63 | initSwagger() 64 | 65 | // 初始化 bleve 66 | bleve.Init() 67 | 68 | // 初始化定时任务 69 | cron.Init() 70 | 71 | // 初始化 gin 72 | service.Run(logDir) 73 | } 74 | 75 | func versionFlag() { 76 | version := flag.Bool("v", false, "version") 77 | config := flag.String("c", configPath, "configuration file") 78 | logDirectory := flag.String("l", logDir, "log directory") 79 | flag.Parse() 80 | if *version { 81 | fmt.Println("Version: " + g.Version) 82 | fmt.Println("Git Version: " + g.GitVersion) 83 | fmt.Println("Build Time: " + g.BuildTime) 84 | os.Exit(0) 85 | } 86 | 87 | if *config != "" { 88 | configPath = *config 89 | } 90 | 91 | if *logDirectory != "" { 92 | logDir = *logDirectory 93 | } 94 | } 95 | 96 | func initLogger() { 97 | expireDefault := time.Hour * 24 * 1 98 | expireStr := g.Config.Get("log", "expire", expireDefault.String()) 99 | expire, err := time.ParseDuration(expireStr) 100 | if err != nil { 101 | expire = expireDefault 102 | } 103 | level := g.Config.GetInt64("log", "level", 15) 104 | logger.InitLogger(logDir, expire, int(level)) 105 | 106 | topic := g.Config.Get("log", "topic", "studio") 107 | logFile := filepath.Join(logDir, "studio.log") 108 | g.LogEntry = rlog.NewRLog(context.Background(), topic, 109 | logFile).WithFields(map[string]interface{}{"app": "studio"}) 110 | } 111 | 112 | func initIPIP() { 113 | path := g.Config.Get("ipip", "path", "/data/ipipfree.ipdb") 114 | ipip.InitIPIP(path) 115 | } 116 | 117 | func initSwagger() { 118 | listenAddress := g.Config.Get("system", "listen", "127.0.0.1:20192") 119 | docs.SwaggerInfo.Host = listenAddress 120 | } 121 | 122 | func getCacheOption() *cache.CacheOption { 123 | readTimeout, _ := strconv.Atoi(g.Config.Get("redis", "timeout", "4")) 124 | db, _ := strconv.Atoi(g.Config.Get("redis", "db", "11")) 125 | return &cache.CacheOption{ 126 | Type: g.Config.Get("cache", "type", "bolt"), 127 | Redis: &cache.CacheRedisOption{ 128 | Timeout: readTimeout, 129 | DB: db, 130 | Addr: g.Config.Get("redis", "addr", ""), 131 | Password: g.Config.Get("redis", "password", ""), 132 | PoolSize: int(g.Config.GetInt64("redis", "pool-size", 1000)), 133 | }, 134 | BoltPath: g.Config.Get("cache", "path", "cache/cache.db"), 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /service/models/response.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019. All rights reserved. 2 | // This file is part of duguying project 3 | // I am coding in Tencent 4 | // Created by rainesli on 2019/8/29. 5 | 6 | package models 7 | 8 | type CommonResponse struct { 9 | Ok bool `json:"ok"` 10 | Msg string `json:"msg"` 11 | } 12 | 13 | type CommonCreateResponse struct { 14 | Ok bool `json:"ok"` 15 | Msg string `json:"msg"` 16 | ID uint `json:"id"` 17 | } 18 | 19 | type CommonListResponse struct { 20 | Ok bool `json:"ok"` 21 | Msg string `json:"msg"` 22 | Total uint `json:"total"` 23 | List interface{} `json:"list"` 24 | } 25 | 26 | type CommonSearchListResponse struct { 27 | Ok bool `json:"ok"` 28 | Msg string `json:"msg"` 29 | Total uint `json:"total"` 30 | List interface{} `json:"list"` 31 | } 32 | 33 | type ArticleContentListResponse struct { 34 | Ok bool `json:"ok"` 35 | Msg string `json:"msg"` 36 | Total uint `json:"total"` 37 | List []*ArticleShowContent `json:"list"` 38 | } 39 | 40 | type ArticleTitleListResponse struct { 41 | Ok bool `json:"ok"` 42 | Msg string `json:"msg"` 43 | Total uint `json:"total"` 44 | List []*ArticleTitle `json:"list"` 45 | } 46 | 47 | type ArticleAdminTitleListResponse struct { 48 | Ok bool `json:"ok"` 49 | Msg string `json:"msg"` 50 | Total uint `json:"total"` 51 | List []*ArticleAdminTitle `json:"list"` 52 | } 53 | 54 | type ArticleArchListResponse struct { 55 | Ok bool `json:"ok"` 56 | Msg string `json:"msg"` 57 | List []*ArchInfo `json:"list"` 58 | } 59 | 60 | type ArticleShowContentGetResponse struct { 61 | Ok bool `json:"ok"` 62 | Msg string `json:"msg"` 63 | Data *ArticleShowContent `json:"data"` 64 | } 65 | 66 | type ArticleContentGetResponse struct { 67 | Ok bool `json:"ok"` 68 | Msg string `json:"msg"` 69 | Data *ArticleContent `json:"data"` 70 | } 71 | 72 | type ArticleContentMD5 struct { 73 | ID int `json:"id"` 74 | MD5 string `json:"md5"` 75 | } 76 | 77 | type ArticleContentMD5Response struct { 78 | Ok bool `json:"ok"` 79 | Msg string `json:"msg"` 80 | Data *ArticleContentMD5 `json:"data"` 81 | } 82 | 83 | type UserInfoResponse struct { 84 | Ok bool `json:"ok"` 85 | Msg string `json:"msg"` 86 | Data *UserInfo `json:"data"` 87 | } 88 | 89 | type LoginResponse struct { 90 | Ok bool `json:"ok"` 91 | Msg string `json:"msg"` 92 | Sid string `json:"sid"` 93 | } 94 | 95 | type UploadResponse struct { 96 | Ok bool `json:"ok"` 97 | Msg string `json:"msg"` 98 | URL string `json:"url"` 99 | Name string `json:"name"` 100 | } 101 | 102 | type FileAdminListResponse struct { 103 | Ok bool `json:"ok"` 104 | Msg string `json:"msg"` 105 | Total int `json:"total"` 106 | List []*File `json:"list"` 107 | } 108 | 109 | type FileLsResponse struct { 110 | Ok bool `json:"ok"` 111 | Msg string `json:"msg"` 112 | List []*FsItem `json:"list"` 113 | } 114 | 115 | type ListUserLoginHistoryResponse struct { 116 | Ok bool `json:"ok"` 117 | Msg string `json:"msg"` 118 | Total int `json:"total"` 119 | List []*LoginHistory `json:"list"` 120 | } 121 | 122 | type ListMediaFileResponse struct { 123 | Ok bool `json:"ok"` 124 | Msg string `json:"msg"` 125 | List []*MediaFile `json:"list"` 126 | } 127 | 128 | type MediaDetailResponse struct { 129 | Ok bool `json:"ok"` 130 | Msg string `json:"msg"` 131 | Data *MediaFile `json:"data"` 132 | } 133 | 134 | type CoverListResponse struct { 135 | Ok bool `json:"ok"` 136 | Msg string `json:"msg"` 137 | List []*Cover `json:"list"` 138 | } 139 | -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WORKSPACE=$(cd $(dirname $0)/; pwd) 4 | cd $WORKSPACE 5 | 6 | mkdir -p var 7 | 8 | app=studio 9 | pidfile=var/app.pid 10 | logfile=var/app.log 11 | 12 | function check_pid() { 13 | if [ -f $pidfile ];then 14 | pid=`cat $pidfile` 15 | if [ -n $pid ]; then 16 | running=`ps -p $pid|grep -v "PID TTY" |wc -l` 17 | return $running 18 | fi 19 | fi 20 | return 0 21 | } 22 | 23 | function tagversion() { 24 | git tag -l --sort=v:refname | tail -1 25 | } 26 | 27 | function build() { 28 | version=`tagversion` 29 | gitversion=`git log --format='%h' | head -1` 30 | buildtime=`date +%Y-%m-%d_%H:%M:%S` 31 | export GOPROXY=https://goproxy.cn,direct 32 | export GO111MODULE=on 33 | go mod download 34 | GOOS=$1 GOARCH=$2 go build -tags=jsoniter -ldflags "-X duguying/$app/g.GitVersion=$gitversion -X duguying/$app/g.BuildTime=$buildtime -X duguying/$app/g.Version=$version" -o $app . 35 | } 36 | 37 | function pack() { 38 | version=`git tag | head -1` 39 | rm -rf dist/ 40 | mkdir -p dist/${app}/bin 41 | mkdir -p dist/${app}/etc 42 | cp studio dist/${app}/bin 43 | cp control dist/${app} 44 | cp etc/17monipdb.datx dist/${app}/etc 45 | cd dist 46 | zip -r release-${version}.zip ${app}/* 47 | cd .. 48 | } 49 | 50 | function start() { 51 | check_pid 52 | running=$? 53 | if [ $running -gt 0 ];then 54 | echo -n "$app now is running already, pid=" 55 | cat $pidfile 56 | return 1 57 | fi 58 | 59 | 60 | nohup ./$app >> $logfile & 61 | echo $! > $pidfile 62 | echo "$app started..., pid=$!" 63 | } 64 | 65 | function stop() { 66 | pid=`cat $pidfile` 67 | kill $pid 68 | echo "$app stoped..." 69 | } 70 | 71 | function restart() { 72 | stop 73 | sleep 1 74 | start 75 | } 76 | 77 | function status() { 78 | check_pid 79 | running=$? 80 | if [ $running -gt 0 ];then 81 | echo started 82 | else 83 | echo stoped 84 | fi 85 | } 86 | 87 | function proto(){ 88 | echo "compile protobuf..." 89 | protoc -I=./service/message/proto --go_out=./service/message/model ./service/message/proto/*.proto 90 | echo "compile finished." 91 | } 92 | 93 | function doc() { 94 | swag init 95 | } 96 | 97 | function docker_prebuild() { 98 | build `go env GOOS` `go env GOARCH` 99 | rm -rf dockerdist 100 | mkdir -p dockerdist 101 | cp ./studio ./dockerdist 102 | cp ./setenv ./dockerdist 103 | cp ./ipipfree.ipdb ./dockerdist 104 | } 105 | 106 | function docker_build() { 107 | build `go env GOOS` `go env GOARCH` 108 | rm -rf dockerdist 109 | mkdir -p dockerdist 110 | cp ./studio ./dockerdist 111 | cp ./setenv ./dockerdist 112 | cp ./ipipfree.ipdb ./dockerdist 113 | image=duguying/studio 114 | version=`tagversion` 115 | docker build -t $image -t $image:$version . 116 | docker push $image:$version 117 | docker push $image:latest 118 | } 119 | 120 | function docker_tags() { 121 | version=`tagversion` 122 | echo -n $version",latest" > .tags 123 | cat .tags 124 | } 125 | 126 | function pull_tags() { 127 | git fetch --tags 128 | ls -al 129 | } 130 | 131 | function tailf() { 132 | tail -f $logfile 133 | } 134 | 135 | function help() { 136 | echo "$0 build|pack|start|stop|restart|status|tail|docker|ptag" 137 | } 138 | 139 | if [ "$1" == "" ]; then 140 | help 141 | elif [ "$1" == "build" ];then 142 | build `go env GOOS` `go env GOARCH` 143 | elif [ "$1" == "doc" ];then 144 | doc 145 | elif [ "$1" == "stop" ];then 146 | stop 147 | elif [ "$1" == "start" ];then 148 | start 149 | elif [ "$1" == "restart" ];then 150 | restart 151 | elif [ "$1" == "status" ];then 152 | status 153 | elif [ "$1" == "tail" ];then 154 | tailf 155 | elif [ "$1" == "pack" ];then 156 | pack 157 | elif [ "$1" == "proto" ];then 158 | proto 159 | elif [ "$1" == "prebuild" ];then 160 | docker_prebuild 161 | elif [ "$1" == "dtag" ];then 162 | docker_tags 163 | elif [ "$1" == "ptag" ];then 164 | pull_tags 165 | elif [ "$1" == "docker" ];then 166 | docker_build 167 | else 168 | help 169 | fi 170 | -------------------------------------------------------------------------------- /modules/cache/bolt.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | ujson "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/boltdb/bolt" 9 | "github.com/gogather/json" 10 | ) 11 | 12 | // BoltCache 基于bolt实现的kv缓存 13 | type BoltCache struct { 14 | db *bolt.DB 15 | bucketName string 16 | disabled bool 17 | } 18 | 19 | type boltCacheItem struct { 20 | Value string `json:"value"` 21 | CreatedAt int64 `json:"created_at"` 22 | } 23 | 24 | func NewBoltCache(path string) *BoltCache { 25 | db, err := bolt.Open(path, 0600, &bolt.Options{ 26 | Timeout: 1 * time.Second, 27 | InitialMmapSize: 1024 * 1024 * 1024 * 1, // 1G 28 | }) 29 | if err != nil { 30 | panic(err) 31 | } 32 | return NewBolt(db, "session") 33 | } 34 | 35 | // NewBolt 新建Bolt缓存实例 36 | func NewBolt(instance *bolt.DB, bucket string) *BoltCache { 37 | bc := &BoltCache{db: instance, bucketName: bucket} 38 | err := instance.Update(func(tx *bolt.Tx) error { 39 | _, err := tx.CreateBucketIfNotExists([]byte(bc.bucketName)) 40 | if err != nil { 41 | return err 42 | } 43 | return nil 44 | }) 45 | if err != nil { 46 | panic(err) 47 | } 48 | return bc 49 | } 50 | 51 | // Disable 是否禁用 52 | func (bc *BoltCache) Disable(disable bool) { 53 | bc.disabled = disable 54 | } 55 | 56 | // SetTTL 设置 57 | func (bc *BoltCache) SetTTL(key, value string, expiration time.Duration) error { 58 | if bc.disabled { 59 | return nil 60 | } 61 | 62 | err := bc.db.Update(func(tx *bolt.Tx) error { 63 | var exp *time.Time = nil 64 | if expiration > 0 { 65 | expPoint := time.Now().Add(expiration) 66 | exp = &expPoint 67 | } 68 | item := boltCacheItem{Value: value, CreatedAt: exp.Unix()} 69 | val, _ := json.Marshal(item) 70 | b, err := tx.CreateBucketIfNotExists([]byte(bc.bucketName)) 71 | if err != nil { 72 | return err 73 | } 74 | return b.Put([]byte(key), val) 75 | }) 76 | return err 77 | } 78 | 79 | // Set 设置 80 | func (bc *BoltCache) Set(key, value string) error { 81 | if bc.disabled { 82 | return nil 83 | } 84 | 85 | err := bc.db.Update(func(tx *bolt.Tx) error { 86 | item := boltCacheItem{Value: value, CreatedAt: -1} 87 | b, err := tx.CreateBucketIfNotExists([]byte(bc.bucketName)) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | existValue := b.Get([]byte(key)) 93 | if existValue != nil { 94 | itemExist := boltCacheItem{} 95 | err = ujson.Unmarshal(existValue, &itemExist) 96 | if err == nil { 97 | item.Value = itemExist.Value 98 | item.CreatedAt = itemExist.CreatedAt 99 | } 100 | } 101 | 102 | val, _ := json.Marshal(item) 103 | return b.Put([]byte(key), val) 104 | }) 105 | return err 106 | } 107 | 108 | // Get 获取 109 | func (bc *BoltCache) Get(key string) (val string, err error) { 110 | if bc.disabled { 111 | return "", fmt.Errorf("bolt cache disabled") 112 | } 113 | 114 | exist := false 115 | err = bc.db.View(func(tx *bolt.Tx) error { 116 | b := tx.Bucket([]byte(bc.bucketName)) 117 | value := b.Get([]byte(key)) 118 | if value == nil { 119 | exist = false 120 | val = "" 121 | return nil 122 | } 123 | 124 | // check expiration 125 | item := boltCacheItem{} 126 | err := ujson.Unmarshal(value, &item) 127 | if err != nil { 128 | exist = false 129 | val = "" 130 | return nil 131 | } 132 | 133 | // no expiration 134 | if item.CreatedAt <= 0 { 135 | exist = true 136 | val = item.Value 137 | return nil 138 | } 139 | 140 | // get value with calc expiration 141 | if time.Unix(item.CreatedAt, 0).After(time.Now()) { 142 | exist = true 143 | val = item.Value 144 | return nil 145 | } 146 | 147 | // expired 148 | exist = false 149 | val = "" 150 | 151 | return nil 152 | }) 153 | if err != nil { 154 | return "", err 155 | } 156 | if exist { 157 | return val, nil 158 | } else { 159 | return val, fmt.Errorf("not exist") 160 | } 161 | } 162 | 163 | // Delete 删除 164 | func (bc *BoltCache) Delete(key string) error { 165 | if bc.disabled { 166 | return fmt.Errorf("bolt cache disabled") 167 | } 168 | 169 | err := bc.db.Update(func(tx *bolt.Tx) error { 170 | b, err := tx.CreateBucketIfNotExists([]byte(bc.bucketName)) 171 | if err != nil { 172 | return err 173 | } 174 | return b.Delete([]byte(key)) 175 | }) 176 | if err != nil { 177 | return err 178 | } 179 | return nil 180 | } 181 | -------------------------------------------------------------------------------- /service/action/deployer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of duguying project 3 | // Created by duguying on 2018/8/10. 4 | 5 | package action 6 | 7 | import ( 8 | "archive/tar" 9 | "compress/gzip" 10 | "duguying/studio/g" 11 | "fmt" 12 | "github.com/gin-gonic/gin" 13 | "github.com/gogather/com" 14 | "io" 15 | "log" 16 | "net/http" 17 | "os" 18 | "path/filepath" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | func CheckToken(c *gin.Context) { 24 | token := g.Config.Get("deployer", "token", "") 25 | reqToken := c.GetHeader("Token") 26 | if token == reqToken { 27 | c.Next() 28 | } else { 29 | c.JSON(http.StatusForbidden, gin.H{ 30 | "ok": false, 31 | "err": "auth failed", 32 | }) 33 | c.Abort() 34 | } 35 | return 36 | } 37 | 38 | func PackageUpload(c *gin.Context) { 39 | appName := c.GetHeader("name") 40 | appPath := g.Config.Get("deployer", fmt.Sprintf("%s-path", appName), "") 41 | fh, err := c.FormFile("file") 42 | if err != nil { 43 | log.Println("get form file failed,", err.Error()) 44 | c.JSON(http.StatusOK, gin.H{ 45 | "ok": false, 46 | "err": err.Error(), 47 | }) 48 | return 49 | } 50 | 51 | // 检查 tar.gz 是否已经存在,若已存在则可能正在部署,停止此次部署 52 | fpath := fmt.Sprintf("%s.%s", appPath, "tar.gz") 53 | if com.FileExist(fpath) { 54 | log.Println("tgz file exist, maybe someone else is deploying, deploy stopped.") 55 | c.JSON(http.StatusOK, gin.H{ 56 | "ok": false, 57 | "err": "tgz文件已存在", 58 | }) 59 | return 60 | } 61 | 62 | // 检查旧版展开目录是否已经存在,若已经存在则备份 63 | if com.FileExist(appPath) { 64 | os.Rename(appPath, fmt.Sprintf("%s.%s", appPath, time.Now().Format("20060102150405"))) 65 | } 66 | 67 | // 检查上传文件是否为 tar.gz 后缀,不是则终止 68 | if !strings.HasSuffix(fh.Filename, ".tar.gz") { 69 | c.JSON(http.StatusOK, gin.H{ 70 | "ok": false, 71 | "err": "invalid file type", 72 | }) 73 | return 74 | } 75 | 76 | // 创建待存储文件 77 | f, err := os.Create(fpath) 78 | if err != nil { 79 | log.Println("create file failed,", err.Error()) 80 | c.JSON(http.StatusOK, gin.H{ 81 | "ok": false, 82 | "err": err.Error(), 83 | }) 84 | return 85 | } 86 | 87 | // 打开上传文件流 88 | hFile, err := fh.Open() 89 | if err != nil { 90 | c.JSON(http.StatusOK, gin.H{ 91 | "ok": false, 92 | "err": err.Error(), 93 | }) 94 | return 95 | } 96 | defer hFile.Close() 97 | 98 | // 报存文件 99 | _, err = io.Copy(f, hFile) 100 | if err != nil { 101 | log.Println("copy file failed,", err.Error()) 102 | c.JSON(http.StatusOK, gin.H{ 103 | "ok": false, 104 | "err": err.Error(), 105 | }) 106 | return 107 | } 108 | 109 | f.Close() 110 | 111 | // unzip file 112 | err = untgz(fpath, strings.TrimSuffix(fpath, ".tar.gz")) 113 | if err != nil { 114 | log.Println("untgz failed,", err.Error()) 115 | c.JSON(http.StatusOK, gin.H{ 116 | "ok": false, 117 | "err": err.Error(), 118 | }) 119 | } else { 120 | c.JSON(http.StatusOK, gin.H{ 121 | "ok": true, 122 | }) 123 | } 124 | 125 | // 移除 tar.gz 包 126 | err = os.Remove(fpath) 127 | if err != nil { 128 | log.Println("remove tgz failed,", err.Error()) 129 | c.JSON(http.StatusOK, gin.H{ 130 | "ok": false, 131 | "err": err.Error(), 132 | }) 133 | } 134 | 135 | return 136 | 137 | } 138 | 139 | func untgz(tarFile, dest string) error { 140 | srcFile, err := os.Open(tarFile) 141 | if err != nil { 142 | return err 143 | } 144 | defer srcFile.Close() 145 | gr, err := gzip.NewReader(srcFile) 146 | if err != nil { 147 | return err 148 | } 149 | defer gr.Close() 150 | tr := tar.NewReader(gr) 151 | for { 152 | hdr, err := tr.Next() 153 | if err != nil { 154 | if err == io.EOF { 155 | break 156 | } else { 157 | return err 158 | } 159 | } 160 | filename := filepath.Join(dest, hdr.Name) 161 | file, err := createFile(filename, os.FileMode(hdr.Mode), hdr.FileInfo().IsDir()) 162 | if err != nil { 163 | return err 164 | } 165 | if file != nil { 166 | io.Copy(file, tr) 167 | } 168 | } 169 | return nil 170 | } 171 | 172 | func createFile(name string, perm os.FileMode, isDir bool) (*os.File, error) { 173 | if isDir { 174 | err := os.MkdirAll(name, perm) 175 | if err != nil { 176 | return nil, err 177 | } else { 178 | return nil, nil 179 | } 180 | } else { 181 | err := os.MkdirAll(filepath.Dir(name), perm) 182 | if err != nil { 183 | return nil, err 184 | } 185 | return os.Create(name) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /service/action/wrapper.go: -------------------------------------------------------------------------------- 1 | // Package action 业务层 2 | package action 3 | 4 | import ( 5 | "bytes" 6 | "duguying/studio/g" 7 | "duguying/studio/service/middleware" 8 | "duguying/studio/service/models" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "net/url" 14 | 15 | "duguying/studio/utils" 16 | 17 | "github.com/gin-gonic/gin" 18 | "github.com/go-errors/errors" 19 | "github.com/gogather/json" 20 | "github.com/sirupsen/logrus" 21 | ) 22 | 23 | // CustomContext 自定义web上下文(Context) 24 | type CustomContext struct { 25 | *gin.Context 26 | } 27 | 28 | // UserID 用户 ID 29 | func (cc *CustomContext) UserID() uint { 30 | return uint(cc.GetInt64("user_id")) 31 | } 32 | 33 | // Logger 获取 logger 34 | func (cc *CustomContext) Logger() *logrus.Entry { 35 | return g.LogEntry.WithContext(cc).WithField("request_id", cc.RequestID()) 36 | } 37 | 38 | // RequestID 获取 request_id 39 | func (cc *CustomContext) RequestID() string { 40 | return cc.GetString("X-RequestId") 41 | } 42 | 43 | // HandlerResponseFunc 带响应信息的处理函数 44 | type HandlerResponseFunc func(c *CustomContext) (interface{}, error) 45 | 46 | type APILog struct { 47 | Method string `json:"method"` 48 | URI string `json:"uri"` 49 | Query string `json:"query"` 50 | User string `json:"user"` 51 | SessionID string `json:"session_id"` 52 | Body string `json:"body" sql:"type:longtext"` 53 | Response string `json:"response" sql:"type:longtext"` 54 | Ok bool `json:"ok"` 55 | Trace string `json:"trace"` 56 | ClientIP string `json:"client_ip"` 57 | DomainID string `json:"domain_id,omitempty"` 58 | ServerID string `json:"server_id,omitempty"` 59 | LocationID string `json:"location_id,omitempty"` 60 | Operator string `json:"operator"` 61 | RequestID string `json:"request_id"` 62 | Cost string `json:"cost"` 63 | } 64 | 65 | func (al *APILog) ToMap() map[string]interface{} { 66 | c, _ := json.Marshal(al) 67 | out := map[string]interface{}{} 68 | _ = json.Unmarshal(c, &out) 69 | return out 70 | } 71 | 72 | // APIWrapper 带响应信息的api的action包裹器 73 | func APIWrapper(handler HandlerResponseFunc) func(c *gin.Context) { 74 | return func(c *gin.Context) { 75 | defer func() { 76 | if err := recover(); err != nil { 77 | stack := utils.Stack(3) 78 | stackInfo := fmt.Sprintf("[panic] %v\n%s", err, string(stack)) 79 | c.Set("error_trace", stackInfo) 80 | fmt.Println("panic error trace:", stackInfo) 81 | recordPanicReq(c, stackInfo) 82 | c.AbortWithStatus(http.StatusInternalServerError) 83 | } 84 | }() 85 | 86 | response, err := handler(&CustomContext{Context: c}) 87 | if err != nil { 88 | trace := fmt.Sprintf("[err] %s", getErrorTrace(err)) 89 | log.Println("error trace:", trace) 90 | c.Set("error_trace", trace) 91 | if response == nil { 92 | c.JSON(http.StatusOK, models.CommonResponse{ 93 | Ok: false, 94 | Msg: err.Error(), 95 | }) 96 | } else { 97 | c.JSON(http.StatusOK, response) 98 | } 99 | return 100 | } 101 | c.JSON(http.StatusOK, response) 102 | return 103 | } 104 | } 105 | 106 | func recordPanicReq(c *gin.Context, stack string) { 107 | uri := "" 108 | u, err := url.ParseRequestURI(c.Request.RequestURI) 109 | if err != nil { 110 | uri = c.Request.RequestURI 111 | } else { 112 | uri = u.Path 113 | } 114 | 115 | rl := middleware.RequestLog{ 116 | Method: c.Request.Method, 117 | URI: uri, 118 | Query: c.Request.URL.RawQuery, 119 | Headers: c.Request.Header, 120 | ClientIP: c.ClientIP(), 121 | } 122 | 123 | buf, err := ioutil.ReadAll(c.Request.Body) 124 | if err != nil { 125 | g.LogEntry.WithField("slice", "request").Println("read body error:", err.Error()) 126 | } 127 | rdr2 := ioutil.NopCloser(bytes.NewBuffer(buf)) 128 | c.Request.Body = rdr2 129 | body := string(buf) 130 | rl.Body = body 131 | 132 | // store api log 133 | apiLog := &APILog{ 134 | Method: rl.Method, 135 | URI: rl.URI, 136 | Query: rl.Query, 137 | User: c.GetString("user"), 138 | SessionID: c.GetString("sid"), 139 | Body: rl.Body, 140 | Ok: false, 141 | Trace: stack, 142 | Operator: c.GetHeader("X-From"), 143 | RequestID: c.GetHeader("X-RequestId"), 144 | ClientIP: c.ClientIP(), 145 | } 146 | 147 | g.LogEntry.WithFields(apiLog.ToMap()).Println() 148 | } 149 | 150 | func getErrorTrace(err error) (trace string) { 151 | e, ok := err.(*errors.Error) 152 | if ok { 153 | trace = trace + e.ErrorStack() 154 | } else { 155 | trace = trace + fmt.Sprintf("%v", err) 156 | } 157 | return trace 158 | } 159 | -------------------------------------------------------------------------------- /service/message/model/hb.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.0 4 | // protoc v3.14.0 5 | // source: hb.proto 6 | 7 | package model 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | // cmd 0 24 | type HeartBeat struct { 25 | state protoimpl.MessageState 26 | sizeCache protoimpl.SizeCache 27 | unknownFields protoimpl.UnknownFields 28 | 29 | Timestamp uint64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` 30 | } 31 | 32 | func (x *HeartBeat) Reset() { 33 | *x = HeartBeat{} 34 | if protoimpl.UnsafeEnabled { 35 | mi := &file_hb_proto_msgTypes[0] 36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 37 | ms.StoreMessageInfo(mi) 38 | } 39 | } 40 | 41 | func (x *HeartBeat) String() string { 42 | return protoimpl.X.MessageStringOf(x) 43 | } 44 | 45 | func (*HeartBeat) ProtoMessage() {} 46 | 47 | func (x *HeartBeat) ProtoReflect() protoreflect.Message { 48 | mi := &file_hb_proto_msgTypes[0] 49 | if protoimpl.UnsafeEnabled && x != nil { 50 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 51 | if ms.LoadMessageInfo() == nil { 52 | ms.StoreMessageInfo(mi) 53 | } 54 | return ms 55 | } 56 | return mi.MessageOf(x) 57 | } 58 | 59 | // Deprecated: Use HeartBeat.ProtoReflect.Descriptor instead. 60 | func (*HeartBeat) Descriptor() ([]byte, []int) { 61 | return file_hb_proto_rawDescGZIP(), []int{0} 62 | } 63 | 64 | func (x *HeartBeat) GetTimestamp() uint64 { 65 | if x != nil { 66 | return x.Timestamp 67 | } 68 | return 0 69 | } 70 | 71 | var File_hb_proto protoreflect.FileDescriptor 72 | 73 | var file_hb_proto_rawDesc = []byte{ 74 | 0x0a, 0x08, 0x68, 0x62, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x6d, 0x6f, 0x64, 0x65, 75 | 0x6c, 0x22, 0x29, 0x0a, 0x09, 0x48, 0x65, 0x61, 0x72, 0x74, 0x42, 0x65, 0x61, 0x74, 0x12, 0x1c, 76 | 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 77 | 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, 0x0a, 0x5a, 0x08, 78 | 0x2e, 0x2e, 0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 79 | } 80 | 81 | var ( 82 | file_hb_proto_rawDescOnce sync.Once 83 | file_hb_proto_rawDescData = file_hb_proto_rawDesc 84 | ) 85 | 86 | func file_hb_proto_rawDescGZIP() []byte { 87 | file_hb_proto_rawDescOnce.Do(func() { 88 | file_hb_proto_rawDescData = protoimpl.X.CompressGZIP(file_hb_proto_rawDescData) 89 | }) 90 | return file_hb_proto_rawDescData 91 | } 92 | 93 | var file_hb_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 94 | var file_hb_proto_goTypes = []interface{}{ 95 | (*HeartBeat)(nil), // 0: model.HeartBeat 96 | } 97 | var file_hb_proto_depIdxs = []int32{ 98 | 0, // [0:0] is the sub-list for method output_type 99 | 0, // [0:0] is the sub-list for method input_type 100 | 0, // [0:0] is the sub-list for extension type_name 101 | 0, // [0:0] is the sub-list for extension extendee 102 | 0, // [0:0] is the sub-list for field type_name 103 | } 104 | 105 | func init() { file_hb_proto_init() } 106 | func file_hb_proto_init() { 107 | if File_hb_proto != nil { 108 | return 109 | } 110 | if !protoimpl.UnsafeEnabled { 111 | file_hb_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 112 | switch v := v.(*HeartBeat); i { 113 | case 0: 114 | return &v.state 115 | case 1: 116 | return &v.sizeCache 117 | case 2: 118 | return &v.unknownFields 119 | default: 120 | return nil 121 | } 122 | } 123 | } 124 | type x struct{} 125 | out := protoimpl.TypeBuilder{ 126 | File: protoimpl.DescBuilder{ 127 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 128 | RawDescriptor: file_hb_proto_rawDesc, 129 | NumEnums: 0, 130 | NumMessages: 1, 131 | NumExtensions: 0, 132 | NumServices: 0, 133 | }, 134 | GoTypes: file_hb_proto_goTypes, 135 | DependencyIndexes: file_hb_proto_depIdxs, 136 | MessageInfos: file_hb_proto_msgTypes, 137 | }.Build() 138 | File_hb_proto = out.File 139 | file_hb_proto_rawDesc = nil 140 | file_hb_proto_goTypes = nil 141 | file_hb_proto_depIdxs = nil 142 | } 143 | -------------------------------------------------------------------------------- /modules/orm/context_logger.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "gorm.io/gorm/logger" 11 | "gorm.io/gorm/utils" 12 | ) 13 | 14 | // Colors 15 | const ( 16 | Reset = "\033[0m" 17 | Red = "\033[31m" 18 | Green = "\033[32m" 19 | Yellow = "\033[33m" 20 | Blue = "\033[34m" 21 | Magenta = "\033[35m" 22 | Cyan = "\033[36m" 23 | White = "\033[37m" 24 | MagentaBold = "\033[35;1m" 25 | RedBold = "\033[31;1m" 26 | YellowBold = "\033[33;1m" 27 | ) 28 | 29 | type Config struct { 30 | SlowThreshold time.Duration 31 | Colorful bool 32 | LogLevel logger.LogLevel 33 | } 34 | 35 | var Default = New(Config{ 36 | SlowThreshold: 100 * time.Millisecond, 37 | LogLevel: logger.Warn, 38 | Colorful: true, 39 | }) 40 | 41 | type slogger struct { 42 | Config 43 | infoStr, warnStr, errStr string 44 | traceStr, traceErrStr, traceWarnStr string 45 | } 46 | 47 | // LogMode log mode 48 | func (l *slogger) LogMode(level logger.LogLevel) logger.Interface { 49 | newlogger := *l 50 | newlogger.LogLevel = level 51 | return &newlogger 52 | } 53 | 54 | func (l slogger) Printf(ctx context.Context, format string, args ...interface{}) { 55 | reqPrefix := "" 56 | reqid, ok := ctx.Value("reqid").(string) 57 | if ok { 58 | reqPrefix = fmt.Sprintf("[%s] ", reqid) 59 | } 60 | 61 | logseg := fmt.Sprintf(format, args...) 62 | buf, ok := ctx.Value("lbw").(*bytes.Buffer) 63 | if ok { 64 | buf.WriteString(logseg + "\n") 65 | } 66 | 67 | sqlog := "" 68 | segs := strings.Split(logseg, "\n") 69 | for _, seg := range segs { 70 | sqlog = sqlog + fmt.Sprintln(reqPrefix+seg) 71 | } 72 | 73 | fmt.Print(sqlog) 74 | } 75 | 76 | // Info print info 77 | func (l slogger) Info(ctx context.Context, msg string, data ...interface{}) { 78 | if l.LogLevel >= logger.Info { 79 | l.Printf(ctx, l.infoStr+msg, append([]interface{}{utils.FileWithLineNum()}, data...)...) 80 | } 81 | } 82 | 83 | // Warn print warn messages 84 | func (l slogger) Warn(ctx context.Context, msg string, data ...interface{}) { 85 | if l.LogLevel >= logger.Warn { 86 | l.Printf(ctx, l.warnStr+msg, append([]interface{}{utils.FileWithLineNum()}, data...)...) 87 | } 88 | } 89 | 90 | // Error print error messages 91 | func (l slogger) Error(ctx context.Context, msg string, data ...interface{}) { 92 | if l.LogLevel >= logger.Error { 93 | l.Printf(ctx, l.errStr+msg, append([]interface{}{utils.FileWithLineNum()}, data...)...) 94 | } 95 | } 96 | 97 | // Trace print sql message 98 | func (l slogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) { 99 | if l.LogLevel > 0 { 100 | elapsed := time.Since(begin) 101 | switch { 102 | case err != nil && l.LogLevel >= logger.Error: 103 | sql, rows := fc() 104 | l.Printf(ctx, l.traceErrStr, utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, rows, sql) 105 | case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && l.LogLevel >= logger.Warn: 106 | sql, rows := fc() 107 | l.Printf(ctx, l.traceWarnStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, rows, sql) 108 | case l.LogLevel >= logger.Info: 109 | sql, rows := fc() 110 | l.Printf(ctx, l.traceStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, rows, sql) 111 | } 112 | } 113 | } 114 | 115 | func New(config Config) logger.Interface { 116 | var ( 117 | infoStr = "%s [info] " 118 | warnStr = "%s [warn] " 119 | errStr = "%s [error] " 120 | traceStr = "%s [%v] [rows:%d] %s" 121 | traceWarnStr = "%s [%v] [rows:%d] %s" 122 | traceErrStr = "%s %s [%v] [rows:%d] %s" 123 | ) 124 | 125 | if config.Colorful { 126 | infoStr = Green + "%s\n" + Reset + Green + "[info] " + Reset 127 | warnStr = Blue + "%s\n" + Reset + Magenta + "[warn] " + Reset 128 | errStr = Magenta + "%s\n" + Reset + Red + "[error] " + Reset 129 | traceStr = Green + "%s\n" + Reset + Yellow + "[%.3fms] " + Blue + "[rows:%d]" + Reset + " %s" 130 | traceWarnStr = Green + "%s\n" + Reset + RedBold + "[%.3fms] " + Yellow + "[rows:%d]" + Magenta + " %s" + Reset 131 | traceErrStr = RedBold + "%s " + MagentaBold + "%s\n" + Reset + Yellow + "[%.3fms] " + Blue + "[rows:%d]" + Reset + " %s" 132 | } 133 | 134 | return &slogger{ 135 | // Writer: writer, 136 | Config: config, 137 | infoStr: infoStr, 138 | warnStr: warnStr, 139 | errStr: errStr, 140 | traceStr: traceStr, 141 | traceWarnStr: traceWarnStr, 142 | traceErrStr: traceErrStr, 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /service/middleware/restlog.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019. All rights reserved. 2 | // This file is part of sparta-admin project 3 | // I am coding in Tencent 4 | // Created by rainesli on 2019/3/19. 5 | 6 | // Package middleware gin中间件 7 | package middleware 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "io/ioutil" 13 | "net/http" 14 | "net/url" 15 | "time" 16 | 17 | "duguying/studio/g" 18 | "duguying/studio/service/models" 19 | 20 | "github.com/gin-gonic/gin" 21 | "github.com/gogather/json" 22 | "github.com/google/uuid" 23 | ) 24 | 25 | // RequestLog 请求日志结构 26 | type RequestLog struct { 27 | Method string `json:"method"` 28 | URI string `json:"uri"` 29 | Query string `json:"query"` 30 | Headers http.Header `json:"headers"` 31 | Body string `json:"body"` 32 | ClientIP string `json:"client_ip"` 33 | } 34 | 35 | // String 序列化 36 | func (rl *RequestLog) String() string { 37 | c, _ := json.Marshal(rl) 38 | return string(c) 39 | } 40 | 41 | type bodyLogWriter struct { 42 | gin.ResponseWriter 43 | body *bytes.Buffer 44 | } 45 | 46 | // Write 写入 47 | func (w bodyLogWriter) Write(b []byte) (int, error) { 48 | w.body.Write(b) 49 | return w.ResponseWriter.Write(b) 50 | } 51 | 52 | // ResponseLog 响应日志 53 | type ResponseLog struct { 54 | Status int `json:"status"` 55 | Ok bool `json:"ok"` 56 | Msg string `json:"msg"` 57 | } 58 | 59 | // String 序列化 60 | func (rlog *ResponseLog) String() string { 61 | c, _ := json.Marshal(rlog) 62 | return string(c) 63 | } 64 | 65 | // RestLog Restful接口日志中间件 66 | func RestLog() gin.HandlerFunc { 67 | return func(c *gin.Context) { 68 | startTime := time.Now() 69 | startTimeMs := startTime.UnixNano() / int64(time.Millisecond) 70 | blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer} 71 | c.Writer = blw 72 | uri := "" 73 | u, err := url.ParseRequestURI(c.Request.RequestURI) 74 | if err != nil { 75 | uri = c.Request.RequestURI 76 | } else { 77 | uri = u.Path 78 | } 79 | if skipURI(uri) { 80 | c.Next() 81 | return 82 | } 83 | reqID := uuid.New().String() 84 | if c.GetHeader("X-RequestId") != "" { 85 | reqID = c.GetHeader("X-RequestId") 86 | } 87 | c.Header("X-RequestId", reqID) 88 | c.Set("X-RequestId", reqID) 89 | rl := RequestLog{ 90 | Method: c.Request.Method, 91 | URI: uri, 92 | Query: c.Request.URL.RawQuery, 93 | Headers: c.Request.Header, 94 | ClientIP: c.ClientIP(), 95 | } 96 | buf, err := ioutil.ReadAll(c.Request.Body) 97 | if err != nil { 98 | g.LogEntry.WithField("slice", "request").Println("read body error:", err.Error()) 99 | } 100 | rdr2 := ioutil.NopCloser(bytes.NewBuffer(buf)) 101 | c.Request.Body = rdr2 102 | body := string(buf) 103 | rl.Body = body 104 | g.LogEntry.WithField("slice", "request").Println("request:", rl.String()) 105 | c.Next() 106 | statusCode := c.Writer.Status() 107 | if isMethodRecord(rl.Method) { 108 | rlog := &ResponseLog{} 109 | rawBytes := blw.body.Bytes() 110 | rsp := string(rawBytes) 111 | err = json.Unmarshal(rawBytes, rlog) 112 | if err != nil { 113 | g.LogEntry.WithField("slice", "request").Println("parse response failed, err:", err.Error(), "raw:", string(rawBytes)) 114 | if len(rawBytes) > 1024*512 { 115 | rsp = fmt.Sprintf("[len:%d]", len(rawBytes)) 116 | } 117 | } else { 118 | rlog.Status = statusCode 119 | g.LogEntry.WithField("slice", "request").Println("response:", rlog.String()) 120 | } 121 | 122 | apiLog := &models.APILog{ 123 | Method: rl.Method, 124 | URI: rl.URI, 125 | Query: rl.Query, 126 | Body: rl.Body, 127 | Response: rsp, 128 | Ok: rlog.Ok, 129 | RequestID: reqID, 130 | ClientIP: c.ClientIP(), 131 | CreatedAt: time.Now(), 132 | Cost: time.Since(startTime).String(), 133 | } 134 | if g.Db.Dialector.Name() == "sqlite3" { 135 | } else { 136 | go recordLog(c, apiLog, startTimeMs) 137 | } 138 | } 139 | } 140 | } 141 | 142 | func recordLog(c *gin.Context, logInfo *models.APILog, startTime int64) { 143 | finishTime := time.Now().UnixNano() / int64(time.Millisecond) 144 | cost := finishTime - startTime 145 | g.LogEntry.WithFields(logInfo.ToMap()). 146 | Printf("cost: %s", (time.Duration(cost) * time.Millisecond).String()) 147 | } 148 | 149 | func isMethodRecord(method string) bool { 150 | if g.Config.Get("api-log", "write-only", "false") == "true" { 151 | if method != http.MethodGet && method != http.MethodOptions { 152 | return true 153 | } else { 154 | return false 155 | } 156 | } else { 157 | return true 158 | } 159 | } 160 | 161 | func skipURI(uri string) bool { 162 | skipURIMap := g.Config.GetSectionAsMap("skip-uri-log") 163 | val, ok := skipURIMap[uri] 164 | if ok && val == "enable" { 165 | return true 166 | } 167 | return false 168 | } 169 | -------------------------------------------------------------------------------- /modules/rlog/rlog.go: -------------------------------------------------------------------------------- 1 | // Package rlog rlog 2 | package rlog 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "io" 8 | "net" 9 | "os" 10 | 11 | "github.com/dogenzaka/rotator" 12 | "github.com/gogather/com" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type RemoteAdaptor interface { 17 | Report(string) error 18 | Close() 19 | } 20 | 21 | type chanWriter struct { 22 | logChan chan string 23 | allowBlock bool 24 | } 25 | 26 | // NewChanWriter 创建chanWriter 27 | func NewChanWriter(logChan chan string, allowBlock bool) *chanWriter { 28 | return &chanWriter{logChan: logChan, allowBlock: allowBlock} 29 | } 30 | 31 | // Write 写 32 | func (c *chanWriter) Write(p []byte) (n int, err error) { 33 | if c.allowBlock { 34 | // 阻塞,不建议开启:本地日志有且level小于cfg或者有业务逻辑,消费速度基本足够,有丢失可考虑增加obj数量 35 | c.logChan <- string(p) 36 | } else { 37 | // 非阻塞,可能丢数据,避免消费性能不足影响业务逻辑 38 | select { 39 | case c.logChan <- string(p): 40 | default: 41 | } 42 | } 43 | 44 | return len(p), nil 45 | } 46 | 47 | // RLog RLog 48 | type RLog struct { 49 | remoteAddr string 50 | remoteCli RemoteAdaptor 51 | remoteSendThread int 52 | logrusInstance *logrus.Logger 53 | logChan chan string 54 | writer *chanWriter 55 | enableRemote bool 56 | logFilePath string 57 | rotatorFile *rotator.SizeRotator 58 | myIP string 59 | } 60 | 61 | // NewRLog 创建RLog 62 | func NewRLog(ctx context.Context, topic string, path string) *RLog { 63 | rl := &RLog{ 64 | enableRemote: true, 65 | remoteAddr: "http://jump.duguying.net:19200", 66 | remoteSendThread: 4, 67 | logFilePath: path, 68 | } 69 | rl.initRotatorLog() 70 | 71 | rl.logrusInstance = logrus.New() 72 | rl.logrusInstance.WithContext(ctx) 73 | rl.logChan = make(chan string, 100000) 74 | rl.writer = NewChanWriter(rl.logChan, false) 75 | 76 | multiWriter := io.MultiWriter(rl.writer, rl.rotatorFile) 77 | rl.logrusInstance.SetOutput(multiWriter) 78 | rl.logrusInstance.SetFormatter(&logrus.JSONFormatter{}) 79 | rl.logrusInstance.SetReportCaller(false) 80 | 81 | lineNumHook := &lineNumberHook{} 82 | lineNumHook.SetShortPath(true) 83 | rl.logrusInstance.AddHook(lineNumHook) 84 | 85 | if rl.enableRemote { 86 | err := rl.initRemoteClient(topic) 87 | if err != nil { 88 | fmt.Println("init remote client err:", err) 89 | return nil 90 | } 91 | } 92 | rl.initChanReader() 93 | 94 | return rl 95 | } 96 | 97 | // WithFields 添加属性 98 | func (rl *RLog) WithFields(fields map[string]interface{}) (entry *logrus.Entry) { 99 | return rl.logrusInstance.WithFields(fields) 100 | } 101 | 102 | func (rl *RLog) initRotatorLog() { 103 | err := com.WriteFileAppendWithCreatePath(rl.logFilePath, "") 104 | if err != nil { 105 | fmt.Println("create local log file failed, err:", err) 106 | return 107 | } 108 | rl.rotatorFile = rotator.NewSizeRotator(rl.logFilePath) 109 | rl.rotatorFile.MaxRotation = 99 // 99 files 110 | rl.rotatorFile.RotationSize = int64(1024 * 1024 * 1) // 100M 111 | } 112 | 113 | // Close 关闭日志 114 | func (rl *RLog) Close() { 115 | close(rl.logChan) 116 | rl.closeRotatorLog() 117 | if rl.enableRemote { 118 | rl.closeRemote() 119 | } 120 | } 121 | 122 | func (rl *RLog) closeRotatorLog() { 123 | if rl.rotatorFile != nil { 124 | err := rl.rotatorFile.Close() 125 | if err != nil { 126 | fmt.Println("close rotator file failed, err:", err.Error()) 127 | } 128 | } 129 | } 130 | 131 | func (rl *RLog) closeRemote() { 132 | rl.remoteCli.Close() 133 | } 134 | 135 | // SetZhiyanEnable 设置开启Zhiyan日志 136 | func (rl *RLog) SetZhiyanEnable(enable bool) { 137 | rl.enableRemote = enable 138 | if !enable { 139 | rl.remoteCli = nil 140 | } 141 | } 142 | 143 | func (rl *RLog) initChanReader() { 144 | for i := 0; i < rl.remoteSendThread; i++ { 145 | go func() { 146 | for { 147 | select { 148 | case line := <-rl.logChan: 149 | { 150 | rl.send(line) 151 | } 152 | } 153 | } 154 | }() 155 | } 156 | } 157 | 158 | func (rl *RLog) send(line string) { 159 | err := rl.sendRemoteMessage(line) 160 | if err != nil { 161 | fmt.Println("send remote failed, err:", err.Error()) 162 | } 163 | } 164 | 165 | func (rl *RLog) initRemoteClient(topic string) (err error) { 166 | myIP := rl.getIPAddr() 167 | rl.myIP = myIP 168 | 169 | rl.remoteCli, err = NewEsAdaptor(rl.remoteAddr, topic) 170 | if err != nil { 171 | return err 172 | } 173 | return nil 174 | } 175 | 176 | func (rl *RLog) sendRemoteMessage(msg string) error { 177 | if rl.remoteCli != nil { 178 | return rl.remoteCli.Report(msg) 179 | } 180 | return nil 181 | } 182 | 183 | func (rl *RLog) getIPAddr() string { 184 | addrs, err := net.InterfaceAddrs() 185 | 186 | if err != nil { 187 | fmt.Println(err) 188 | os.Exit(1) 189 | } 190 | 191 | for _, address := range addrs { 192 | // 检查ip地址判断是否回环地址 193 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 194 | if ipnet.IP.To4() != nil { 195 | return ipnet.IP.String() 196 | } 197 | } 198 | } 199 | return "" 200 | } 201 | -------------------------------------------------------------------------------- /modules/dbmodels/file.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2017/11/2. 4 | 5 | package dbmodels 6 | 7 | import ( 8 | "database/sql/driver" 9 | "duguying/studio/service/models" 10 | "duguying/studio/utils" 11 | "fmt" 12 | "reflect" 13 | "strconv" 14 | "time" 15 | 16 | "github.com/gogather/json" 17 | ) 18 | 19 | const ( 20 | LOCAL StorageType = 0 21 | OSS StorageType = 1 22 | 23 | FileTypeUnknown FileType = 0 24 | FileTypeImage FileType = 1 25 | FileTypeVideo FileType = 2 26 | FileTypeArchive FileType = 3 27 | 28 | RecognizeNotNeed RecognizeStatus = 0 29 | RecognizeDone RecognizeStatus = 1 30 | ) 31 | 32 | var ( 33 | FileTypeMap = map[FileType]string{ 34 | FileTypeUnknown: "unknown", 35 | FileTypeImage: "image", 36 | FileTypeVideo: "video", 37 | FileTypeArchive: "archive", 38 | } 39 | ) 40 | 41 | type StorageType int64 42 | 43 | func (tt StorageType) Value() (driver.Value, error) { 44 | return int64(tt), nil 45 | } 46 | 47 | func (tt *StorageType) Scan(value interface{}) error { 48 | val, ok := value.(int64) 49 | if !ok { 50 | switch reflect.TypeOf(value).String() { 51 | case "[]uint8": 52 | { 53 | ba := []byte{} 54 | for _, b := range value.([]uint8) { 55 | ba = append(ba, byte(b)) 56 | } 57 | val, _ = strconv.ParseInt(string(ba), 10, 64) 58 | break 59 | } 60 | default: 61 | { 62 | return fmt.Errorf("value: %v, is not int, is %s", value, reflect.TypeOf(value)) 63 | } 64 | } 65 | } 66 | *tt = StorageType(val) 67 | return nil 68 | } 69 | 70 | // --------- 71 | 72 | type FileType int64 73 | 74 | func (tt FileType) Value() (driver.Value, error) { 75 | return int64(tt), nil 76 | } 77 | 78 | func (tt *FileType) Scan(value interface{}) error { 79 | val, ok := value.(int64) 80 | if !ok { 81 | switch reflect.TypeOf(value).String() { 82 | case "[]uint8": 83 | { 84 | ba := []byte{} 85 | for _, b := range value.([]uint8) { 86 | ba = append(ba, byte(b)) 87 | } 88 | val, _ = strconv.ParseInt(string(ba), 10, 64) 89 | break 90 | } 91 | default: 92 | { 93 | return fmt.Errorf("value: %v, is not int, is %s", value, reflect.TypeOf(value)) 94 | } 95 | } 96 | } 97 | *tt = FileType(val) 98 | return nil 99 | } 100 | 101 | // --------- 102 | 103 | type RecognizeStatus int64 104 | 105 | func (rs RecognizeStatus) Value() (driver.Value, error) { 106 | return int64(rs), nil 107 | } 108 | 109 | func (rs *RecognizeStatus) Scan(value interface{}) error { 110 | val, ok := value.(int64) 111 | if !ok { 112 | switch reflect.TypeOf(value).String() { 113 | case "[]uint8": 114 | { 115 | ba := []byte{} 116 | for _, b := range value.([]uint8) { 117 | ba = append(ba, byte(b)) 118 | } 119 | val, _ = strconv.ParseInt(string(ba), 10, 64) 120 | break 121 | } 122 | default: 123 | { 124 | return fmt.Errorf("value: %v, is not int, is %s", value, reflect.TypeOf(value)) 125 | } 126 | } 127 | } 128 | *rs = RecognizeStatus(val) 129 | return nil 130 | } 131 | 132 | // --------- 133 | 134 | type File struct { 135 | UUID 136 | 137 | Filename string `json:"filename"` 138 | Path string `json:"path"` 139 | Store StorageType `json:"store"` 140 | Mime string `json:"mime"` 141 | Size uint64 `json:"size"` 142 | FileType FileType `json:"file_type" gorm:"default:0" sql:"comment:'文件类型'"` 143 | Md5 string `json:"md5" sql:"comment:'MD5'"` 144 | Recognized RecognizeStatus `json:"recognized" gorm:"default:0" sql:"comment:'识别状态'"` 145 | UserID uint `json:"user_id" gorm:"comment:'文件所有者';index"` 146 | MediaWidth uint64 `json:"media_width"` 147 | MediaHeight uint64 `json:"media_height"` 148 | Thumbnail string `json:"thumbnail"` 149 | CreatedAt time.Time `json:"created_at"` 150 | } 151 | 152 | func (f *File) String() string { 153 | c, _ := json.Marshal(f) 154 | return string(c) 155 | } 156 | 157 | func (f *File) ToModel() *models.File { 158 | return &models.File{ 159 | ID: f.ID, 160 | Filename: f.Filename, 161 | Path: f.Path, 162 | Store: int64(f.Store), 163 | Mime: f.Mime, 164 | Size: f.Size, 165 | FileType: int64(f.FileType), 166 | Md5: f.Md5, 167 | Recognized: int64(f.Recognized), 168 | UserID: f.UserID, 169 | MediaWidth: f.MediaWidth, 170 | MediaHeight: f.MediaHeight, 171 | CreatedAt: f.CreatedAt, 172 | } 173 | } 174 | 175 | func (f *File) ToMediaFile() *models.MediaFile { 176 | return &models.MediaFile{ 177 | ID: f.ID, 178 | Filename: f.Filename, 179 | URL: utils.GetFileURL(f.Path), 180 | Mime: f.Mime, 181 | Size: f.Size, 182 | FileType: FileTypeMap[f.FileType], 183 | Md5: f.Md5, 184 | UserID: f.UserID, 185 | Width: f.MediaWidth, 186 | Height: f.MediaHeight, 187 | ThumbnailURL: utils.GetFileURL(f.Thumbnail), 188 | CreatedAt: f.CreatedAt, 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /modules/lock/db_lock.go: -------------------------------------------------------------------------------- 1 | package lock 2 | 3 | import ( 4 | "duguying/studio/modules/dbmodels" 5 | "fmt" 6 | "log" 7 | "net" 8 | "os" 9 | "time" 10 | 11 | "github.com/google/uuid" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | // DBShareLock 数据库共享锁 16 | type DBShareLock struct { 17 | lockKey string 18 | lockOwner string 19 | isLock bool 20 | lockTimeoutSecond time.Duration 21 | isLockExtend bool // 表示是否定期延长锁 22 | lockExtendIntervalSecond time.Duration 23 | db *gorm.DB 24 | } 25 | 26 | // MakeDBLock 创建数据库共享锁 27 | func MakeDBLock(db *gorm.DB, lockKey string, lockTimeoutSecond time.Duration) (shareLock *DBShareLock) { 28 | var lockOwner = uuid.New().String() 29 | localIp := getIpAddr() 30 | if localIp == "" { 31 | log.Println("localIp is empty!") 32 | } else { 33 | lockOwner = localIp + "_" + uuid.New().String() 34 | } 35 | shareLock = &DBShareLock{ 36 | lockKey: lockKey, 37 | lockOwner: lockOwner, 38 | isLock: false, 39 | lockTimeoutSecond: lockTimeoutSecond, 40 | db: db, 41 | isLockExtend: false, // 默认不定期延长锁时间 42 | lockExtendIntervalSecond: lockTimeoutSecond / 2, // 默认每隔超时间的一半就把锁时间延长 43 | } 44 | return shareLock 45 | } 46 | 47 | // GetIsLockExtend 获取IsLockExtend 48 | func (shareLock *DBShareLock) GetIsLockExtend() bool { 49 | return shareLock.isLockExtend 50 | } 51 | 52 | // SetIsLockExtend 设置isLockExtend 53 | func (shareLock *DBShareLock) SetIsLockExtend(isLockExtend bool) { 54 | shareLock.isLockExtend = isLockExtend 55 | } 56 | 57 | // SetLockExtendIntervalSecond 设置Interval 58 | func (shareLock *DBShareLock) SetLockExtendIntervalSecond(lockExtendIntervalSecond time.Duration) { 59 | shareLock.lockExtendIntervalSecond = lockExtendIntervalSecond 60 | } 61 | 62 | // GetLock 获取锁 63 | func (shareLock *DBShareLock) GetLock() bool { 64 | succ := getShareLock(shareLock) 65 | if succ { 66 | shareLock.isLock = true 67 | } 68 | if shareLock.isLock && shareLock.isLockExtend { 69 | // 后台更新锁时间 70 | go func() { 71 | for { 72 | time.Sleep(shareLock.lockExtendIntervalSecond) 73 | if !shareLock.isLock || !shareLock.isLockExtend { 74 | // log.Printf("stop update lock update_time\n") 75 | break 76 | } 77 | // log.Printf("update lock update_time\n") 78 | updateShareLockTime(shareLock) 79 | } 80 | }() 81 | } 82 | return succ 83 | } 84 | 85 | // ReleaseLock 释放锁 86 | func (shareLock *DBShareLock) ReleaseLock() bool { 87 | if releaseShareLock(shareLock) { 88 | shareLock.isLock = false 89 | return true 90 | } else { 91 | return false 92 | } 93 | } 94 | 95 | // LockKey 返回Lock Key 96 | func (shareLock *DBShareLock) LockKey() string { 97 | return shareLock.lockKey 98 | } 99 | 100 | func getShareLock(shareLock *DBShareLock) bool { 101 | // 查找尚未过期的锁 102 | exLock := &dbmodels.ShareLock{} 103 | timeoutTime := time.Now().Add(-1 * shareLock.lockTimeoutSecond) 104 | err := shareLock.db.Model(dbmodels.ShareLock{}). 105 | Where("lock_key=? and update_time>?", shareLock.lockKey, timeoutTime).First(exLock).Error 106 | if err != nil { 107 | if gorm.ErrRecordNotFound == err { 108 | // 没有现成的未过期的锁,尝试插入,注意,时间更新为当前的 109 | // 先删除过期的 110 | err := shareLock.db.Model(dbmodels.ShareLock{}). 111 | Where("lock_key=? and update_time 0 { 101 | where = where + " and user_id=?" 102 | params = append(params, userID) 103 | } 104 | err = tx.Model(dbmodels.File{}).Where(where, params...).Order("created_at desc").Find(&list).Error 105 | if err != nil { 106 | return nil, err 107 | } else { 108 | return list, nil 109 | } 110 | } 111 | 112 | // ListAllFile 列举文件 113 | func ListAllFile(tx *gorm.DB, userID uint, dirPrefix string) (list []*dbmodels.File, err error) { 114 | dirPrefix = strings.TrimPrefix(dirPrefix, "/") 115 | 116 | list = []*dbmodels.File{} 117 | where := "1=1 " 118 | params := []interface{}{} 119 | if userID > 0 { 120 | where = where + " and user_id=?" 121 | params = append(params, userID) 122 | } 123 | if dirPrefix != "" { 124 | where = where + " and path like ?" 125 | params = append(params, fmt.Sprintf("%s%%", dirPrefix)) 126 | } 127 | err = tx.Model(dbmodels.File{}).Where(where, params...).Order("created_at desc").Find(&list).Error 128 | if err != nil { 129 | return nil, err 130 | } else { 131 | return list, nil 132 | } 133 | } 134 | 135 | // ListCurrentDir 列举当前目录下的内容 136 | func ListCurrentDir(tx *gorm.DB, userID uint, dirPrefix string) (list []*models.FsItem, err error) { 137 | dirPrefix = strings.TrimPrefix(dirPrefix, "/") 138 | 139 | files, err := ListAllFile(tx, userID, dirPrefix) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | list = []*models.FsItem{} 145 | for _, file := range files { 146 | if file.Path == "" { 147 | continue 148 | } 149 | segs := strings.Split(file.Path, "/") 150 | if len(segs) <= 0 { 151 | continue 152 | } 153 | fsItem := &models.FsItem{ 154 | Name: segs[0], 155 | } 156 | if len(segs) == 1 { 157 | fsItem.Type = models.FileType 158 | } else { 159 | fsItem.Type = models.DirType 160 | } 161 | list = append(list) 162 | } 163 | 164 | return list, nil 165 | } 166 | 167 | // UpdateFileMediaSize 更新媒体文件尺寸 168 | func UpdateFileMediaSize(tx *gorm.DB, fileID string, width, height int) (err error) { 169 | return tx.Model(dbmodels.File{}).Where("id=?", fileID).Updates(map[string]interface{}{ 170 | "media_width": width, 171 | "media_height": height, 172 | }).Error 173 | } 174 | 175 | // UpdateFileThumbneil 更新媒体缩略图 176 | func UpdateFileThumbneil(tx *gorm.DB, fileID string, thumbneil string) (err error) { 177 | return tx.Model(dbmodels.File{}).Where("id=?", fileID).Updates(map[string]interface{}{ 178 | "thumbnail": thumbneil, 179 | }).Error 180 | } 181 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2018/5/18. 4 | 5 | package utils 6 | 7 | import ( 8 | "bytes" 9 | "crypto/hmac" 10 | "crypto/sha1" 11 | "duguying/studio/g" 12 | "fmt" 13 | "io" 14 | "math/rand" 15 | "net/http" 16 | "os" 17 | "path/filepath" 18 | "regexp" 19 | "strings" 20 | "time" 21 | 22 | "github.com/google/uuid" 23 | "github.com/martinlindhe/base36" 24 | "github.com/microcosm-cc/bluemonday" 25 | ) 26 | 27 | func init() { 28 | rand.Seed(time.Now().UnixNano()) 29 | } 30 | 31 | func GenUUID() string { 32 | guuid := uuid.New() 33 | return strings.Replace(guuid.String(), "-", "", -1) 34 | } 35 | 36 | func HmacSha1(content string, key string) string { 37 | //hmac ,use sha1 38 | mac := hmac.New(sha1.New, []byte(key)) 39 | mac.Write([]byte(content)) 40 | return fmt.Sprintf("%x", mac.Sum(nil)) 41 | } 42 | 43 | func GetFileContentType(out *os.File) (string, error) { 44 | // Only the first 512 bytes are used to sniff the content type. 45 | out.Seek(0, 0) 46 | buffer := make([]byte, 512) 47 | 48 | _, err := out.Read(buffer) 49 | if err != nil { 50 | return "", err 51 | } 52 | 53 | // Use the net/http package's handy DectectContentType function. Always returns a valid 54 | // content-type by returning "application/octet-stream" if no others seemed to match. 55 | contentType := http.DetectContentType(buffer) 56 | 57 | return contentType, nil 58 | } 59 | 60 | func StrContain(keyword string, vendor []string) bool { 61 | for _, item := range vendor { 62 | if keyword == item { 63 | return true 64 | } 65 | } 66 | return false 67 | } 68 | 69 | var ( 70 | base36map = map[rune]int{ 71 | '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, 72 | '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, 73 | 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 74 | 'F': 15, 'G': 16, 'H': 17, 'I': 18, 'J': 19, 75 | 'K': 20, 'L': 21, 'M': 22, 'N': 23, 'O': 24, 76 | 'P': 25, 'Q': 26, 'R': 27, 'S': 28, 'T': 29, 77 | 'U': 30, 'V': 31, 'W': 32, 'X': 33, 'Y': 34, 78 | 'Z': 35, 79 | } 80 | base36mix = []rune{ 81 | 'L', '9', 'M', 'U', '7', 'B', '2', 'H', 'S', '3', 82 | 'O', 'R', 'I', 'G', '5', 'K', 'Q', '6', 'J', 'T', 83 | '0', 'Y', 'N', '8', 'F', 'P', 'E', 'A', '1', 'Z', 84 | 'D', 'W', 'V', 'X', '4', 'C', 85 | } 86 | ) 87 | 88 | // GenUID 生成随机短号 89 | func GenUID() string { 90 | uidMin := base36.Decode("10000000") 91 | uidMax := base36.Decode("zzzzzzzz") 92 | uid := rand.Intn(int(uidMax-uidMin)) + int(uidMin) 93 | b36s := base36.Encode(uint64(uid)) 94 | mb36b := bytes.Buffer{} 95 | for _, c := range b36s { 96 | idx := base36map[c] 97 | mb36b.WriteRune(base36mix[idx]) 98 | } 99 | return strings.ToLower(mb36b.String()) 100 | } 101 | 102 | // TrimHTML 剔除HTML标签 103 | func TrimHTML(content string) string { 104 | p := bluemonday.StripTagsPolicy() 105 | return p.Sanitize(content) 106 | } 107 | 108 | var ( 109 | inlineMathReg, _ = regexp.Compile(`\$([\d\D][^\$]+)\$`) 110 | ) 111 | 112 | // ParseMath 解析数学公式标签 113 | func ParseMath(content string) string { 114 | count := 0 115 | out := "" 116 | rd := strings.NewReader(content) 117 | lexer := NewLexer(rd) 118 | for { 119 | start, pos, tok := lexer.Lex() 120 | out = out + string([]rune(content)[start:pos]) 121 | 122 | if tok == EOF { 123 | break 124 | } 125 | if tok == MATH { 126 | count++ 127 | if count%2 == 1 { 128 | out = out + "${1}" //`` 129 | } else if count%2 == 0 { 130 | out = out + "${0}" //`` 131 | out = strings.ReplaceAll(out, "${1}", ``) 132 | out = strings.ReplaceAll(out, "${0}", ``) 133 | } 134 | } 135 | } 136 | 137 | out = strings.ReplaceAll(out, "${1}", "$$") 138 | 139 | // 处理行内 math 140 | matches := inlineMathReg.FindAllString(out, -1) 141 | for _, match := range matches { 142 | policy := bluemonday.StripTagsPolicy() 143 | strippedMatch := policy.Sanitize(match) 144 | if strippedMatch == match { 145 | matchTmp := "" + strings.TrimPrefix(match, "$") 146 | matchTmp = strings.TrimSuffix(matchTmp, "$") + "" 147 | out = strings.ReplaceAll(out, match, matchTmp) 148 | } 149 | } 150 | 151 | return out 152 | } 153 | 154 | // GetFileURL 获取文件地址 155 | func GetFileURL(key string) string { 156 | imgHost := g.Config.Get("store", "img-host-url", "https://image.duguying.net") 157 | key = strings.TrimPrefix(key, "img") 158 | return imgHost + key 159 | } 160 | 161 | // GetFileLocalPath 获取文件本地路径 162 | func GetFileLocalPath(key string) string { 163 | store := g.Config.Get("upload", "store-path", "store") 164 | return filepath.Join(store, key) 165 | } 166 | 167 | // Movefile 移动文件 168 | func Movefile(src, dest string) error { 169 | _, err := Copyfile(src, dest) 170 | if err != nil { 171 | return err 172 | } 173 | return os.Remove(src) 174 | } 175 | 176 | // Copyfile 复制文件 177 | func Copyfile(src, dst string) (int64, error) { 178 | sourceFileStat, err := os.Stat(src) 179 | if err != nil { 180 | return 0, err 181 | } 182 | 183 | if !sourceFileStat.Mode().IsRegular() { 184 | return 0, fmt.Errorf("%s is not a regular file", src) 185 | } 186 | 187 | source, err := os.Open(src) 188 | if err != nil { 189 | return 0, err 190 | } 191 | defer source.Close() 192 | 193 | destination, err := os.Create(dst) 194 | if err != nil { 195 | return 0, err 196 | } 197 | defer destination.Close() 198 | nBytes, err := io.Copy(destination, source) 199 | return nBytes, err 200 | } 201 | -------------------------------------------------------------------------------- /service/message/model/cli_pipe.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.0 4 | // protoc v3.14.0 5 | // source: cli_pipe.proto 6 | 7 | package model 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | // cmd 3 24 | type CliPipe struct { 25 | state protoimpl.MessageState 26 | sizeCache protoimpl.SizeCache 27 | unknownFields protoimpl.UnknownFields 28 | 29 | Session string `protobuf:"bytes,1,opt,name=session,proto3" json:"session,omitempty"` 30 | Pid uint32 `protobuf:"varint,2,opt,name=pid,proto3" json:"pid,omitempty"` 31 | Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` 32 | DataLen uint32 `protobuf:"varint,4,opt,name=data_len,json=dataLen,proto3" json:"data_len,omitempty"` 33 | } 34 | 35 | func (x *CliPipe) Reset() { 36 | *x = CliPipe{} 37 | if protoimpl.UnsafeEnabled { 38 | mi := &file_cli_pipe_proto_msgTypes[0] 39 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 40 | ms.StoreMessageInfo(mi) 41 | } 42 | } 43 | 44 | func (x *CliPipe) String() string { 45 | return protoimpl.X.MessageStringOf(x) 46 | } 47 | 48 | func (*CliPipe) ProtoMessage() {} 49 | 50 | func (x *CliPipe) ProtoReflect() protoreflect.Message { 51 | mi := &file_cli_pipe_proto_msgTypes[0] 52 | if protoimpl.UnsafeEnabled && x != nil { 53 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 54 | if ms.LoadMessageInfo() == nil { 55 | ms.StoreMessageInfo(mi) 56 | } 57 | return ms 58 | } 59 | return mi.MessageOf(x) 60 | } 61 | 62 | // Deprecated: Use CliPipe.ProtoReflect.Descriptor instead. 63 | func (*CliPipe) Descriptor() ([]byte, []int) { 64 | return file_cli_pipe_proto_rawDescGZIP(), []int{0} 65 | } 66 | 67 | func (x *CliPipe) GetSession() string { 68 | if x != nil { 69 | return x.Session 70 | } 71 | return "" 72 | } 73 | 74 | func (x *CliPipe) GetPid() uint32 { 75 | if x != nil { 76 | return x.Pid 77 | } 78 | return 0 79 | } 80 | 81 | func (x *CliPipe) GetData() []byte { 82 | if x != nil { 83 | return x.Data 84 | } 85 | return nil 86 | } 87 | 88 | func (x *CliPipe) GetDataLen() uint32 { 89 | if x != nil { 90 | return x.DataLen 91 | } 92 | return 0 93 | } 94 | 95 | var File_cli_pipe_proto protoreflect.FileDescriptor 96 | 97 | var file_cli_pipe_proto_rawDesc = []byte{ 98 | 0x0a, 0x0e, 0x63, 0x6c, 0x69, 0x5f, 0x70, 0x69, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 99 | 0x12, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x22, 0x64, 0x0a, 0x07, 0x43, 0x6c, 0x69, 0x50, 0x69, 100 | 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 101 | 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 102 | 0x70, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x70, 0x69, 0x64, 0x12, 0x12, 103 | 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 104 | 0x74, 0x61, 0x12, 0x19, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6c, 0x65, 0x6e, 0x18, 0x04, 105 | 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x64, 0x61, 0x74, 0x61, 0x4c, 0x65, 0x6e, 0x42, 0x0a, 0x5a, 106 | 0x08, 0x2e, 0x2e, 0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 107 | 0x33, 108 | } 109 | 110 | var ( 111 | file_cli_pipe_proto_rawDescOnce sync.Once 112 | file_cli_pipe_proto_rawDescData = file_cli_pipe_proto_rawDesc 113 | ) 114 | 115 | func file_cli_pipe_proto_rawDescGZIP() []byte { 116 | file_cli_pipe_proto_rawDescOnce.Do(func() { 117 | file_cli_pipe_proto_rawDescData = protoimpl.X.CompressGZIP(file_cli_pipe_proto_rawDescData) 118 | }) 119 | return file_cli_pipe_proto_rawDescData 120 | } 121 | 122 | var file_cli_pipe_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 123 | var file_cli_pipe_proto_goTypes = []interface{}{ 124 | (*CliPipe)(nil), // 0: model.CliPipe 125 | } 126 | var file_cli_pipe_proto_depIdxs = []int32{ 127 | 0, // [0:0] is the sub-list for method output_type 128 | 0, // [0:0] is the sub-list for method input_type 129 | 0, // [0:0] is the sub-list for extension type_name 130 | 0, // [0:0] is the sub-list for extension extendee 131 | 0, // [0:0] is the sub-list for field type_name 132 | } 133 | 134 | func init() { file_cli_pipe_proto_init() } 135 | func file_cli_pipe_proto_init() { 136 | if File_cli_pipe_proto != nil { 137 | return 138 | } 139 | if !protoimpl.UnsafeEnabled { 140 | file_cli_pipe_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 141 | switch v := v.(*CliPipe); i { 142 | case 0: 143 | return &v.state 144 | case 1: 145 | return &v.sizeCache 146 | case 2: 147 | return &v.unknownFields 148 | default: 149 | return nil 150 | } 151 | } 152 | } 153 | type x struct{} 154 | out := protoimpl.TypeBuilder{ 155 | File: protoimpl.DescBuilder{ 156 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 157 | RawDescriptor: file_cli_pipe_proto_rawDesc, 158 | NumEnums: 0, 159 | NumMessages: 1, 160 | NumExtensions: 0, 161 | NumServices: 0, 162 | }, 163 | GoTypes: file_cli_pipe_proto_goTypes, 164 | DependencyIndexes: file_cli_pipe_proto_depIdxs, 165 | MessageInfos: file_cli_pipe_proto_msgTypes, 166 | }.Build() 167 | File_cli_pipe_proto = out.File 168 | file_cli_pipe_proto_rawDesc = nil 169 | file_cli_pipe_proto_goTypes = nil 170 | file_cli_pipe_proto_depIdxs = nil 171 | } 172 | -------------------------------------------------------------------------------- /modules/dbmodels/article.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2017/11/2. 4 | 5 | package dbmodels 6 | 7 | import ( 8 | "duguying/studio/g" 9 | "duguying/studio/service/models" 10 | "duguying/studio/utils" 11 | "strings" 12 | "time" 13 | 14 | "github.com/gogather/blackfriday/v2" 15 | "github.com/gogather/com" 16 | "github.com/gogather/json" 17 | ) 18 | 19 | const ( 20 | ArtStatusDraft = 0 21 | ArtStatusPublish = 1 22 | ) 23 | 24 | var ( 25 | ArtStatusMap = map[int]string{ 26 | ArtStatusDraft: "草稿", 27 | ArtStatusPublish: "已发布", 28 | } 29 | ) 30 | 31 | const ( 32 | ContentTypeHTML = 0 33 | ContentTypeMarkDown = 1 34 | ) 35 | 36 | type Article struct { 37 | ID uint `json:"id"` 38 | Title string `json:"title" gorm:"index:,unique"` 39 | URI string `json:"uri" gorm:"index"` 40 | Keywords string `json:"keywords" gorm:"index:,class:FULLTEXT"` 41 | Abstract string `json:"abstract"` 42 | Type int `json:"type" gorm:"default:0;index"` 43 | Content string `json:"content" gorm:"type:longtext;index:,class:FULLTEXT"` 44 | Author string `json:"author" gorm:"index"` 45 | AuthorID uint `json:"author_id" gorm:"index"` 46 | Count uint `json:"count" gorm:"index:,sort:desc"` 47 | Status int `json:"status" gorm:"index"` 48 | PublishTime *time.Time `json:"publish_time" gorm:"index"` 49 | UpdatedBy uint `json:"updated_by"` 50 | UpdatedAt time.Time `json:"updated_at"` 51 | CreatedAt time.Time `json:"created_at" gorm:"index:,sort:desc"` 52 | DeletedAt *time.Time `json:"deleted_at" gorm:"index"` 53 | } 54 | 55 | type ArticleIndex struct { 56 | ID uint `json:"id"` 57 | Title string `json:"title"` 58 | Keywords string `json:"keywords"` 59 | Abstract string `json:"abstract"` 60 | Type int `json:"type"` 61 | Content string `json:"content"` 62 | Author string `json:"author"` 63 | Status int `json:"status"` 64 | PublishTime *time.Time `json:"publish_time"` 65 | UpdatedBy uint `json:"updated_by"` 66 | UpdatedAt time.Time `json:"updated_at"` 67 | CreatedAt time.Time `json:"created_at"` 68 | DeletedAt *time.Time `json:"deleted_at"` 69 | } 70 | 71 | func (a *Article) ToArticleIndex() *ArticleIndex { 72 | return &ArticleIndex{ 73 | ID: a.ID, 74 | Title: a.Title, 75 | Keywords: a.Keywords, 76 | Abstract: a.Abstract, 77 | Type: a.Type, 78 | Content: utils.TrimHTML(a.Content), 79 | Author: a.Author, 80 | Status: a.Status, 81 | PublishTime: a.PublishTime, 82 | UpdatedBy: a.UpdatedBy, 83 | UpdatedAt: a.UpdatedAt, 84 | CreatedAt: a.CreatedAt, 85 | DeletedAt: a.DeletedAt, 86 | } 87 | } 88 | 89 | func (a *Article) String() string { 90 | c, _ := json.Marshal(a) 91 | return string(c) 92 | } 93 | 94 | // MarkdownFull markdown全量转html,带缓存 95 | func (a *Article) MarkdownFull(input []byte) []byte { 96 | md5sign := com.Md5(string(input)) 97 | key := "art:" + md5sign 98 | output, err := g.Cache.Get(key) 99 | if err != nil { 100 | htmlContent := a.markdownFull(input) 101 | g.Cache.SetTTL(key, string(htmlContent), time.Hour*24*30) 102 | return htmlContent 103 | } 104 | return []byte(output) 105 | } 106 | 107 | func (a *Article) markdownFull(input []byte) []byte { 108 | // set up the HTML renderer 109 | renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{ 110 | Flags: blackfriday.CommonHTMLFlags, 111 | Extensions: blackfriday.CommonExtensions | blackfriday.LaTeXMath, 112 | }) 113 | options := blackfriday.Options{ 114 | Extensions: blackfriday.CommonExtensions | blackfriday.LaTeXMath, 115 | } 116 | return blackfriday.Markdown(input, renderer, options) 117 | } 118 | 119 | func (a *Article) ToArticleShowContent() *models.ArticleShowContent { 120 | content := []byte(a.Content) 121 | if a.Type == ContentTypeMarkDown { 122 | content = a.MarkdownFull([]byte(a.Content)) 123 | // content = []byte(utils.ParseMath(string(content))) 124 | } 125 | tags := []string{} 126 | segs := strings.Split(strings.Replace(a.Keywords, ",", ",", -1), ",") 127 | for _, seg := range segs { 128 | tags = append(tags, strings.TrimSpace(seg)) 129 | } 130 | return &models.ArticleShowContent{ 131 | ID: a.ID, 132 | Title: a.Title, 133 | URI: a.URI, 134 | Author: a.Author, 135 | Tags: tags, 136 | CreatedAt: a.CreatedAt, 137 | ViewCount: a.Count, 138 | Content: string(content), 139 | } 140 | } 141 | 142 | func (a *Article) ToArticleContent() *models.ArticleContent { 143 | tags := []string{} 144 | segs := strings.Split(strings.Replace(a.Keywords, ",", ",", -1), ",") 145 | for _, seg := range segs { 146 | tags = append(tags, strings.TrimSpace(seg)) 147 | } 148 | return &models.ArticleContent{ 149 | ID: a.ID, 150 | Title: a.Title, 151 | URI: a.URI, 152 | Author: a.Author, 153 | Tags: tags, 154 | Type: a.Type, 155 | Status: a.Status, 156 | CreatedAt: a.CreatedAt, 157 | ViewCount: a.Count, 158 | Content: a.Content, 159 | } 160 | } 161 | 162 | func (a *Article) ToArticleTitle() *models.ArticleTitle { 163 | return &models.ArticleTitle{ 164 | ID: a.ID, 165 | Title: a.Title, 166 | URI: "/article/" + a.URI, 167 | Author: a.Author, 168 | CreatedAt: a.CreatedAt, 169 | ViewCount: a.Count, 170 | } 171 | } 172 | 173 | func (a *Article) ToArticleAdminTitle() *models.ArticleAdminTitle { 174 | return &models.ArticleAdminTitle{ 175 | ID: a.ID, 176 | Title: a.Title, 177 | URI: "/article/" + a.URI, 178 | Author: a.Author, 179 | CreatedAt: a.CreatedAt, 180 | ViewCount: a.Count, 181 | Status: a.Status, 182 | StatusName: ArtStatusMap[a.Status], 183 | } 184 | } 185 | 186 | type ArchInfo struct { 187 | Date string `json:"date"` 188 | Number uint `json:"number"` 189 | Year uint `json:"year"` 190 | Month uint `json:"month"` 191 | } 192 | 193 | func (ai *ArchInfo) String() string { 194 | c, _ := json.Marshal(ai) 195 | return string(c) 196 | } 197 | 198 | func (ai *ArchInfo) ToModel() *models.ArchInfo { 199 | return &models.ArchInfo{ 200 | Date: ai.Date, 201 | Number: ai.Number, 202 | Year: ai.Year, 203 | Month: ai.Month, 204 | } 205 | } 206 | 207 | type ArchInfoList []*ArchInfo 208 | 209 | func (al ArchInfoList) Len() int { 210 | return len(al) 211 | } 212 | 213 | func (al ArchInfoList) Less(i, j int) bool { 214 | return (al[i].Year*100 + al[i].Month) > (al[j].Year*100 + al[j].Month) 215 | } 216 | 217 | func (al ArchInfoList) Swap(i, j int) { 218 | al[i], al[j] = al[j], al[i] 219 | } 220 | -------------------------------------------------------------------------------- /service/action/xterm.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of duguying project 3 | // Created by duguying on 2018/6/13. 4 | 5 | package action 6 | 7 | import ( 8 | "duguying/studio/g" 9 | "duguying/studio/service/message/model" 10 | "duguying/studio/service/message/pipe" 11 | "duguying/studio/utils" 12 | "log" 13 | "net/http" 14 | "sync" 15 | "time" 16 | 17 | "github.com/gin-gonic/gin" 18 | "github.com/gogather/json" 19 | "github.com/gorilla/websocket" 20 | "google.golang.org/protobuf/proto" 21 | ) 22 | 23 | type TermLayout struct { 24 | Width uint32 `json:"cols"` 25 | Height uint32 `json:"rows"` 26 | } 27 | 28 | func (tl *TermLayout) String() string { 29 | c, _ := json.Marshal(tl) 30 | return string(c) 31 | } 32 | 33 | func ConnectXTerm(c *gin.Context) { 34 | clientID := c.Query("client_id") 35 | 36 | if clientID == "" { 37 | c.JSON(http.StatusOK, gin.H{ 38 | "ok": false, 39 | "err": "client_id is required", 40 | }) 41 | return 42 | } 43 | 44 | // create cli 45 | reqID := utils.GenUUID() 46 | openCliCmd := model.CliCmd{ 47 | Cmd: model.CliCmd_OPEN, 48 | Session: clientID, 49 | RequestId: reqID, 50 | Pid: 0, 51 | } 52 | pcmdData, err := proto.Marshal(&openCliCmd) 53 | if err != nil { 54 | c.JSON(http.StatusOK, gin.H{ 55 | "ok": false, 56 | "err": err.Error(), 57 | }) 58 | return 59 | } 60 | success := pipe.SendMsg(clientID, model.Msg{ 61 | Type: websocket.BinaryMessage, 62 | Cmd: model.CMD_CLI_CMD, 63 | ClientId: clientID, 64 | Data: pcmdData, 65 | }) 66 | if !success { 67 | c.JSON(http.StatusOK, gin.H{ 68 | "ok": false, 69 | "err": "created cli cmd send failed", 70 | }) 71 | return 72 | } 73 | 74 | // wait creation cli response and get pid 75 | pid := uint32(0) 76 | for i := 0; i < 10000; i++ { 77 | time.Sleep(time.Millisecond) 78 | var exist = false 79 | pid, exist = pipe.GetCliPid(clientID, reqID) 80 | if exist { 81 | break 82 | } 83 | } 84 | 85 | if pid <= 0 { 86 | c.JSON(http.StatusOK, gin.H{ 87 | "ok": false, 88 | "err": "invalid pid, maybe create cli failed", 89 | }) 90 | return 91 | } 92 | 93 | // upgrade to websocket 94 | var upgrader = websocket.Upgrader{} 95 | upgrader.CheckOrigin = func(r *http.Request) bool { 96 | return true 97 | } 98 | conn, err := upgrader.Upgrade(c.Writer, c.Request, c.Writer.Header()) 99 | if err != nil { 100 | log.Println("upgrade:", err) 101 | c.JSON(http.StatusForbidden, map[string]interface{}{ 102 | "ok": false, 103 | "error": err.Error(), 104 | }) 105 | return 106 | } 107 | 108 | wsExit := false 109 | defer conn.Close() 110 | defer func() { wsExit = true }() 111 | 112 | pair := pipe.NewCliChanPair() 113 | pipe.SetCliChanPair(clientID, pid, pair) 114 | pipe.SetPidCon(clientID, pid, conn) // store connection 115 | 116 | // send xterm data into cli 117 | go func() { 118 | for { 119 | select { 120 | case data := <-pair.ChanOut: 121 | { 122 | if wsExit { 123 | return 124 | } 125 | pipeStruct := model.CliPipe{ 126 | Session: clientID, 127 | Pid: pid, 128 | Data: data, 129 | } 130 | g.LogEntry.WithField("slice", "agentrcv").Printf("agent received in: %s\n", string(data)) 131 | pipeData, err := proto.Marshal(&pipeStruct) 132 | if err != nil { 133 | log.Println("proto marshal failed, err:", err.Error()) 134 | continue 135 | } 136 | msg := model.Msg{ 137 | Type: websocket.BinaryMessage, 138 | Cmd: model.CMD_CLI_PIPE, 139 | ClientId: clientID, 140 | Data: pipeData, 141 | } 142 | pipe.SendMsg(clientID, msg) 143 | } 144 | } 145 | } 146 | }() 147 | 148 | // read from client, put into in channel 149 | go func(con *websocket.Conn) { 150 | for { 151 | _, data, err := con.ReadMessage() 152 | if err != nil { 153 | // ws has closed 154 | log.Println("read:", err) 155 | 156 | // try to send close cmd to agent cli 157 | _, exist := pipe.GetPidCon(clientID, pid) 158 | if exist { 159 | cliCmdStruct := model.CliCmd{ 160 | Cmd: model.CliCmd_CLOSE, 161 | Session: clientID, 162 | RequestId: reqID, 163 | Pid: pid, 164 | } 165 | cliCmdData, err := proto.Marshal(&cliCmdStruct) 166 | if err != nil { 167 | log.Println("marshal cmd data failed, err:", err.Error()) 168 | } else { 169 | cmdCloseMsg := model.Msg{ 170 | Type: websocket.BinaryMessage, 171 | Cmd: model.CMD_CLI_CMD, 172 | ClientId: clientID, 173 | Data: cliCmdData, 174 | } 175 | pipe.SendMsg(clientID, cmdCloseMsg) 176 | } 177 | } 178 | break 179 | } 180 | 181 | if data[0] == model.TERM_PONG { 182 | //log.Println("pong") 183 | } else if data[0] == model.TERM_SIZE { 184 | layout := TermLayout{} 185 | err = json.Unmarshal(data[1:], &layout) 186 | if err != nil { 187 | log.Printf("parse layout failed, err: %s, raw content is: %s\n", err.Error(), string(data[1:])) 188 | continue 189 | } 190 | log.Println("resize...", layout) 191 | cliCmdStruct := model.CliCmd{ 192 | Cmd: model.CliCmd_RESIZE, 193 | Session: clientID, 194 | RequestId: reqID, 195 | Pid: pid, 196 | Width: layout.Width, 197 | Height: layout.Height, 198 | } 199 | cliCmdData, err := proto.Marshal(&cliCmdStruct) 200 | if err != nil { 201 | log.Println("marshal cmd data failed, err:", err.Error()) 202 | } else { 203 | cmdCloseMsg := model.Msg{ 204 | Type: websocket.BinaryMessage, 205 | Cmd: model.CMD_CLI_CMD, 206 | ClientId: clientID, 207 | Data: cliCmdData, 208 | } 209 | pipe.SendMsg(clientID, cmdCloseMsg) 210 | } 211 | } else if data[0] == model.TERM_PIPE { 212 | //log.Printf("what's header: %d\n", data[0]) 213 | g.LogEntry.WithField("slice", "browsersnt").Printf("browser sent: %s\n", string(data[1:])) 214 | pair.ChanOut <- data[1:] 215 | } 216 | 217 | } 218 | }(conn) 219 | 220 | // send hb to xterm 221 | go func() { 222 | xtermHbPeriod := g.Config.GetInt64("xterm", "hb", 10) 223 | for { 224 | if wsExit { 225 | return 226 | } 227 | pair.ChanIn <- []byte{model.TERM_PING} 228 | time.Sleep(time.Second * time.Duration(xtermHbPeriod)) 229 | } 230 | }() 231 | 232 | var wsLock sync.Mutex 233 | // write into client, get from out channel 234 | for { 235 | select { 236 | case data := <-pair.ChanIn: 237 | { 238 | wsLock.Lock() 239 | g.LogEntry.WithField("slice", "browserrcv").Printf("browser received: len --> %d\n", len(data)) 240 | err = conn.WriteMessage(websocket.BinaryMessage, data) 241 | if err != nil { 242 | log.Println("即时消息发送到客户端:", err) 243 | return 244 | } 245 | wsLock.Unlock() 246 | } 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /service/action/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018. All rights reserved. 2 | // This file is part of blog project 3 | // Created by duguying on 2018/1/25. 4 | 5 | package action 6 | 7 | import ( 8 | "duguying/studio/g" 9 | "duguying/studio/modules/db" 10 | "duguying/studio/modules/dbmodels" 11 | "duguying/studio/modules/session" 12 | "duguying/studio/service/models" 13 | "fmt" 14 | "net/http" 15 | "time" 16 | 17 | "github.com/gin-gonic/gin" 18 | "github.com/gogather/com" 19 | ) 20 | 21 | // UserSimpleInfo 用户简单信息 22 | // @Router /admin/user_info [get] 23 | // @Tags 用户 24 | // @Description 当前用户信息 25 | // @Success 200 {object} models.UserInfoResponse 26 | func UserSimpleInfo(c *CustomContext) (interface{}, error) { 27 | return models.CommonResponse{Ok: true}, nil 28 | } 29 | 30 | // UserInfo 用户信息 31 | // @Router /admin/user_info [get] 32 | // @Tags 用户 33 | // @Description 当前用户信息 34 | // @Success 200 {object} models.UserInfoResponse 35 | func UserInfo(c *CustomContext) (interface{}, error) { 36 | user, err := db.GetUserByID(g.Db, c.UserID()) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return models.UserInfoResponse{ 42 | Ok: true, 43 | Data: user.ToInfo(), 44 | }, nil 45 | } 46 | 47 | func UserRegister(c *gin.Context) { 48 | register := &models.RegisterArgs{} 49 | err := c.BindJSON(register) 50 | if err != nil { 51 | c.JSON(http.StatusOK, gin.H{ 52 | "ok": false, 53 | "msg": err.Error(), 54 | }) 55 | return 56 | } 57 | user, err := db.RegisterUser(g.Db, register.Username, register.Password, register.Email) 58 | if err != nil { 59 | c.JSON(http.StatusOK, gin.H{ 60 | "ok": false, 61 | "msg": err.Error(), 62 | }) 63 | return 64 | } else { 65 | c.JSON(http.StatusOK, gin.H{ 66 | "ok": true, 67 | "user": gin.H{ 68 | "id": user.ID, 69 | "username": user.Username, 70 | }, 71 | }) 72 | return 73 | } 74 | } 75 | 76 | // UserLogin 用户登录 77 | // @Router /user_login [put] 78 | // @Tags 用户 79 | // @Description 用户登录 80 | // @Param auth body models.LoginArgs true "登录鉴权信息" 81 | // @Success 200 {object} models.LoginResponse 82 | func UserLogin(c *CustomContext) (interface{}, error) { 83 | login := &models.LoginArgs{} 84 | err := c.BindJSON(login) 85 | if err != nil { 86 | return nil, err 87 | } 88 | user, err := db.GetUser(g.Db, login.Username) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | // validate 94 | passwd := com.Md5(login.Password + user.Salt) 95 | if passwd != user.Password { 96 | return nil, fmt.Errorf("登陆失败,密码错误") 97 | } else { 98 | sid := session.SessionID() 99 | if sid == "" { 100 | return nil, fmt.Errorf("生成会话失败") 101 | } else { 102 | defaultSessionTime := time.Hour * 24 103 | sessionTimeCfg := g.Config.Get("session", "expire", defaultSessionTime.String()) 104 | sessionExpire, err := time.ParseDuration(sessionTimeCfg) 105 | if err != nil { 106 | return nil, err 107 | } else { 108 | // store session 109 | entity := &session.Entity{ 110 | UserID: user.ID, 111 | IP: c.ClientIP(), 112 | LoginAt: time.Now(), 113 | UserAgent: c.Request.UserAgent(), 114 | } 115 | session.SessionSet(sid, sessionExpire, entity) 116 | 117 | err = db.AddLoginHistory(g.Db, sid, entity) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | return models.LoginResponse{ 123 | Ok: true, 124 | Sid: sid, 125 | }, nil 126 | } 127 | 128 | } 129 | } 130 | } 131 | 132 | func UserLogout(c *CustomContext) (interface{}, error) { 133 | sid := c.GetString("sid") 134 | session.SessionDel(sid) 135 | return gin.H{ 136 | "ok": true, 137 | "msg": "logout success", 138 | "user_id": c.UserID(), 139 | }, nil 140 | } 141 | 142 | func UsernameCheck(c *gin.Context) { 143 | username := c.DefaultQuery("username", "") 144 | if username == "" { 145 | c.JSON(http.StatusOK, gin.H{ 146 | "ok": false, 147 | "msg": "username could not be empty", 148 | }) 149 | return 150 | } else { 151 | valid, err := db.CheckUsername(g.Db, username) 152 | if err != nil { 153 | c.JSON(http.StatusOK, gin.H{ 154 | "ok": false, 155 | "msg": err.Error(), 156 | }) 157 | return 158 | } else { 159 | c.JSON(http.StatusOK, gin.H{ 160 | "ok": true, 161 | "valid": valid, 162 | }) 163 | return 164 | } 165 | } 166 | } 167 | 168 | // ListUserLoginHistory 登陆历史列表 169 | // @Router admin/login_history [get] 170 | // @Tags 用户 171 | // @Description 登陆历史列表 172 | // @Param page query uint true "页码" 173 | // @Param size query uint true "每页数" 174 | // @Success 200 {object} models.ListUserLoginHistoryResponse 175 | func ListUserLoginHistory(c *CustomContext) (interface{}, error) { 176 | req := models.CommonPagerRequest{} 177 | err := c.BindQuery(&req) 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | list, total, err := db.PageLoginHistoryByUserID(g.Db, c.UserID(), req.Page, req.Size) 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | apiList := []*models.LoginHistory{} 188 | for _, item := range list { 189 | hist := item.ToModel() 190 | entity := session.SessionGet(hist.SessionID) 191 | if entity != nil { 192 | hist.Expired = false 193 | } else { 194 | hist.Expired = true 195 | } 196 | apiList = append(apiList, hist) 197 | } 198 | 199 | return models.ListUserLoginHistoryResponse{ 200 | Ok: true, 201 | Total: int(total), 202 | List: apiList, 203 | }, nil 204 | } 205 | 206 | func UserMessageCount(c *CustomContext) (interface{}, error) { 207 | return gin.H{ 208 | "ok": true, 209 | "data": map[string]interface{}{ 210 | "count": 0, 211 | }, 212 | }, nil 213 | } 214 | 215 | // ChangePassword 修改密码 216 | // @Router /admin/change_password [put] 217 | // @Tags 用户 218 | // @Description 修改密码 219 | // @Param auth body models.LoginArgs true "登录鉴权信息" 220 | // @Success 200 {object} models.LoginResponse 221 | func ChangePassword(c *CustomContext) (interface{}, error) { 222 | req := models.ChangePasswordRequest{} 223 | err := c.BindJSON(&req) 224 | if err != nil { 225 | return nil, err 226 | } 227 | 228 | currentUser, err := db.GetUserByID(g.Db, c.UserID()) 229 | if err != nil { 230 | return nil, err 231 | } 232 | 233 | // 非管理员不能修改他人密码 234 | if currentUser.Role != dbmodels.RoleAdmin && currentUser.Username != req.Username { 235 | return nil, fmt.Errorf("非管理员不能修改他人密码") 236 | } 237 | 238 | // 获取要修改密码的账号信息 239 | user, err := db.GetUser(g.Db, req.Username) 240 | if err != nil { 241 | return nil, err 242 | } 243 | 244 | // 如果管理员修改他人密码,不需要校验原密码,修改自己帐号的密码,才需要校验旧密码 245 | if currentUser.Username == req.Username { 246 | passwd := com.Md5(req.OldPassword + user.Salt) 247 | if passwd != user.Password { 248 | return nil, fmt.Errorf("旧密码错误") 249 | } 250 | } 251 | 252 | // 修改密码并登出账号 253 | tx := g.Db.Begin() 254 | err = db.UserChangePassword(tx, req.Username, req.NewPassword) 255 | if err != nil { 256 | return nil, err 257 | } 258 | list, _, err := db.PageLoginHistoryByUserID(tx, user.ID, 1, 1000) 259 | if err != nil { 260 | return nil, err 261 | } 262 | for _, sess := range list { 263 | session.SessionDel(sess.SessionID) 264 | } 265 | err = tx.Commit().Error 266 | if err != nil { 267 | return nil, err 268 | } 269 | 270 | return models.CommonResponse{ 271 | Ok: true, 272 | }, nil 273 | } 274 | --------------------------------------------------------------------------------