Browse Source

Merge pull request #48 from IceWhaleTech/dev

feat: Multiple updates
link 3 năm trước cách đây
mục cha
commit
e7ebdc040f

+ 1 - 1
UI

@@ -1 +1 @@
-Subproject commit f7c46d7379ab31bc70a35900ef6a50f7f3c2ef4f
+Subproject commit a982eb4bddafe0beaa629fcfef9581b8ef1eddf3

+ 2 - 2
go.mod

@@ -48,8 +48,8 @@ require (
 	github.com/smartystreets/goconvey v1.6.4 // indirect
 	github.com/swaggo/gin-swagger v1.3.0
 	github.com/swaggo/swag v1.7.3
-	github.com/tidwall/gjson v1.8.0
-	github.com/tidwall/pretty v1.2.0 // indirect
+	github.com/tidwall/gjson v1.10.2
+	github.com/tidwall/sjson v1.2.3
 	github.com/tklauser/go-sysconf v0.3.6 // indirect
 	github.com/ugorji/go v1.2.6 // indirect
 	go.opencensus.io v0.23.0 // indirect

+ 6 - 5
go.sum

@@ -741,13 +741,14 @@ github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG
 github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
-github.com/tidwall/gjson v1.8.0 h1:Qt+orfosKn0rbNTZqHYDqBrmm3UDA4KRkv70fDzG+PQ=
-github.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
-github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
-github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
-github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo=
+github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
 github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
 github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/sjson v1.2.3 h1:5+deguEhHSEjmuICXZ21uSSsXotWMA0orU783+Z7Cp8=
+github.com/tidwall/sjson v1.2.3/go.mod h1:5WdjKx3AQMvCJ4RG6/2UYT7dLrGvJUV1x4jdTAyGvZs=
 github.com/tklauser/go-sysconf v0.3.4/go.mod h1:Cl2c8ZRWfHD5IrfHo9VN+FX9kCFjIOyVklgXycLB6ek=
 github.com/tklauser/go-sysconf v0.3.6 h1:oc1sJWvKkmvIxhDHeKWvZS4f6AW+YcoguSfRF2/Hmo4=
 github.com/tklauser/go-sysconf v0.3.6/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI=

+ 3 - 2
middleware/gin.go

@@ -2,8 +2,9 @@ package middleware
 
 import (
 	"fmt"
-	"github.com/gin-gonic/gin"
 	"net/http"
+
+	"github.com/gin-gonic/gin"
 )
 
 func Cors() gin.HandlerFunc {
@@ -17,7 +18,7 @@ func Cors() gin.HandlerFunc {
 		//服务器支持的所有跨域请求的方法
 		c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE")
 		//允许跨域设置可以返回其他子段,可以自定义字段
-		c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session")
+		c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,Language")
 		// 允许浏览器(客户端)可以解析的头部 (重要)
 		c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
 		//设置缓存时间

+ 1 - 0
model/disk.go

@@ -24,6 +24,7 @@ type LSBLKModel struct {
 	Tran        string       `json:"tran"`
 	MinIO       uint64       `json:"min-io"`
 	UsedPercent float64      `json:"used_percent"`
+	Serial      string       `json:"serial"`
 	Children    []LSBLKModel `json:"children"`
 	//详情特有
 	StartSector uint64 `json:"start_sector,omitempty"`

+ 8 - 0
model/docker.go

@@ -0,0 +1,8 @@
+package model
+
+type DockerStatsModel struct {
+	Icon  string      `json:"icon"`
+	Title string      `json:"title"`
+	Data  interface{} `json:"data"`
+	Pre   interface{} `json:"pre"`
+}

+ 4 - 0
model/sys_common.go

@@ -68,3 +68,7 @@ type SystemConfig struct {
 	SyncPort   string `json:"sync_port"`
 	SyncKey    string `json:"sync_key"`
 }
+
+type CasaOSGlobalVariables struct {
+	AddApp bool
+}

+ 2 - 0
pkg/config/init.go

@@ -33,6 +33,8 @@ var ServerInfo = &model.ServerModel{}
 
 var SystemConfigInfo = &model.SystemConfig{}
 
+var CasaOSGlobalVariables = &model.CasaOSGlobalVariables{}
+
 var Cfg *ini.File
 
 //初始化设置,获取系统的部分信息。

+ 2 - 0
pkg/utils/oasis_err/e.go

@@ -16,6 +16,7 @@ const (
 	DIR_ALREADY_EXISTS  = 20001
 	FILE_ALREADY_EXISTS = 20002
 	FILE_OR_DIR_EXISTS  = 20003
+	PORT_IS_OCCUPIED    = 20004
 
 	//zerotier
 	GET_TOKEN_ERROR = 30001
@@ -49,6 +50,7 @@ var MsgFlags = map[int]string{
 	DIR_ALREADY_EXISTS:  "Directory already exists",
 	FILE_ALREADY_EXISTS: "File already exists",
 	FILE_OR_DIR_EXISTS:  "File or directory already exists",
+	PORT_IS_OCCUPIED:    "Port is occupied",
 
 	//zerotier
 	GET_TOKEN_ERROR: "Get token error,Please log in to zerotier's official website to confirm whether the account is available",

+ 9 - 0
route/init.go

@@ -10,6 +10,7 @@ import (
 	"github.com/IceWhaleTech/CasaOS/model/system_app"
 	"github.com/IceWhaleTech/CasaOS/pkg/config"
 	"github.com/IceWhaleTech/CasaOS/pkg/docker"
+	"github.com/IceWhaleTech/CasaOS/pkg/utils/command"
 	"github.com/IceWhaleTech/CasaOS/pkg/utils/env_helper"
 	"github.com/IceWhaleTech/CasaOS/pkg/utils/file"
 	"github.com/IceWhaleTech/CasaOS/pkg/utils/port"
@@ -20,6 +21,7 @@ import (
 
 func InitFunction() {
 	go checkSystemApp()
+	Update2_3()
 }
 
 var syncIsExistence = false
@@ -190,3 +192,10 @@ func checkSystemApp() {
 		installSyncthing("44")
 	}
 }
+func CheckSerialDiskMount() {
+	// 检查挂载点重新挂载
+	// 检查新硬盘是否有多个分区,如有多个分区需提示
+}
+func Update2_3() {
+	command.OnlyExec("source " + config.AppInfo.ProjectPath + "/shell/assist.sh")
+}

+ 12 - 1
route/route.go

@@ -141,6 +141,8 @@ func InitRouter() *gin.Engine {
 		{
 			//获取我的已安装的列表
 			v1AppGroup.GET("/mylist", v1.MyAppList)
+			//
+			v1AppGroup.GET("/usage", v1.AppUsageList)
 			//app详情
 			v1AppGroup.GET("/appinfo/:id", v1.AppInfo)
 			//获取未安装的列表
@@ -192,6 +194,9 @@ func InitRouter() *gin.Engine {
 			v1SysGroup.POST("/config", v1.PostSetSystemConfig)
 			v1SysGroup.GET("/widget/config", v1.GetWidgetConfig)
 			v1SysGroup.POST("/widget/config", v1.PostSetWidgetConfig)
+			v1SysGroup.GET("/port", v1.GetCasaOSPort)
+			v1SysGroup.PUT("/port", v1.PutCasaOSPort)
+			v1SysGroup.POST("/kill", v1.PostKillCasaOS)
 		}
 		v1FileGroup := v1Group.Group("/file")
 		v1FileGroup.Use()
@@ -221,7 +226,7 @@ func InitRouter() *gin.Engine {
 			v1DiskGroup.POST("/format", v1.FormatDisk)
 
 			//添加分区
-			v1DiskGroup.POST("/addpart", v1.AddPartition)
+			v1DiskGroup.POST("/part", v1.AddPartition)
 
 			//获取可以格式化的内容
 			v1DiskGroup.GET("/type", v1.FormatDiskType)
@@ -229,6 +234,12 @@ func InitRouter() *gin.Engine {
 			//删除分区
 			v1DiskGroup.DELETE("/delpart", v1.RemovePartition)
 
+			//mount SATA disk
+			v1DiskGroup.POST("/mount", v1.PostMountDisk)
+
+			//umount SATA disk
+			v1DiskGroup.POST("/umount", v1.DeleteUmountDisk)
+
 		}
 		v1ShareGroup := v1Group.Group("/share")
 		v1ShareGroup.Use()

+ 23 - 0
route/v1/app.go

@@ -103,6 +103,18 @@ func MyAppList(c *gin.Context) {
 	c.JSON(http.StatusOK, &model.Result{Success: oasis_err2.SUCCESS, Message: oasis_err2.GetMsg(oasis_err2.SUCCESS), Data: list})
 }
 
+// @Summary my app hardware usage list
+// @Produce  application/json
+// @Accept application/json
+// @Tags app
+// @Security ApiKeyAuth
+// @Success 200 {string} string "ok"
+// @Router /app/usage [get]
+func AppUsageList(c *gin.Context) {
+	list := service.MyService.App().GetHardwareUsage()
+	c.JSON(http.StatusOK, &model.Result{Success: oasis_err2.SUCCESS, Message: oasis_err2.GetMsg(oasis_err2.SUCCESS), Data: list})
+}
+
 // @Summary 应用详情
 // @Produce  application/json
 // @Accept application/json
@@ -212,3 +224,14 @@ func ShareAppFile(c *gin.Context) {
 	content := service.MyService.OAPI().ShareAppFile(str)
 	c.JSON(http.StatusOK, json.RawMessage(content))
 }
+
+// @Summary Resource Usage
+// @Produce  application/json
+// @Accept application/json
+// @Tags app
+// @Security ApiKeyAuth
+// @Success 200 {string} string "ok"
+// @Router /app/share [post]
+func AppListResourceUsage() {
+
+}

+ 32 - 20
route/v1/disk.go

@@ -1,13 +1,13 @@
 package v1
 
 import (
+	"net/http"
+
 	"github.com/IceWhaleTech/CasaOS/model"
 	"github.com/IceWhaleTech/CasaOS/pkg/utils/oasis_err"
 	"github.com/IceWhaleTech/CasaOS/service"
 	"github.com/gin-gonic/gin"
 	"github.com/shirou/gopsutil/v3/disk"
-	"net/http"
-	"strconv"
 )
 
 // @Summary 获取磁盘列表
@@ -59,7 +59,7 @@ func GetPlugInDisk(c *gin.Context) {
 	c.JSON(http.StatusOK, model.Result{Success: oasis_err.SUCCESS, Message: oasis_err.GetMsg(oasis_err.SUCCESS), Data: lst})
 }
 
-// @Summary 获取磁盘列表
+// @Summary get disk list
 // @Produce  application/json
 // @Accept application/json
 // @Tags disk
@@ -76,12 +76,12 @@ func GetPlugInDisks(c *gin.Context) {
 	c.JSON(http.StatusOK, model.Result{Success: oasis_err.SUCCESS, Message: oasis_err.GetMsg(oasis_err.SUCCESS), Data: result})
 }
 
-// @Summary 磁盘详情
+// @Summary disk detail
 // @Produce  application/json
 // @Accept application/json
 // @Tags disk
 // @Security ApiKeyAuth
-// @Param  path query string true "要获取的磁盘详情 例如/dev/sda"
+// @Param  path query string true "for example /dev/sda"
 // @Success 200 {string} string "ok"
 // @Router /disk/info [get]
 func GetDiskInfo(c *gin.Context) {
@@ -93,7 +93,7 @@ func GetDiskInfo(c *gin.Context) {
 	c.JSON(http.StatusOK, model.Result{Success: oasis_err.SUCCESS, Message: oasis_err.GetMsg(oasis_err.SUCCESS), Data: m})
 }
 
-// @Summary 磁盘详情
+// @Summary format disk
 // @Produce  application/json
 // @Accept multipart/form-data
 // @Tags disk
@@ -109,15 +109,9 @@ func FormatDisk(c *gin.Context) {
 	if len(path) == 0 || len(t) == 0 {
 		c.JSON(http.StatusOK, model.Result{Success: oasis_err.INVALID_PARAMS, Message: oasis_err.GetMsg(oasis_err.INVALID_PARAMS)})
 	}
-
-	//删除挂载点
-	service.MyService.Disk().UmountPointAndRemoveDir(path)
-
 	//格式化磁盘
 	service.MyService.Disk().FormatDisk(path, t)
 
-	//重新挂载
-
 	c.JSON(http.StatusOK, model.Result{Success: oasis_err.SUCCESS, Message: oasis_err.GetMsg(oasis_err.SUCCESS)})
 }
 
@@ -154,25 +148,43 @@ func RemovePartition(c *gin.Context) {
 	c.JSON(http.StatusOK, model.Result{Success: oasis_err.SUCCESS, Message: oasis_err.GetMsg(oasis_err.SUCCESS)})
 }
 
-// @Summary 添加分区
+// @Summary serial number
 // @Produce  application/json
 // @Accept multipart/form-data
 // @Tags disk
 // @Security ApiKeyAuth
 // @Param  path formData string true "磁盘路径 例如/dev/sda"
-// @Param  size formData string true "需要分区容量大小(MB)"
-// @Param  num formData string true "磁盘符号"
+// @Param  serial formData string true "serial"
 // @Success 200 {string} string "ok"
 // @Router /disk/addpart [post]
 func AddPartition(c *gin.Context) {
 	path := c.PostForm("path")
-	size, _ := strconv.Atoi(c.DefaultPostForm("size", "0"))
-	num := c.DefaultPostForm("num", "9")
-	if len(path) == 0 {
+	serial := c.PostForm("serial")
+	if len(path) == 0 || len(serial) == 0 {
 		c.JSON(http.StatusOK, model.Result{Success: oasis_err.INVALID_PARAMS, Message: oasis_err.GetMsg(oasis_err.INVALID_PARAMS)})
+		return
 	}
+	service.MyService.Disk().AddPartition(path)
+	c.JSON(http.StatusOK, model.Result{Success: oasis_err.SUCCESS, Message: oasis_err.GetMsg(oasis_err.SUCCESS)})
+}
+
+func PostMountDisk(c *gin.Context) {
+	// for example: path=/dev/sda1
+	path := c.PostForm("path")
+	//执行挂载目录
+	service.MyService.Disk().MountDisk(path, "volume")
+	//添加到数据库
+
+	c.JSON(http.StatusOK, model.Result{Success: oasis_err.SUCCESS, Message: oasis_err.GetMsg(oasis_err.SUCCESS)})
+}
+
+func DeleteUmountDisk(c *gin.Context) {
+
+	// for example: path=/dev/sda1
+	path := c.PostForm("path")
+	service.MyService.Disk().UmountPointAndRemoveDir(path)
+
+	//删除数据库记录
 
-	//size*1024*1024/512
-	service.MyService.Disk().AddPartition(path, num, uint64(size*1024*2))
 	c.JSON(http.StatusOK, model.Result{Success: oasis_err.SUCCESS, Message: oasis_err.GetMsg(oasis_err.SUCCESS)})
 }

+ 10 - 12
route/v1/docker.go

@@ -2,7 +2,6 @@ package v1
 
 import (
 	"bytes"
-	"encoding/json"
 	json2 "encoding/json"
 	"net/http"
 	"reflect"
@@ -11,6 +10,7 @@ import (
 	"time"
 
 	"github.com/IceWhaleTech/CasaOS/model"
+	"github.com/IceWhaleTech/CasaOS/pkg/config"
 	"github.com/IceWhaleTech/CasaOS/pkg/docker"
 	upnp2 "github.com/IceWhaleTech/CasaOS/pkg/upnp"
 	"github.com/IceWhaleTech/CasaOS/pkg/utils/file"
@@ -421,9 +421,12 @@ func InstallApp(c *gin.Context) {
 		rely := model.MapStrings{}
 
 		copier.Copy(&rely, &relyMap)
-		for i := 0; i < len(m.Volumes); i++ {
-			m.Volumes[i].Path = docker.GetDir(id, m.Volumes[i].ContainerPath)
+		if m.Origin != "custom" {
+			for i := 0; i < len(m.Volumes); i++ {
+				m.Volumes[i].Path = docker.GetDir(id, m.Volumes[i].ContainerPath)
+			}
 		}
+
 		portsStr, _ := json2.Marshal(m.Ports)
 		envsStr, _ := json2.Marshal(m.Envs)
 		volumesStr, _ := json2.Marshal(m.Volumes)
@@ -463,6 +466,7 @@ func InstallApp(c *gin.Context) {
 		//	m.PortMap = m.Port
 		//}
 		service.MyService.App().SaveContainer(md)
+		config.CasaOSGlobalVariables.AddApp = true
 
 	}()
 
@@ -677,7 +681,7 @@ func UnInstallApp(c *gin.Context) {
 	}
 
 	//step:删除容器
-	err = service.MyService.Docker().DockerContainerRemove(appId)
+	err = service.MyService.Docker().DockerContainerRemove(appId, false)
 	if err != nil {
 		c.JSON(http.StatusOK, model.Result{Success: oasis_err2.UNINSTALL_APP_ERROR, Message: oasis_err2.GetMsg(oasis_err2.UNINSTALL_APP_ERROR), Data: err.Error()})
 		return
@@ -908,16 +912,10 @@ func UpdateSetting(c *gin.Context) {
 	//如果容器端口均未修改,这不进行处理
 	portsStr, _ := json2.Marshal(m.Ports)
 
-	list := []model.PathMap{}
-	json.Unmarshal([]byte(appInfo.Volumes), &list)
-	for i := 0; i < len(list); i++ {
-		list[i].Path = docker.GetDir(id, list[i].ContainerPath)
-	}
 	envsStr, _ := json2.Marshal(m.Envs)
 	volumesStr, _ := json2.Marshal(m.Volumes)
 	devicesStr, _ := json2.Marshal(m.Devices)
-	listStr, _ := json2.Marshal(list)
-	if !reflect.DeepEqual(string(portsStr), appInfo.Ports) || !reflect.DeepEqual(string(envsStr), appInfo.Envs) || !reflect.DeepEqual(volumesStr, listStr) || m.PortMap != appInfo.PortMap || m.NetworkModel != appInfo.NetModel {
+	if !reflect.DeepEqual(string(portsStr), appInfo.Ports) || !reflect.DeepEqual(string(envsStr), appInfo.Envs) || !reflect.DeepEqual(string(volumesStr), appInfo.Volumes) || m.PortMap != appInfo.PortMap || m.NetworkModel != appInfo.NetModel {
 
 		var newUUid = uuid.NewV4().String()
 		var err error
@@ -934,7 +932,7 @@ func UpdateSetting(c *gin.Context) {
 			return
 		}
 
-		err = service.MyService.Docker().DockerContainerRemove(id)
+		err = service.MyService.Docker().DockerContainerRemove(id, true)
 		if err != nil {
 			c.JSON(http.StatusOK, model.Result{Success: oasis_err2.ERROR, Message: oasis_err2.GetMsg(oasis_err2.ERROR)})
 			return

+ 66 - 1
route/v1/system.go

@@ -4,12 +4,14 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"os"
 	"strconv"
 	"time"
 
 	"github.com/IceWhaleTech/CasaOS/model"
 	"github.com/IceWhaleTech/CasaOS/pkg/config"
 	"github.com/IceWhaleTech/CasaOS/pkg/utils/oasis_err"
+	port2 "github.com/IceWhaleTech/CasaOS/pkg/utils/port"
 	"github.com/IceWhaleTech/CasaOS/pkg/utils/version"
 	"github.com/IceWhaleTech/CasaOS/service"
 	model2 "github.com/IceWhaleTech/CasaOS/service/model"
@@ -80,7 +82,7 @@ func GetCasaOSErrorLogs(c *gin.Context) {
 // @Produce  application/json
 // @Accept multipart/form-data
 // @Tags sys
-// @Param file formData file true "用户头像"
+// @Param config formData string true "config json string"
 // @Security ApiKeyAuth
 // @Success 200 {string} string "ok"
 // @Router /sys/changhead [post]
@@ -135,6 +137,58 @@ func PostSetWidgetConfig(c *gin.Context) {
 		})
 }
 
+// @Summary get casaos server port
+// @Produce  application/json
+// @Accept application/json
+// @Tags sys
+// @Security ApiKeyAuth
+// @Success 200 {string} string "ok"
+// @Router /sys/port [get]
+func GetCasaOSPort(c *gin.Context) {
+	c.JSON(http.StatusOK,
+		model.Result{
+			Success: oasis_err.SUCCESS,
+			Message: oasis_err.GetMsg(oasis_err.SUCCESS),
+			Data:    config.ServerInfo.HttpPort,
+		})
+}
+
+// @Summary edit casaos server port
+// @Produce  application/json
+// @Accept application/json
+// @Tags sys
+// @Security ApiKeyAuth
+// @Param port formData string true "port"
+// @Success 200 {string} string "ok"
+// @Router /sys/port [put]
+func PutCasaOSPort(c *gin.Context) {
+	port, err := strconv.Atoi(c.PostForm("port"))
+	if err != nil {
+		c.JSON(http.StatusOK,
+			model.Result{
+				Success: oasis_err.ERROR,
+				Message: err.Error(),
+			})
+		return
+	}
+
+	isAvailable := port2.IsPortAvailable(port, "tcp")
+	if !isAvailable {
+		c.JSON(http.StatusOK,
+			model.Result{
+				Success: oasis_err.PORT_IS_OCCUPIED,
+				Message: oasis_err.GetMsg(oasis_err.PORT_IS_OCCUPIED),
+			})
+		return
+	}
+	service.MyService.System().UpSystemPort(strconv.Itoa(port))
+	c.JSON(http.StatusOK,
+		model.Result{
+			Success: oasis_err.SUCCESS,
+			Message: oasis_err.GetMsg(oasis_err.SUCCESS),
+		})
+}
+
 // @Summary 检查是否进入引导状态
 // @Produce  application/json
 // @Accept application/json
@@ -156,3 +210,14 @@ func GetGuideCheck(c *gin.Context) {
 			Data:    data,
 		})
 }
+
+// @Summary active killing casaos
+// @Produce  application/json
+// @Accept application/json
+// @Tags sys
+// @Security ApiKeyAuth
+// @Success 200 {string} string "ok"
+// @Router /sys/kill [post]
+func PostKillCasaOS(c *gin.Context) {
+	os.Exit(0)
+}

+ 113 - 2
service/app.go

@@ -2,9 +2,15 @@ package service
 
 import (
 	"context"
+	"encoding/json"
+	"io"
+	"io/ioutil"
+	"runtime"
 	"strings"
+	"sync"
 	"time"
 
+	"github.com/IceWhaleTech/CasaOS/model"
 	"github.com/IceWhaleTech/CasaOS/pkg/config"
 	"github.com/IceWhaleTech/CasaOS/pkg/utils/command"
 	loger2 "github.com/IceWhaleTech/CasaOS/pkg/utils/loger"
@@ -27,6 +33,9 @@ type AppService interface {
 	GetSimpleContainerInfo(name string) (types.Container, error)
 	DelAppConfigDir(path string)
 	GetSystemAppList() *[]model2.MyAppList
+	GetHardwareUsageSteam()
+	GetHardwareUsage() []model.DockerStatsModel
+	GetAppStats(id string) string
 }
 
 type appStruct struct {
@@ -48,7 +57,6 @@ func (a *appStruct) GetMyList(index, size int, position bool) *[]model2.MyAppLis
 	if err != nil {
 		a.log.Error("获取docker容器失败", "app.getmylist", "line:42", err)
 	}
-
 	//获取本地数据库应用
 
 	var lm []model2.AppListDBModel
@@ -154,7 +162,6 @@ func (a *appStruct) GetSystemAppList() *[]model2.MyAppList {
 				//Rely:     m.Rely,
 			})
 		}
-
 	}
 
 	return &list
@@ -233,6 +240,110 @@ func (a *appStruct) RemoveContainerById(id string) {
 	a.db.Table(model2.CONTAINERTABLENAME).Where("custom_id = ?", id).Delete(&model2.AppListDBModel{})
 }
 
+var dataStr map[string]model.DockerStatsModel
+
+var isFinish bool = false
+
+func (a *appStruct) GetAppStats(id string) string {
+	cli, err := client2.NewClientWithOpts(client2.FromEnv)
+	if err != nil {
+		return ""
+	}
+	defer cli.Close()
+	con, err := cli.ContainerStats(context.Background(), id, false)
+	if err != nil {
+		return err.Error()
+	}
+	defer con.Body.Close()
+	c, _ := ioutil.ReadAll(con.Body)
+	return string(c)
+}
+
+func (a *appStruct) GetHardwareUsage() []model.DockerStatsModel {
+
+	steam := true
+	for !isFinish {
+		if steam {
+			steam = false
+			go func() {
+				a.GetHardwareUsageSteam()
+			}()
+		}
+		// 切一下,再次分配任务
+		runtime.Gosched()
+	}
+	list := []model.DockerStatsModel{}
+	for _, v := range dataStr {
+		list = append(list, v)
+	}
+
+	return list
+
+}
+
+func (a *appStruct) GetHardwareUsageSteam() {
+	var lock = &sync.Mutex{}
+	if len(dataStr) == 0 {
+		lock.Lock()
+		dataStr = make(map[string]model.DockerStatsModel)
+		lock.Unlock()
+	}
+
+	cli, err := client2.NewClientWithOpts(client2.FromEnv)
+	if err != nil {
+		return
+	}
+	defer cli.Close()
+
+	ctx := context.Background()
+	ctx, cancel := context.WithCancel(ctx)
+
+	var lm []model2.AppListDBModel
+	a.db.Table(model2.CONTAINERTABLENAME).Select("label,icon,container_id").Where("origin != ?", "system").Find(&lm)
+	var list []types.ContainerStats
+	for i := 0; i < 100; i++ {
+		if config.CasaOSGlobalVariables.AddApp {
+			a.db.Table(model2.CONTAINERTABLENAME).Select("label,icon,container_id").Where("origin != ?", "system").Find(&lm)
+		}
+		var wg sync.WaitGroup
+		for _, v := range lm {
+			wg.Add(1)
+			go func(v model2.AppListDBModel, lock *sync.Mutex) {
+				defer wg.Done()
+				stats, err := cli.ContainerStats(ctx, v.ContainerId, true)
+				if err != nil {
+					return
+				}
+				decode := json.NewDecoder(stats.Body)
+				var data interface{}
+				if err := decode.Decode(&data); err == io.EOF {
+					return
+				}
+				lock.Lock()
+				dockerStats := model.DockerStatsModel{}
+				dockerStats.Pre = dataStr[v.ContainerId].Data
+
+				dockerStats.Data = data
+				dockerStats.Icon = v.Icon
+				dockerStats.Title = v.Label
+				dataStr[v.ContainerId] = dockerStats
+				lock.Unlock()
+			}(v, lock)
+		}
+		wg.Wait()
+		isFinish = true
+		if i == 99 {
+			for _, v := range list {
+				v.Body.Close()
+			}
+
+		}
+		time.Sleep(time.Second * 2)
+	}
+	isFinish = false
+	cancel()
+}
+
 // init install
 func Init() {
 

+ 31 - 33
service/disk.go

@@ -10,8 +10,10 @@ import (
 	"github.com/IceWhaleTech/CasaOS/pkg/config"
 	command2 "github.com/IceWhaleTech/CasaOS/pkg/utils/command"
 	loger2 "github.com/IceWhaleTech/CasaOS/pkg/utils/loger"
+	model2 "github.com/IceWhaleTech/CasaOS/service/model"
 	"github.com/shirou/gopsutil/v3/disk"
 	"github.com/tidwall/gjson"
+	"gorm.io/gorm"
 )
 
 type DiskService interface {
@@ -21,11 +23,14 @@ type DiskService interface {
 	UmountPointAndRemoveDir(path string) string
 	GetDiskInfo(path string) model.LSBLKModel
 	DelPartition(path, num string) string
-	AddPartition(path, num string, size uint64) string
+	AddPartition(path string) string
 	GetDiskInfoByPath(path string) *disk.UsageStat
+	MountDisk(path, volume string)
+	SerialAll(mountPoint string) *[]model2.SerialDisk
 }
 type diskService struct {
 	log loger2.OLog
+	db  *gorm.DB
 }
 
 //通过脚本获取外挂磁盘
@@ -55,28 +60,17 @@ func (d *diskService) DelPartition(path, num string) string {
 	return ""
 }
 
-//添加分区
-func (d *diskService) AddPartition(path, num string, size uint64) string {
-
-	var maxSector uint64 = 0
-
-	chiList := command2.ExecResultStrArray("source " + config.AppInfo.ProjectPath + "/shell/helper.sh ;GetPartitionSectors " + path)
-	if len(chiList) == 0 {
-		d.log.Error("chiList length error")
-	}
-	for i := 0; i < len(chiList); i++ {
-		tempArr := strings.Split(chiList[i], ",")
-		tempSector, _ := strconv.ParseUint(tempArr[2], 10, 64)
-		if tempSector > maxSector {
-			maxSector = tempSector
-		}
-	}
-
-	r := command2.ExecResultStrArray("source ./shell/helper.sh ;AddPartition " + path + " " + num + " " + strconv.FormatUint(maxSector+1, 10) + " " + strconv.FormatUint(size+maxSector+1, 10))
+//part
+func (d *diskService) AddPartition(path string) string {
+	r := command2.ExecResultStrArray("source " + config.AppInfo.ProjectPath + "/shell/helper.sh ;AddPartition " + path)
 	fmt.Println(r)
 	return ""
 }
 
+func (d *diskService) AddAllPartition(path string) {
+
+}
+
 //获取硬盘详情
 func (d *diskService) GetDiskInfoByPath(path string) *disk.UsageStat {
 	diskInfo, err := disk.Usage(path + "1")
@@ -90,7 +84,7 @@ func (d *diskService) GetDiskInfoByPath(path string) *disk.UsageStat {
 	return diskInfo
 }
 
-//获取磁盘信息
+//get disk details
 func (d *diskService) LSBLK() []model.LSBLKModel {
 	str := command2.ExecLSBLK()
 	if str == nil {
@@ -111,7 +105,7 @@ func (d *diskService) LSBLK() []model.LSBLKModel {
 
 	var health = true
 	for _, i := range m {
-		if i.Children != nil {
+		if i.Type != "loop" && !i.RO {
 			fsused = 0
 			for _, child := range i.Children {
 				if child.RM {
@@ -134,7 +128,7 @@ func (d *diskService) LSBLK() []model.LSBLKModel {
 			i.Children = c
 			if fsused > 0 {
 				i.UsedPercent, err = strconv.ParseFloat(fmt.Sprintf("%.4f", float64(fsused)/float64(i.Size)), 64)
-				fmt.Println(err)
+				d.log.Fatal("diskservice_lsblk_fsused", err)
 			}
 			n = append(n, i)
 			health = true
@@ -197,17 +191,21 @@ func (d *diskService) GetDiskInfo(path string) model.LSBLKModel {
 	return m
 }
 
-//func GetDiskInfo(path string) *disk.UsageStat {
-//	diskInfo, _ := disk.Usage(path)
-//	diskInfo.UsedPercent, _ = strconv.ParseFloat(fmt.Sprintf("%.1f", diskInfo.UsedPercent), 64)
-//	diskInfo.InodesUsedPercent, _ = strconv.ParseFloat(fmt.Sprintf("%.1f", diskInfo.InodesUsedPercent), 64)
-//	return diskInfo
-//}
+func (d *diskService) MountDisk(path, volume string) {
+	r := command2.ExecResultStr("source " + config.AppInfo.ProjectPath + "/shell/helper.sh ;do_mount " + path + " " + volume)
+	fmt.Print(r)
+}
+
+func (d *diskService) SaveMountPoint(m model2.SerialDisk) {
+	d.db.Save(&m)
+}
 
-//func (d *diskService) GetPlugInDisk() []string {
-//	return disk.Partitions(false)
-//}
+func (d *diskService) SerialAll(mountPoint string) *[]model2.SerialDisk {
+	var m []model2.SerialDisk
+	d.db.Find(&m)
+	return &m
+}
 
-func NewDiskService(log loger2.OLog) DiskService {
-	return &diskService{log: log}
+func NewDiskService(log loger2.OLog, db *gorm.DB) DiskService {
+	return &diskService{log: log, db: db}
 }

+ 8 - 6
service/docker.go

@@ -53,7 +53,7 @@ type DockerService interface {
 	DockerListByImage(image, version string) (*types.Container, error)
 	DockerContainerInfo(name string) (*types.ContainerJSON, error)
 	DockerImageRemove(name string) error
-	DockerContainerRemove(name string) error
+	DockerContainerRemove(name string, update bool) error
 	DockerContainerStop(id string) error
 	DockerContainerUpdateName(name, id string) (err error)
 	DockerContainerUpdate(m model.CustomizationPostData, id string) (err error)
@@ -352,7 +352,7 @@ func (ds *dockerService) DockerPullImage(imageName string, m model2.AppNotify) e
 //param udp 容器其他udp端口
 func (ds *dockerService) DockerContainerCreate(imageName string, containerDbId string, m model.CustomizationPostData, net string) (containerId string, err error) {
 	if len(net) == 0 {
-		net = "oasis"
+		net = "bridge"
 	}
 
 	cli, err := client2.NewClientWithOpts(client2.FromEnv)
@@ -515,7 +515,7 @@ func (ds *dockerService) DockerContainerCreate(imageName string, containerDbId s
 }
 
 //删除容器
-func (ds *dockerService) DockerContainerRemove(name string) error {
+func (ds *dockerService) DockerContainerRemove(name string, update bool) error {
 	cli, err := client2.NewClientWithOpts(client2.FromEnv)
 	if err != nil {
 		return err
@@ -524,9 +524,11 @@ func (ds *dockerService) DockerContainerRemove(name string) error {
 	err = cli.ContainerRemove(context.Background(), name, types.ContainerRemoveOptions{})
 
 	//路径处理
-	path := docker.GetDir(name, "/config")
-	if !file.CheckNotExist(path) {
-		file.RMDir(path)
+	if !update {
+		path := docker.GetDir(name, "/config")
+		if !file.CheckNotExist(path) {
+			file.RMDir(path)
+		}
 	}
 
 	if err != nil {

+ 4 - 8
service/model/o_container.go

@@ -26,14 +26,10 @@ type AppListDBModel struct {
 	PortMap    string `json:"port_map"`
 	Label      string `json:"label"`
 	EnableUPNP bool   `json:"enable_upnp"`
-	//Envs       model.EnvArrey  `json:"envs" bson:"envs"`
-	//Ports      model.PortArrey `json:"ports" bson:"ports"`
-	//Volumes    model.PathArrey `json:"volumes" bson:"volumes"`
-	//Devices    model.PathArrey `json:"devices" bson:"devices"`
-	Envs    string `json:"envs"`
-	Ports   string `json:"ports"`
-	Volumes string `json:"volumes"`
-	Devices string `json:"devices"`
+	Envs       string `json:"envs"`
+	Ports      string `json:"ports"`
+	Volumes    string `json:"volumes"`
+	Devices    string `json:"devices"`
 	//Envs      []model.Env      `json:"envs"`
 	//Ports     []model.PortMap  `gorm:"type:json" json:"ports"`
 	//Volumes   []model.PathMap  `gorm:"type:json" json:"volumes"`

+ 14 - 0
service/model/o_disk.go

@@ -0,0 +1,14 @@
+package model
+
+//SerialAdvanced Technology Attachment (STAT)
+type SerialDisk struct {
+	Id         uint   `gorm:"column:id;primary_key" json:"id"`
+	DiskId     string `json:"disk_id"`
+	Path       string `json:"path"`
+	State      int    `json:"state"`
+	MountPoint string `json:"mount_point"`
+}
+
+func (p *SerialDisk) TableName() string {
+	return "o_disk"
+}

+ 1 - 1
service/service.go

@@ -40,7 +40,7 @@ func NewService(db *gorm.DB, log loger2.OLog) Repository {
 		zerotier:       NewZeroTierService(),
 		zima:           NewZiMaService(),
 		oapi:           NewOasisService(),
-		disk:           NewDiskService(log),
+		disk:           NewDiskService(log, db),
 		notify:         NewNotifyService(db),
 		shareDirectory: NewShareDirService(db, log),
 		task:           NewTaskService(db, log),

+ 12 - 0
service/system.go

@@ -14,6 +14,8 @@ type SystemService interface {
 	UpdateSystemVersion(version string)
 	GetSystemConfigDebug() []string
 	GetCasaOSLogs(lineNumber int) string
+	UpdateAssist()
+	UpSystemPort(port string)
 }
 type systemService struct {
 	log loger.OLog
@@ -25,6 +27,9 @@ func (s *systemService) UpdateSystemVersion(version string) {
 	s.log.Error(command2.ExecResultStrArray("source " + config.AppInfo.ProjectPath + "/shell/tools.sh ;update " + version))
 	//s.log.Error(command2.ExecResultStr(config.AppInfo.ProjectPath + "/shell/tool.sh -r " + version))
 }
+func (s *systemService) UpdateAssist() {
+	s.log.Error(command2.ExecResultStrArray("source " + config.AppInfo.ProjectPath + "/shell/assist.sh"))
+}
 func (s *systemService) GetSystemConfigDebug() []string {
 	return command2.ExecResultStrArray("source " + config.AppInfo.ProjectPath + "/shell/helper.sh ;GetSysInfo")
 }
@@ -39,6 +44,13 @@ func (s *systemService) UpSystemConfig(str string, widget string) {
 	}
 	config.Cfg.SaveTo(config.SystemConfigInfo.ConfigPath)
 }
+func (s *systemService) UpSystemPort(port string) {
+	if len(port) > 0 && port != config.ServerInfo.HttpPort {
+		config.Cfg.Section("server").Key("HttpPort").SetValue(port)
+		config.ServerInfo.HttpPort = port
+	}
+	config.Cfg.SaveTo(config.SystemConfigInfo.ConfigPath)
+}
 func (s *systemService) GetCasaOSLogs(lineNumber int) string {
 	file, err := os.Open(s.log.Path())
 	if err != nil {

+ 0 - 1
service/zima_info.go

@@ -116,7 +116,6 @@ func (c *zima) GetNetState(name string) string {
 
 //网络信息
 func (c *zima) GetNetInfo() []net.IOCountersStat {
-	//loger.Error("输出个内容试试")
 	parts, _ := net.IOCounters(true)
 	//fmt.Println(net.ConntrackStatsWithContext(true))
 	return parts

+ 12 - 0
shell/assist.sh

@@ -0,0 +1,12 @@
+#!/bin/bash
+
+#add in v0.2.3
+version_0_2_3() {
+  ((EUID)) && sudo_cmd="sudo"
+  $sudo_cmd cp -rf /casaOS/server/shell/11-usb-mount.rules /etc/udev/rules.d/
+  $sudo_cmd chmod +x /casaOS/server/shell/usb-mount.sh
+  $sudo_cmd cp -rf /casaOS/server/shell/usb-mount@.service /etc/systemd/system/
+
+}
+
+version_0_2_3

+ 89 - 11
shell/helper.sh

@@ -103,20 +103,22 @@ DelPartition() {
 EOF
 }
 
-#添加分区
+#添加分区只有一个分区
 #param 路径   /dev/sdb
-#param 磁盘号   最大128
-#param 磁盘大小 字节   512*2048=1024kb=1M
+#param 要挂载的目录
 AddPartition() {
-  #  fdisk $1 <<EOF
-  #  n
-  #  $2
-  #  $3
-  #  $4
-  #  wq
-  #EOF
 
-  parted $1 mkpart primary ext4 s3 s4
+  DelPartition $1
+  parted -s $1 mklabel gpt
+
+  parted -s $1 mkpart primary ext4 0 100%
+
+  mkfs.ext4 $11
+
+  partprobe $1
+
+  #  mount $11 $2
+
 }
 
 #磁盘类型
@@ -151,7 +153,83 @@ GetPartitionSectors() {
   fdisk $1 -l | grep "/dev/sda[1-9]" | awk 'BEGIN{OFS=","}{print $1,$2,$3,$4}'
 }
 
+#检查没有使用的挂载点删除文件夹
+AutoRemoveUnuseDir() {
+  DIRECTORY="/mnt/"
+  dir=$(ls -l $DIRECTORY | awk '/^d/ {print $NF}')
+  for i in $dir; do
+
+    path="$DIRECTORY$i"
+    mountStr=$(mountpoint $path)
+    notMountpoint="is not a mountpoint"
+    if [[ $mountStr =~ $notMountpoint ]]; then
+      if [ "$(ls -A $path)" = "" ]; then
+        rm -fr $path
+      else
+        echo "$path is not empty"
+      fi
+    fi
+  done
+}
+
 #重载samba服务
 ReloadSamba() {
   /etc/init.d/smbd reload
 }
+
+# $1=sda1
+# $2=volume{1}
+do_mount() {
+  DEVBASE=$1
+  DEVICE="${DEVBASE}"
+  # See if this drive is already mounted, and if so where
+  MOUNT_POINT=$(mount | grep ${DEVICE} | awk '{ print $3 }')
+
+  if [ -n "${MOUNT_POINT}" ]; then
+    ${log} "Warning: ${DEVICE} is already mounted at ${MOUNT_POINT}"
+    exit 1
+  fi
+
+  # Get info for this drive: $ID_FS_LABEL and $ID_FS_TYPE
+  eval $(blkid -o udev ${DEVICE} | grep -i -e "ID_FS_LABEL" -e "ID_FS_TYPE")
+
+  LABEL=$2
+  if grep -q " /media/${LABEL} " /etc/mtab; then
+    # Already in use, make a unique one
+    LABEL+="-${DEVBASE}"
+  fi
+  DEV_LABEL="${LABEL}"
+
+  # Use the device name in case the drive doesn't have label
+  if [ -z ${DEV_LABEL} ]; then
+    DEV_LABEL="${DEVBASE}"
+  fi
+
+  MOUNT_POINT="/media/${DEV_LABEL}"
+
+  ${log} "Mount point: ${MOUNT_POINT}"
+
+  mkdir -p ${MOUNT_POINT}
+
+  case ${ID_FS_TYPE} in
+  vfat)
+    mount -t vfat -o rw,relatime,users,gid=100,umask=000,shortname=mixed,utf8=1,flush ${DEVICE} ${MOUNT_POINT}
+    ;;
+  ext[2-4])
+    mount -o noatime ${DEVICE} ${MOUNT_POINT} >/dev/null 2>&1
+    ;;
+  exfat)
+    mount -t exfat ${DEVICE} ${MOUNT_POINT} >/dev/null 2>&1
+    ;;
+  ntfs)
+    ntfs-3g ${DEVICE} ${MOUNT_POINT}
+    ;;
+  iso9660)
+    mount -t iso9660 ${DEVICE} ${MOUNT_POINT}
+    ;;
+  *)
+    /bin/rmdir "${MOUNT_POINT}"
+    exit 0
+    ;;
+  esac
+}

+ 4 - 4
shell/tools.sh

@@ -60,7 +60,7 @@ show() {
 }
 
 run_external_script() {
-  show 0 "run_external_script"
+  assist.sh
 }
 
 update() {
@@ -99,13 +99,13 @@ update() {
     target_arch="386"
     ;;
   *armv5*)
-    target_arch="armv5"
+    target_arch="arm-5"
     ;;
   *armv6*)
-    target_arch="armv6"
+    target_arch="arm-6"
     ;;
   *armv7*)
-    target_arch="armv7"
+    target_arch="arm-7"
     ;;
   *)
     show 1 "Aborted, unsupported or unknown architecture: $unamem"

+ 5 - 7
shell/usb-mount.sh

@@ -1,12 +1,10 @@
 #!/bin/sh
 
-# copy to /oasis/util/shell path
+# copy to /casaOS/util/shell path
 # chmod 755
 
 log="logger -t usb-mount.sh -s "
 
-${log} "变量:$1 $2"
-
 ACTION=$1
 
 DEVBASE=$2
@@ -18,10 +16,10 @@ MOUNT_POINT=$(mount | grep ${DEVICE} | awk '{ print $3 }')
 
 do_mount() {
 
-  if [[ -n ${MOUNT_POINT} ]]; then
+  if [ -n "${MOUNT_POINT}" ]; then
     ${log} "Warning: ${DEVICE} is already mounted at ${MOUNT_POINT}"
     exit 1
-  fib
+  fi
 
   # Get info for this drive: $ID_FS_LABEL and $ID_FS_TYPE
   eval $(blkid -o udev ${DEVICE} | grep -i -e "ID_FS_LABEL" -e "ID_FS_TYPE")
@@ -33,7 +31,7 @@ do_mount() {
   # Figure out a mount point to use
   # LABEL=${ID_FS_LABEL}
   LABEL=${DEVBASE}
-  if grep -q " /media/${LABEL} " /etc/mtab; then
+  if grep -q " /mnt/casa_${LABEL} " /etc/mtab; then
     # Already in use, make a unique one
     LABEL+="-${DEVBASE}"
   fi
@@ -44,7 +42,7 @@ do_mount() {
     DEV_LABEL="${DEVBASE}"
   fi
 
-  MOUNT_POINT="/media/${DEV_LABEL}"
+  MOUNT_POINT="/mnt/casa_${DEV_LABEL}"
 
   ${log} "Mount point: ${MOUNT_POINT}"
 

+ 2 - 2
shell/usb-mount@.service

@@ -4,5 +4,5 @@ Description=Mount USB Drive on %i
 [Service]
 Type=oneshot
 RemainAfterExit=true
-ExecStart=/oasis/util/shell/usb-mount.sh add %i
-ExecStop=/oasis/util/shell/usb-mount.sh remove %i
+ExecStart=/casaOS/server/shell/usb-mount.sh add %i
+ExecStop=/casaOS/server/shell/usb-mount.sh remove %i

+ 2 - 2
types/system.go

@@ -1,4 +1,4 @@
 package types
 
-const CURRENTVERSION = "0.2.2"
-const BODY = "<li>ui adjustment</li><li>fixed bugs</li>"
+const CURRENTVERSION = "0.2.3"
+const BODY = "<li>Add detailed CPU and memory statistics.</li><li>Add the multi-language function and add Chinese translation.</li><li>Add the function to modify the search engine.</li><li>Add the function of modifying the WebUI port</li><li>fixed some bugs</li><li>Preprocessing usb automounting</li><li>Update update script</li>"

+ 11 - 0
web/img/add_button.76237e85.svg

@@ -0,0 +1,11 @@
+<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect opacity="0.32" x="0.435625" y="0.435625" width="71.1288" height="71.1288" rx="7.56437" fill="white" stroke="url(#paint0_linear_812_2050)" stroke-width="0.87125"/>
+<path d="M36.0606 22L36.0239 50" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M22 36H50" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<defs>
+<linearGradient id="paint0_linear_812_2050" x1="77.6757" y1="64.5405" x2="35.9839" y2="53.5747" gradientUnits="userSpaceOnUse">
+<stop stop-color="#CBEFFF" stop-opacity="0.16"/>
+<stop offset="1" stop-color="white" stop-opacity="0"/>
+</linearGradient>
+</defs>
+</svg>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
web/js/2.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
web/js/3.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
web/js/4.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
web/js/app.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 8 - 0
web/js/chunk-vendors.js


Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác