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) }