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  }