github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/client/controllers/main_update.go (about) 1 package controllers 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "compress/gzip" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "log" 13 "net/http" 14 "os" 15 "os/exec" 16 "path" 17 "path/filepath" 18 "runtime" 19 20 "github.com/benoitkugler/goACVE/client/version" 21 22 "github.com/benoitkugler/goACVE/server/core/apiserver" 23 "github.com/benoitkugler/goACVE/server/core/utils" 24 ) 25 26 const pathClientVersions = "local/versions.json" 27 28 type monFunc = func(string, uint8) 29 30 type clientInfo struct { 31 // `true` si première utilisation depuis la mise à jour 32 IsNew bool 33 } 34 35 func isNewlyUpdated() bool { 36 var out clientInfo 37 b, err := ioutil.ReadFile(pathClientVersions) 38 if err != nil { 39 return false 40 } 41 if err := json.Unmarshal(b, &out); err != nil { 42 log.Println(err) 43 } 44 return out.IsNew 45 } 46 47 func setNewlyUpdated(new bool) { 48 f, err := os.Create(pathClientVersions) 49 if err != nil { 50 log.Println(err) 51 return 52 } 53 defer f.Close() 54 err = json.NewEncoder(f).Encode(clientInfo{IsNew: new}) 55 if err != nil { 56 log.Println(err) 57 } 58 } 59 60 func verifyChecksum(archiveContent []byte, reference []byte) error { 61 checksum, err := utils.HashBytes(archiveContent) 62 if err != nil { 63 return err 64 } 65 if !bytes.Equal(reference, checksum) { 66 return fmt.Errorf("updated file has wrong checksum. Expected: %x, got: %x", reference, checksum) 67 } 68 return nil 69 } 70 71 // buf est un tar.gz contenant un ou plusieurs dossiers / fichiers 72 func applyUpdate(archive *bytes.Buffer, version apiserver.VersionInfos, monitor monFunc) error { 73 if err := verifyChecksum(archive.Bytes(), version.Hash); err != nil { 74 return fmt.Errorf("Somme de contrôle invalide : %s", err) 75 } 76 if version.NbFiles == 0 { 77 return errors.New("La mise à jour semble ne contenir aucun fichier.") 78 } 79 80 gz, err := gzip.NewReader(archive) 81 if err != nil { 82 return err 83 } 84 defer gz.Close() 85 innerTr := tar.NewReader(gz) 86 current, step := 0, version.NbFiles/100 87 for { 88 current += 1 89 hdr, err := innerTr.Next() 90 if err == io.EOF { 91 break // End of archive 92 } 93 if err != nil { 94 return fmt.Errorf("Archive illisible : %s", err) 95 } 96 if hdr.FileInfo().Mode().IsDir() { 97 continue 98 } 99 100 targetPath := hdr.Name 101 102 dirPath := filepath.Dir(targetPath) 103 if _, err := os.Stat(dirPath); err != nil { 104 if err := os.MkdirAll(dirPath, os.ModePerm); err != nil { 105 return err 106 } 107 } 108 109 if hdr.Typeflag == tar.TypeSymlink { 110 if _, err := os.Lstat(targetPath); err == nil { 111 if err := os.Remove(targetPath); err != nil { 112 return fmt.Errorf("Impossible de supprimer le lien symolique : %+v", err) 113 } 114 } else if os.IsNotExist(err) { 115 return fmt.Errorf("Impossible de vérifier le lien symbolique: %+v", err) 116 } 117 118 if err := os.Symlink(hdr.Linkname, targetPath); err != nil { 119 return err 120 } 121 continue 122 } 123 124 // pour éviter les problèmes de fichier en cours d'utilisation 125 // on utilise un fichier temporaire 126 newPath := targetPath + ".new" 127 f, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) 128 if err != nil { 129 return fmt.Errorf("Impossible de créer le fichier : %s", err) 130 } 131 132 // copy over contents 133 if _, err := io.Copy(f, innerTr); err != nil { 134 return fmt.Errorf("Impossible d'enregistrer la mise à jour : %s", err) 135 } 136 137 // manually close here after each file operation; defering would cause each file close 138 // to wait until all operations have completed. 139 if err = f.Close(); err != nil { 140 return err 141 } 142 143 // this is where we'll move the existing file to so that we can swap in the updated replacement 144 oldPath := filepath.Join(targetPath + ".old") 145 146 // delete any existing old exec file - this is necessary on Windows for two reasons: 147 // 1. after a successful update, Windows can't remove the .old file because the process is still running 148 // 2. windows rename operations fail if the destination file already exists 149 _ = os.Remove(oldPath) 150 151 if _, err := os.Lstat(targetPath); err == nil { // le fichier existe 152 // move the existing file to a new file in the same directory 153 // freeing targetPath 154 err = os.Rename(targetPath, oldPath) 155 if err != nil { 156 return err 157 } 158 } 159 160 // move the new exectuable in to become the new program 161 if err = os.Rename(newPath, targetPath); err != nil { 162 return err 163 } 164 165 // we try to cleanup, may fail on windows 166 _ = os.Remove(oldPath) 167 168 if current%step == 0 { 169 monitor("Copie des fichiers", uint8(100*current/version.NbFiles)) 170 } 171 } 172 return nil 173 } 174 175 // CheckUpdate implémente un système d'auto-update 176 func (c *MainController) CheckUpdate(monitor monFunc) (restartNeeded bool, err error) { 177 goos := runtime.GOOS 178 179 monitor("Lecture des versions...", 0) 180 news := &apiserver.VersionInfos{} 181 182 url := path.Join(apiserver.UrlUpdate, goos) 183 184 err = requete(url, http.MethodGet, nil, news) 185 fmt.Printf("version infos : %v \n", *news) 186 if err != nil { // Impossible de vérifier les MAJ -> mode offline 187 Server.actif = false 188 return false, err 189 } 190 monitor("Comparaison...", 50) 191 needed := utils.IsNewer(version.VERSION, news.Version) 192 if !needed { 193 return false, nil // pas de mise à jour à télécharger 194 } 195 196 monitor(fmt.Sprintf("Une <b>mise à jour</b> a été trouvée (<i>%s</i>).", news.Version), 0) 197 downloadCb := func(percent uint8) { monitor("Téléchargement de la mise à jour...", percent) } 198 buf := new(bytes.Buffer) 199 err = requestDownload(url, http.MethodPost, nil, downloadCb, buf) 200 if err != nil { 201 return false, err 202 } 203 monitor("Dépaquettage et redémarrage...", 0) 204 205 if goos == "darwin" { // executable is not in root folder 206 if err := os.Chdir("../.."); err != nil { 207 return false, err 208 } // now in "ACVE-Gestion.app" 209 } 210 211 if err = applyUpdate(buf, *news, monitor); err != nil { 212 return false, err 213 } 214 215 if goos == "darwin" { // executable is not in root folder 216 if err := os.Chdir("Contents/MacOS/"); err != nil { 217 return false, err 218 } // back next to client 219 } 220 221 // mise à jour de la version 222 setNewlyUpdated(true) 223 monitor("Mise à jour réussie.", 100) 224 return true, nil 225 } 226 227 // Restart start a new program. The main function should be quit as soon as possible. 228 // An error is returned if the launch wasn't successful 229 func (c MainController) Restart() error { 230 exePath, err := os.Executable() 231 if err != nil { 232 return err 233 } 234 log.Println("restarting with :", exePath, os.Args[1:]) 235 cmd := exec.Command(exePath, os.Args[1:]...) 236 err = cmd.Start() 237 if err != nil { 238 return err 239 } 240 os.Exit(0) 241 return nil 242 }