├── 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 [](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", shareLock.lockKey, timeoutTime).
112 | Delete(&dbmodels.ShareLock{}).Error
113 | if err != nil {
114 | log.Printf("delete lock err=%s\n", err.Error())
115 | return false
116 | }
117 | err = shareLock.db.Model(dbmodels.ShareLock{}).Create(&dbmodels.ShareLock{
118 | LockKey: shareLock.lockKey,
119 | LockOwner: shareLock.lockOwner,
120 | UpdateTime: time.Now(),
121 | }).Error
122 | if err != nil {
123 | // 因为设置了unique约束,可能获得锁失败
124 | return false
125 | } else {
126 | // 获得锁成功
127 | return true
128 | }
129 | } else {
130 | log.Println("getShareLock err=", err.Error())
131 | return false
132 | }
133 | } else {
134 | // 有现成的锁,检查owner是否是它
135 | if exLock.LockOwner == shareLock.lockOwner {
136 | return true
137 | } else {
138 | return false
139 | }
140 | }
141 | }
142 |
143 | func releaseShareLock(shareLock *DBShareLock) bool {
144 | err := shareLock.db.Model(dbmodels.ShareLock{}).
145 | Where("lock_key=? and lock_owner=?", shareLock.lockKey, shareLock.lockOwner).
146 | Delete(&dbmodels.ShareLock{}).Error
147 | if err != nil {
148 | log.Printf("try delete lock error,%v\n", err)
149 | return false
150 | }
151 | return true
152 | }
153 |
154 | func updateShareLockTime(shareLock *DBShareLock) {
155 | // 定时更新锁
156 | err := shareLock.db.Model(dbmodels.ShareLock{}).
157 | Where("lock_key=? and lock_owner=?", shareLock.lockKey, shareLock.lockOwner).
158 | Updates(map[string]interface{}{
159 | "update_time": time.Now(),
160 | }).Error
161 | if err != nil {
162 | log.Printf("update lock err=%s\n", err.Error())
163 | }
164 | }
165 |
166 | func getIpAddr() string {
167 | addrs, err := net.InterfaceAddrs()
168 |
169 | if err != nil {
170 | fmt.Println(err)
171 | os.Exit(1)
172 | }
173 | for _, address := range addrs {
174 |
175 | // 检查ip地址判断是否回环地址
176 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
177 | if ipnet.IP.To4() != nil {
178 | return ipnet.IP.String()
179 | }
180 |
181 | }
182 | }
183 | return ""
184 | }
185 |
--------------------------------------------------------------------------------
/modules/db/file.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018. All rights reserved.
2 | // This file is part of duguying project
3 | // Created by duguying on 2018/6/30.
4 |
5 | package db
6 |
7 | import (
8 | "duguying/studio/modules/dbmodels"
9 | "duguying/studio/service/models"
10 | "duguying/studio/utils"
11 | "fmt"
12 | "path"
13 | "strings"
14 | "time"
15 |
16 | "gorm.io/gorm"
17 | )
18 |
19 | func SaveFile(tx *gorm.DB, fpath string, mime string, size uint64, md5 string, userID uint, fileType dbmodels.FileType) (f *dbmodels.File, err error) {
20 | filename := path.Base(fpath)
21 | f = &dbmodels.File{
22 | Filename: filename,
23 | Path: fpath,
24 | Store: dbmodels.LOCAL,
25 | Mime: mime,
26 | Size: size,
27 | FileType: fileType,
28 | Md5: md5,
29 | UserID: userID,
30 | CreatedAt: time.Now(),
31 | }
32 | err = tx.Model(dbmodels.File{}).Create(f).Error
33 | if err != nil {
34 | return nil, err
35 | } else {
36 | return f, nil
37 | }
38 | }
39 |
40 | // DeleteFile 删除文件
41 | func DeleteFile(tx *gorm.DB, id string) (err error) {
42 | return tx.Model(dbmodels.File{}).Where("id=?", id).Delete(&dbmodels.File{}).Error
43 | }
44 |
45 | // CheckFileRef 检查文件引用
46 | func CheckFileRef(tx *gorm.DB, file *dbmodels.File) (cnt int64, err error) {
47 | url := utils.GetFileURL(file.Path)
48 | cnt, err = FileCountArticleRef(tx, url)
49 | if err != nil {
50 | return 0, err
51 | }
52 | coverRefCnt, err := FileCountCoverRef(tx, file.ID)
53 | if err != nil {
54 | return 0, err
55 | }
56 | return cnt + coverRefCnt, nil
57 | }
58 |
59 | // FileCountCoverRef 封面文件引用计数
60 | func FileCountCoverRef(tx *gorm.DB, fileID string) (cnt int64, err error) {
61 | err = tx.Model(dbmodels.Cover{}).Where("file_id=?", fileID).Count(&cnt).Error
62 | if err != nil {
63 | return 0, err
64 | }
65 | return cnt, nil
66 | }
67 |
68 | // PageFile 文件分页列表
69 | func PageFile(tx *gorm.DB, page uint64, size uint64, userID uint) (list []*dbmodels.File, total int64, err error) {
70 | list = []*dbmodels.File{}
71 | total = 0
72 | err = tx.Model(dbmodels.File{}).Where("user_id=?", userID).Count(&total).Error
73 | if err != nil {
74 | return nil, 0, err
75 | }
76 |
77 | err = tx.Model(dbmodels.File{}).Where("user_id=?", userID).Order("created_at desc").Offset(int((page - 1) * size)).Limit(int(size)).Find(&list).Error
78 | if err != nil {
79 | return nil, 0, err
80 | } else {
81 | return list, total, nil
82 | }
83 | }
84 |
85 | // GetFile 获取文件信息
86 | func GetFile(tx *gorm.DB, id string) (file *dbmodels.File, err error) {
87 | file = &dbmodels.File{}
88 | err = tx.Model(dbmodels.File{}).Where("id=?", id).First(file).Error
89 | if err != nil {
90 | return nil, err
91 | }
92 | return file, nil
93 | }
94 |
95 | // ListAllMediaFile 列举媒体文件
96 | func ListAllMediaFile(tx *gorm.DB, userID uint) (list []*dbmodels.File, err error) {
97 | list = []*dbmodels.File{}
98 | where := "file_type in (?)"
99 | params := []interface{}{[]int{int(dbmodels.FileTypeImage), int(dbmodels.FileTypeVideo)}}
100 | if userID > 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 |
--------------------------------------------------------------------------------