393 lines
8.3 KiB
Go
393 lines
8.3 KiB
Go
package client
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"repositories.action2quare.com/ayo/gocommon/logger"
|
|
"repositories.action2quare.com/ayo/houston/shared"
|
|
"repositories.action2quare.com/ayo/houston/shared/protos"
|
|
|
|
"golang.org/x/text/encoding/korean"
|
|
|
|
"golang.org/x/text/transform"
|
|
)
|
|
|
|
func pof2(x int64, min int64) (out int64) {
|
|
out = 1
|
|
org := x
|
|
for (x >> 1) > 0 {
|
|
out = out << 1
|
|
x = x >> 1
|
|
}
|
|
if org > out {
|
|
out = out << 1
|
|
}
|
|
|
|
if out < min {
|
|
out = min
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func download(dir string, urlpath string, accessToken string, cb func(int64, int64)) (target string, err error) {
|
|
logger.Println("start downloading", dir, urlpath)
|
|
defer func() {
|
|
if err != nil {
|
|
logger.Println("downloading failed :", err)
|
|
} else {
|
|
logger.Println("downloading succeeded")
|
|
}
|
|
}()
|
|
|
|
parsed, err := url.Parse(urlpath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
req, _ := http.NewRequest("GET", urlpath, nil)
|
|
req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.51")
|
|
req.Header.Add("As-X-UrlHash", accessToken)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("download failed : %d %s", resp.StatusCode, parsed.Path)
|
|
}
|
|
|
|
out, err := os.Create(path.Join(dir, path.Base(filepath.ToSlash(parsed.Path))))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer out.Close()
|
|
|
|
cl := resp.Header.Get("Content-Length")
|
|
totalLength, _ := strconv.ParseInt(cl, 10, 0)
|
|
totalWritten := int64(0)
|
|
chunkSize := pof2(totalLength/100, 1024*1024)
|
|
if cb != nil {
|
|
cb(0, totalLength)
|
|
}
|
|
|
|
for {
|
|
written, err := io.CopyN(out, resp.Body, chunkSize)
|
|
totalWritten += written
|
|
if cb != nil {
|
|
cb(totalWritten, totalLength)
|
|
}
|
|
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
return filepath.ToSlash(out.Name()), nil
|
|
}
|
|
|
|
func unzip(fname string) error {
|
|
archive, err := zip.OpenReader(fname)
|
|
if err != nil {
|
|
os.Remove(fname)
|
|
return err
|
|
}
|
|
defer func() {
|
|
archive.Close()
|
|
os.Remove(fname)
|
|
}()
|
|
|
|
verpath := path.Dir(fname)
|
|
for _, f := range archive.File {
|
|
var name string
|
|
if f.NonUTF8 {
|
|
name, _, _ = transform.String(korean.EUCKR.NewDecoder(), f.Name)
|
|
} else {
|
|
name = f.Name
|
|
}
|
|
|
|
name = strings.ReplaceAll(name, `\`, "/")
|
|
filePath := path.Join(verpath, name)
|
|
if f.FileInfo().IsDir() || strings.HasSuffix(f.FileInfo().Name(), `\`) {
|
|
if err = os.MkdirAll(filePath, 0775); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
if err := os.MkdirAll(path.Dir(filePath), 0775); err != nil {
|
|
return err
|
|
}
|
|
|
|
dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fileInArchive, err := f.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := io.Copy(dstFile, fileInArchive); err != nil {
|
|
return err
|
|
}
|
|
|
|
dstFile.Close()
|
|
fileInArchive.Close()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func untar(fname string) error {
|
|
file, err := os.Open(fname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
file.Close()
|
|
os.Remove(fname)
|
|
}()
|
|
|
|
verpath := path.Dir(fname)
|
|
tarReader := tar.NewReader(file)
|
|
for {
|
|
header, err := tarReader.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch header.Typeflag {
|
|
case tar.TypeDir:
|
|
if err := os.MkdirAll(path.Join(verpath, header.Name), 0775); err != nil {
|
|
return err
|
|
}
|
|
case tar.TypeReg:
|
|
fileWriter, err := os.Create(path.Join(verpath, header.Name))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fileWriter.Close()
|
|
|
|
if _, err := io.Copy(fileWriter, tarReader); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return errors.New("unknown type")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (hc *houstonClient) prepareDeploy(name string, version string) (destPath string, err error) {
|
|
// houston관리용임을 표시하기 위해 더미파일 생성
|
|
defer func() {
|
|
var flagf *os.File
|
|
markerPath := path.Join(hc.config.StorageRoot, name, "@houston")
|
|
if _, err := os.Stat(markerPath); os.IsNotExist(err) {
|
|
flagf, err = os.Create(markerPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer flagf.Close()
|
|
flagf.Write([]byte(hc.timestamp))
|
|
}
|
|
}()
|
|
|
|
verpath := path.Join(hc.config.StorageRoot, name, version)
|
|
if _, err := os.Stat(verpath); os.IsNotExist(err) {
|
|
// 없네? 만들면 된다.
|
|
err = os.MkdirAll(verpath, 0775)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
} else {
|
|
// 있네? 재배포 가능한가?
|
|
for _, child := range hc.childProcs {
|
|
if child.version == version && child.name == name {
|
|
// 이미 실행 중인 버전이다. 실패
|
|
return "", fmt.Errorf("%s %s is already running. deploy is failed", name, version)
|
|
}
|
|
}
|
|
// 재배포 가능
|
|
}
|
|
return verpath, nil
|
|
}
|
|
|
|
func (hc *houstonClient) makeDownloadUrl(rel string) string {
|
|
out := rel
|
|
if !strings.HasPrefix(out, "http") {
|
|
tks := strings.SplitN(hc.config.HttpAddress, "://", 2)
|
|
out = fmt.Sprintf("%s://%s", tks[0], path.Join(tks[1], rel))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func copyfile(src, dst string) error {
|
|
fi, err := os.Stat(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
inmode := fi.Mode()
|
|
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer in.Close()
|
|
|
|
out, err := os.Create(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
copied, err := io.Copy(out, in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if copied < fi.Size() {
|
|
return errors.New("copy not completed")
|
|
}
|
|
if err := out.Sync(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := out.Chmod(inmode); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (hc *houstonClient) prepareUpdateSelf(req *shared.DeployRequest) (srcdir string, replacer string, err error) {
|
|
// 내가 스스로 업데이트
|
|
// 다운로드 받고 압축 푼 다음에 교체용 프로세스 시작
|
|
tempdir, err := os.MkdirTemp(os.TempDir(), "*")
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
fname, err := download(tempdir, hc.makeDownloadUrl(req.Url), req.AccessToken, nil)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
switch path.Ext(fname) {
|
|
case ".zip":
|
|
err = unzip(fname)
|
|
case ".tar":
|
|
err = untar(fname)
|
|
}
|
|
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
// houston version 파일
|
|
err = os.WriteFile(path.Join(path.Dir(fname), "@version"), []byte(req.Version), 0644)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
selfname, _ := os.Executable()
|
|
srcreplacer := path.Join(path.Dir(fname), "replacer") + path.Ext(selfname)
|
|
replacer = "./" + filepath.ToSlash("replacer"+path.Ext(selfname))
|
|
err = copyfile(srcreplacer, replacer)
|
|
if err == nil {
|
|
err = os.Chmod(replacer, 0775)
|
|
}
|
|
|
|
// replacer먼저 가져옴
|
|
return filepath.ToSlash(tempdir), replacer, err
|
|
}
|
|
|
|
func (hc *houstonClient) deploy(req *shared.DeployRequest, cb func(*protos.DeployingProgress)) error {
|
|
logger.Println("start deploying")
|
|
root, err := hc.prepareDeploy(req.Name, req.Version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// verpath에 배포 시작
|
|
h := md5.New()
|
|
h.Write([]byte(strings.Trim(req.Url, "/")))
|
|
at := hex.EncodeToString(h.Sum(nil))
|
|
fname, err := download(root, hc.makeDownloadUrl(req.Url), at, func(written int64, total int64) {
|
|
prog := protos.DeployingProgress{
|
|
State: "download",
|
|
Progress: written,
|
|
Total: total,
|
|
}
|
|
cb(&prog)
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cb(&protos.DeployingProgress{
|
|
State: "unpack",
|
|
Progress: 0,
|
|
Total: 0,
|
|
})
|
|
|
|
switch path.Ext(fname) {
|
|
case ".zip":
|
|
err = unzip(fname)
|
|
case ".tar":
|
|
err = untar(fname)
|
|
}
|
|
|
|
if err == nil && len(req.Config) > 0 {
|
|
// config.json도 다운로드
|
|
h := md5.New()
|
|
h.Write([]byte(strings.Trim(req.Config, "/")))
|
|
at = hex.EncodeToString(h.Sum(nil))
|
|
|
|
_, err = download(root, hc.makeDownloadUrl(req.Config), at, nil)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (hc *houstonClient) withdraw(req *shared.WithdrawRequest) error {
|
|
targetPath := path.Join(hc.config.StorageRoot, req.Name, req.Version)
|
|
fd, _ := os.Stat(targetPath)
|
|
if fd != nil {
|
|
if fd.IsDir() {
|
|
for _, running := range hc.childProcs {
|
|
if running.name == req.Name && (len(req.Version) == 0 || running.version == req.Version) {
|
|
// 회수하려는 버전이 돌고 있다
|
|
return fmt.Errorf("withdraw failed. %s@%s is still running", req.Name, req.Version)
|
|
}
|
|
}
|
|
|
|
return os.RemoveAll(targetPath)
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("withdraw failed. %s@%s is not deployed", req.Name, req.Version)
|
|
}
|