From 7277a23ca81123a95cbf60cbda65ca2fd6e27c17 Mon Sep 17 00:00:00 2001 From: liuxiaolong <liuxiaolong@aiotlink.com> Date: 星期五, 05 六月 2020 18:26:27 +0800 Subject: [PATCH] get sdks from remote shop --- service/SdkDownLoad_test.go | 10 + go.sum | 5 service/SdkInstall.go | 209 +++++++++++++++++++++++++--------- service/SdkDownLoad.go | 108 ++++++++++++++--- go.mod | 4 controllers/sdk.go | 4 6 files changed, 259 insertions(+), 81 deletions(-) diff --git a/controllers/sdk.go b/controllers/sdk.go index ae10470..c2f2cc9 100644 --- a/controllers/sdk.go +++ b/controllers/sdk.go @@ -81,8 +81,10 @@ // @Router /data/api-v/sdk/findAllSdk [GET] func (sc SdkController) FindAllSdk(c *gin.Context) { sdkName := c.Query("sdkName") + logger.Debug("FindAllSdk sdkName:", sdkName) - sdks := service.GetSdkList(sdkName) + sdks := service.GetSdkList("") //鏈満宸插畨瑁呯殑绠楁硶 + util.ResponseFormat(c, code.Success, sdks) } diff --git a/go.mod b/go.mod index e833ee6..e449034 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ basic.com/gb28181api.git v0.0.0-20191028082253-472438a8407b basic.com/pubsub/cache.git v0.0.0-20190718093725-6a413e1d7d48 basic.com/pubsub/esutil.git v0.0.0-20200114073900-ad9de8362777 - basic.com/pubsub/protomsg.git v0.0.0-20200306071852-76af8d2ca810 + basic.com/pubsub/protomsg.git v0.0.0-20200605082339-fe3f28d45337 basic.com/valib/capture.git v0.0.0-20191204103802-89c923cf2abe basic.com/valib/deliver.git v0.0.0-20190531095353-25d8c3b20051 basic.com/valib/godraw.git v0.0.0-20191122082247-26e9987cd183 @@ -17,7 +17,7 @@ basic.com/valib/gopherdiscovery.git v0.0.0-20190605034340-15d89d8b4e28 basic.com/valib/logger.git v0.0.0-20190928113028-4907b08c4159 github.com/Microsoft/go-winio v0.4.12 // indirect - github.com/ajg/form v1.5.1 // indirect + github.com/ajg/form v1.5.1 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/disintegration/imaging v1.6.2 // indirect diff --git a/go.sum b/go.sum index 8bfa0f0..7d3e94c 100644 --- a/go.sum +++ b/go.sum @@ -10,9 +10,8 @@ basic.com/pubsub/esutil.git v0.0.0-20191120125514-865efa73a9ae/go.mod h1:yIvppFPFGC61DOdm71ujnsxZBMFUu2yKjr5O43bMWCw= basic.com/pubsub/esutil.git v0.0.0-20200114073900-ad9de8362777 h1:gTeuhepfLgOchD6bqydsGGV6KCj/UaseQQgo4DFyhGQ= basic.com/pubsub/esutil.git v0.0.0-20200114073900-ad9de8362777/go.mod h1:yIvppFPFGC61DOdm71ujnsxZBMFUu2yKjr5O43bMWCw= -basic.com/pubsub/protomsg.git v0.0.0-20200304101716-2f4c0110fab2/go.mod h1:un5NV5VWQoblVLZfx1Rt5vyLgwR0jI92d3VJhfrJhWU= -basic.com/pubsub/protomsg.git v0.0.0-20200306071852-76af8d2ca810 h1:xGOKeo8E89NxioWCFySRhom9i8zWvu3Gsi9IYu6LYJw= -basic.com/pubsub/protomsg.git v0.0.0-20200306071852-76af8d2ca810/go.mod h1:un5NV5VWQoblVLZfx1Rt5vyLgwR0jI92d3VJhfrJhWU= +basic.com/pubsub/protomsg.git v0.0.0-20200605082339-fe3f28d45337 h1:6LTdIfHUZkfemhkuQnXXaeXRdnm4459PyxMd36AXV20= +basic.com/pubsub/protomsg.git v0.0.0-20200605082339-fe3f28d45337/go.mod h1:un5NV5VWQoblVLZfx1Rt5vyLgwR0jI92d3VJhfrJhWU= basic.com/valib/capture.git v0.0.0-20191204103802-89c923cf2abe h1:uh3u7DuSOw6AwzvPC1EM19sw1Skks1EUJddcbHDKI9M= basic.com/valib/capture.git v0.0.0-20191204103802-89c923cf2abe/go.mod h1:y+h7VUnoSQ3jOtf2K3twXNA8fYDfyUsifSswcyKLgNw= basic.com/valib/deliver.git v0.0.0-20190531095353-25d8c3b20051/go.mod h1:bkYiTUGzckyNOjAgn9rB/DOjFzwoSHJlruuWQ6hu6IY= diff --git a/service/SdkDownLoad.go b/service/SdkDownLoad.go index ed364ca..3567234 100644 --- a/service/SdkDownLoad.go +++ b/service/SdkDownLoad.go @@ -12,6 +12,9 @@ "os" "path/filepath" "strings" + "webserver/extend/config" + + reqUtil "webserver/extend/util" "webserver/util" @@ -127,30 +130,59 @@ } } -func GetSdkList(sdkName string) []map[string]interface{} { - // 寰呰繑鍥炵殑鏁版嵁 - sdks, idsCache := queryDatabase(sdkName) - - // 鏌ヨ鏈湴宸茬粡瀛樺湪鐨勭畻娉曞拰杩滅鐨勫叏閮ㄧ畻娉�, 鐒跺悗鍜屽凡瀹夎鐨勬瘮瀵�, 杩斿洖鍏ㄩ儴鍙敤鐨勭畻娉曠粍. 鏈畨瑁呯殑绠楁硶鍦ㄥ墠绔蛋涓嬭浇娴佺▼ - localSdks := GetLocalSdks() - for _, sdk := range localSdks { - id := sdk["id"].(string) - if !idsCache[id] { - idsCache[id] = true - - sdk["enable"] = false - sdk["installed"] = false - - // 涓嶈繑鍥炲弬鏁颁俊鎭� - delete(sdk, "args") - sdks = append(sdks, sdk) +func GetSdkList(sdkName string) []SdkInsOrUpgrade { + var api dbapi.SdkApi + localSdks := api.FindAll(sdkName) //鏈湴宸插畨瑁呮墍鏈夌畻娉� + localSdkM := make(map[string]SdkInsOrUpgrade) + for _,ls :=range localSdks { + siou := SdkInsOrUpgrade{ + Installed: true, } - + siou.Sdk = Sdk{ + Id: ls.Id, + IpcId: ls.IpcId, + SdkType: ls.SdkType, + SdkName: ls.SdkName, + Icon: ls.Icon, + Url: ls.Url, + CreateTime: ls.CreateTime, + CreateBy: ls.CreateBy, + UpdateTime: ls.UpdateTime, + Enable: ls.Enable, + DelFlag: int(ls.DelFlag), + IconBlob: ls.IconBlob, + Version: ls.Version, + } + localSdkM[siou.Id] = siou } - // 鐩墠杩樻病鏈夎繙绔畻娉曞簱, - // remoteSdks := getRemoteSdks() + //杩滅鍙敤鐨勭畻娉� + remoteSdks := findAllMySdk() + for sdkId,sdk := range remoteSdks { + if v,ok := localSdkM[sdkId];ok { //鏈湴宸插畨瑁� + ls := v + if shouldVersionBeUpgrade(ls.Version, sdk.Version) { //鍒ゆ柇鐗堟湰鍙锋槸鍚﹂渶瑕佸崌绾� + ls.Installed = true + ls.IsUpgrade = true //闇�瑕佸崌绾� + ls.RemoteVersion = sdk.Version //杩滅鐨勭増鏈彿 + } + localSdkM[sdkId] = ls + } else { //鏈湴鏈畨瑁咃紝闇�瑕佸畨瑁� + bIns := SdkInsOrUpgrade{ + RemoteVersion: sdk.Version, + IsUpgrade: false, + Installed: false, + } + bIns.Sdk = sdk + localSdkM[sdkId] = bIns + } + } + + sdks := make([]SdkInsOrUpgrade, 0) + for _,v := range localSdkM { + sdks = append(sdks, v) + } return sdks } @@ -177,6 +209,42 @@ return sdks, ids } + +type shopSdks struct { + Sdks []Sdk `json:"sdks"` +} + +func findAllMySdk() map[string]Sdk { + token := "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTEzODA3ODksInVzZXIiOiJ7XCJpZFwiOlwiZTZjY2QzNmQtNGYxNi00NmZjLTg4ZDUtMDczNjU4NjZkMjA1XCIsXCJwZXJtaXNzaW9uc1wiOltcIm9yZGVyTWFuZ2U6ZG93bmxvYWRcIixcImRldmljZU1hbmFnZTp1bmJpbmRcIixcImNvZGVNYW5nZVwiLFwidmlwTWFuYWdlOmFkZFwiLFwiYWRtaW5NYW5hZ2U6ZWRpdFwiLFwiY29kZU1hbmdlOnZpZXdcIixcImNvZGVNYW5nZTphZGRcIixcImFkbWluTWFuYWdlXCIsXCJvcmRlck1hbmdlOmNhbmNlbFwiLFwicHJvZHVjdENlbnRlcjpidXlcIixcInByb2R1Y3RDZW50ZXJcIixcInByb2R1Y3RNYW5nZTp2aWV3XCIsXCJ2aXBNYW5hZ2U6dmlld1wiLFwib3JkZXJNYW5nZVwiLFwicHJvZHVjdENlbnRlcjp2aWV3XCIsXCJkZXZpY2VNYW5hZ2VcIixcImFkbWluTWFuYWdlOmFkZFwiLFwicHJvZHVjdE1hbmdlOmFkZFwiLFwiYWRtaW5NYW5hZ2U6dmlld1wiLFwicHJvZHVjdE1hbmdlXCIsXCJvcmRlck1hbmdlOnZpZXdcIixcImRldmljZU1hbmFnZTp2aWV3XCIsXCJvcmRlck1hbmdlOnBheVwiLFwidmlwTWFuYWdlOmVkaXRcIixcInZpcE1hbmFnZVwiLFwicHJvZHVjdE1hbmdlOmVkaXRcIixcInByb2R1Y3RNYW5nZTpwdWJsaXNoXCIsXCJkZXZpY2VNYW5hZ2U6YWRkXCIsXCJhcGlcIixcImhvbWVcIixcInByb2R1Y3RNYW5nZTpvZmZTYWxlXCIsXCJwcm9kdWN0Q2VudGVyOmRvd25sb2FkXCJdLFwidXNlcm5hbWVcIjpcImJhc2ljXCJ9In0.HwRobdFLtMK7ni5OKk4_NAyqpKGuUlUbqF3HBJMJuOk" + url := "http://192.168.20.10:7004/data/api-s/sdk/findAllMySdk" + fmt.Println("token:", token, "url:", url) + paramBody := map[string]interface{} { + "serverId": config.Server.AnalyServerId, + "machineCode": "", + } + header := map[string]string { + "Authorization": token, + } + respBody, err := reqUtil.DoPostRequest(url, reqUtil.CONTENT_TYPE_JSON, paramBody, nil, header) + if err != nil { + return nil + } + var res dbapi.Result + if err = json.Unmarshal(respBody, &res); err != nil { + return nil + } + bytes, _ := json.Marshal(res.Data) + var ss shopSdks + if err := json.Unmarshal(bytes, &ss);err != nil { + return nil + } + m := make(map[string]Sdk) + for _,s := range ss.Sdks { + m[s.Id] = s + } + return m +} + func GetLocalSdks() []map[string]interface{} { var algos = []map[string]interface{}{} diff --git a/service/SdkDownLoad_test.go b/service/SdkDownLoad_test.go index 6d43c33..bb857fa 100644 --- a/service/SdkDownLoad_test.go +++ b/service/SdkDownLoad_test.go @@ -1 +1,11 @@ package service + +import ( + "fmt" + "testing" +) + +func TestGetSdkList(t *testing.T) { + list := GetSdkList("") + fmt.Println("sdks:", list) +} diff --git a/service/SdkInstall.go b/service/SdkInstall.go index b930aab..43f29a3 100644 --- a/service/SdkInstall.go +++ b/service/SdkInstall.go @@ -9,6 +9,8 @@ "os" "path" "path/filepath" + "strconv" + "strings" "webserver/extend/config" "webserver/extend/util" ) @@ -23,17 +25,21 @@ if config.Server.PatchPath != "" { configPatchPath = config.Server.PatchPath } else { - configPatchPath = "/opt/vasystem/patch" + configPatchPath = "../patch" } filenameWithSuffix := path.Base(filename) ext := path.Ext(filenameWithSuffix) zipFilePath := configPatchPath + "/"+identifier+ext + unPackTargetPath := configPatchPath+"/"+identifier+"_basic/" if util.Exists(zipFilePath) { strMd5, e := util.FileMd5(zipFilePath) if e !=nil || strMd5 == "" { go os.Remove(zipFilePath) + if util.DirExists(unPackTargetPath) { + go os.RemoveAll(unPackTargetPath) + } return false,errors.New("鑾峰彇瀹夎鍖卪d5澶辫触") } if strMd5 == identifier { @@ -44,6 +50,9 @@ } else { go os.Remove(zipFilePath) + if util.DirExists(unPackTargetPath) { + go os.RemoveAll(unPackTargetPath) + } logger.Debug("strMd5 is", strMd5,"identifier is",identifier,"not equal") return false,errors.New("鏍¢獙瀹夎鏂囦欢澶辫触") } @@ -58,7 +67,7 @@ if config.Server.PatchPath != "" { configPatchPath = config.Server.PatchPath } else { - configPatchPath = "/opt/vasystem/patch" + configPatchPath = "../patch" } //1.瑙e帇缂╂洿鏂板寘 unPackTargetPath := configPatchPath+"/"+identifier+"_basic/" @@ -122,44 +131,76 @@ //3.灏嗙畻娉晄o銆佷緷璧栨枃浠躲�亃conf銆� soM := make(map[string]SdkDef) var skDefArr []SdkDef + nInsM := make(map[string]string) + aInsM := make(map[string]string) if err = json.Unmarshal(defB, &skDefArr);err == nil { - //娉ㄥ唽绠楁硶淇℃伅鍜岀畻娉曞弬鏁板埌dbserver var sdkApi dbapi.SdkApi for _,skd := range skDefArr { + //娉ㄥ唽绠楁硶淇℃伅鍜岀畻娉曞弬鏁板埌dbserver + //鍏堢湅姝ょ畻娉曟湁娌℃湁娉ㄥ唽锛屽鏋滃凡瀹夎锛屾瘮瀵圭増鏈彿 + bf, curDbSdk := sdkApi.GetById(skd.Def.Id) + if bf { + bytes, _ := json.Marshal(curDbSdk) + var sm map[string]interface{} + unE := json.Unmarshal(bytes, &sm) + if unE != nil { + continue + } + sdkVersion, ok := sm["version"] + if ok { //鏈夌増鏈彿 + vStr := sdkVersion.(string) + if shouldVersionBeUpgrade(vStr, skd.Def.Version) { + nInsM[skd.Def.SdkType] = skd.Def.SdkType + aInsM[skd.Def.Id] = skd.Def.Id + } + } else { + nInsM[skd.Def.SdkType] = skd.Def.SdkType + aInsM[skd.Def.Id] = skd.Def.Id + } + } else { + nInsM[skd.Def.SdkType] = skd.Def.SdkType + aInsM[skd.Def.Id] = skd.Def.Id + } + + if _,ok := soM[skd.Def.SdkType];!ok { soM[skd.Def.SdkType] = skd } - srv := SdkRegisterVo{ - Id: skd.Def.Id, - SdkType: skd.Def.SdkType, - SdkName: skd.Def.SdkName, - Icon: skd.Def.Icon, - Url: skd.Def.Url, - } - for _,ag := range skd.Args { - sra := SdkRegisterArgVo{ - Scope: ag.Scope, + if _,ok := aInsM[skd.Def.Id]; ok { + srv := SdkRegisterVo{ + Id: skd.Def.Id, + SdkType: skd.Def.SdkType, + SdkName: skd.Def.SdkName, + Icon: skd.Def.Icon, + Url: skd.Def.Url, + IconBlob: skd.Def.IconBlob, + Version: skd.Def.Version, } - sra.SdkArg = SdkArg{ - Alias: ag.Alias, - Name: ag.Name, - Type: ag.Type, - ArgType: ag.ArgType, - Must: ag.Must, - Unit: ag.Unit, - Range: ag.Range, - DefaultValue: ag.DefaultValue, - DefaultOperator: ag.DefaultOperator, - Sort: ag.Sort, + for _,ag := range skd.Args { + sra := SdkRegisterArgVo{ + Scope: ag.Scope, + } + sra.SdkArg = SdkArg{ + Alias: ag.Alias, + Name: ag.Name, + Type: ag.Type, + ArgType: ag.ArgType, + Must: ag.Must, + Unit: ag.Unit, + Range: ag.Range, + DefaultValue: ag.DefaultValue, + DefaultOperator: ag.DefaultOperator, + Sort: ag.Sort, + } + srv.Args = append(srv.Args, sra) } - srv.Args = append(srv.Args, sra) + paramBody := util.Struct2Map(srv) + logger.Debug("sdkApi.Register paramBody:", paramBody) + sdkApi.Register(paramBody) //灏嗙畻娉曟敞鍐屽埌鏁版嵁搴撲腑 } - paramBody := util.Struct2Map(srv) - logger.Debug("sdkApi.Register paramBody:", paramBody) - sdkApi.Register(paramBody) //灏嗙畻娉曟敞鍐屽埌鏁版嵁搴撲腑 } - zconfPath := "/opt/vasystem/bin/zconf/" - libPath := "/opt/vasystem/libs/" + zconfPath := "./zconf/" + libPath := "../libs/" if !util.DirExists(zconfPath) { os.MkdirAll(zconfPath, 0777) } @@ -167,24 +208,28 @@ os.MkdirAll(libPath, 0777) } for sdkType,_ := range soM { - //鍏堣В鍘婼dkType.tar.gz鏂囦欢 - if unTarGzE := util.UnTarGz(unPackPath+sdkType+".tar.gz", unPackPath);unTarGzE == nil { - //澶嶅埗json鍚姩鏂囦欢 - if util.Exists(unPackPath+sdkType+"/zconf/"+sdkType+".json") { - util.CopyFile(unPackPath+sdkType+"/zconf/"+sdkType+".json", zconfPath+sdkType+".json") - } - if util.DirExists(unPackPath+sdkType) { - if _,cE := util.CopyDirByCmd(unPackPath+sdkType, libPath);cE != nil { - return false, cE + if _,iOk := nInsM[sdkType];iOk { + //鍏堣В鍘婼dkType.tar.gz鏂囦欢 + if unTarGzE := util.UnTarGz(unPackPath+sdkType+".tar.gz", unPackPath);unTarGzE == nil { + //澶嶅埗json鍚姩鏂囦欢 + if util.Exists(unPackPath+sdkType+"/zconf/"+sdkType+".json") { + util.CopyFile(unPackPath+sdkType+"/zconf/"+sdkType+".json", zconfPath+sdkType+".json") } - } - if util.DirExists(unPackPath+sdkType+"/models") { - if _,cE := util.CopyDirByCmd(unPackPath+sdkType+"/models", "/opt/vasystem/bin/zconf"); cE != nil { - return false, cE + if util.DirExists(unPackPath+sdkType) { + if _,cE := util.CopyDirByCmd(unPackPath+sdkType, libPath);cE != nil { + return false, cE + } } + if util.DirExists(unPackPath+sdkType+"/models") { + if _,cE := util.CopyDirByCmd(unPackPath+sdkType+"/models", "./zconf"); cE != nil { + return false, cE + } + } + } else { + logger.Debug("unTarGzE sdkType.tar.gz err:", unTarGzE) } } else { - logger.Debug("unTarGzE sdkType.tar.gz err:", unTarGzE) + logger.Debug("upper version has been installed") } } @@ -208,6 +253,49 @@ } return true, nil +} + +func shouldVersionBeUpgrade(curVersion,dstVersion string) bool { + if curVersion == "" { + return true + } + if dstVersion == "" { + return false + } + curIVArr := strings.Split(curVersion, ".") + dstIVArr := strings.Split(dstVersion, ".") + if len(curIVArr) !=3 || len(dstIVArr) != 3{ + return false + } + cH,cEH := strconv.Atoi(curIVArr[0]) + cM,cEM := strconv.Atoi(curIVArr[1]) + cL,cEL := strconv.Atoi(curIVArr[2]) + + dH,dEH := strconv.Atoi(curIVArr[0]) + dM,dEM := strconv.Atoi(curIVArr[1]) + dL,dEL := strconv.Atoi(curIVArr[2]) + if cEH !=nil || cEM != nil || cEL != nil || dEH != nil ||dEM !=nil || dEL !=nil { + return false + } + if cH > dH { + return false + } else if cH == dH { + if cM > dM { + return false + } else if cM == dM { + if cL > dL { + return false + } else if cL == dL { + return false + } else { + return true + } + } else { + return true + } + } else { + return true + } } @@ -243,6 +331,15 @@ Enable bool `gorm:"column:enable;default:1" json:"enable"` DelFlag int `gorm:"column:del_flag;default:0" json:"del_flag"` Env string `gorm:"column:env" json:"env"` //杩愯鐜鍙婄粨鏋滆鏄庯紝json鏍煎紡锛屽寘鍚玸o_file_path,runtime,param,depends(cuda鐗堟湰锛宱pencv鐗堟湰锛宼ensorflow鐗堟湰绛�) + IconBlob string `gorm:"column:iconBlob;type:text;" json:"iconBlob"` //鍥剧墖 + Version string `gorm:"column:version;type:varchar(50);default:'1.0';" json:"version"` //鐗堟湰鍙� +} + +type SdkInsOrUpgrade struct { + Sdk + RemoteVersion string `json:"remoteVersion"` //鍟嗗煄浠撳簱鐗堟湰鍙� + Installed bool `json:"installed"` //鏄惁宸插畨瑁� + IsUpgrade bool `json:"isUpgrade"` //鏄惁闇�瑕佸崌绾� } type SdkArgEntity struct { @@ -267,23 +364,25 @@ type SdkRegisterVo struct { - Id string `json:"id"` - SdkType string `json:"sdk_type"`//浜鸿劯妫�娴嬶細FaceDetect,浜鸿劯鎻愬彇锛欶aceExtract,浜鸿劯姣斿锛欶aceCompare,琛屼负锛歒olo - SdkName string `json:"sdk_name"` //绠楁硶鍚嶇О - Args []SdkRegisterArgVo `json:"args"` //绠楁硶鍙傛暟 - Icon string `json:"icon"` //绠楁硶鍥炬爣 - Url string `json:"url"` //绠楁硶涓嬭浇鍦板潃 + Id string `json:"id"` + SdkType string `json:"sdk_type"`//浜鸿劯妫�娴嬶細FaceDetect,浜鸿劯鎻愬彇锛欶aceExtract,浜鸿劯姣斿锛欶aceCompare,琛屼负锛歒olo + SdkName string `json:"sdk_name"` //绠楁硶鍚嶇О + Args []SdkRegisterArgVo `json:"args"` //绠楁硶鍙傛暟 + Icon string `json:"icon"` //绠楁硶鍥炬爣 + Url string `json:"url"` //绠楁硶涓嬭浇鍦板潃 + IconBlob string `json:"iconBlob"` //鍥剧墖 + Version string `json:"version"` //鐗堟湰鍙� } type SdkRegisterArgVo struct { - Scope string `json:"scope"` + Scope string `json:"scope"` SdkArg - Dics []SdkArgDic `json:"dics"` //濡傛灉姝ょ畻娉曞弬鏁版槸琚�夐」锛岄渶瑕佸皢鍙傛暟鏋氫妇鍊煎啓鍏ュ埌瀛楀吀琛ㄤ腑 + Dics []SdkArgDic `json:"dics"` //濡傛灉姝ょ畻娉曞弬鏁版槸琚�夐」锛岄渶瑕佸皢鍙傛暟鏋氫妇鍊煎啓鍏ュ埌瀛楀吀琛ㄤ腑 } type SdkArgDic struct { - Value string `json:"value"` - Name string `json:"name"` - Sort int `json:"sort"` + Value string `json:"value"` + Name string `json:"name"` + Sort int `json:"sort"` } \ No newline at end of file -- Gitblit v1.8.0