Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func main() {
authApi.POST("/createDeployment", request.App{}.CreateDeployment)
authApi.POST("/createBundle", request.App{}.CreateBundle)
authApi.POST("/checkBundle", request.App{}.CheckBundle)
authApi.POST("/lsBundle", request.App{}.LsBundle)
authApi.POST("/delApp", request.App{}.DelApp)
authApi.POST("/delDeployment", request.App{}.DelDeployment)
authApi.POST("/lsDeployment", request.App{}.LsDeployment)
Expand Down
13 changes: 13 additions & 0 deletions model/deploymentVersion.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,16 @@ func (DeploymentVersion) GetNewVersionByKeyDeploymentId(deploymentId int) *Deplo
func (DeploymentVersion) UpdateCurrentPackage(id int, pid *int) {
userDb.Raw("update deployment_version set current_package=? where id=?", pid, id).Scan(&DeploymentVersion{})
}

// ListLatestForDeployment returns deployment_version rows for a deployment, newest activity first.
func (DeploymentVersion) ListLatestForDeployment(deploymentID int, limit int) []DeploymentVersion {
var rows []DeploymentVersion
err := userDb.Where("deployment_id = ?", deploymentID).
Order("update_time DESC, id DESC").
Limit(limit).
Find(&rows).Error
if err != nil {
return []DeploymentVersion{}
}
return rows
}
172 changes: 146 additions & 26 deletions request/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ package request

import (
"bytes"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"

"com.lc.go.codepush/server/config"
"com.lc.go.codepush/server/db"
Expand All @@ -24,6 +29,9 @@ import (
"gorm.io/gorm"
)

// maxBundleUploadBytes is the maximum allowed size for a single CodePush bundle upload.
const maxBundleUploadBytes int64 = 50 << 20 // 50 MiB

type App struct{}

type createAppReq struct {
Expand Down Expand Up @@ -65,13 +73,17 @@ type createBundleReq struct {
Version *string `json:"version" binding:"required"`
Size *int64 `json:"size" binding:"required"`
Hash *string `json:"hash" binding:"required"`
Mandatory *bool `json:"mandatory"`
}

func (App) CreateBundle(ctx *gin.Context) {
createBundleReq := createBundleReq{}
if err := ctx.ShouldBindBodyWith(&createBundleReq, binding.JSON); err == nil {
uid := ctx.MustGet(constants.GIN_USER_ID).(int)

mandatory := false
if createBundleReq.Mandatory != nil {
mandatory = *createBundleReq.Mandatory
}
Comment on lines +83 to +86
app := model.App{}.GetAppByUidAndAppName(uid, *createBundleReq.AppName)
if app == nil {
log.Panic("App not found")
Expand Down Expand Up @@ -128,6 +140,10 @@ func (App) CreateBundle(ctx *gin.Context) {
deploymentVersion.UpdateTime = utils.GetTimeNow()
model.Update[model.DeploymentVersion](deploymentVersion)
redis.DelRedisObj(constants.REDIS_UPDATE_INFO + *deployment.Key + "*")
ctx.JSON(http.StatusOK, gin.H{
"success": true,
"mandatory": mandatory,
})
} else {
log.Panic(err.Error())
}
Expand All @@ -136,6 +152,7 @@ func (App) CreateBundle(ctx *gin.Context) {
type createDeploymentInfo struct {
AppName *string `json:"appName" binding:"required"`
DeploymentName *string `json:"deploymentName" binding:"required"`
Key *string `json:"key"`
}

func (App) CreateDeployment(ctx *gin.Context) {
Expand All @@ -150,12 +167,15 @@ func (App) CreateDeployment(ctx *gin.Context) {
if deployment != nil {
log.Panic("Deployment name " + *createDeploymentInfo.DeploymentName + " exist")
}
uuid, _ := uuid.NewUUID()
key := uuid.String()
if createDeploymentInfo.Key == nil || *createDeploymentInfo.Key == "" {
uuid, _ := uuid.NewUUID()
uuidStr := uuid.String()
createDeploymentInfo.Key = &uuidStr
}
newDeployment := model.Deployment{
AppId: app.Id,
Name: createDeploymentInfo.DeploymentName,
Key: &key,
Key: createDeploymentInfo.Key,
Comment on lines +170 to +178
CreateTime: utils.GetTimeNow(),
}
err := model.Create[model.Deployment](&newDeployment)
Expand All @@ -164,7 +184,7 @@ func (App) CreateDeployment(ctx *gin.Context) {
}
ctx.JSON(http.StatusOK, gin.H{
"name": createDeploymentInfo.DeploymentName,
"key": key,
"key": *createDeploymentInfo.Key,
})
} else {
log.Panic(err.Error())
Expand All @@ -173,62 +193,85 @@ func (App) CreateDeployment(ctx *gin.Context) {
func (App) UploadBundle(ctx *gin.Context) {
_, headers, err := ctx.Request.FormFile("file")
if err != nil {
Comment on lines 194 to 195
log.Printf("Error when try to get file: %v", err)
ctx.JSON(http.StatusBadRequest, gin.H{
"success": false,
"msg": "file is required",
})
return
}
if headers.Size > maxBundleUploadBytes {
ctx.JSON(http.StatusRequestEntityTooLarge, gin.H{
"success": false,
"msg": "bundle size must be at most 50 MB",
})
return
}

file, err := headers.Open()
if err != nil {
log.Panic(err.Error())
}
defer file.Close()
config := config.GetConfig()

// Read at most max+1 bytes so streams larger than the cap fail without loading the whole file.
data, err := io.ReadAll(io.LimitReader(file, maxBundleUploadBytes+1))
if err != nil {
log.Panic(err.Error())
}
if int64(len(data)) > maxBundleUploadBytes {
ctx.JSON(http.StatusRequestEntityTooLarge, gin.H{
"success": false,
"msg": "bundle size must be at most 50 MB",
})
return
}

cfg := config.GetConfig()
key := headers.Filename
body := bytes.NewReader(data)

switch config.CodePush.FileLocal {
switch cfg.CodePush.FileLocal {
case "local":
exist := utils.Exists(config.CodePush.Local.SavePath)
exist := utils.Exists(cfg.CodePush.Local.SavePath)
if !exist {
err := os.MkdirAll(config.CodePush.Local.SavePath, 0777)
err := os.MkdirAll(cfg.CodePush.Local.SavePath, 0777)
if err != nil {
log.Panic(err.Error())
}
}
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(file)
if err != nil {
if err := os.WriteFile(path.Clean(cfg.CodePush.Local.SavePath+"/"+key), data, 0777); err != nil {
log.Panic(err.Error())
}
Comment on lines +242 to 244
os.WriteFile(path.Clean(config.CodePush.Local.SavePath+"/"+key), buf.Bytes(), 0777)
case "aws":
s3Config := &aws.Config{
Credentials: credentials.NewStaticCredentials(config.CodePush.Aws.KeyId, config.CodePush.Aws.Secret, ""),
Endpoint: aws.String(config.CodePush.Aws.Endpoint),
Region: aws.String(config.CodePush.Aws.Region),
S3ForcePathStyle: aws.Bool(config.CodePush.Aws.S3ForcePathStyle),
Credentials: credentials.NewStaticCredentials(cfg.CodePush.Aws.KeyId, cfg.CodePush.Aws.Secret, ""),
Endpoint: aws.String(cfg.CodePush.Aws.Endpoint),
Region: aws.String(cfg.CodePush.Aws.Region),
S3ForcePathStyle: aws.Bool(cfg.CodePush.Aws.S3ForcePathStyle),
}
newSession, _ := session.NewSession(s3Config)

s3Client := s3.New(newSession)

_, err = s3Client.PutObject(&s3.PutObjectInput{
Body: file,
Bucket: aws.String(config.CodePush.Aws.Bucket),
Body: body,
Bucket: aws.String(cfg.CodePush.Aws.Bucket),
Key: &key,
})
if err != nil {
log.Panic(err.Error())
}
case "ftp":
f, err := ftp.Dial(config.CodePush.Ftp.ServerUrl)
f, err := ftp.Dial(cfg.CodePush.Ftp.ServerUrl)
if err != nil {
log.Panic(err.Error())
}
err = f.Login(config.CodePush.Ftp.UserName, config.CodePush.Ftp.Password)
err = f.Login(cfg.CodePush.Ftp.UserName, cfg.CodePush.Ftp.Password)
if err != nil {
log.Panic(err.Error())
}

err = f.Stor(key, file)
err = f.Stor(key, body)
if err != nil {
log.Panic(err.Error())
}
Expand All @@ -238,8 +281,36 @@ func (App) UploadBundle(ctx *gin.Context) {

}

fileName := path.Base(strings.TrimSpace(key))
if fileName == "" || fileName == "." {
fileName = key
}

downloadURL := ""
if ru := strings.TrimSpace(cfg.ResourceUrl); ru != "" {
downloadURL = strings.TrimSuffix(ru, "/") + "/" + url.PathEscape(fileName)
} else {
switch cfg.CodePush.FileLocal {
case "aws":
a := cfg.CodePush.Aws
ep := strings.TrimSuffix(strings.TrimSpace(a.Endpoint), "/")
if a.S3ForcePathStyle || strings.HasPrefix(ep, "http://") || strings.HasPrefix(ep, "https://") {
downloadURL = fmt.Sprintf("%s/%s/%s", ep, a.Bucket, url.PathEscape(fileName))
} else {
downloadURL = fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s", a.Bucket, a.Region, url.PathEscape(fileName))
}
Comment on lines +289 to +301
case "local":
downloadURL = filepath.ToSlash(filepath.Join(cfg.CodePush.Local.SavePath, fileName))
case "ftp":
base := strings.TrimSuffix(strings.TrimSpace(cfg.CodePush.Ftp.ServerUrl), "/")
downloadURL = base + "/" + url.PathEscape(fileName)
}
}

ctx.JSON(http.StatusOK, gin.H{
"success": true,
"success": true,
"fileName": fileName,
"downloadUrl": downloadURL,
})
}

Expand Down Expand Up @@ -311,10 +382,13 @@ func (App) LsApp(ctx *gin.Context) {
if len(*apps) <= 0 {
log.Panic("No app")
}
var appsRep []string
var appsRep []createAppReq

for _, v := range *apps {
appsRep = append(appsRep, *v.AppName)
appsRep = append(appsRep, createAppReq{
AppName: v.AppName,
OS: v.OS,
})
}
ctx.JSON(http.StatusOK, appsRep)
}
Expand All @@ -325,6 +399,52 @@ type checkBundleReq struct {
Version *string `json:"version" binding:"required"`
}

type listBundlesReq struct {
AppName *string `json:"appName" binding:"required"`
Deployment *string `json:"deployment" binding:"required"`
}

// LsBundle returns the three most recently updated deployment_version rows for the app/deployment,
// each with the current package (bundle) when current_package is set.
func (App) LsBundle(ctx *gin.Context) {
req := listBundlesReq{}
if err := ctx.ShouldBindBodyWith(&req, binding.JSON); err != nil {
log.Panic(err.Error())
}
uid := ctx.MustGet(constants.GIN_USER_ID).(int)
app := model.App{}.GetAppByUidAndAppName(uid, *req.AppName)
if app == nil {
log.Panic("App not found")
}
deployment := model.Deployment{}.GetByAppidAndName(*app.Id, *req.Deployment)
if deployment == nil {
log.Panic("Deployment " + *req.Deployment + " not found")
}
const listBundlesLimit = 3
versions := model.DeploymentVersion{}.ListLatestForDeployment(*deployment.Id, listBundlesLimit)
bundles := make([]gin.H, 0, len(versions))
for i := range versions {
dv := &versions[i]
item := gin.H{
"deploymentVersionId": dv.Id,
"appVersion": dv.AppVersion,
"versionNum": dv.VersionNum,
"updateTime": dv.UpdateTime,
"createTime": dv.CreateTime,
}
if dv.CurrentPackage != nil {
if p := model.GetOne[model.Package]("id=?", *dv.CurrentPackage); p != nil {
item["package"] = p
}
}
bundles = append(bundles, item)
}
ctx.JSON(http.StatusOK, gin.H{
"success": true,
"bundles": bundles,
})
}

func (App) CheckBundle(ctx *gin.Context) {
checkBundleReq := checkBundleReq{}
if err := ctx.ShouldBindBodyWith(&checkBundleReq, binding.JSON); err == nil {
Expand Down
Loading