├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── add.go ├── cmd.go ├── scan.go └── web.go ├── config.ini ├── config ├── config.go └── config_test.go ├── main.go ├── restful ├── auth │ ├── auth.go │ └── model.go ├── http.go ├── response.go ├── session │ ├── memory.go │ └── model.go ├── uuid │ ├── uuid.go │ └── uuid_test.go └── v1 │ ├── login.go │ └── symbols.go ├── route ├── handler.go ├── midware.go └── routes.go ├── symbol ├── branch.go ├── branch_test.go ├── models.go ├── server.go └── server_test.go ├── util ├── unzip.go └── unzip_test.go └── web ├── .babelrc ├── .gitignore ├── LICENSE ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── dev.env.js ├── index.js └── prod.env.js ├── index.html ├── jsconfig.json ├── package.json ├── postcss.config.js ├── src ├── App.vue ├── api │ └── pdb.js ├── assets │ ├── element-#169bd9.zip │ ├── logo.png │ ├── user.jpg │ └── user.png ├── components │ ├── branchs.vue │ ├── builds.vue │ ├── header.vue │ ├── message.vue │ └── symbols.vue ├── main.js ├── utils │ ├── chart.js │ ├── openWin.js │ ├── routes.js │ ├── store.js │ └── types.js └── view │ └── authorize.vue └── static ├── 1.png ├── 2.png └── favicon.ico /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | *.log 3 | symbols.json 4 | log/ 5 | custom/ 6 | data/ 7 | testdata/ 8 | .vscode/ 9 | .vendor/ 10 | .idea/ 11 | *.cookies 12 | *.png 13 | *.iml 14 | *.exe 15 | *.exe~ 16 | *.pem 17 | *.bak 18 | output* 19 | debug/ 20 | test/ 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Azlan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GoSymbols 2 | 3 | Windows PDB self-service symbol server by Golang and Vuejs. 4 | 5 | Index Page 6 | ![Index](/web/static/1.png?raw=true "Index") 7 | 8 | Symbols Page 9 | ![Download](/web/static/2.png?raw=true "Download PDB") 10 | 11 | 12 | ## Config 13 | 14 | The web portal has integrated with Windows Azure AD OAuth Authorization. 15 | 16 | Replace the correct AppID/AppKey and RedirectURI in the `config.ini`. 17 | 18 | ``` ini 19 | [base] 20 | SYMSTORE_EXE = "C:\Program Files (x86)\Windows Kits\8.1\Debuggers\x86\symstore.exe" 21 | BUILD_SOURCE = "Z:\BuildServer" 22 | DESTINATION = "D:\\SymbolServer" 23 | LATEST_BUILD = latestbuild.txt 24 | EXCLUDE_LIST = vc120.pdb,zlib10.pdb 25 | DEBUG_ZIP = debug.zip 26 | LOG_PATH = 27 | 28 | [app] 29 | CLIENT_ID = # Windows Azure AD Application ID 30 | CLIENT_KEY = # Application Key 31 | REDIRECT_URI = http://localhost:8010/api/auth/authorize # Windows AD OAuth redirect URL 32 | GRAPH_SCOPE = https://graph.microsoft.com/User.Read 33 | ``` 34 | 35 | 36 | ## Build 37 | 38 | Golang 39 | ``` bash 40 | go build 41 | ``` 42 | 43 | Vuejs 44 | ``` bash 45 | # web src folder 46 | cd web 47 | 48 | # install dependencies 49 | npm install 50 | 51 | # build for production with minification 52 | npm run build 53 | ``` 54 | 55 | 56 | ## Run 57 | 58 | ``` bash 59 | GoSymbols serve 60 | ``` 61 | -------------------------------------------------------------------------------- /cmd/add.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/adyzng/GoSymbols/symbol" 7 | "github.com/urfave/cli" 8 | 9 | log "gopkg.in/clog.v1" 10 | ) 11 | 12 | // AddBuild ... 13 | var AddBuild = cli.Command{ 14 | Name: "add", 15 | Usage: "Add given build for specified branch.", 16 | Description: "Add the given build of specified branch exist in symbol store.", 17 | Action: addBuild, 18 | Flags: []cli.Flag{ 19 | stringFlag("branch, b", "", "The branch name in the symbol store."), 20 | stringFlag("version, v", "", "The build version, empty version for the latest build."), 21 | }, 22 | } 23 | 24 | func addBuild(c *cli.Context) error { 25 | bname := "" 26 | if c.IsSet("branch") { 27 | bname = c.String("branch") 28 | } 29 | if bname == "" { 30 | return errors.New("empty branch name") 31 | } 32 | 33 | build := "" 34 | if c.IsSet("build") { 35 | build = c.String("build") 36 | } 37 | 38 | log.Info("[App] Add build %s for branch %s", build, bname) 39 | ss := symbol.GetServer() 40 | if err := ss.LoadBranchs(); err != nil { 41 | return err 42 | } 43 | 44 | builder := ss.Get(bname) 45 | if builder == nil { 46 | log.Warn("[App] Branch %s not exist.", bname) 47 | return errors.New("branch not exist") 48 | } 49 | 50 | return builder.AddBuild(build) 51 | } 52 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/urfave/cli" 4 | 5 | func stringFlag(name, value, usage string) cli.StringFlag { 6 | return cli.StringFlag{ 7 | Name: name, 8 | Value: value, 9 | Usage: usage, 10 | } 11 | } 12 | 13 | func boolFlag(name, usage string) cli.BoolFlag { 14 | return cli.BoolFlag{ 15 | Name: name, 16 | Usage: usage, 17 | } 18 | } 19 | 20 | func intFlag(name string, value int, usage string) cli.IntFlag { 21 | return cli.IntFlag{ 22 | Name: name, 23 | Value: value, 24 | Usage: usage, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/scan.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/adyzng/GoSymbols/config" 5 | "github.com/adyzng/GoSymbols/symbol" 6 | "github.com/urfave/cli" 7 | 8 | log "gopkg.in/clog.v1" 9 | ) 10 | 11 | // Admin ... 12 | var Admin = cli.Command{ 13 | Name: "scan", 14 | Usage: "Scan the exist symbol store", 15 | Description: "Scan the exist symbol store, and generate the config.", 16 | Action: runAdmin, 17 | Flags: []cli.Flag{ 18 | stringFlag("path, p", `D:\SymbolServer`, "Exist symbol store path"), 19 | }, 20 | } 21 | 22 | func runAdmin(c *cli.Context) error { 23 | path := config.Destination 24 | if c.IsSet("path") { 25 | path = c.String("path") 26 | log.Trace("[App] Scan path %s", path) 27 | } 28 | 29 | log.Info("[App] Scan store %s", path) 30 | ss := symbol.GetServer() 31 | 32 | ss.WalkBuilders(func(b symbol.Builder) error { 33 | log.Info("[App] Load branch %s.", b.Name()) 34 | return nil 35 | }) 36 | 37 | return ss.ScanStore(path) 38 | } 39 | -------------------------------------------------------------------------------- /cmd/web.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "sync" 10 | "time" 11 | 12 | "github.com/adyzng/GoSymbols/config" 13 | "github.com/adyzng/GoSymbols/route" 14 | "github.com/adyzng/GoSymbols/symbol" 15 | "github.com/urfave/cli" 16 | 17 | log "gopkg.in/clog.v1" 18 | ) 19 | 20 | // Web server subcommand 21 | // 22 | var Web = cli.Command{ 23 | Name: "serve", 24 | Usage: "Start symbol store server", 25 | Description: "Symbol server will take care of symbols, and serve the web portal.", 26 | Action: runWeb, 27 | Flags: []cli.Flag{ 28 | stringFlag("port, p", "3000", "Given port number to prevent conflict"), 29 | stringFlag("config, c", "config/server.ini", "Custom configuration file path"), 30 | }, 31 | } 32 | 33 | func runWeb(c *cli.Context) error { 34 | if c.IsSet("config") { 35 | config.LoadConfig(c.String("config")) 36 | } 37 | if c.IsSet("port") { 38 | config.Port = c.Uint("port") 39 | } 40 | 41 | done := make(chan struct{}, 1) 42 | serv := http.Server{ 43 | Addr: fmt.Sprintf("%s:%d", config.Address, config.Port), 44 | Handler: route.NewRouter(), 45 | ReadTimeout: time.Second * 15, 46 | WriteTimeout: time.Second * 15, 47 | } 48 | 49 | log.Info("[App] Start %s ...", config.AppName) 50 | var wg sync.WaitGroup 51 | wg.Add(3) 52 | 53 | go func() { 54 | defer wg.Done() 55 | log.Info("[App] Listening %s", serv.Addr) 56 | serv.ListenAndServe() 57 | }() 58 | go func() { 59 | defer wg.Done() 60 | symbol.GetServer().Run(done) 61 | }() 62 | go func() { 63 | defer wg.Done() 64 | sigs := make(chan os.Signal, 1) 65 | signal.Notify(sigs, os.Interrupt, os.Kill) 66 | 67 | <-sigs 68 | close(done) 69 | close(sigs) 70 | log.Info("[App] Receive terminate signal!") 71 | serv.Shutdown(context.Background()) 72 | }() 73 | 74 | wg.Wait() 75 | log.Info("[App] Stopped %s.", config.AppName) 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [base] 2 | SYMSTORE_EXE = "C:\Program Files (x86)\Windows Kits\8.1\Debuggers\x86\symstore.exe" 3 | BUILD_SOURCE = "Z:\" 4 | DESTINATION = "C:\Go\huan\src\github.com\adyzng\GoSymbols\testdata\" #"D:\\SymbolServer" 5 | LATEST_BUILD = latestbuild.txt 6 | EXCLUDE_LIST = vc120.pdb,zlib10.pdb 7 | DEBUG_ZIP = debug.zip 8 | LOG_PATH = 9 | 10 | [app] 11 | CLIENT_ID = 12 | CLIENT_KEY = 13 | REDIRECT_URI = http://localhost:8010/api/auth/authorize 14 | GRAPH_SCOPE = https://graph.microsoft.com/User.Read 15 | 16 | [web] 17 | PORT = 8080 18 | ADDRESS = 0.0.0.0 19 | WEB_ROOT = .\web\dist\ -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "runtime" 9 | "strings" 10 | 11 | log "gopkg.in/clog.v1" 12 | ini "gopkg.in/ini.v1" 13 | ) 14 | 15 | var ( 16 | Debug bool 17 | AppPath string 18 | AppName string 19 | IsWindows bool 20 | 21 | WebRoot string // website assets folder 22 | Address string // website listen address 23 | Port uint // website listen port 24 | 25 | ClientID string // 26 | ClientKey string // 27 | ADAuthURI string 28 | RedirectURI string 29 | GraphScope string 30 | 31 | LogPath string 32 | SymStoreExe string 33 | Destination string // pdb server destination 34 | BuildSource string // pdb source folder 35 | PDBZipFile string // pdb zip file, default `debug.zip` 36 | LatestBuildFile string // latest build trigger file `latestbuild.txt` 37 | ScheduleTime string // default trigger time in 24H, eg: 5:00 => 5:00AM 38 | SymExcludeList []string 39 | ) 40 | 41 | func init() { 42 | if err := LoadConfig(); err != nil { 43 | LoadConfig("..\\config.ini") 44 | } 45 | } 46 | 47 | func exePath() (string, error) { 48 | file, err := exec.LookPath(os.Args[0]) 49 | if err != nil { 50 | return "", err 51 | } 52 | 53 | return filepath.Abs(file) 54 | } 55 | 56 | // LoadConfig ... 57 | // 58 | func LoadConfig(files ...interface{}) error { 59 | var file = "config.ini" 60 | if len(files) > 0 { 61 | file = files[0].(string) 62 | files = files[1:] 63 | } 64 | if _, err := os.Stat(file); err != nil { 65 | return err 66 | } 67 | 68 | AppName = "GoSymbols" 69 | AppPath, _ = os.Getwd() //exePath() 70 | IsWindows = runtime.GOOS == "windows" 71 | log.Info("[Config] App path %s.", AppPath) 72 | 73 | cfg, err := ini.LoadSources(ini.LoadOptions{ 74 | AllowBooleanKeys: true, 75 | Insensitive: true, 76 | Loose: true, 77 | }, 78 | file, 79 | files..., 80 | ) 81 | 82 | if err != nil { 83 | log.Fatal(2, "[Config] load config failed : %v.", err) 84 | return err 85 | } 86 | 87 | base := cfg.Section("base") 88 | Debug, _ = base.Key("Debug").Bool() 89 | 90 | SymStoreExe = base.Key("SYMSTORE_EXE").String() 91 | if SymStoreExe == "" { 92 | fmt.Println("[Config] SYMSTORE_EXE is missing.") 93 | log.Fatal(2, "[Config] SYMSTORE_EXE is missing.") 94 | } 95 | 96 | Destination = base.Key("DESTINATION").String() 97 | if Destination == "" { 98 | fmt.Println("[Config] DESTINATION is missing.") 99 | log.Fatal(2, "[Config] DESTINATION is missing.") 100 | } 101 | 102 | BuildSource = base.Key("BUILD_SOURCE").String() 103 | if BuildSource == "" { 104 | fmt.Println("[Config] BUILD_SOURCE is missing.") 105 | log.Fatal(2, "[Config] BUILD_SOURCE is missing.") 106 | } 107 | 108 | PDBZipFile = base.Key("DEBUG_ZIP").String() 109 | if PDBZipFile == "" { 110 | fmt.Println("[Config] BUILD_SOURCE is missing.") 111 | log.Fatal(2, "[Config] BUILD_SOURCE is missing.") 112 | } 113 | LatestBuildFile = base.Key("LATEST_BUILD").String() 114 | if LatestBuildFile == "" { 115 | fmt.Println("[Config] BUILD_SOURCE is missing.") 116 | log.Fatal(2, "[Config] BUILD_SOURCE is missing.") 117 | } 118 | LogPath = base.Key("LOG_PATH").String() 119 | if LogPath == "" { 120 | LogPath = "logs" 121 | } 122 | SymExcludeList = base.Key("EXCLUDE_LIST").Strings(",") 123 | for index, v := range SymExcludeList { 124 | SymExcludeList[index] = strings.ToLower(v) 125 | } 126 | 127 | appSec := cfg.Section("app") 128 | ClientID = appSec.Key("CLIENT_ID").String() 129 | ClientKey = appSec.Key("CLIENT_KEY").String() 130 | GraphScope = appSec.Key("GRAPH_SCOPE").String() 131 | RedirectURI = appSec.Key("REDIRECT_URI").String() 132 | 133 | web := cfg.Section("web") 134 | Address = web.Key("ADDRESS").String() 135 | WebRoot = web.Key("WEB_ROOT").String() 136 | if WebRoot == "" { 137 | WebRoot = "." 138 | } 139 | Port, _ = web.Key("PORT").Uint() 140 | if Port == 0 { 141 | Port = 8080 142 | } 143 | 144 | return nil 145 | } 146 | 147 | // GetTriggerTime ... 148 | // 149 | func GetTriggerTime() (hour, min int) { 150 | fmt.Sscanf(ScheduleTime, "%d:%d", &hour, &min) 151 | return 152 | } 153 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestLoadConfig(t *testing.T) { 8 | err := LoadConfig("..\\config.ini") 9 | if err != nil { 10 | t.Error(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/adyzng/GoSymbols/cmd" 9 | "github.com/adyzng/GoSymbols/config" 10 | "github.com/urfave/cli" 11 | 12 | log "gopkg.in/clog.v1" 13 | ) 14 | 15 | func init() { 16 | fpath, _ := filepath.Abs(config.LogPath) 17 | if err := os.MkdirAll(filepath.Dir(fpath), 666); err != nil { 18 | fmt.Printf("[App] Create log folder failed: %v.", err) 19 | } 20 | 21 | log.New(log.FILE, log.FileConfig{ 22 | Level: log.TRACE, 23 | Filename: filepath.Join(fpath, "app.log"), 24 | BufferSize: 2048, 25 | FileRotationConfig: log.FileRotationConfig{ 26 | Rotate: true, 27 | MaxDays: 30, 28 | MaxSize: 50 * (1 << 20), 29 | }, 30 | }) 31 | } 32 | 33 | const APP_VER = "0.0.0.1" 34 | 35 | func main() { 36 | app := cli.NewApp() 37 | app.Name = config.AppName 38 | app.Usage = "A self-service symbol store" 39 | app.Version = APP_VER 40 | app.Commands = []cli.Command{ 41 | cmd.Web, 42 | cmd.Admin, 43 | cmd.AddBuild, 44 | } 45 | 46 | app.Flags = append(app.Flags, []cli.Flag{}...) 47 | app.Run(os.Args) 48 | log.Shutdown() 49 | } 50 | -------------------------------------------------------------------------------- /restful/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "time" 12 | 13 | "github.com/adyzng/GoSymbols/restful/session" 14 | 15 | "github.com/adyzng/GoSymbols/config" 16 | "github.com/adyzng/GoSymbols/restful" 17 | log "gopkg.in/clog.v1" 18 | ) 19 | 20 | const ( 21 | // MicrosoftAuthURI restful api 22 | adAuthURI = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" 23 | adTokenURI = "https://login.microsoftonline.com/common/oauth2/v2.0/token" 24 | graphURL = "https://graph.microsoft.com/v1.0" 25 | ) 26 | 27 | func getURL(typ string) string { 28 | switch typ { 29 | case "me": 30 | return graphURL + "/me" 31 | case "photo": 32 | return graphURL + "/me/photo/$value" 33 | default: 34 | return graphURL 35 | } 36 | } 37 | 38 | // AuthURL combine the auth url 39 | // 40 | func AuthURL() string { 41 | if location, err := url.Parse(adAuthURI); err == nil { 42 | params := location.Query() 43 | params.Add("client_id", config.ClientID) 44 | params.Add("redirect_uri", config.RedirectURI) 45 | params.Add("response_type", "code") 46 | params.Add("response_mode", "form_post") 47 | params.Add("scope", config.GraphScope) 48 | params.Add("state", fmt.Sprintf("%d", time.Now().Unix())) 49 | location.RawQuery = params.Encode() 50 | return location.String() 51 | } 52 | return "" 53 | } 54 | 55 | // QueryToken get token from provider 56 | // 57 | /***Example 58 | POST /{tenant}/oauth2/v2.0/token HTTP/1.1 59 | Host: https://login.microsoftonline.com 60 | Content-Type: application/x-www-form-urlencoded 61 | 62 | client_id=6731de76-14a6-49ae-97bc-6eba6914391e 63 | &scope=https%3A%2F%2Fgraph.microsoft.com%2Fmail.read 64 | &code=OAAABAAAAiL9Kn2Z27UubvWFPbm0gLWQJVzCTE9UkP3pSx1aXxUjq3n8b2JRLk4OxVXr... 65 | &redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F 66 | &grant_type=authorization_code 67 | &client_secret=JqQX2PNo9bpM0uEihUPzyrh 68 | */ 69 | func QueryToken(code, state string) (*GraphToken, error) { 70 | payload := url.Values{ 71 | "client_id": {config.ClientID}, 72 | "redirect_uri": {config.RedirectURI}, 73 | "client_secret": {config.ClientKey}, 74 | "scope": {config.GraphScope}, 75 | "grant_type": {"authorization_code"}, 76 | "code": {code}, 77 | } 78 | 79 | buff, err := restful.HttpPost(adTokenURI, strings.NewReader(payload.Encode()), nil) 80 | if err != nil { 81 | if buff != nil { 82 | gErr := GraphError{} 83 | if json.NewDecoder(buff).Decode(&gErr); gErr.Error != "" { 84 | log.Warn("[Auth] Request Access Token error: %+v.", gErr) 85 | err = errors.New(gErr.Description) 86 | } 87 | } 88 | log.Error(2, "[Auth] Request Access Token failed: %v.", err) 89 | return nil, err 90 | } 91 | //log.Info("[Auth] Response %s.", string(buff.Bytes())) 92 | token := GraphToken{} 93 | if err := json.NewDecoder(buff).Decode(&token); err != nil { 94 | log.Error(2, "[Auth] Decode token failed: %v.", err) 95 | return nil, err 96 | } 97 | if len(token.AccessToken) == 0 { 98 | log.Warn("[Auth] Get invalid token.") 99 | return nil, fmt.Errorf("invalid access token") 100 | } 101 | 102 | log.Trace("[Auth] Token: %s %s.", token.Type, token.AccessToken[:10]) 103 | token.State = state 104 | token.ExpireAt = time.Now().Unix() + token.ExpireAt 105 | return &token, nil 106 | } 107 | 108 | // RefreshToken refresh token if access token is expired 109 | // 110 | /***Refresh Example: 111 | POST /{tenant}/oauth2/v2.0/token HTTP/1.1 112 | Host: https://login.microsoftonline.com 113 | Content-Type: application/x-www-form-urlencoded 114 | 115 | client_id=6731de76-14a6-49ae-97bc-6eba6914391e 116 | &scope=https%3A%2F%2Fgraph.microsoft.com%2Fmail.read 117 | &refresh_token=OAAABAAAAiL9Kn2Z27UubvWFPbm0gLWQJVzCTE9UkP3pSx1aXxUjq... 118 | &redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F 119 | &grant_type=refresh_token 120 | &client_secret=JqQX2PNo9bpM0uEihUPzyrh 121 | */ 122 | func RefreshToken(token *GraphToken) (*GraphToken, error) { 123 | payload := url.Values{ 124 | "client_id": {config.ClientID}, 125 | "redirect_uri": {config.RedirectURI}, 126 | "client_secret": {config.ClientKey}, 127 | "scope": {config.GraphScope}, 128 | "refresh_token": {token.RefreshToken}, 129 | "grant_type": {"refresh_token"}, 130 | } 131 | 132 | buff, err := restful.HttpPost(adTokenURI, strings.NewReader(payload.Encode()), nil) 133 | if err != nil { 134 | if buff != nil { 135 | gErr := GraphError{} 136 | if json.NewDecoder(buff).Decode(&gErr); gErr.Error != "" { 137 | log.Warn("[Auth] Refresh Token error: %+v.", gErr) 138 | err = errors.New(gErr.Description) 139 | } 140 | } 141 | log.Error(2, "[Auth] Refresh Token failed: %v.", err) 142 | return nil, err 143 | } 144 | 145 | var tokenNew GraphToken 146 | if err := json.NewDecoder(buff).Decode(&tokenNew); err != nil { 147 | log.Error(2, "[Auth] Decode token failed: %v.", err) 148 | return nil, err 149 | } 150 | log.Info("[Auth] Refresh token (%+v).", tokenNew) 151 | 152 | tokenNew.State = token.State 153 | tokenNew.ExpireAt = time.Now().Unix() + tokenNew.ExpireAt 154 | return &tokenNew, nil 155 | } 156 | 157 | func refreshToken(sessID string, token *GraphToken) *GraphToken { 158 | // 159 | // if token expired, add 10s extra cost. 160 | // 161 | if token.ExpireAt != time.Now().Unix()+10 { 162 | return token 163 | } 164 | log.Info("[User] Refresh token for %s.", token.UserName) 165 | if tokenNew, _ := RefreshToken(token); tokenNew != nil { 166 | if sessID != "" { 167 | session.GetManager().Set(sessID, tokenNew) 168 | } 169 | return tokenNew 170 | } 171 | return token 172 | } 173 | 174 | // GetUserProfile request user profile by access token 175 | // 176 | func GetUserProfile(sessID string, token *GraphToken) (*GraphUser, error) { 177 | token = refreshToken(sessID, token) 178 | 179 | buff, err := restful.HttpGet(getURL("me"), func(req *http.Request) { 180 | str := fmt.Sprintf("%s %s", token.Type, token.AccessToken) 181 | req.Header.Set("Authorization", str) 182 | }) 183 | 184 | if err != nil { 185 | if buff != nil { 186 | var gErr *GraphError 187 | if json.NewDecoder(buff).Decode(&gErr); gErr != nil { 188 | log.Warn("[Auth] Request profile error: %+v.", gErr) 189 | err = errors.New(gErr.Description) 190 | } 191 | } 192 | log.Error(2, "[Auth] Request profile failed: %v.", err) 193 | return nil, err 194 | } 195 | 196 | user := GraphUser{} 197 | if err := json.NewDecoder(buff).Decode(&user); err != nil { 198 | log.Error(2, "[Auth] Decode user info failed: %v.", err) 199 | return nil, err 200 | } 201 | return &user, nil 202 | } 203 | 204 | // GetUserPhoto get user photo with access token 205 | // 206 | // refer: https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/profilephoto_get 207 | // 208 | func GetUserPhoto(sessID string, token *GraphToken, w io.Writer) error { 209 | token = refreshToken(sessID, token) 210 | buff, err := restful.HttpGet(getURL("photo"), func(req *http.Request) { 211 | str := fmt.Sprintf("%s %s", token.Type, token.AccessToken) 212 | req.Header.Set("Authorization", str) 213 | }) 214 | 215 | if err != nil { 216 | if buff != nil { 217 | var gErr *GraphError 218 | if json.NewDecoder(buff).Decode(&gErr); gErr != nil { 219 | log.Warn("[Auth] Request user photo error: %+v.", gErr) 220 | err = errors.New(gErr.Description) 221 | } 222 | } 223 | log.Error(2, "[Auth] Request user photo failed: %v.", err) 224 | return err 225 | } 226 | 227 | _, err = io.Copy(w, buff) 228 | return err 229 | } 230 | -------------------------------------------------------------------------------- /restful/auth/model.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | /** 4 | * 5 | * Following struct are from graph api response json object. 6 | * 7 | * refer: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols-oauth-code 8 | * 9 | **/ 10 | 11 | // GraphUser from microsoft Graph API 12 | // 13 | type GraphUser struct { 14 | ID string `json:"id,omitempty"` 15 | Enabled bool `json:"accountEnabled,omitempty"` 16 | UserType string `json:"userType,omitempty"` 17 | DisplayName string `json:"displayName,omitempty"` 18 | GivenName string `json:"givenName,omitempty"` 19 | AboutMe string `json:"aboutMe,omitempty"` 20 | Mail string `json:"mail,omitempty"` 21 | JobTitle string `json:"jobTitle,omitempty"` 22 | MobilePhone string `json:"mobilePhone,omitempty"` 23 | CompanyName string `json:"companyName,omitempty"` 24 | Department string `json:"department,omitempty"` 25 | BusinessPhones []string `json:"businessPhones,omitempty"` 26 | } 27 | 28 | // GraphToken response data from ad server 29 | // 30 | type GraphToken struct { 31 | AccessToken string `json:"access_token,omitempty"` 32 | Type string `json:"token_type,omitempty"` 33 | ExpireAt int64 `json:"expires_in,omitempty"` 34 | Scope string `json:"scope,omitempty"` 35 | RefreshToken string `json:"refresh_token,omitempty"` 36 | IDToken string `json:"id_token,omitempty"` 37 | State string `json:"-"` 38 | UserName string `json:"-"` 39 | } 40 | 41 | // GraphError wrap of Graph API error 42 | // 43 | type GraphError struct { 44 | Error string `json:"error,omitempty"` 45 | Description string `json:"error_description,omitempty"` 46 | Codes []int `json:"error_codes,omitempty"` 47 | Timestamp string `json:"timestamp,omitempty"` 48 | TraceID string `json:"trace_id,omitempty"` 49 | } 50 | -------------------------------------------------------------------------------- /restful/http.go: -------------------------------------------------------------------------------- 1 | package restful 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "net/http" 8 | "sync" 9 | "time" 10 | 11 | log "gopkg.in/clog.v1" 12 | ) 13 | 14 | var ( 15 | bufferSize4K = 4096 16 | bufferPool4K = sync.Pool{ 17 | New: func() interface{} { 18 | return bytes.NewBuffer(make([]byte, bufferSize4K)) 19 | }, 20 | } 21 | 22 | httpHeaders = map[string]string{ 23 | "User-Agent": "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36", 24 | "ContentType": "application/json", //"text/html; charset=utf-8", 25 | "Connection": "keep-alive", 26 | } 27 | httpClient = &http.Client{ 28 | Timeout: time.Minute * 1, 29 | } 30 | ) 31 | 32 | // HttpGet wrap of http.Get 33 | // 34 | func HttpGet(uri string, fnCallback func(*http.Request)) (*bytes.Buffer, error) { 35 | var ( 36 | err error 37 | buffer *bytes.Buffer 38 | resp *http.Response 39 | req *http.Request 40 | ) 41 | if req, err = http.NewRequest("GET", uri, nil); err != nil { 42 | log.Warn("[GET] New request of %s failed: %s.", uri, err) 43 | return nil, err 44 | } 45 | req.Header.Set("User-Agent", httpHeaders["User-Agent"]) 46 | 47 | if fnCallback != nil { 48 | fnCallback(req) 49 | } 50 | 51 | if resp, err = httpClient.Do(req); err != nil { 52 | log.Warn("[GET] Http GET %s failed: %s.", uri, err) 53 | return nil, err 54 | } 55 | if resp.StatusCode != http.StatusOK { 56 | err = errors.New(resp.Status) 57 | } 58 | 59 | defer resp.Body.Close() 60 | buffer = bufferPool4K.Get().(*bytes.Buffer) 61 | buffer.Reset() 62 | 63 | if _, err2 := io.Copy(buffer, resp.Body); err2 != nil { 64 | log.Warn("[GET] Read response failed: %s.", err2) 65 | bufferPool4K.Put(buffer) 66 | return nil, err2 67 | } 68 | return buffer, err 69 | } 70 | 71 | // HttpPost wrap of http.Post 72 | // 73 | func HttpPost(uri string, body io.Reader, fnCallback func(*http.Request)) (*bytes.Buffer, error) { 74 | var ( 75 | err error 76 | buffer *bytes.Buffer 77 | resp *http.Response 78 | req *http.Request 79 | ) 80 | if req, err = http.NewRequest("POST", uri, body); err != nil { 81 | log.Warn("[POST] New request of %s failed: %s.", uri, err) 82 | return nil, err 83 | } 84 | req.Header.Set("User-Agent", httpHeaders["User-Agent"]) 85 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") // must 86 | 87 | if fnCallback != nil { 88 | fnCallback(req) 89 | } 90 | 91 | if resp, err = httpClient.Do(req); err != nil { 92 | log.Warn("[POST] Http POST %s failed: %s.", uri, err) 93 | return nil, err 94 | } 95 | if resp.StatusCode != http.StatusOK { 96 | err = errors.New(resp.Status) 97 | } 98 | 99 | defer resp.Body.Close() 100 | buffer = bufferPool4K.Get().(*bytes.Buffer) 101 | buffer.Reset() 102 | 103 | if _, err2 := io.Copy(buffer, resp.Body); err2 != nil { 104 | log.Warn("[POST] Read response failed: %s.", err2) 105 | bufferPool4K.Put(buffer) 106 | return nil, err2 107 | } 108 | return buffer, err 109 | } 110 | -------------------------------------------------------------------------------- /restful/response.go: -------------------------------------------------------------------------------- 1 | package restful 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | 8 | "github.com/adyzng/GoSymbols/symbol" 9 | 10 | log "gopkg.in/clog.v1" 11 | ) 12 | 13 | var ( 14 | buffSize4k = 4096 15 | /* buffPool4K = sync.Pool{ 16 | New: func() interface{} { 17 | // 4k buffer pool by default 18 | return bytes.NewBuffer(make([]byte, buffSize4k)) 19 | }, 20 | } */ 21 | ) 22 | 23 | var ( 24 | ErrSucceed = ErrCodeMsg{0, "ok"} 25 | ErrInvalidParam = ErrCodeMsg{100, "invalid parameter"} 26 | ErrServerInner = ErrCodeMsg{101, "server inner error"} 27 | ErrLoginFailed = ErrCodeMsg{102, "login failed, please retry"} 28 | ErrLoginNeeded = ErrCodeMsg{103, "login first"} 29 | 30 | ErrInvalidBranch = ErrCodeMsg{200, "branch unavailable"} 31 | ErrExistOnLocal = ErrCodeMsg{201, "branch exist in symbol store"} 32 | ErrUnknownBranch = ErrCodeMsg{202, "unknown branch"} 33 | ErrUnauthorized = ErrCodeMsg{203, "unauthorized operation"} 34 | ) 35 | 36 | // ErrCodeMsg is predefined error code and error message 37 | // 38 | type ErrCodeMsg struct { 39 | Code int `json:"code"` 40 | Message string `json:"message"` 41 | } 42 | 43 | // BranchList return branch list of current symbol store 44 | // 45 | type BranchList struct { 46 | Total int `json:"total"` 47 | Branchs []*symbol.Branch `json:"branchs"` 48 | } 49 | type BuildList struct { 50 | Branch string `json:"branchName"` 51 | Total int `json:"total"` 52 | Builds []*symbol.Build `json:"builds"` 53 | } 54 | type SymbolList struct { 55 | Branch string `json:"branchName"` 56 | Build string `json:"buildID"` 57 | Total int `json:"total"` 58 | Symbols []*symbol.Symbol `json:"symbols"` 59 | } 60 | type Message struct { 61 | Status int `json:"status,omitempty"` 62 | Branch string `json:"branch,omitempty"` 63 | Build string `json:"build,omitempty"` 64 | Date string `json:"date,omitempty"` 65 | } 66 | 67 | // RestResponse is the basic struct used to wrap data back to client in json format. 68 | // 69 | type RestResponse struct { 70 | ErrCodeMsg 71 | Data interface{} `json:"data"` 72 | } 73 | 74 | // ToJSON encoding to json string 75 | // 76 | func (r *RestResponse) ToJSON() string { 77 | buff := bytes.NewBuffer(make([]byte, buffSize4k)) 78 | err := json.NewEncoder(buff).Encode(r) 79 | if err != nil { 80 | log.Error(2, "[Restful] json encoding failed with %v.", err) 81 | return "" 82 | } 83 | return buff.String() 84 | } 85 | 86 | // WriteJSON write json reponse to client 87 | // 88 | func (r *RestResponse) WriteJSON(w http.ResponseWriter) error { 89 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 90 | w.WriteHeader(http.StatusOK) 91 | 92 | if err := json.NewEncoder(w).Encode(r); err != nil { 93 | log.Error(2, "[Restful] JSON.Encode(%+v) failed with %v.", r, err) 94 | w.WriteHeader(http.StatusInternalServerError) 95 | return err 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /restful/session/memory.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | 8 | "github.com/adyzng/GoSymbols/restful/uuid" 9 | ) 10 | 11 | // MemoryStore store sessin in memory 12 | // 13 | type MemoryStore struct { 14 | mx sync.RWMutex 15 | sess map[string]*sessData 16 | } 17 | type sessData struct { 18 | expireAt int64 19 | data interface{} 20 | } 21 | 22 | // NewMemStore ... 23 | // 24 | func NewMemStore() SessStore { 25 | return &MemoryStore{ 26 | sess: make(map[string]*sessData), 27 | } 28 | } 29 | 30 | // Get session 31 | func (m *MemoryStore) Get(id string) interface{} { 32 | m.mx.RLock() 33 | defer m.mx.RUnlock() 34 | if d, ok := m.sess[id]; ok { 35 | return d.data 36 | } 37 | return nil 38 | } 39 | 40 | // Set session data 41 | func (m *MemoryStore) Set(id string, data interface{}) error { 42 | m.mx.Lock() 43 | defer m.mx.Unlock() 44 | if sd, ok := m.sess[id]; ok { 45 | sd.expireAt = time.Now().Add(SessTimeout).Unix() 46 | sd.data = data 47 | return nil 48 | } 49 | return errors.New("session not exist") 50 | } 51 | 52 | // Delete session 53 | func (m *MemoryStore) Delete(id string) interface{} { 54 | m.mx.Lock() 55 | defer m.mx.Unlock() 56 | if data, ok := m.sess[id]; ok { 57 | delete(m.sess, id) 58 | return data.data 59 | } 60 | return nil 61 | } 62 | 63 | // Create new session 64 | func (m *MemoryStore) Create(data interface{}) string { 65 | id := uuid.NewUUID() 66 | m.mx.Lock() 67 | defer m.mx.Unlock() 68 | m.sess[id] = &sessData{ 69 | data: data, 70 | expireAt: time.Now().Add(SessTimeout).Unix(), 71 | } 72 | return id 73 | } 74 | 75 | // Udpate all sessions, if timeout, delete it 76 | // update `max` items at most each time. 77 | func (m *MemoryStore) Udpate(max int) int { 78 | m.mx.Lock() 79 | defer m.mx.Unlock() 80 | 81 | total := max 82 | nowUnix := time.Now().Unix() 83 | 84 | for key, val := range m.sess { 85 | if val.expireAt < nowUnix { 86 | delete(m.sess, key) 87 | } 88 | if total--; total == 0 { 89 | break 90 | } 91 | } 92 | return max 93 | } 94 | -------------------------------------------------------------------------------- /restful/session/model.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | log "gopkg.in/clog.v1" 8 | ) 9 | 10 | // StorType ... 11 | type StorType int 12 | 13 | const ( 14 | SessTimeout = time.Hour * 24 15 | CookieSessID = "session_id" 16 | CookieMaxAge = time.Hour * 24 / time.Second 17 | ) 18 | const ( 19 | _ StorType = iota 20 | MemStore // memory store 21 | RedisStore // redis store 22 | ) 23 | 24 | var ( 25 | sessMgr *SessManager 26 | once sync.Once 27 | ) 28 | 29 | // SessStore interface 30 | // 31 | type SessStore interface { 32 | // Get session data 33 | Get(id string) interface{} 34 | // Set session data 35 | Set(id string, data interface{}) error 36 | // Update sessions if timeout, delete it 37 | Udpate(max int) int 38 | 39 | // Delete session 40 | Delete(id string) interface{} 41 | // Create an new session 42 | Create(data interface{}) string 43 | } 44 | 45 | // SessManager manage all sessions 46 | // 47 | type SessManager struct { 48 | SessStore 49 | } 50 | 51 | // GetManager create an session manager 52 | // 53 | func GetManager(typ ...StorType) *SessManager { 54 | once.Do(func() { 55 | typ = append(typ, MemStore) 56 | switch typ[0] { 57 | case MemStore: 58 | sessMgr = &SessManager{ 59 | SessStore: NewMemStore(), 60 | } 61 | go sessMgr.scan() 62 | case RedisStore: 63 | panic("redis session store isn't implement yet") 64 | default: 65 | panic("unknown session storage type") 66 | } 67 | }) 68 | return sessMgr 69 | } 70 | 71 | func (s *SessManager) scan() { 72 | ticker := time.NewTicker(time.Minute) 73 | defer ticker.Stop() 74 | 75 | log.Trace("[Session] Session updater starting ...") 76 | for { 77 | select { 78 | case <-ticker.C: 79 | s.Udpate(1000) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /restful/uuid/uuid.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | Size = 16 13 | version = 0x20 14 | format = "%08x-%04x-%04x-%04x-%012x" 15 | ) 16 | 17 | var ( 18 | ErrInvalidUUID = fmt.Errorf("invalid uuid string") 19 | ) 20 | 21 | // UUID ... 22 | type UUID [Size]byte 23 | 24 | // Generate a random UUID 25 | func Generate() (u UUID) { 26 | var ( 27 | retry = 10 28 | count = 0 29 | ) 30 | for ; retry >= 0; retry-- { 31 | n, err := io.ReadFull(rand.Reader, u[count:]) 32 | if count == len(u) { 33 | break 34 | } 35 | 36 | if err != nil { 37 | if retry == 0 { 38 | panic(fmt.Sprintf("generate uuid failed with : %s", err)) 39 | } 40 | count += n 41 | } 42 | } 43 | 44 | // set version 45 | u[4] = (u[4] & 0x0F) | version 46 | return 47 | } 48 | 49 | // NewUUID ... 50 | func NewUUID() string { 51 | return Generate().RawString() 52 | } 53 | 54 | // Parse UUID from string 55 | func Parse(s string) (u UUID, e error) { 56 | sLen := len(s) 57 | if sLen != 32 && sLen != 36 { 58 | return UUID{}, ErrInvalidUUID 59 | } 60 | 61 | if sLen == 36 { 62 | s = strings.Replace(s, "-", "", -1) 63 | } 64 | 65 | for i, n := 0, len(s); i < n; i += 2 { 66 | v, err := strconv.ParseUint(s[i:i+2], 16, 8) 67 | if err != nil { 68 | return UUID{}, err 69 | } 70 | 71 | u[i/2] = byte(v) 72 | } 73 | return 74 | } 75 | 76 | // String returns format 974AFFD3-1BCC-4475-8910-A967AFAE51FE 77 | func (u UUID) String() string { 78 | return fmt.Sprintf(format, u[:4], u[4:6], u[6:8], u[8:10], u[10:]) 79 | } 80 | 81 | // RawString returns format 974AFFD31BCC44758910A967AFAE51FE 82 | func (u UUID) RawString() string { 83 | return fmt.Sprintf("%x", u[:]) 84 | } 85 | 86 | // Version return current version 87 | func (u UUID) Version() byte { 88 | return (u[4] & 0xF0) 89 | } 90 | -------------------------------------------------------------------------------- /restful/uuid/uuid_test.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | const iterations = 1000 8 | 9 | func TestGenerate(t *testing.T) { 10 | for i := 0; i < iterations; i++ { 11 | u := Generate() 12 | 13 | if u.Version() != 0x20 { 14 | t.Fatalf("generated uuid is not valid <%s>", u) 15 | } 16 | 17 | sid := u.String() 18 | if len(sid) != len("974AFFD3-1BCC-4475-8910-A967AFAE51FE") { 19 | t.Fatalf("uuid to string failed. <%s>", sid) 20 | } 21 | 22 | rid := u.RawString() 23 | if len(rid) != len("974AFFD31BCC44758910A967AFAE51FE") { 24 | t.Fatalf("uuid to raw string failed. <%s>", rid) 25 | } 26 | } 27 | } 28 | 29 | func TestParse(t *testing.T) { 30 | for i := 0; i < iterations; i++ { 31 | u := Generate() 32 | parsed, err := Parse(u.String()) 33 | 34 | if err != nil { 35 | t.Fatalf("parse uuid failed %v: %v", u, err) 36 | } 37 | 38 | if parsed != u { 39 | t.Fatalf("parsed uuid not equal origin %v: %v", u, parsed) 40 | } 41 | } 42 | 43 | for _, c := range []string{ 44 | "abcded", 45 | "{0AD4A3E1-AA6F-4986-97BC-26BCA7E113E5}", // invalid format 46 | " 0C79A2E2-D363-4E0F-8A61-091027200D6B", // leading space 47 | "59AEBE38-7B8A-4185-9D15-685B85154EE2 ", // trailing space 48 | "00000000-0000-0000-0000-x00000000000", // correct length, invalid character 49 | } { 50 | if _, err := Parse(c); err == nil { 51 | t.Fatalf("parsing %q should fail", c) 52 | } else { 53 | t.Logf("parsing expected error : %v", err) 54 | } 55 | } 56 | } 57 | 58 | func BenchmarkGenerate(b *testing.B) { 59 | b.RunParallel(func(pb *testing.PB) { 60 | for pb.Next() { 61 | Generate() 62 | //b.Logf("uuid: %v", u) 63 | } 64 | }) 65 | } 66 | 67 | func BenchmarkParse(b *testing.B) { 68 | idx, cnt := 0, 4 69 | idList := []string{ 70 | "D1A6710587134CE3AA6FF50465B2A37A", 71 | "B156EF0B-83A2-4922-8308-A24631E285A6", 72 | "D03A9D910A8F4ADBA41DCDD52EDF121C", 73 | "E45830C4-1E08-411D-99CE-6140102BD683", 74 | } 75 | 76 | b.RunParallel(func(pb *testing.PB) { 77 | for pb.Next() { 78 | c := idx % cnt 79 | idx++ 80 | 81 | if _, err := Parse(idList[c]); err != nil { 82 | b.Fatalf("parse uuid failed <%v : %v>", idList[c], err) 83 | } 84 | } 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /restful/v1/login.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/adyzng/GoSymbols/restful" 8 | "github.com/adyzng/GoSymbols/restful/auth" 9 | "github.com/adyzng/GoSymbols/restful/session" 10 | log "gopkg.in/clog.v1" 11 | ) 12 | 13 | // check if login required 14 | // 15 | func loginRequired(r *http.Request) (string, *auth.GraphToken) { 16 | ssid := "" 17 | if s, _ := r.Cookie(session.CookieSessID); s != nil { 18 | ssid = s.Value 19 | } 20 | if data := session.GetManager().Get(ssid); data != nil { 21 | return ssid, data.(*auth.GraphToken) 22 | } 23 | return ssid, nil 24 | } 25 | 26 | // AuthLogin login by oauth to Arcserve domain 27 | // [:]/auth/login 28 | // 29 | func AuthLogin(w http.ResponseWriter, r *http.Request) { 30 | redirect := "/" 31 | if _, token := loginRequired(r); token == nil { 32 | redirect = auth.AuthURL() 33 | } else { 34 | log.Info("[Login] User %s already logined.", token.UserName) 35 | } 36 | 37 | log.Trace("[Login] Redirect URL: %s.", redirect) 38 | w.Header().Set("Location", redirect) 39 | w.WriteHeader(http.StatusFound) 40 | } 41 | 42 | // AuthLogout user logout 43 | // [:]/auth/logout 44 | // 45 | func AuthLogout(w http.ResponseWriter, r *http.Request) { 46 | sess, _ := r.Cookie(session.CookieSessID) 47 | if sess == nil { 48 | log.Trace("[Logout] Session ID is empty.") 49 | w.WriteHeader(http.StatusOK) 50 | return 51 | } 52 | if data := session.GetManager().Delete(sess.Value); data != nil { 53 | if token, ok := data.(*auth.GraphToken); ok { 54 | log.Info("[Logout] user %s.", token.UserName) 55 | } 56 | } 57 | w.Header().Set("Location", "/") 58 | w.WriteHeader(http.StatusFound) 59 | } 60 | 61 | // Authorize handle response from the microsoft online authorize 62 | // [:]/auth/authorize 63 | // 64 | func Authorize(w http.ResponseWriter, r *http.Request) { 65 | r.ParseForm() 66 | code, state := r.FormValue("code"), r.FormValue("state") 67 | 68 | errType, errDesc := r.FormValue("error"), r.FormValue("error_description") 69 | if errType != "" { 70 | log.Error(2, "[Login] Authorize error: %s, desc: %s.", errType, errDesc) 71 | w.WriteHeader(http.StatusUnauthorized) 72 | return 73 | } 74 | if code == "" || state == "" { 75 | log.Warn("[Login] Empty auth code.") 76 | w.WriteHeader(http.StatusBadRequest) 77 | return 78 | } 79 | 80 | token, err := auth.QueryToken(code, state) 81 | if err != nil { 82 | res := restful.RestResponse{ErrCodeMsg: restful.ErrLoginFailed} 83 | res.Message = fmt.Sprintf("%s", err) 84 | 85 | res.WriteJSON(w) 86 | log.Error(2, "[Login] Query token failed: %v.", err) 87 | return 88 | } 89 | 90 | sessID := session.GetManager().Create(token) 91 | if user, _ := auth.GetUserProfile("", token); user != nil { 92 | token.UserName = user.DisplayName 93 | log.Info("[Login] User (%s) login succeed.", user.DisplayName) 94 | } 95 | 96 | // set http cookie 97 | http.SetCookie(w, &http.Cookie{ 98 | Name: session.CookieSessID, 99 | MaxAge: int(session.CookieMaxAge), 100 | HttpOnly: false, 101 | Value: sessID, 102 | Path: "/", 103 | }) 104 | 105 | w.Header().Set("Location", "/") 106 | w.WriteHeader(http.StatusFound) 107 | } 108 | 109 | // GetUserProfile get user information 110 | // [:]/user/profile 111 | // 112 | func GetUserProfile(w http.ResponseWriter, r *http.Request) { 113 | ssid, token := loginRequired(r) 114 | if token == nil { 115 | w.WriteHeader(http.StatusUnauthorized) 116 | log.Warn("[User] Login required.") 117 | return 118 | } 119 | 120 | resp := restful.RestResponse{} 121 | if user, err := auth.GetUserProfile(ssid, token); err == nil { 122 | resp.Data = user 123 | log.Trace("[User] Get user profile for %s.", user.DisplayName) 124 | } else { 125 | resp.ErrCodeMsg = restful.ErrLoginNeeded 126 | resp.Message = fmt.Sprintf("%s", err) 127 | } 128 | 129 | resp.WriteJSON(w) 130 | } 131 | 132 | // GetUserPhoto get user profile photo 133 | // [:]/user/photo 134 | // 135 | func GetUserPhoto(w http.ResponseWriter, r *http.Request) { 136 | ssid, token := loginRequired(r) 137 | if token == nil { 138 | w.WriteHeader(http.StatusUnauthorized) 139 | log.Warn("[User] Login required.") 140 | return 141 | } 142 | if err := auth.GetUserPhoto(ssid, token, w); err != nil { 143 | log.Error(2, "[User] Get user photo failed: %v.", err) 144 | w.WriteHeader(http.StatusServiceUnavailable) 145 | return 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /restful/v1/symbols.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/adyzng/GoSymbols/restful" 13 | "github.com/adyzng/GoSymbols/symbol" 14 | "github.com/gorilla/mux" 15 | 16 | log "gopkg.in/clog.v1" 17 | ) 18 | 19 | // RestBranchList response to restful API 20 | // [:]/api/branches [GET] 21 | // 22 | // @ return { 23 | // Total: int 24 | // Branchs: []*symbol.Branch 25 | // } 26 | // 27 | func RestBranchList(w http.ResponseWriter, r *http.Request) { 28 | bs := restful.BranchList{} 29 | symbol.GetServer().WalkBuilders(func(bu symbol.Builder) error { 30 | if b, ok := bu.(*symbol.BrBuilder); ok { 31 | bs.Total++ 32 | nb := b.Branch 33 | bs.Branchs = append(bs.Branchs, &nb) 34 | } 35 | return nil 36 | }) 37 | resp := restful.RestResponse{ 38 | Data: &bs, 39 | } 40 | resp.WriteJSON(w) 41 | } 42 | 43 | // RestBuildList response to restful API 44 | // [:]/api/branches/{name} [GET] 45 | // 46 | // @:name {branch name} 47 | // 48 | // @return { 49 | // Total: int 50 | // Builds: []*symbol.Build 51 | // } 52 | // 53 | func RestBuildList(w http.ResponseWriter, r *http.Request) { 54 | var vars = mux.Vars(r) 55 | resp := restful.RestResponse{ 56 | ErrCodeMsg: restful.ErrInvalidParam, 57 | } 58 | 59 | if sname, ok := vars["name"]; ok { 60 | builder := symbol.GetServer().Get(sname) 61 | if builder != nil { 62 | blst := restful.BuildList{ 63 | Branch: sname, 64 | } 65 | _, err := builder.ParseBuilds(func(build *symbol.Build) error { 66 | blst.Total++ 67 | blst.Builds = append(blst.Builds, build) 68 | return nil 69 | }) 70 | if err != nil { 71 | log.Error(2, "[Restful] Parse builds for %s failed: %v.", sname, err) 72 | } 73 | resp.Data = blst 74 | resp.ErrCodeMsg = restful.ErrSucceed 75 | } else { 76 | resp.ErrCodeMsg = restful.ErrUnknownBranch 77 | } 78 | } 79 | resp.WriteJSON(w) 80 | } 81 | 82 | // RestSymbolList response to restful API 83 | // [:]/api/branches/:name/:bid [GET] 84 | // 85 | // @:name {branch name} 86 | // @:bid {build id} 87 | // 88 | // @ return { 89 | // Total: int 90 | // Builds: []*symbol.Build 91 | // } 92 | // 93 | func RestSymbolList(w http.ResponseWriter, r *http.Request) { 94 | var vars = mux.Vars(r) 95 | resp := restful.RestResponse{ 96 | ErrCodeMsg: restful.ErrInvalidParam, 97 | } 98 | 99 | sname, bid := vars["name"], vars["bid"] 100 | if sname != "" && bid != "" { 101 | buider := symbol.GetServer().Get(sname) 102 | if buider != nil { 103 | symLst := restful.SymbolList{ 104 | Branch: sname, 105 | Build: bid, 106 | } 107 | _, err := buider.ParseSymbols(bid, func(sym *symbol.Symbol) error { 108 | symLst.Total++ 109 | symLst.Symbols = append(symLst.Symbols, sym) 110 | return nil 111 | }) 112 | if err != nil { 113 | log.Error(2, "[Restful] Parse symbols for %s:%s failed: %v.", 114 | sname, bid, err) 115 | } 116 | resp.Data = symLst 117 | resp.ErrCodeMsg = restful.ErrSucceed 118 | } else { 119 | resp.ErrCodeMsg.Message = "no such build" 120 | } 121 | } 122 | resp.WriteJSON(w) 123 | } 124 | 125 | // DownloadSymbol response download symbol file api 126 | // [:]/api/symbol/{branch}/{hash}/{name} [GET] 127 | // 128 | // @:branch {branch name} 129 | // @:hash {file hash} 130 | // @:name {file name} 131 | // 132 | // @ return file 133 | // 134 | func DownloadSymbol(w http.ResponseWriter, r *http.Request) { 135 | vars := mux.Vars(r) 136 | bname := vars["branch"] 137 | fname := vars["name"] 138 | hash := vars["hash"] 139 | 140 | if bname == "" || hash == "" || fname == "" { 141 | log.Warn("[Restful] Download symbol invalid param: [%s, %s, %s]", 142 | bname, hash, fname) 143 | w.WriteHeader(http.StatusNotFound) 144 | return 145 | } 146 | 147 | buider := symbol.GetServer().Get(bname) 148 | if buider == nil { 149 | log.Warn("[Restful] Download symbol branch not exist: [%s, %s, %s]", 150 | bname, hash, fname) 151 | w.WriteHeader(http.StatusNotFound) 152 | return 153 | } 154 | 155 | fpath := buider.GetSymbolPath(hash, fname) 156 | fd, err := os.OpenFile(fpath, os.O_RDONLY, 666) 157 | if err != nil { 158 | log.Warn("[Restful] Open symbol file %s failed: %v.", fpath, err) 159 | w.WriteHeader(http.StatusNotFound) 160 | return 161 | } 162 | defer fd.Close() 163 | 164 | // set response header 165 | w.Header().Set("Content-Type", "application/octet-stream") 166 | w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fname)) 167 | 168 | // send fil content 169 | var size int64 170 | if size, err = io.Copy(w, fd); err != nil { 171 | log.Error(2, "[Restful] Send file failed: %v.", err) 172 | w.WriteHeader(http.StatusInternalServerError) 173 | return 174 | } 175 | 176 | //w.WriteHeader(http.StatusOK) 177 | log.Trace("[Restful] Send file complete. [%d: %s]", size, fpath) 178 | } 179 | 180 | // ValidateBranch response to check branch api 181 | // [:]/api/branch/check [POST] 182 | // 183 | // @:BODY {branch infomation} 184 | // 185 | // @ return { 186 | // RestResponse 187 | // } 188 | // 189 | func ValidateBranch(w http.ResponseWriter, r *http.Request) { 190 | var branch symbol.Branch 191 | if err := json.NewDecoder(r.Body).Decode(&branch); err != nil { 192 | log.Error(2, "[Restful] Decode request body failed: %v.", err) 193 | w.WriteHeader(http.StatusBadRequest) 194 | return 195 | } 196 | 197 | resp := restful.RestResponse{} 198 | br := symbol.NewBranch2(&branch) 199 | if !br.CanUpdate() { 200 | resp.ErrCodeMsg = restful.ErrInvalidBranch 201 | resp.Message = "branch is not accessable from build server." 202 | resp.WriteJSON(w) 203 | return 204 | } 205 | 206 | if br.CanBrowse() { 207 | resp.ErrCodeMsg = restful.ErrExistOnLocal 208 | resp.WriteJSON(w) 209 | return 210 | } 211 | 212 | resp.WriteJSON(w) 213 | } 214 | 215 | // ModifyBranch response to modify branch api 216 | // [:]/api/branches/modify [POST] 217 | // 218 | // @:BODY {branch infomation} 219 | // 220 | // @ return { 221 | // RestResponse 222 | // } 223 | // 224 | func ModifyBranch(w http.ResponseWriter, r *http.Request) { 225 | resp := restful.RestResponse{} 226 | ss := symbol.GetServer() 227 | 228 | var branch symbol.Branch 229 | if err := json.NewDecoder(r.Body).Decode(&branch); err != nil { 230 | log.Error(2, "[Restful] Decode request body failed: %v.", err) 231 | w.WriteHeader(http.StatusBadRequest) 232 | } 233 | 234 | if br := ss.Modify(&branch); br == nil { 235 | log.Warn("[Restful] Modify invalid branch %v.", branch) 236 | resp.ErrCodeMsg = restful.ErrInvalidBranch 237 | resp.WriteJSON(w) 238 | return 239 | } 240 | if err := ss.SaveBranchs(""); err != nil { 241 | log.Warn("[Restful] Save branch (%v) failed: %v.", branch, err) 242 | } 243 | resp.WriteJSON(w) 244 | } 245 | 246 | // DeleteBranch response to modify branch api 247 | // [:]/api/branches/{name} [DELETE] 248 | // 249 | // @:name {branch name} 250 | // 251 | // @ return { 252 | // RestResponse 253 | // } 254 | // 255 | func DeleteBranch(w http.ResponseWriter, r *http.Request) { 256 | vars := mux.Vars(r) 257 | bname := vars["name"] 258 | resp := restful.RestResponse{} 259 | 260 | branch := symbol.GetServer().Get(bname) 261 | if branch == nil { 262 | log.Warn("[Restful] Delete unknown branch %s.", bname) 263 | resp.ErrCodeMsg = restful.ErrUnknownBranch 264 | resp.WriteJSON(w) 265 | } else { 266 | resp.ErrCodeMsg = restful.ErrUnauthorized 267 | w.WriteHeader(http.StatusUnauthorized) // not allow for now 268 | } 269 | } 270 | 271 | // FetchTodayMsg get today symbols update information 272 | // [:]/api/messages [GET] 273 | // 274 | // @ return { 275 | // RestResponse 276 | // } 277 | // 278 | func FetchTodayMsg(w http.ResponseWriter, r *http.Request) { 279 | resp := restful.RestResponse{} 280 | msgs := make([]*restful.Message, 0, 5) 281 | today := time.Now().Format("2006-01-02") 282 | 283 | symbol.GetServer().WalkBuilders(func(builder symbol.Builder) error { 284 | if b := builder.GetBranch(); b != nil { 285 | if strings.Index(b.UpdateDate, today) == 0 { 286 | msg := &restful.Message{ 287 | Status: 1, // succeed 288 | Branch: b.StoreName, 289 | Build: b.LatestBuild, 290 | Date: b.UpdateDate, 291 | } 292 | msgs = append(msgs, msg) 293 | } 294 | } 295 | return nil 296 | }) 297 | 298 | resp.Data = msgs 299 | resp.ErrCodeMsg = restful.ErrSucceed 300 | resp.WriteJSON(w) 301 | } 302 | 303 | // CreateBranch response to create branch api 304 | // [:]/api/branhes/create [POST] 305 | // 306 | // @:BODY {branch infomation} 307 | // 308 | // @ return { 309 | // RestResponse 310 | // } 311 | // 312 | func CreateBranch(w http.ResponseWriter, r *http.Request) { 313 | _, token := loginRequired(r) 314 | if token == nil { 315 | w.WriteHeader(http.StatusUnauthorized) 316 | log.Warn("[Restful] Login required.") 317 | return 318 | } 319 | branch := symbol.Branch{} 320 | if err := json.NewDecoder(r.Body).Decode(&branch); err != nil { 321 | log.Error(2, "[Restful] Decode request body failed: %v.", err) 322 | w.WriteHeader(http.StatusBadRequest) 323 | } 324 | 325 | resp := restful.RestResponse{} 326 | br := symbol.GetServer().Add(&branch) 327 | if br == nil { 328 | log.Warn("[Restful] Create invalid branch %v.", branch) 329 | resp.ErrCodeMsg = restful.ErrInvalidBranch 330 | resp.WriteJSON(w) 331 | return 332 | } 333 | if !br.CanUpdate() { 334 | resp.Message = fmt.Sprintf("path not accessable (%s)", branch.BuildPath) 335 | } else { 336 | // trigger add new build 337 | go br.AddBuild("") 338 | } 339 | if !br.CanBrowse() { 340 | resp.Message = fmt.Sprintf("path not accessable (%s)", branch.StorePath) 341 | } 342 | log.Info("[Restful] User %s create branch %s.", token.UserName, br.Name()) 343 | 344 | if err := symbol.GetServer().SaveBranchs(""); err != nil { 345 | log.Warn("[Restful] Save branch (%v) failed: %v.", branch, err) 346 | } 347 | resp.WriteJSON(w) 348 | } 349 | -------------------------------------------------------------------------------- /route/handler.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "net/http" 7 | "path/filepath" 8 | "strings" 9 | "time" 10 | 11 | "github.com/adyzng/GoSymbols/config" 12 | 13 | clog "gopkg.in/clog.v1" 14 | ) 15 | 16 | // IndexHandle for index page 17 | // 18 | func IndexHandle(w http.ResponseWriter, r *http.Request) { 19 | index := filepath.Join(config.WebRoot, "index.html") 20 | tmpl, err := template.ParseFiles(index) 21 | if err == nil { 22 | tmpl.Execute(w, nil) 23 | //w.WriteHeader(http.StatusOK) 24 | } else { 25 | w.WriteHeader(http.StatusInternalServerError) 26 | fmt.Fprintf(w, "%v", err) 27 | } 28 | } 29 | 30 | // StaticHandler serve public files, exclude folder 31 | // 32 | func StaticHandler(folder string) http.Handler { 33 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 34 | // if request folder, return not found 35 | if strings.TrimRight(r.RequestURI, "/") != r.RequestURI { 36 | clog.Warn("[Res] Access (%s) forbidden.", r.RequestURI) 37 | http.NotFound(w, r) 38 | } else { 39 | http.FileServer(http.Dir(folder)).ServeHTTP(w, r) 40 | } 41 | }) 42 | } 43 | 44 | // LogHandler print request trace log 45 | // 46 | func LogHandler(h http.Handler, name string) http.Handler { 47 | return http.HandlerFunc(func(resp http.ResponseWriter, r *http.Request) { 48 | start := time.Now() 49 | w := &ResponseLogger{w: resp} 50 | h.ServeHTTP(w, r) 51 | 52 | // "GET / HTTP/1.1" 200 2552 UserAgent 53 | clog.Info("[API] %s - %d %s %s %s - %s", 54 | r.RemoteAddr, 55 | w.StatusCode, 56 | r.Proto, 57 | r.Method, 58 | r.RequestURI, 59 | time.Since(start)) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /route/midware.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import "net/http" 4 | 5 | // ResponseLogger is a middleware used to keep an copy of Response.StatusCode. 6 | // 7 | type ResponseLogger struct { 8 | w http.ResponseWriter 9 | StatusCode int 10 | } 11 | 12 | // Header returns the header map that will be sent by 13 | // WriteHeader. The Header map also is the mechanism with which 14 | // Handlers can set HTTP trailers. 15 | func (m *ResponseLogger) Header() http.Header { 16 | return m.w.Header() 17 | } 18 | 19 | // Write writes the data to the connection as part of an HTTP reply. 20 | func (m *ResponseLogger) Write(data []byte) (int, error) { 21 | return m.w.Write(data) 22 | } 23 | 24 | // WriteHeader sends an HTTP response header with status code. 25 | // If WriteHeader is not called explicitly, the first call to Write 26 | // will trigger an implicit WriteHeader(http.StatusOK). 27 | // Thus explicit calls to WriteHeader are mainly used to 28 | // send error codes. 29 | func (m *ResponseLogger) WriteHeader(status int) { 30 | if m.StatusCode == 0 { 31 | // since status code can only write once. 32 | m.StatusCode = status 33 | } 34 | m.w.WriteHeader(status) 35 | } 36 | -------------------------------------------------------------------------------- /route/routes.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/adyzng/GoSymbols/config" 7 | "github.com/adyzng/GoSymbols/restful/v1" 8 | "github.com/gorilla/mux" 9 | ) 10 | 11 | // Route define the basic route 12 | // 13 | type Route struct { 14 | Name string 15 | Method []string 16 | Pattern string 17 | Handler http.HandlerFunc 18 | } 19 | 20 | var resRoutes = []Route{ 21 | { 22 | Name: "Index", 23 | Method: []string{"GET"}, 24 | Pattern: "/", 25 | Handler: IndexHandle, 26 | }, 27 | } 28 | 29 | var apiRoutes = []Route{ 30 | { 31 | Name: "CreateBranch", 32 | Method: []string{"POST"}, 33 | Pattern: "/branches/create", 34 | Handler: v1.CreateBranch, 35 | }, 36 | { 37 | Name: "ModifyBranch", 38 | Method: []string{"POST"}, 39 | Pattern: "/branches/modify", 40 | Handler: v1.ModifyBranch, 41 | }, 42 | { 43 | Name: "ValidateBranch", 44 | Method: []string{"POST"}, 45 | Pattern: "/branches/check", 46 | Handler: v1.ValidateBranch, 47 | }, 48 | { 49 | Name: "DeleteBranch", 50 | Method: []string{"DELETE"}, 51 | Pattern: "/branches/{name}", 52 | Handler: v1.DeleteBranch, 53 | }, 54 | { 55 | Name: "GetBranchList", 56 | Method: []string{"GET"}, 57 | Pattern: "/branches", 58 | Handler: v1.RestBranchList, 59 | }, 60 | { 61 | Name: "GetBuildList", 62 | Method: []string{"GET"}, 63 | Pattern: "/branches/{name}", 64 | Handler: v1.RestBuildList, 65 | }, 66 | { 67 | Name: "GetSymbolList", 68 | Method: []string{"GET"}, 69 | Pattern: "/branches/{name}/{bid}", 70 | Handler: v1.RestSymbolList, 71 | }, 72 | { 73 | Name: "DownloadSymbol", 74 | Method: []string{"GET"}, 75 | Pattern: "/symbol/{branch}/{hash}/{name}", 76 | Handler: v1.DownloadSymbol, 77 | }, 78 | { 79 | Name: "FetchTodayMessage", 80 | Method: []string{"GET"}, 81 | Pattern: "/messages", 82 | Handler: v1.FetchTodayMsg, 83 | }, 84 | { 85 | Name: "Login", 86 | Method: []string{"GET"}, 87 | Pattern: "/auth/login", 88 | Handler: v1.AuthLogin, 89 | }, 90 | { 91 | Name: "Authorize", 92 | Method: []string{"POST"}, 93 | Pattern: "/auth/authorize", 94 | Handler: v1.Authorize, 95 | }, 96 | { 97 | Name: "Logout", 98 | Method: []string{"GET"}, 99 | Pattern: "/auth/logout", 100 | Handler: v1.AuthLogout, 101 | }, 102 | { 103 | Name: "UserProfile", 104 | Method: []string{"GET"}, 105 | Pattern: "/user/profile", 106 | Handler: v1.GetUserProfile, 107 | }, 108 | { 109 | Name: "UserPhoto", 110 | Method: []string{"GET"}, 111 | Pattern: "/user/photo", 112 | Handler: v1.GetUserPhoto, 113 | }, 114 | } 115 | 116 | // NewRouter return the registered router 117 | // 118 | func NewRouter() *mux.Router { 119 | router := mux.NewRouter() 120 | router.StrictSlash(true) 121 | 122 | // static files handler 123 | router. 124 | PathPrefix("/static/"). 125 | Handler(StaticHandler(config.WebRoot)) 126 | //Handler(http.StripPrefix("/static/", StaticHandler(config.WebRoot))) 127 | 128 | // normal handler 129 | for _, route := range resRoutes { 130 | logHandler := LogHandler(route.Handler, route.Name) 131 | router. 132 | Methods(route.Method...). 133 | Path(route.Pattern). 134 | Handler(logHandler). 135 | Name(route.Name) 136 | } 137 | 138 | // restful api handler 139 | for _, route := range apiRoutes { 140 | logHandler := LogHandler(route.Handler, route.Name) 141 | router.PathPrefix("/api/"). 142 | Methods(route.Method...). 143 | Path(route.Pattern). 144 | Handler(logHandler). 145 | Name(route.Name) 146 | } 147 | 148 | return router 149 | } 150 | -------------------------------------------------------------------------------- /symbol/branch.go: -------------------------------------------------------------------------------- 1 | package symbol 2 | 3 | import ( 4 | "bufio" 5 | "encoding/gob" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/adyzng/GoSymbols/config" 16 | "github.com/adyzng/GoSymbols/util" 17 | 18 | log "gopkg.in/clog.v1" 19 | ) 20 | 21 | const ( 22 | adminDir = "000Admin" 23 | unzipDir = "000Unzip" 24 | lastidTxt = "lastid.txt" // build ID generated by symstore.exe 25 | serverTxt = "server.txt" // build history generated by symstore.exe 26 | branchBin = "branch.bin" // current branch information generated by GoSymbols 27 | d2dNative = "\\D2D\\Native" 28 | 29 | ArchX86 = "x86" 30 | ArchX64 = "x64" 31 | ) 32 | 33 | var ( 34 | symPrefixs = []string{"\\D2D", "\\Central", "\\ExternalLib"} 35 | ) 36 | 37 | var ( 38 | ErrBuildNotExist = fmt.Errorf("build not exist") 39 | ErrBranchNotInit = fmt.Errorf("branch not initialized") 40 | ErrBranchOnSymbolStore = fmt.Errorf("invalid branch on symbol store") 41 | ErrBranchOnBuildServer = fmt.Errorf("invalid branch on build server") 42 | ) 43 | 44 | // BrBuilder represent pdb release 45 | // 46 | type BrBuilder struct { 47 | Branch 48 | builds map[string]*Build // save all builds for current branch 49 | symbols map[string]*Symbol // save symbols 50 | symPath string // path that unzip debug.zip to 51 | mx sync.RWMutex 52 | } 53 | 54 | func init() { 55 | log.New(log.CONSOLE, log.ConsoleConfig{ 56 | Level: log.INFO, 57 | BufferSize: 100, 58 | }) 59 | } 60 | 61 | // NewBranch create an new `BrBuilder`, `Init` must be called after `NewBranch`. 62 | // 63 | func NewBranch(buildName, storeName string) Builder { 64 | return NewBranch2(&Branch{ 65 | BuildName: buildName, 66 | StoreName: storeName, 67 | UpdateDate: time.Now().Format("2006-01-02 15:04:05"), 68 | }) 69 | } 70 | 71 | // NewBranch2 ... 72 | func NewBranch2(branch *Branch) Builder { 73 | b := &BrBuilder{ 74 | Branch: *branch, 75 | builds: make(map[string]*Build, 1), 76 | symbols: make(map[string]*Symbol, 1), 77 | } 78 | if b.StorePath == "" { 79 | b.StorePath = filepath.Join(config.Destination, b.StoreName) 80 | } 81 | if b.BuildPath == "" { 82 | b.BuildPath = filepath.Join(config.BuildSource, b.BuildName, "Release") 83 | } 84 | return b 85 | } 86 | 87 | // Name return name in symbol store 88 | // 89 | func (b *BrBuilder) Name() string { 90 | return b.StoreName 91 | } 92 | 93 | // GetBranch get branch information 94 | // 95 | func (b *BrBuilder) GetBranch() *Branch { 96 | return &b.Branch 97 | } 98 | 99 | // CanBrowse check if current branch is valid on local symbol store. 100 | func (b *BrBuilder) CanBrowse() bool { 101 | fpath := filepath.Join(b.StorePath, adminDir) 102 | if st, _ := os.Stat(fpath); st != nil && st.IsDir() { 103 | return true 104 | } 105 | log.Trace("[Branch] Access sympol path %s failed.", fpath) 106 | return false 107 | } 108 | 109 | // CanUpdate check if current branch is valid on build server. 110 | func (b *BrBuilder) CanUpdate() bool { 111 | fpath := filepath.Join(b.BuildPath, config.LatestBuildFile) 112 | if st, _ := os.Stat(fpath); st != nil && !st.IsDir() { 113 | return true 114 | } 115 | log.Trace("[Branch] Access build path %s failed.", fpath) 116 | return false 117 | } 118 | 119 | // SetSubpath change the subpath on build server and local store. 120 | // `buildserver` is the subpath relative to config.BuildSource. 121 | // `localstore` is the subpath relative to config.Destination. 122 | // 123 | func (b *BrBuilder) SetSubpath(buildserver, localstore string) error { 124 | lpath := filepath.Join(config.Destination, b.StoreName) 125 | fpath := filepath.Join(config.BuildSource, b.BuildName, "Release") 126 | 127 | if localstore != "" { 128 | // by given subpath 129 | lpath = filepath.Join(config.Destination, localstore) 130 | } 131 | if err := os.MkdirAll(filepath.Join(lpath, adminDir), 666); err != nil { 132 | log.Error(2, "[Branch] Init sympol store path %s failed: %v.", lpath, err) 133 | return err 134 | } 135 | b.StorePath = lpath 136 | 137 | if buildserver != "" { 138 | // by given subpath 139 | fpath = filepath.Join(config.BuildSource, buildserver) 140 | } 141 | b.BuildPath = fpath 142 | 143 | // check if can be update from server 144 | if _, err := os.Stat(fpath); os.IsNotExist(err) { 145 | log.Error(2, "[Branch] Invalid path %s for %s.", fpath, b.Name()) 146 | return fmt.Errorf("invalid path on build server") 147 | } 148 | return nil 149 | } 150 | 151 | // Persist will save branch information into 000Admin/branch.bin 152 | // 153 | func (b *BrBuilder) Persist() error { 154 | fpath := filepath.Join(b.StorePath, adminDir, branchBin) 155 | fd, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 666) 156 | if err != nil { 157 | log.Error(2, "[Branch] Persist branch %s failed: %v.", b.Name(), err) 158 | return err 159 | } 160 | 161 | defer fd.Close() 162 | log.Trace("[Branch] Save branch %+v.", b.Branch) 163 | return gob.NewEncoder(fd).Encode(&b.Branch) 164 | } 165 | 166 | // Delete current branch 167 | // 168 | func (b *BrBuilder) Delete() error { 169 | log.Info("[Branch] Delete branch %+v.", b.Branch) 170 | fpath := filepath.Join(b.StorePath, adminDir, branchBin) 171 | err := os.Remove(fpath) 172 | return err 173 | } 174 | 175 | // Load will load branch information from 000Admin/branch.bin 176 | // 177 | func (b *BrBuilder) Load() error { 178 | fpath := filepath.Join(b.StorePath, adminDir, branchBin) 179 | fd, err := os.OpenFile(fpath, os.O_RDONLY, 666) 180 | if err != nil { 181 | //log.Error(2, "[Branch] Load branch %s failed: %v.", b.Name(), err) 182 | return err 183 | } 184 | 185 | defer fd.Close() 186 | return gob.NewDecoder(fd).Decode(&b.Branch) 187 | } 188 | 189 | // getSymbols copy pdb zip file to local temp path and return the path 190 | // 191 | func (b *BrBuilder) getSymbols(buildver string) (string, error) { 192 | var ( 193 | fs *os.File 194 | fd *os.File 195 | err error 196 | bytes int64 197 | ) 198 | 199 | fsrc := fmt.Sprintf("%s\\Build%s\\%s", b.BuildPath, buildver, config.PDBZipFile) 200 | fzip := filepath.Join(b.symPath, config.PDBZipFile) 201 | 202 | fd, err = os.OpenFile(fzip, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModeTemporary) 203 | if err != nil { 204 | log.Error(2, "[Branch] create zip file %s failed: %v.", fzip, err) 205 | return "", err 206 | } 207 | defer fd.Close() 208 | 209 | fs, err = os.OpenFile(fsrc, os.O_RDONLY, 666) 210 | if err != nil { 211 | log.Error(2, "[Branch] open source file %s failed: %v.", fsrc, err) 212 | return "", err 213 | } 214 | defer fs.Close() 215 | 216 | log.Info("[Branch] Copy %s to %s.", fsrc, fzip) 217 | start := time.Now() 218 | bytes, err = io.Copy(fd, fs) 219 | log.Info("[Branch] Copy complete: Size = %d, Time = %s.", bytes, time.Since(start)) 220 | 221 | if err != nil { 222 | log.Error(2, "[Branch] Copy zip file failed: %v.", fsrc, err) 223 | return "", err 224 | } 225 | return fzip, nil 226 | } 227 | 228 | // getLatestBuild return latest build no. on build server 229 | // 230 | func (b *BrBuilder) getLatestBuild(local bool) (string, error) { 231 | fpath := "" 232 | if local { 233 | fpath = filepath.Join(b.StorePath, adminDir, config.LatestBuildFile) 234 | } else { 235 | fpath = filepath.Join(b.BuildPath, config.LatestBuildFile) 236 | } 237 | 238 | fd, err := os.OpenFile(fpath, os.O_RDONLY, 666) 239 | if err != nil { 240 | return "", err 241 | } 242 | 243 | defer fd.Close() 244 | r := bufio.NewReader(fd) 245 | 246 | str, _ := r.ReadString('\n') 247 | return strings.Trim(str, " \r\n"), nil 248 | } 249 | 250 | // GetLatestID return the last symbol build id 251 | // 252 | func (b *BrBuilder) GetLatestID() string { 253 | fpath := filepath.Join(b.StorePath, adminDir, lastidTxt) 254 | fd, err := os.OpenFile(fpath, os.O_RDONLY, 666) 255 | if err != nil { 256 | log.Error(2, "[Branch] Read latest build (%s) failed with %v.", fpath, err) 257 | return "" 258 | } 259 | 260 | defer fd.Close() 261 | r := bufio.NewReader(fd) 262 | 263 | str, _ := r.ReadString('\n') 264 | return strings.Trim(str, " \r\n") 265 | } 266 | 267 | // updateLatestBuild update local latest build file 268 | // 269 | func (b *BrBuilder) updateLatestBuild(latest string) error { 270 | fpath := filepath.Join(b.StorePath, adminDir, config.LatestBuildFile) 271 | fd, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 666) 272 | if err != nil { 273 | log.Error(2, "[Branch] Open local latest build (%s) failed with %v.", fpath, err) 274 | return err 275 | } 276 | defer fd.Close() 277 | 278 | if _, err = fd.WriteString(latest); err != nil { 279 | log.Error(2, "[Branch] Write local latest build (%s) failed with %v.", fpath, err) 280 | return err 281 | } 282 | return nil 283 | } 284 | 285 | // addSymStore call symstore.exe to add symbols to symbol store. 286 | // 287 | func (b *BrBuilder) addSymStore(latestbuild, symbols string) (*Build, error) { 288 | start := time.Now() 289 | comment := start.Format("2006-01-02_15:04:05") 290 | log.Info("[Branch] Call symbol store command for build %s ...", latestbuild) 291 | 292 | /* 293 | "C:\Program Files (x86)\Windows Kits\8.1\Debuggers\x86\symstore.exe" 294 | add 295 | /r 296 | /l 297 | /f \\rmdm-bldvm-l902\CurrentRelease\UDP\UDPMAIN\Intermediate\B%BUILD_NUMBER%\debug\*.pdb 298 | /s S:\SymbolServer\Titanium 299 | /t Titanium 300 | /v %BUILD_NUMBER% 301 | /c %date:~-10%_%time:~0,8% 302 | */ 303 | cmd := exec.Command(config.SymStoreExe, "add", "/r", 304 | "/f", symbols, 305 | "/s", b.StorePath, 306 | "/t", b.Name(), 307 | "/v", latestbuild, 308 | "/c", comment) 309 | 310 | var ( 311 | err error 312 | output []byte 313 | done = make(chan struct{}, 1) 314 | ) 315 | go func() { 316 | output, err = cmd.CombinedOutput() 317 | done <- struct{}{} 318 | }() 319 | 320 | <-done 321 | log.Info("[Branch] Symbol store output: %s.", string(output)) 322 | log.Info("[Branch] Symbol store complete: %s.", time.Since(start)) 323 | 324 | if err != nil { 325 | log.Info("[Branch] Symbol store command failed with %s.", err) 326 | return nil, err 327 | } 328 | build := &Build{ 329 | ID: b.GetLatestID(), 330 | Date: start.Format("2006-01-02 15:04:05"), 331 | Branch: b.Name(), 332 | Version: latestbuild, 333 | Comment: comment, 334 | } 335 | return build, nil 336 | } 337 | 338 | func (b *BrBuilder) getBuild(version string, id string) *Build { 339 | b.mx.RLock() 340 | defer b.mx.RUnlock() 341 | if version != "" { 342 | for _, val := range b.builds { 343 | if val.Version == version { 344 | return val 345 | } 346 | } 347 | } 348 | if id != "" { 349 | if build, ok := b.builds[id]; ok { 350 | return build 351 | } 352 | } 353 | return nil 354 | } 355 | 356 | func (b *BrBuilder) addBuild(build *Build) { 357 | b.mx.Lock() 358 | defer b.mx.Unlock() 359 | 360 | b.BuildsCount++ 361 | b.UpdateDate = build.Date 362 | b.builds[build.ID] = build 363 | } 364 | 365 | // AddBuild add new version of pdb 366 | // 367 | func (b *BrBuilder) AddBuild(buildVerion string) error { 368 | latest := buildVerion 369 | local, err := b.getLatestBuild(true) 370 | 371 | if buildVerion == "" { 372 | if latest, err = b.getLatestBuild(false); err != nil { 373 | log.Error(2, "[Branch] Get server latest build failed: %v.", err) 374 | return fmt.Errorf("invalid build server latestbuild.txt file") 375 | } 376 | if latest == local { 377 | log.Trace("[Branch] Branch %s already updated to latest %s.", b.Name(), latest) 378 | return nil 379 | } 380 | } 381 | if b.getBuild(latest, "") != nil { 382 | log.Warn("[Branch] Symbols for build %s already exist.", latest) 383 | return nil 384 | } 385 | log.Info("[Branch] Add symbols for build %s. Local: %s.", latest, local) 386 | 387 | b.symPath = filepath.Join(b.StorePath, unzipDir) 388 | if err = os.MkdirAll(b.symPath, 666); err != nil { 389 | log.Error(2, "[Branch] Create symbol path %s failed with %v.", b.symPath, err) 390 | return err 391 | } 392 | defer os.RemoveAll(b.symPath) 393 | 394 | var symbolZip string 395 | if symbolZip, err = b.getSymbols(latest); err != nil { 396 | log.Error(2, "[Branch] Get symbols failed: %v.", err) 397 | return err 398 | } 399 | if err = util.Unzip(symbolZip, b.symPath); err != nil { 400 | log.Error(2, "[Branch] Unzip symbols failed: %v.", err) 401 | return err 402 | } 403 | 404 | var build *Build 405 | if build, err = b.addSymStore(latest, b.symPath); err != nil { 406 | log.Error(2, "[Branch] Add to symbol store failed with %v.", err) 407 | return err 408 | } 409 | if err = b.updateLatestBuild(latest); err != nil { 410 | return err 411 | } 412 | 413 | b.addBuild(build) 414 | b.LatestBuild = latest 415 | return nil 416 | } 417 | 418 | // ParseBuilds parse server.txt to get pdb history 419 | // 420 | func (b *BrBuilder) ParseBuilds(handler func(b *Build) error) (int, error) { 421 | if handler == nil { 422 | handler = func(bd *Build) error { 423 | //fmt.Println(bd) 424 | return nil 425 | } 426 | } 427 | 428 | total := 0 429 | if len(b.builds) != 0 { 430 | for _, bd := range b.builds { 431 | if err := handler(bd); err != nil { 432 | log.Error(2, "[Branch] Parse build(%v) failed: %v.", bd, err) 433 | return total, err 434 | } 435 | total++ 436 | } 437 | return total, nil 438 | } 439 | 440 | txtPath := filepath.Join(b.StorePath, adminDir, serverTxt) 441 | fc, err := os.OpenFile(txtPath, os.O_RDONLY, 666) 442 | if err != nil { 443 | log.Error(2, "[Branch] Open file (%s) failed with %v.", txtPath, err) 444 | return 0, err 445 | } 446 | defer fc.Close() 447 | 448 | // clean, will re-calculate it 449 | b.BuildsCount = 0 450 | r := bufio.NewReader(fc) 451 | for { 452 | str, err := r.ReadString('\n') 453 | if err == io.EOF { 454 | break 455 | } 456 | str = strings.Trim(str, "\r\n") 457 | 458 | // 0 1 2 3 4 5 6 7 459 | //0000000001,add,file,07/04/2017,14:44:14,"UDPv6.5U2","4175.2-538","2017/7/4_14:44:14", 460 | ss := strings.Split(str, ",") 461 | if len(ss) < 8 { 462 | log.Warn("[Branch] Invalid line (%s) in server.txt.", str) 463 | continue 464 | } 465 | 466 | dateStr := ss[3] + " " + ss[4] 467 | dateLoc, err := time.ParseInLocation("01/02/2006 15:04:05", dateStr, time.Local) 468 | if err != nil { 469 | log.Warn("[Branch] Parse date failed with %v.", err) 470 | } else { 471 | dateStr = dateLoc.Format("2006-01-02 15:04:05") 472 | } 473 | 474 | build := &Build{ 475 | ID: ss[0], 476 | Date: dateStr, 477 | Branch: strings.Trim(ss[5], "\""), 478 | Version: strings.Trim(ss[6], "\""), 479 | Comment: strings.Trim(ss[7], "\""), 480 | } 481 | 482 | total++ 483 | b.addBuild(build) 484 | b.LatestBuild = build.Version 485 | 486 | if err = handler(build); err != nil { 487 | return total, err 488 | } 489 | } 490 | 491 | return total, nil 492 | } 493 | 494 | // ParseSymbols parse 000000001(*) from pdb path 495 | // 496 | func (b *BrBuilder) ParseSymbols(buildID string, handler func(sym *Symbol) error) (int, error) { 497 | build := b.getBuild("", buildID) 498 | if build == nil { 499 | log.Error(2, "[Branch] Build %s not exist for %s.", buildID, b.Name()) 500 | return 0, ErrBuildNotExist 501 | } 502 | 503 | idPath := filepath.Join(b.StorePath, adminDir, buildID) 504 | fd, err := os.OpenFile(idPath, os.O_RDONLY, 666) 505 | if err != nil { 506 | log.Error(2, "[Branch] Open file (%s) failed with %v.", idPath, err) 507 | return 0, err 508 | } 509 | defer fd.Close() 510 | 511 | if handler == nil { 512 | handler = func(sym *Symbol) error { 513 | //fmt.Println(sym) 514 | return nil 515 | } 516 | } 517 | skipFn := func(name string) bool { 518 | for _, v := range config.SymExcludeList { 519 | if strings.ToLower(name) == v { 520 | return true 521 | } 522 | } 523 | return false 524 | } 525 | archDetect := func(sympath string) string { 526 | x64Caps := []string{"x64", "amd64"} 527 | sympath = strings.ToLower(sympath) 528 | for _, cap := range x64Caps { 529 | if strings.Index(sympath, cap) != -1 { 530 | return ArchX64 531 | } 532 | } 533 | return ArchX86 534 | } 535 | 536 | total := 0 537 | r := bufio.NewReader(fd) 538 | unqMap := make(map[string]*Symbol, 0) 539 | 540 | for { 541 | str, err := r.ReadString('\n') //0D 0A 542 | if err == io.EOF { 543 | break 544 | } 545 | str = strings.Trim(str, "\r\n") 546 | 547 | // 548 | // "cbt_client.pdb\8E3868FEE1FA4AC8A42D0FACA65E0BE41","S:\script\temp\ExternalLib\RHAPdbfile\cbt_client.pdb" 549 | ss := strings.Split(str, ",") 550 | if len(ss) < 2 { 551 | log.Warn("[Branch] Invalid line (%s) in %s.", str, buildID) 552 | continue 553 | } 554 | 555 | pName := strings.Split(strings.Trim(ss[0], "\""), "\\") 556 | if len(pName) != 2 { 557 | // invalid format 558 | continue 559 | } 560 | if skipFn(pName[0]) { 561 | // exclude list 562 | continue 563 | } 564 | if _, ok := unqMap[pName[1]]; ok { 565 | // deplicate symbol 566 | continue 567 | } 568 | 569 | spath := strings.Trim(ss[1], "\"") 570 | if idx := strings.Index(spath, unzipDir); idx != -1 { 571 | spath = spath[idx+len(unzipDir):] 572 | } else { 573 | for _, prefix := range symPrefixs { 574 | if idx = strings.Index(spath, prefix); idx != -1 { 575 | break 576 | } 577 | } 578 | if idx != -1 { 579 | spath = spath[idx:] 580 | } 581 | } 582 | 583 | sym := &Symbol{ 584 | Name: pName[0], 585 | Hash: pName[1], 586 | Path: spath, 587 | Arch: archDetect(spath), 588 | Version: build.Version, 589 | } 590 | // download url: /api/symbol/{branch}/{hash}/{name} 591 | sym.URL = fmt.Sprintf("/api/symbol/%s/%s/%s", b.StoreName, sym.Hash, sym.Name) 592 | if err = handler(sym); err != nil { 593 | return total, err 594 | } 595 | total++ 596 | unqMap[sym.Hash] = sym 597 | } 598 | return total, err 599 | } 600 | 601 | // GetSymbolPath return symbol's full path 602 | // 603 | func (b *BrBuilder) GetSymbolPath(hash, name string) string { 604 | return filepath.Join(b.StorePath, name, hash, name) 605 | } 606 | -------------------------------------------------------------------------------- /symbol/branch_test.go: -------------------------------------------------------------------------------- 1 | package symbol 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestParseBuilds(t *testing.T) { 10 | lastBuild := "" 11 | builder := NewBranch("UDP_6_5_U2", "UDPv6.5U2") 12 | 13 | total, err := builder.ParseBuilds(func(b *Build) error { 14 | fmt.Printf("%s: %+v \n", b.ID, b) 15 | lastBuild = b.ID 16 | return nil 17 | }) 18 | fmt.Printf("Branch %s has %d builds.\n", builder.Name(), total) 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | 23 | idx := 0 24 | total, err = builder.ParseSymbols(lastBuild, func(sym *Symbol) error { 25 | fmt.Printf(" %d: %+v\n", idx, sym) 26 | idx++ 27 | return nil 28 | }) 29 | fmt.Printf("Branch %s build %s has %d symbols.\n", builder.Name(), lastBuild, total) 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | } 34 | 35 | func TestAddBuild(t *testing.T) { 36 | builder := NewBranch("UDP_6_5_U2", "UDPv6.5U2") 37 | if err := builder.AddBuild(""); err != nil { 38 | time.Sleep(time.Second) 39 | t.Fatal(err) 40 | } 41 | 42 | idx, lastBuild := 0, builder.GetLatestID() 43 | total, err := builder.ParseSymbols(lastBuild, func(sym *Symbol) error { 44 | fmt.Printf(" %d: %+v\n", idx, sym) 45 | idx++ 46 | return nil 47 | }) 48 | if err != nil { 49 | t.Error(err) 50 | } 51 | fmt.Printf("Branch %s build %s has %d symbols.\n", builder.Name(), lastBuild, total) 52 | } 53 | -------------------------------------------------------------------------------- /symbol/models.go: -------------------------------------------------------------------------------- 1 | package symbol 2 | 3 | // Branch ... information 4 | // 5 | type Branch struct { 6 | BuildName string `json:"buildName"` 7 | StoreName string `json:"storeName"` 8 | BuildPath string `json:"buildPath"` 9 | StorePath string `json:"storePath"` 10 | UpdateDate string `json:"updateDate"` 11 | LatestBuild string `json:"latestBuild"` 12 | BuildsCount int `json:"buildsCount"` 13 | } 14 | 15 | // Build ... analyze from server.txt 16 | // 17 | type Build struct { 18 | ID string `json:"id"` 19 | Date string `json:"date"` 20 | Branch string `json:"branch"` 21 | Version string `json:"version"` 22 | Comment string `json:"comment"` 23 | } 24 | 25 | // Symbol represent each symbol file's detail 26 | // 27 | type Symbol struct { 28 | Arch string `json:"arch"` // x64 or x86 29 | Hash string `json:"hash"` 30 | Name string `json:"name"` 31 | Path string `json:"path"` 32 | URL string `json:"url"` 33 | Version string `json:"version"` 34 | } 35 | 36 | // Builder interface 37 | // 38 | type Builder interface { 39 | // Name return builder name 40 | Name() string 41 | // Get struct *Branch {} 42 | GetBranch() *Branch 43 | 44 | // CanUpdate check if current branch is valid on build server. 45 | CanUpdate() bool 46 | // CanBrowse check if current branch is valid on local symbol store. 47 | CanBrowse() bool 48 | 49 | // SetSubpath change the subpath on build server and local store. 50 | SetSubpath(buildserver, localstore string) error 51 | 52 | // Add an given build pdb to symbol server. 53 | // if `buildVersion` is empty, it will try to add the latest build on build server if exist. 54 | AddBuild(buildVerion string) error 55 | 56 | // GetSymbolPath get given symbol file full path on symbol server. 57 | // The path can be used to serve download. 58 | GetSymbolPath(hash, name string) string 59 | 60 | // ParseBuilds parse all version of builds that already in the symbol server of curent branch. 61 | // 62 | ParseBuilds(handler func(b *Build) error) (int, error) 63 | 64 | // ParseSymbols parse all the symbols of given build vesrion 65 | // 66 | ParseSymbols(buildID string, handler func(sym *Symbol) error) (int, error) 67 | } 68 | -------------------------------------------------------------------------------- /symbol/server.go: -------------------------------------------------------------------------------- 1 | package symbol 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/adyzng/GoSymbols/config" 13 | log "gopkg.in/clog.v1" 14 | ) 15 | 16 | var ( 17 | symSvr *sserver 18 | once sync.Once 19 | ) 20 | 21 | const ( 22 | symConfig = "symbols.json" 23 | ) 24 | 25 | // sserver ... 26 | // 27 | type sserver struct { 28 | lck sync.RWMutex 29 | builders map[string]Builder 30 | } 31 | 32 | // GetServer return single instance of sserver 33 | // 34 | func GetServer() *sserver { 35 | once.Do(func() { 36 | symSvr = &sserver{ 37 | builders: make(map[string]Builder, 1), 38 | } 39 | if st, err := os.Stat(config.Destination); err != nil || st == nil { 40 | log.Error(2, "[SS] Access destination %s error: %s.", config.Destination, err) 41 | panic("destination isn't accessable") 42 | } 43 | }) 44 | return symSvr 45 | } 46 | 47 | // Scan exist symbol store 48 | func (ss *sserver) ScanStore(path string) error { 49 | fs, err := ioutil.ReadDir(path) 50 | if err != nil { 51 | log.Error(2, "[SS] Enum symbol store %s failed: %v.", path, err) 52 | return err 53 | } 54 | 55 | func() { 56 | ss.lck.Lock() 57 | defer ss.lck.Unlock() 58 | for _, f := range fs { 59 | if !f.IsDir() { 60 | continue 61 | } 62 | b := NewBranch(f.Name(), f.Name()) 63 | if b.CanBrowse() || b.CanUpdate() { 64 | ss.builders[strings.ToLower(f.Name())] = b 65 | log.Info("[SS] Load branch %s.", b.Name()) 66 | } 67 | } 68 | }() 69 | return ss.SaveBranchs("") 70 | } 71 | 72 | // Modify branch 73 | func (ss *sserver) Modify(branch *Branch) Builder { 74 | ss.lck.Lock() 75 | defer ss.lck.Unlock() 76 | 77 | lower := strings.ToLower(branch.StoreName) 78 | if b, ok := ss.builders[lower]; ok { 79 | nb := NewBranch2(branch) 80 | if nb.CanUpdate() || nb.CanBrowse() { 81 | b1, b2 := b.GetBranch(), nb.GetBranch() 82 | b1.BuildName = b2.BuildName 83 | b1.StoreName = b2.StoreName 84 | b1.BuildPath = b2.BuildPath 85 | b1.StorePath = b2.StorePath 86 | return b 87 | } 88 | } 89 | return nil 90 | } 91 | 92 | // Get reture given branch, if not exist return nil 93 | func (ss *sserver) Get(storeName string) Builder { 94 | ss.lck.RLock() 95 | defer ss.lck.RUnlock() 96 | 97 | lower := strings.ToLower(storeName) 98 | b, _ := ss.builders[lower] 99 | return b 100 | } 101 | 102 | // Add Branch if already exist, do nothing. 103 | func (ss *sserver) Add(b *Branch) Builder { 104 | ss.lck.Lock() 105 | defer ss.lck.Unlock() 106 | 107 | // exist one 108 | if b, ok := ss.builders[b.StoreName]; ok { 109 | return b 110 | } 111 | 112 | // new one 113 | br := NewBranch2(b) 114 | if br.CanBrowse() || br.CanUpdate() { 115 | ss.builders[strings.ToLower(b.StoreName)] = br 116 | return br 117 | } 118 | 119 | return nil 120 | } 121 | 122 | // DeleteBranch remove given branch 123 | func (ss *sserver) Delete(storeName string) Builder { 124 | ss.lck.Lock() 125 | defer ss.lck.Unlock() 126 | 127 | lower := strings.ToLower(storeName) 128 | if b, ok := ss.builders[lower]; ok { 129 | delete(ss.builders, lower) 130 | return b 131 | } 132 | return nil 133 | } 134 | 135 | // WalkBuilders walk all exist builders, the handler should be return asap. 136 | func (ss *sserver) WalkBuilders(handler func(branch Builder) error) error { 137 | var err error 138 | if handler == nil { 139 | return nil 140 | } 141 | ss.lck.RLock() 142 | defer ss.lck.RUnlock() 143 | 144 | for _, b := range ss.builders { 145 | if err = handler(b); err != nil { 146 | break 147 | } 148 | } 149 | return err 150 | } 151 | 152 | // LoadBranchs scan local symbol store for exist branchs. 153 | func (ss *sserver) LoadBranchs() error { 154 | fpath := filepath.Join(config.AppPath, symConfig) 155 | fd, err := os.OpenFile(fpath, os.O_RDONLY, 666) 156 | if err != nil { 157 | log.Error(2, "[SS] Read symbols config file %s failed: %v.", fpath, err) 158 | return err 159 | } 160 | 161 | var arr []*Branch 162 | if err := json.NewDecoder(fd).Decode(&arr); err != nil { 163 | return err 164 | } 165 | 166 | ss.lck.Lock() 167 | defer ss.lck.Unlock() 168 | 169 | for _, b := range arr { 170 | ss.builders[strings.ToLower(b.StoreName)] = NewBranch2(b) 171 | log.Info("[SS] Load branch %s", b.StoreName) 172 | } 173 | return nil 174 | } 175 | 176 | // SaveBranchs ... 177 | func (ss *sserver) SaveBranchs(path string) error { 178 | if path == "" { 179 | path = config.AppPath 180 | } 181 | 182 | fpath := filepath.Join(path, symConfig) 183 | fd, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 666) 184 | if err != nil { 185 | log.Error(2, "[SS] Open file %s failed: %v.", fpath, err) 186 | return err 187 | } 188 | 189 | ss.lck.Lock() 190 | defer func() { 191 | fd.Close() 192 | ss.lck.Unlock() 193 | }() 194 | 195 | arr := make([]*Branch, 0, len(ss.builders)) 196 | for _, b := range ss.builders { 197 | arr = append(arr, b.GetBranch()) 198 | } 199 | 200 | enc := json.NewEncoder(fd) 201 | enc.SetIndent("", "\t") 202 | return enc.Encode(arr) 203 | } 204 | 205 | // Run ... 206 | func (ss *sserver) Run(done <-chan struct{}) { 207 | var wg sync.WaitGroup 208 | log.Info("[SS] Symbol server start ...") 209 | 210 | ticker := time.NewTicker(time.Hour * 2) 211 | defer ticker.Stop() 212 | 213 | if err := ss.LoadBranchs(); err != nil { 214 | log.Error(2, "[SS] Load branchs failed: %v.", err) 215 | return 216 | } 217 | 218 | ss.WalkBuilders(func(bu Builder) error { 219 | wg.Add(1) 220 | log.Info("[SS] Parse branch %s.", bu.Name()) 221 | go func() { 222 | defer wg.Done() 223 | bu.ParseBuilds(nil) 224 | }() 225 | return nil 226 | }) 227 | wg.Wait() 228 | 229 | LOOP: 230 | for { 231 | ss.WalkBuilders(func(bu Builder) error { 232 | if bu.CanUpdate() { 233 | go func() { 234 | wg.Add(1) 235 | defer wg.Done() 236 | log.Trace("[SS] Trigger branch %s.", bu.Name()) 237 | bu.AddBuild("") 238 | }() 239 | } else { 240 | log.Trace("[SS] Can't update branch %s.", bu.Name()) 241 | } 242 | return nil 243 | }) 244 | 245 | if err := ss.SaveBranchs(""); err != nil { 246 | log.Error(2, "[SS] Save branchs list failed: %v.", err) 247 | } 248 | 249 | select { 250 | case <-done: 251 | log.Warn("[SS] Receive stop signal.") 252 | break LOOP 253 | case <-ticker.C: 254 | wg.Wait() 255 | break 256 | } 257 | } 258 | if err := ss.SaveBranchs(""); err != nil { 259 | log.Error(2, "[SS] Save branchs list failed: %v.", err) 260 | } 261 | 262 | wg.Wait() 263 | log.Info("[SS] Symbol server stop.") 264 | } 265 | -------------------------------------------------------------------------------- /symbol/server_test.go: -------------------------------------------------------------------------------- 1 | package symbol 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/adyzng/GoSymbols/config" 8 | ) 9 | 10 | func TestLoadBranch(t *testing.T) { 11 | ss := GetServer() 12 | if err := ss.LoadBranchs(); err != nil { 13 | t.Error(err) 14 | } 15 | total := 0 16 | ss.WalkBuilders(func(b Builder) error { 17 | fmt.Printf("Load %d: %+v\n", total, b) 18 | total++ 19 | return nil 20 | }) 21 | } 22 | 23 | func TestAddBranch(t *testing.T) { 24 | bn, sn := "UDP_6_5_U2", "UDPv6.5U2" 25 | builder := GetServer().Add(bn, sn) 26 | 27 | if builder != nil { 28 | fmt.Printf("Add branch: %+v.\n", builder) 29 | } else { 30 | t.Fatal("Add branch failed.\n") 31 | } 32 | } 33 | 34 | func TestScanBranchs(t *testing.T) { 35 | ss := GetServer() 36 | if err := ss.ScanStore(config.Destination); err != nil { 37 | t.Error(err) 38 | } 39 | 40 | total := 0 41 | ss.WalkBuilders(func(b Builder) error { 42 | fmt.Printf("%d: %+v\n", total, b) 43 | total++ 44 | return nil 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /util/unzip.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | log "gopkg.in/clog.v1" 13 | ) 14 | 15 | // Unzip file `srcZip` to given folder `destFolder` 16 | // 17 | func Unzip(srcZip string, destFolder string) error { 18 | if _, err := os.Stat(srcZip); os.IsNotExist(err) { 19 | return fmt.Errorf("input is not an zip file") 20 | } 21 | if st, err := os.Stat(destFolder); os.IsNotExist(err) { 22 | err = os.MkdirAll(destFolder, 666) 23 | if err != nil { 24 | return fmt.Errorf("failed to create destination folder") 25 | } 26 | } else if !st.IsDir() { 27 | return fmt.Errorf("destination is not an valid folder") 28 | } 29 | 30 | log.Info("[Unzip] Unzip file %s.", srcZip) 31 | start := time.Now() 32 | 33 | rzip, err := zip.OpenReader(srcZip) 34 | if err != nil { 35 | return err 36 | } 37 | defer rzip.Close() 38 | 39 | for _, file := range rzip.File { 40 | var ( 41 | err error 42 | fd *os.File 43 | fc io.ReadCloser 44 | ) 45 | 46 | fpath := filepath.Join(destFolder, file.Name) 47 | //log.Trace("[Unzip] file : %s.", file.Name) 48 | //fmt.Printf("[Unzip] file : %s\n", file.Name) 49 | 50 | if file.FileInfo().IsDir() { 51 | os.Mkdir(fpath, file.Mode()) 52 | if err != nil { 53 | log.Error(2, "[Unzip] Create dir %s failed with %v.", fpath, err) 54 | } 55 | continue 56 | } else { 57 | idx := strings.LastIndex(fpath, string(os.PathSeparator)) 58 | if idx == -1 { 59 | log.Error(2, "[Unzip] Invalid file name %s.", fpath) 60 | continue 61 | } 62 | ppath := fpath[:idx] 63 | if err = os.MkdirAll(ppath, file.Mode()); err != nil { 64 | log.Error(2, "[Unzip] Create folder %s failed with %v.", err) 65 | continue 66 | } 67 | } 68 | 69 | for { 70 | if fc, err = file.Open(); err != nil { 71 | log.Error(2, "[Unzip] Open zip file %s failed with %v.", file.Name, err) 72 | break 73 | } 74 | if fd, err = os.OpenFile(fpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, file.Mode()); err != nil { 75 | log.Error(2, "[Unzip] Create file %s failed with %v.", fpath, err) 76 | break 77 | } 78 | if _, err = io.Copy(fd, fc); err != nil { 79 | log.Error(2, "[Unzip] Copy file failed with %v.", err) 80 | break 81 | } 82 | break 83 | } 84 | if fc != nil { 85 | fc.Close() 86 | } 87 | if fd != nil { 88 | fd.Close() 89 | } 90 | if err != nil { 91 | return err 92 | } 93 | } 94 | 95 | log.Info("[Unzip] Cost %s.", time.Since(start)) 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /util/unzip_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | func TestUnzip(t *testing.T) { 6 | zip := "wpt.zip" 7 | if err := Unzip(zip, "test"); err != nil { 8 | t.Error(err) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["transform-runtime"], 4 | "comments": false 5 | } 6 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | .editorconfig 5 | dist/ 6 | -------------------------------------------------------------------------------- /web/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-present taylorchen709 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /web/build/build.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | var ora = require('ora') 6 | var rm = require('rimraf') 7 | var path = require('path') 8 | var chalk = require('chalk') 9 | var webpack = require('webpack') 10 | var config = require('../config') 11 | var webpackConfig = require('./webpack.prod.conf') 12 | 13 | var spinner = ora('building for production...') 14 | spinner.start() 15 | 16 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 17 | if (err) throw err 18 | webpack(webpackConfig, function (err, stats) { 19 | spinner.stop() 20 | if (err) throw err 21 | process.stdout.write(stats.toString({ 22 | colors: true, 23 | modules: false, 24 | children: false, 25 | chunks: false, 26 | chunkModules: false 27 | }) + '\n\n') 28 | 29 | console.log(chalk.cyan(' Build complete.\n')) 30 | console.log(chalk.yellow( 31 | ' Tip: built files are meant to be served over an HTTP server.\n' + 32 | ' Opening index.html over file:// won\'t work.\n' 33 | )) 34 | }) 35 | }) -------------------------------------------------------------------------------- /web/build/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../package.json') 4 | var shell = require('shelljs') 5 | 6 | function exec(cmd) { 7 | return require('child_process').execSync(cmd).toString().trim() 8 | } 9 | 10 | var versionRequirements = [{ 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, ] 15 | 16 | if (shell.which('npm')) { 17 | versionRequirements.push({ 18 | name: 'npm', 19 | currentVersion: exec('npm --version'), 20 | versionRequirement: packageConfig.engines.npm 21 | }) 22 | } 23 | 24 | module.exports = function () { 25 | var warnings = [] 26 | for (var i = 0; i < versionRequirements.length; i++) { 27 | var mod = versionRequirements[i] 28 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 29 | warnings.push(mod.name + ': ' + 30 | chalk.red(mod.currentVersion) + ' should be ' + 31 | chalk.green(mod.versionRequirement) 32 | ) 33 | } 34 | } 35 | 36 | if (warnings.length) { 37 | console.log('') 38 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 39 | console.log() 40 | for (var i = 0; i < warnings.length; i++) { 41 | var warning = warnings[i] 42 | console.log(' ' + warning) 43 | } 44 | console.log() 45 | process.exit(1) 46 | } 47 | } -------------------------------------------------------------------------------- /web/build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /web/build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | var config = require('../config') 4 | if (!process.env.NODE_ENV) { 5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 6 | } 7 | 8 | var opn = require('opn') 9 | var path = require('path') 10 | var express = require('express') 11 | var webpack = require('webpack') 12 | var proxyMiddleware = require('http-proxy-middleware') 13 | var webpackConfig = require('./webpack.dev.conf') 14 | 15 | // default port where dev server listens for incoming traffic 16 | var port = process.env.PORT || config.dev.port 17 | // automatically open browser, if not set will be false 18 | var autoOpenBrowser = !!config.dev.autoOpenBrowser 19 | // Define HTTP proxies to your custom API backend 20 | // https://github.com/chimurai/http-proxy-middleware 21 | var proxyTable = config.dev.proxyTable 22 | 23 | var app = express() 24 | var compiler = webpack(webpackConfig) 25 | 26 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 27 | publicPath: webpackConfig.output.publicPath, 28 | quiet: true 29 | }) 30 | 31 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 32 | log: () => {} 33 | }) 34 | // force page reload when html-webpack-plugin template changes 35 | compiler.plugin('compilation', function (compilation) { 36 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 37 | hotMiddleware.publish({ 38 | action: 'reload' 39 | }) 40 | cb() 41 | }) 42 | }) 43 | 44 | // proxy api requests 45 | Object.keys(proxyTable).forEach(function (context) { 46 | var options = proxyTable[context] 47 | if (typeof options === 'string') { 48 | options = { 49 | target: options 50 | } 51 | } 52 | app.use(proxyMiddleware(options.filter || context, options)) 53 | }) 54 | 55 | // handle fallback for HTML5 history API 56 | app.use(require('connect-history-api-fallback')()) 57 | 58 | // serve webpack bundle output 59 | app.use(devMiddleware) 60 | 61 | // enable hot-reload and state-preserving 62 | // compilation error display 63 | app.use(hotMiddleware) 64 | 65 | // serve pure static assets 66 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 67 | app.use(staticPath, express.static('./static')) 68 | 69 | var uri = 'http://localhost:' + port 70 | 71 | var _resolve 72 | var readyPromise = new Promise(resolve => { 73 | _resolve = resolve 74 | }) 75 | 76 | console.log('> Starting dev server...') 77 | devMiddleware.waitUntilValid(() => { 78 | console.log('> Listening at ' + uri + '\n') 79 | // when env is testing, don't need open it 80 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 81 | opn(uri) 82 | } 83 | _resolve() 84 | }) 85 | 86 | var server = app.listen(port) 87 | 88 | module.exports = { 89 | ready: readyPromise, 90 | close: () => { 91 | server.close() 92 | } 93 | } -------------------------------------------------------------------------------- /web/build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' ? 7 | config.build.assetsSubDirectory : 8 | config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | 15 | var cssLoader = { 16 | loader: 'css-loader', 17 | options: { 18 | minimize: process.env.NODE_ENV === 'production', 19 | sourceMap: options.sourceMap 20 | } 21 | } 22 | 23 | // generate loader string to be used with extract text plugin 24 | function generateLoaders(loader, loaderOptions) { 25 | var loaders = [cssLoader] 26 | if (loader) { 27 | loaders.push({ 28 | loader: loader + '-loader', 29 | options: Object.assign({}, loaderOptions, { 30 | sourceMap: options.sourceMap 31 | }) 32 | }) 33 | } 34 | 35 | // Extract CSS when that option is specified 36 | // (which is the case during production build) 37 | if (options.extract) { 38 | return ExtractTextPlugin.extract({ 39 | use: loaders, 40 | fallback: 'vue-style-loader' 41 | }) 42 | } else { 43 | return ['vue-style-loader'].concat(loaders) 44 | } 45 | } 46 | 47 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 48 | return { 49 | css: generateLoaders(), 50 | postcss: generateLoaders(), 51 | less: generateLoaders('less'), 52 | sass: generateLoaders('sass', { 53 | indentedSyntax: true 54 | }), 55 | scss: generateLoaders('sass'), 56 | stylus: generateLoaders('stylus'), 57 | styl: generateLoaders('stylus') 58 | } 59 | } 60 | 61 | // Generate loaders for standalone style files (outside of .vue) 62 | exports.styleLoaders = function (options) { 63 | var output = [] 64 | var loaders = exports.cssLoaders(options) 65 | for (var extension in loaders) { 66 | var loader = loaders[extension] 67 | output.push({ 68 | test: new RegExp('\\.' + extension + '$'), 69 | use: loader 70 | }) 71 | } 72 | return output 73 | } -------------------------------------------------------------------------------- /web/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require('../config') 3 | var isProduction = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders({ 7 | sourceMap: isProduction ? 8 | config.build.productionSourceMap : 9 | config.dev.cssSourceMap, 10 | extract: isProduction 11 | }) 12 | } -------------------------------------------------------------------------------- /web/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var config = require('../config') 4 | var vueLoaderConfig = require('./vue-loader.conf') 5 | 6 | function resolve(dir) { 7 | return path.join(__dirname, '..', dir) 8 | } 9 | 10 | module.exports = { 11 | entry: { 12 | app: './src/main.js' 13 | }, 14 | output: { 15 | path: config.build.assetsRoot, 16 | filename: '[name].js', 17 | publicPath: process.env.NODE_ENV === 'production' ? 18 | config.build.assetsPublicPath : 19 | config.dev.assetsPublicPath 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.vue', '.json'], 23 | alias: { 24 | 'vue$': 'vue/dist/vue.esm.js', 25 | '@': resolve('src'), 26 | 'scss_vars': '@/styles/vars.scss' 27 | } 28 | }, 29 | module: { 30 | rules: [{ 31 | test: /\.vue$/, 32 | loader: 'vue-loader', 33 | options: vueLoaderConfig 34 | }, 35 | { 36 | test: /\.js$/, 37 | loader: 'babel-loader', 38 | include: [resolve('src'), resolve('test')] 39 | }, 40 | { 41 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 42 | loader: 'url-loader', 43 | options: { 44 | limit: 10000, 45 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 46 | } 47 | }, 48 | { 49 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 50 | loader: 'url-loader', 51 | options: { 52 | limit: 10000, 53 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 54 | } 55 | } 56 | ] 57 | } 58 | } -------------------------------------------------------------------------------- /web/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var webpack = require('webpack') 3 | var config = require('../config') 4 | var merge = require('webpack-merge') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ 17 | sourceMap: config.dev.cssSourceMap 18 | }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: '#cheap-module-eval-source-map', 22 | plugins: [ 23 | new webpack.DefinePlugin({ 24 | 'process.env': config.dev.env 25 | }), 26 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 27 | new webpack.HotModuleReplacementPlugin(), 28 | new webpack.NoEmitOnErrorsPlugin(), 29 | // https://github.com/ampedandwired/html-webpack-plugin 30 | new HtmlWebpackPlugin({ 31 | filename: 'index.html', 32 | template: 'index.html', 33 | inject: true 34 | }), 35 | new FriendlyErrorsPlugin() 36 | ] 37 | }) -------------------------------------------------------------------------------- /web/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var CopyWebpackPlugin = require('copy-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 10 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 11 | 12 | var env = config.build.env 13 | 14 | var webpackConfig = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ 17 | sourceMap: config.build.productionSourceMap, 18 | extract: true 19 | }) 20 | }, 21 | devtool: config.build.productionSourceMap ? '#source-map' : false, 22 | output: { 23 | path: config.build.assetsRoot, 24 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 25 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 26 | }, 27 | plugins: [ 28 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 29 | new webpack.DefinePlugin({ 30 | 'process.env': env 31 | }), 32 | new webpack.optimize.UglifyJsPlugin({ 33 | compress: { 34 | warnings: false 35 | }, 36 | sourceMap: true 37 | }), 38 | // extract css into its own file 39 | new ExtractTextPlugin({ 40 | filename: utils.assetsPath('css/[name].[contenthash].css') 41 | }), 42 | // Compress extracted CSS. We are using this plugin so that possible 43 | // duplicated CSS from different components can be deduped. 44 | new OptimizeCSSPlugin({ 45 | cssProcessorOptions: { 46 | safe: true 47 | } 48 | }), 49 | // generate dist index.html with correct asset hash for caching. 50 | // you can customize output by editing /index.html 51 | // see https://github.com/ampedandwired/html-webpack-plugin 52 | new HtmlWebpackPlugin({ 53 | filename: config.build.index, 54 | template: 'index.html', 55 | inject: true, 56 | minify: { 57 | removeComments: true, 58 | collapseWhitespace: true, 59 | removeAttributeQuotes: false, 60 | // more options: 61 | // https://github.com/kangax/html-minifier#options-quick-reference 62 | }, 63 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 64 | chunksSortMode: 'dependency' 65 | }), 66 | // split vendor js into its own file 67 | new webpack.optimize.CommonsChunkPlugin({ 68 | name: 'vendor', 69 | minChunks: function (module, count) { 70 | // any required modules inside node_modules are extracted to vendor 71 | return ( 72 | module.resource && 73 | /\.js$/.test(module.resource) && 74 | module.resource.indexOf( 75 | path.join(__dirname, '../node_modules') 76 | ) === 0 77 | ) 78 | } 79 | }), 80 | // extract webpack runtime and module manifest to its own file in order to 81 | // prevent vendor hash from being updated whenever app bundle is updated 82 | new webpack.optimize.CommonsChunkPlugin({ 83 | name: 'manifest', 84 | chunks: ['vendor'] 85 | }), 86 | // copy custom static assets 87 | new CopyWebpackPlugin([{ 88 | from: path.resolve(__dirname, '../static'), 89 | to: config.build.assetsSubDirectory, 90 | ignore: ['.*'] 91 | }]) 92 | ] 93 | }) 94 | 95 | if (config.build.productionGzip) { 96 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 97 | 98 | webpackConfig.plugins.push( 99 | new CompressionWebpackPlugin({ 100 | asset: '[path].gz[query]', 101 | algorithm: 'gzip', 102 | test: new RegExp( 103 | '\\.(' + 104 | config.build.productionGzipExtensions.join('|') + 105 | ')$' 106 | ), 107 | threshold: 10240, 108 | minRatio: 0.8 109 | }) 110 | ) 111 | } 112 | 113 | if (config.build.bundleAnalyzerReport) { 114 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 115 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 116 | } 117 | 118 | module.exports = webpackConfig -------------------------------------------------------------------------------- /web/config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) -------------------------------------------------------------------------------- /web/config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | // This should be the URL path where your build.assetsRoot 11 | // will be served from over HTTP. In most cases, this will be root (/). 12 | // Only change this if your backend framework serves static assets with a path prefix. 13 | assetsPublicPath: '/', 14 | productionSourceMap: true, 15 | // Gzip off by default as many popular static hosts such as 16 | // Surge or Netlify already gzip all static assets for you. 17 | // Before setting to `true`, make sure to: 18 | // npm install --save-dev compression-webpack-plugin 19 | productionGzip: false, 20 | productionGzipExtensions: ['js', 'css'], 21 | // Run the build command with an extra argument to 22 | // View the bundle analyzer report after build finishes: 23 | // `npm run build --report` 24 | // Set to `true` or `false` to always turn it on or off 25 | bundleAnalyzerReport: process.env.npm_config_report 26 | }, 27 | dev: { 28 | env: require('./dev.env'), 29 | port: 8010, 30 | autoOpenBrowser: true, 31 | assetsSubDirectory: 'static', 32 | assetsPublicPath: '/', 33 | proxyTable: { 34 | '/api/': { 35 | target: 'http://wanji10-svr:8080', 36 | changeOrigin: true, 37 | } 38 | }, 39 | // CSS Sourcemaps off by default because relative paths are "buggy" 40 | // with this option, according to the CSS-Loader README 41 | // (https://github.com/webpack/css-loader#sourcemaps) 42 | // In our experience, they generally work as expected, 43 | // just be aware of this issue when enabling this option. 44 | cssSourceMap: false 45 | } 46 | } -------------------------------------------------------------------------------- /web/config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Symbol Server by Golang 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /web/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ], 5 | 6 | "compilerOptions": { 7 | "baseUrl": ".", 8 | "paths": { 9 | "components/*": [ 10 | "src/components/*" 11 | ] 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symbols", 3 | "description": "Windows symbol server powered by golang", 4 | "author": "adyzng@gmail.com", 5 | "private": true, 6 | "scripts": { 7 | "dev": "node build/dev-server.js", 8 | "start": "node build/dev-server.js", 9 | "build": "node build/build.js" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.17.0", 13 | "chart.js": "^2.7.0", 14 | "element-ui": "^1.4.2", 15 | "vue": "^2.4.2", 16 | "vue-chartjs": "^3.0.0", 17 | "vue-router": "^3.0.1", 18 | "vuex": "^3.0.0" 19 | }, 20 | "engines": { 21 | "node": ">=6", 22 | "npm": ">= 3.0.0" 23 | }, 24 | "devDependencies": { 25 | "autoprefixer": "^6.6.0", 26 | "babel-core": "^6.24.1", 27 | "babel-loader": "^6.4.0", 28 | "babel-preset-stage-2": "^6.24.1", 29 | "babel-preset-vue-app": "^1.2.0", 30 | "copy-webpack-plugin": "^4.2.0", 31 | "css-loader": "^0.27.0", 32 | "element-theme": "^0.7.2", 33 | "element-theme-default": "^1.4.9", 34 | "eventsource-polyfill": "^0.9.6", 35 | "extract-text-webpack-plugin": "^2.1.2", 36 | "file-loader": "^0.10.1", 37 | "friendly-errors-webpack-plugin": "^1.6.1", 38 | "html-webpack-plugin": "^2.24.1", 39 | "node-sass": "^4.6.0", 40 | "optimize-css-assets-webpack-plugin": "^3.2.0", 41 | "postcss-loader": "^1.3.3", 42 | "rimraf": "^2.5.4", 43 | "sass-loader": "^6.0.6", 44 | "shelljs": "^0.7.6", 45 | "style-loader": "^0.13.2", 46 | "url-loader": "^0.5.8", 47 | "vue-loader": "^13.0.4", 48 | "vue-template-compiler": "^2.4.2", 49 | "webpack": "^2.4.1", 50 | "webpack-dev-server": "^2.4.2", 51 | "webpack-hot-middleware": "^2.20.0", 52 | "webpack-merge": "^4.1.1" 53 | } 54 | } -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer')() 4 | ] 5 | } -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 47 | 48 | -------------------------------------------------------------------------------- /web/src/api/pdb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * query data from backend 3 | * 4 | */ 5 | 6 | import axios from 'axios' 7 | import store from '../utils/store'; 8 | 9 | const http = axios.create({ 10 | baseURL: '/api', // base api 11 | timeout: 5000, // request timeout 12 | // `xsrfCookieName` is the name of the cookie to use as a value for xsrf token 13 | //xsrfCookieName: 'XSRF-TOKEN', // default 14 | // `xsrfHeaderName` is the name of the http header that carries the xsrf token value 15 | //xsrfHeaderName: 'X-XSRF-TOKEN', // default 16 | }); 17 | 18 | // request interceptor 19 | http.interceptors.request.use(config => { 20 | let token = store.getters.token 21 | if (token) { 22 | // set token in request header 23 | config.headers['X-Token'] = token; 24 | } 25 | return config; 26 | }, 27 | error => { 28 | // Do something with request error 29 | console.log(`axois request error : ${error}`); 30 | Promise.reject(error); 31 | }); 32 | 33 | // fake build data set 34 | const fakeBuilds = [{ 35 | id: "0000000002", 36 | date: "2017-10-22 05:16:24", 37 | branch: "UDPv6.5U2", 38 | version: "4175.2-646", 39 | comment: "2017-10-22_05:16:23" 40 | }, 41 | { 42 | id: "0000000001", 43 | date: "2017-10-21 17:17:54", 44 | branch: "UDPv6.5U2", 45 | version: "4175.2-645", 46 | comment: "2017-10-21_17:17:51" 47 | } 48 | ]; 49 | 50 | // fake branch data set 51 | const fakeBranchs = [{ 52 | buildName: "UDP_6_5_U2", 53 | storeName: "UDPv6.5U2", 54 | buildPath: "Z:\\UDP_6_5_U2\\Release", 55 | storePath: "c:\\Go\\huan\\src\\github.com\\adyzng\\GoSymbols\\testdata\\UDPv6.5U2", 56 | updateDate: "2017-10-22 15:31:00", 57 | latestBuild: "4175.2-646", 58 | buildsCount: 9, 59 | }, 60 | { 61 | buildName: "UDP_6_5_U1", 62 | storeName: "UDPv6.5U1", 63 | buildPath: "Z:\\UDP_6_5_U1\\Release", 64 | storePath: "c:\\Go\\huan\\src\\github.com\\adyzng\\GoSymbols\\testdata\\UDPv6.5U1", 65 | updateDate: "2017-10-22 15:31:28", 66 | latestBuild: "4175.1-385", 67 | buildsCount: 65, 68 | } 69 | ]; 70 | 71 | 72 | export default { 73 | getFakeBranchs() { 74 | return [...fakeBranchs, ...fakeBranchs] 75 | }, 76 | 77 | getFakeBuilds() { 78 | let d = [] 79 | for (let i in 20) { 80 | d.concat(...fakeBuilds) 81 | } 82 | return d 83 | }, 84 | 85 | getFakeMessages(cb) { 86 | const msgs = [ { 87 | succeed: true, 88 | branch: "UDPv6.5U2", 89 | updateDate: "2017-10-22 15:31:00", 90 | }, { 91 | succeed: true, 92 | branch: "UDPv6.5U1", 93 | updateDate: "2017-10-23 08:21:12", 94 | }, { 95 | succeed: false, 96 | branch: "UDPv6", 97 | updateDate: "", 98 | }]; 99 | if (cb) { 100 | cb(msgs); 101 | } 102 | }, 103 | 104 | getTodayMessages(cb) { 105 | return http.get('/messages').then(resp => { 106 | let res = resp.data 107 | if (Array.isArray(res.data)) { 108 | cb(res.data) 109 | } else { 110 | cb([]) 111 | } 112 | }) 113 | .catch(err => { 114 | cb([]) 115 | console.log("getTodayMessages failed:", err); 116 | }) 117 | }, 118 | 119 | fetchBranchs(cb) { 120 | if (!cb) { 121 | return 122 | } 123 | return http.get("/branches").then(resp => { 124 | let res = resp.data; 125 | let data = []; 126 | if (res.data) { 127 | data = [...res.data.branchs]; 128 | data.sort((a,b) => a.updateDate < b.updateDate); 129 | } else { 130 | console.log("fetchBranchs empty:", res); 131 | } 132 | cb(data); 133 | }) 134 | .catch(error => { 135 | console.log("fetchBranchs failed:", error); 136 | cb([]); 137 | }); 138 | }, 139 | 140 | fetchBuilds(branch, cb) { 141 | if (!branch || !cb) { 142 | return 143 | } 144 | return http.get(`/branches/${branch}`).then(resp => { 145 | let res = resp.data; 146 | let data = []; 147 | if (res.data) { 148 | data = [...res.data.builds]; 149 | data.sort((a, b) => b.id - a.id); 150 | } else { 151 | console.log("fetchBuilds empty:", res) 152 | } 153 | cb(data); 154 | }) 155 | .catch(err => { 156 | console.log("fetchBuilds failed:", err); 157 | cb([]); 158 | }); 159 | }, 160 | 161 | fetchSymbols(branch, build, cb) { 162 | if (!branch || !build || !cb) { 163 | return 164 | } 165 | return http.get(`/branches/${branch}/${build}`).then(resp => { 166 | let res = resp.data; 167 | let data = [] 168 | if (res.data) { 169 | data = [...res.data.symbols]; 170 | data.sort((a, b) => b.id - a.id); 171 | } else { 172 | console.log("fetchSymbols empty:", res) 173 | } 174 | cb(data); 175 | }) 176 | .catch(err => { 177 | console.log("fetchSymbols failed:", err); 178 | cb([]); 179 | }); 180 | }, 181 | 182 | deleteBranch(branch, cb) { 183 | if (!branch) { 184 | return 185 | } 186 | return http.delete(`/branches/${branch.storeName}`).then(resp => { 187 | if (resp.data) { 188 | cb && cb(resp.data); 189 | } 190 | }) 191 | .catch(err => { 192 | console.log(`deleteBranch error ${err}.`); 193 | cb && cb(err); 194 | }) 195 | }, 196 | 197 | validateBranch(branch, cb) { 198 | if (!branch) { 199 | return; 200 | } 201 | return http.post(`/branches/check`, JSON.stringify(branch)).then(resp => { 202 | if (resp.data) { 203 | cb && cb(resp.data); 204 | } 205 | }) 206 | .catch(err => { 207 | console.log(`validateBranch error ${err}.`); 208 | cb && cb(err); 209 | }) 210 | }, 211 | 212 | modifyBranch(create, branch, cb) { 213 | if (!branch) { 214 | return; 215 | } 216 | let uri = !!create ? '/branches/create':'/branches/modify' 217 | return http.post(uri, JSON.stringify(branch)).then(resp => { 218 | if (resp.data) { 219 | cb && cb(resp.data); 220 | } 221 | }) 222 | .catch(err => { 223 | console.log(`modifyBranch error ${err}.`); 224 | cb && cb(err); 225 | }) 226 | }, 227 | 228 | fetchProfile(cb) { 229 | return http.get(`/user/profile`).then(resp => { 230 | cb && cb(resp.data) 231 | console.log(`user ${JSON.stringify(resp.data.data)}`) 232 | }) 233 | .catch(err => { 234 | console.log(`get user profile error ${err}`) 235 | cb && cb({}) 236 | }) 237 | }, 238 | 239 | userLogout(cb) { 240 | return http.get('/auth/logout').then(resp => { 241 | cb && cb(resp.data) 242 | console.log(`user logout ${JSON.stringify(resp.data)}`) 243 | }) 244 | .catch(err => { 245 | console.log(`user logout error ${err}`) 246 | }) 247 | } 248 | } -------------------------------------------------------------------------------- /web/src/assets/element-#169bd9.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adyzng/GoSymbols/1ae3dfb270a9df57254f5abcfa7978daaa0f2b7e/web/src/assets/element-#169bd9.zip -------------------------------------------------------------------------------- /web/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adyzng/GoSymbols/1ae3dfb270a9df57254f5abcfa7978daaa0f2b7e/web/src/assets/logo.png -------------------------------------------------------------------------------- /web/src/assets/user.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adyzng/GoSymbols/1ae3dfb270a9df57254f5abcfa7978daaa0f2b7e/web/src/assets/user.jpg -------------------------------------------------------------------------------- /web/src/assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adyzng/GoSymbols/1ae3dfb270a9df57254f5abcfa7978daaa0f2b7e/web/src/assets/user.png -------------------------------------------------------------------------------- /web/src/components/branchs.vue: -------------------------------------------------------------------------------- 1 | 86 | 87 | 266 | 267 | -------------------------------------------------------------------------------- /web/src/components/builds.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /web/src/components/header.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 153 | 154 | 155 | 236 | 237 | -------------------------------------------------------------------------------- /web/src/components/message.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 46 | 47 | 48 | 100 | 101 | -------------------------------------------------------------------------------- /web/src/components/symbols.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 178 | 179 | 180 | 183 | 184 | -------------------------------------------------------------------------------- /web/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import App from './App.vue' 5 | import routes from './utils/routes' 6 | import store from './utils/store' 7 | 8 | import ElementUI from 'element-ui' 9 | import locale from 'element-ui/lib/locale/lang/en' 10 | import 'element-ui/lib/theme-default/index.css' 11 | 12 | Vue.use(VueRouter) 13 | Vue.use(ElementUI, {locale}) 14 | 15 | const router = new VueRouter({ 16 | mode: 'hash', // hash/history 17 | routes: routes, 18 | linkActiveClass: '', 19 | linkExactActiveClass: 'current', 20 | scrollBehavior (to, from, savedPosition) { 21 | if (savedPosition) { 22 | return savedPosition 23 | } else { 24 | return { x: 0, y: 0 } 25 | } 26 | }, 27 | }) 28 | 29 | Vue.filter('fltUpperCase', function (value) { 30 | if (typeof(value) === 'string') { 31 | return value.toLocaleUpperCase() 32 | } 33 | return value.toString().toUpperCase() 34 | }) 35 | 36 | new Vue({ 37 | el: '#app', 38 | store, 39 | router, 40 | render: h => h(App), 41 | }) -------------------------------------------------------------------------------- /web/src/utils/chart.js: -------------------------------------------------------------------------------- 1 | import {Pie, mixins} from 'vue-chartjs' 2 | const {reactiveProp} = mixins; 3 | 4 | function merge(...objects) { 5 | let result = {}; 6 | function assign(val, key) { 7 | if (typeof result[key] === 'object' && typeof val === 'object') { 8 | result[key] = merge(result[key], val); 9 | } else { 10 | result[key] = val; 11 | } 12 | } 13 | objects.forEach(val => { 14 | for (let key in val) { 15 | assign(val[key], key) 16 | } 17 | }) 18 | return result; 19 | } 20 | 21 | const PieChart = { 22 | name: 'pie-chart', 23 | extends: Pie, 24 | //mixins : [reactiveProp], 25 | props: { 26 | chartLabel: { 27 | type: Array, 28 | required: true, 29 | }, 30 | chartData: { 31 | type: Array, 32 | required: true, 33 | }, 34 | chartOption: { 35 | type: Object, 36 | default: ()=> {}, 37 | }, 38 | }, 39 | watch: { 40 | 'chartData': function(newVal, oldVal) { 41 | const options = merge(this.options, this.chartOption); 42 | //console.log(`renderChart with label: ${this.chartLabel}`); 43 | this.renderChart({ 44 | labels: this.chartLabel, 45 | datasets: this.chartData, 46 | }, options ); 47 | }, 48 | }, 49 | data() { 50 | return { 51 | options : { 52 | responsive: true, 53 | maintainAspectRatio: false 54 | }, 55 | } 56 | }, 57 | }; 58 | 59 | export default { 60 | PieChart, 61 | } 62 | 63 | export { 64 | PieChart, 65 | }; -------------------------------------------------------------------------------- /web/src/utils/openWin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Sting} url 3 | * @param {Sting} title 4 | * @param {Number} w 5 | * @param {Number} h 6 | */ 7 | 8 | export default function openWindow(url, title, w, h) { 9 | // Fixes dual-screen position Most browsers Firefox 10 | const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left 11 | const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top 12 | 13 | const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width 14 | const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height 15 | 16 | const left = ((width / 2) - (w / 2)) + dualScreenLeft 17 | const top = ((height / 2) - (h / 2)) + dualScreenTop 18 | const newWindow = window.open(url, 19 | title, 20 | `width=${w},height=${h},top=${top},left=${left},toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,copyhistory=no` 21 | ) 22 | // Puts focus on the newWindow 23 | if (window.focus) { 24 | newWindow.focus() 25 | } 26 | } -------------------------------------------------------------------------------- /web/src/utils/routes.js: -------------------------------------------------------------------------------- 1 | import Branchs from "../components/branchs.vue" 2 | import Symbols from "../components/symbols.vue" 3 | import Authorize from "../view/authorize.vue" 4 | 5 | export const routes = [ 6 | { 7 | name: 'branchs', 8 | path: '/', 9 | component: Branchs, 10 | }, 11 | { 12 | name: 'symbols', 13 | path: '/symbols', 14 | component: Symbols, 15 | //props: true, 16 | }, 17 | { 18 | name: 'authredirect', 19 | path: '/login/authorize', 20 | component: Authorize, 21 | }, 22 | { 23 | name: 'edit', 24 | path: '/branch', 25 | component: Branchs, 26 | children: [ 27 | { 28 | path: '/edit', 29 | component: Branchs, 30 | meta: { 31 | requireAuth: true 32 | } 33 | }, 34 | { 35 | path: '/update', 36 | component: Branchs, 37 | meta: { 38 | requireAuth: true 39 | } 40 | } 41 | ] 42 | } 43 | 44 | ]; 45 | 46 | export default routes; -------------------------------------------------------------------------------- /web/src/utils/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import * as types from './types' 4 | 5 | Vue.use(Vuex) 6 | 7 | export default new Vuex.Store({ 8 | state : { 9 | branch : '', // current selected branch 10 | branchList : [], // all branchs list 11 | build : '', // current selected build 12 | buildList : [], // current selected build list 13 | token: "", 14 | userProfile : {}, 15 | }, 16 | getters : { 17 | curBranch: store => { 18 | return store.branch; 19 | }, 20 | branchesList : store => { 21 | return store.branchList 22 | }, 23 | curBuild : store => { 24 | return store.build; 25 | }, 26 | buildsList : store => { 27 | return store.buildList 28 | }, 29 | token : store => { 30 | return store.token 31 | }, 32 | userProfile : store => { 33 | return store.userProfile 34 | }, 35 | }, 36 | mutations : { 37 | [types.CHANGE_BRANCH](store, val) { 38 | store.branch = val; 39 | }, 40 | [types.BRANCH_LIST](store, val) { 41 | store.branchList = val; 42 | }, 43 | [types.CHANGE_BUILD](store, val) { 44 | store.build = val; 45 | }, 46 | [types.BUILD_LIST](store, val) { 47 | store.buildList = val; 48 | }, 49 | [types.USER_PROFILE](store, val) { 50 | store.userProfile = val; 51 | }, 52 | }, 53 | actions : { 54 | [types.CHANGE_BRANCH] ({ commit, state }, val) { 55 | commit(types.CHANGE_BRANCH, val) 56 | }, 57 | [types.BRANCH_LIST] ({ commit, state }, val) { 58 | commit(types.BRANCH_LIST, val) 59 | }, 60 | [types.CHANGE_BUILD] ({ commit, state }, val) { 61 | commit(types.CHANGE_BUILD, val) 62 | }, 63 | [types.BUILD_LIST] ({ commit, state }, val) { 64 | commit(types.BUILD_LIST, val) 65 | }, 66 | [types.USER_PROFILE] ({ commit, state }, val) { 67 | commit(types.USER_PROFILE, val) 68 | }, 69 | }, 70 | }) -------------------------------------------------------------------------------- /web/src/utils/types.js: -------------------------------------------------------------------------------- 1 | 2 | export const CHANGE_BRANCH = 'changeBranch'; 3 | 4 | export const CHANGE_BUILD = 'changeBuild'; 5 | 6 | export const USER_PROFILE = 'userInfo' 7 | 8 | export const BRANCH_LIST = 'branchList' 9 | 10 | export const BUILD_LIST = 'buildList' -------------------------------------------------------------------------------- /web/src/view/authorize.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /web/static/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adyzng/GoSymbols/1ae3dfb270a9df57254f5abcfa7978daaa0f2b7e/web/static/1.png -------------------------------------------------------------------------------- /web/static/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adyzng/GoSymbols/1ae3dfb270a9df57254f5abcfa7978daaa0f2b7e/web/static/2.png -------------------------------------------------------------------------------- /web/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adyzng/GoSymbols/1ae3dfb270a9df57254f5abcfa7978daaa0f2b7e/web/static/favicon.ico --------------------------------------------------------------------------------