github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/client/controllers/main.go (about)

     1  // Ce package expose l'objet de contrôle abstrait,
     2  // responsable du lancement, du chargement de la base,
     3  // et du lien entre les interfaces.
     4  // Il est destiné à ettayer une application graphique.
     5  package controllers
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"flag"
    11  	"fmt"
    12  	"log"
    13  	"net/http"
    14  	"net/url"
    15  	"os"
    16  	"path"
    17  
    18  	"github.com/benoitkugler/goACVE/logs"
    19  
    20  	"github.com/benoitkugler/goACVE/server/core/apiserver"
    21  	dm "github.com/benoitkugler/goACVE/server/core/datamodel"
    22  	rd "github.com/benoitkugler/goACVE/server/core/rawdata"
    23  )
    24  
    25  const (
    26  	// Relative path to local folder. May be used for temporary documents.
    27  	LocalFolder        = "local/"
    28  	LogginSettingsPath = LocalFolder + "init.json"
    29  	BasePath           = LocalFolder + "DB.json"
    30  
    31  	endPoint = "/ACVEGestion/api"
    32  
    33  	urlRejoueInscription = "__rejoue_inscription" // cas spécial hors API
    34  )
    35  
    36  var (
    37  	hostLocal   *url.URL
    38  	hostDistant *url.URL
    39  )
    40  
    41  var (
    42  	Server ServerURL // Si vide, désactive les requêtes serveur
    43  
    44  	DebugModules = rd.Modules{
    45  		Personnes:     4,
    46  		Camps:         3,
    47  		Inscriptions:  3,
    48  		SuiviCamps:    3,
    49  		SuiviDossiers: 3,
    50  		Paiements:     3,
    51  		Aides:         3,
    52  		Equipiers:     3,
    53  		Dons:          3,
    54  	}
    55  )
    56  
    57  func init() {
    58  	var err error
    59  	hostLocal, err = url.Parse("http://localhost:1323")
    60  	if err != nil {
    61  		log.Fatal(err)
    62  	}
    63  	hostDistant, err = url.Parse("https://acve.fr")
    64  	if err != nil {
    65  		log.Fatal(err)
    66  	}
    67  }
    68  
    69  type ServerURL struct {
    70  	actif, local bool
    71  }
    72  
    73  func (s ServerURL) host() *url.URL {
    74  	if s.local {
    75  		return hostLocal
    76  	}
    77  	return hostDistant
    78  }
    79  
    80  func (s ServerURL) Host() string {
    81  	return s.host().String()
    82  }
    83  
    84  // cas spécial pour rejouer l'inscription
    85  func (s ServerURL) ApiURL(endpoint string) string {
    86  	if !s.actif {
    87  		return ""
    88  	}
    89  
    90  	if endpoint == urlRejoueInscription { // special case
    91  		return s.EndpointURL("/inscription/api/no-check-mail")
    92  	}
    93  
    94  	fullpath := path.Join(endPoint, endpoint)
    95  	return s.EndpointURL(fullpath)
    96  }
    97  
    98  func (s ServerURL) EndpointURL(endpoint string) string {
    99  	u := *s.host()
   100  	u.Path = endpoint
   101  	return u.String()
   102  }
   103  
   104  type controllerPool struct {
   105  	Personnes     *Personnes
   106  	Aides         *Aides
   107  	Camps         *Camps
   108  	SuiviCamps    *SuiviCamps
   109  	SuiviDossiers *SuiviDossiers
   110  	Equipiers     *Equipiers
   111  	Dons          *Dons
   112  	Inscriptions  *Inscriptions
   113  	Paiements     *Paiements
   114  }
   115  
   116  type MainController struct {
   117  	logsJoomeo  logs.Joomeo // accés direct : dev ou prod
   118  	Controllers controllerPool
   119  	Base        *dm.BaseLocale
   120  	ShowError   func(err error)
   121  	// Has to be thread safe
   122  	ShowStandard func(msg string, wait bool)
   123  	Background   Background
   124  }
   125  
   126  func (c *MainController) ResetAllControllers() {
   127  	c.Controllers.Personnes.Reset()
   128  	c.Controllers.Aides.Reset()
   129  	c.Controllers.Camps.Reset()
   130  	c.Controllers.SuiviCamps.Reset()
   131  	c.Controllers.SuiviDossiers.Reset()
   132  	c.Controllers.Equipiers.Reset()
   133  	c.Controllers.Dons.Reset()
   134  	c.Controllers.Inscriptions.Reset()
   135  	c.Controllers.Paiements.Reset()
   136  }
   137  
   138  func (c MainController) LogsJoomeo() logs.Joomeo {
   139  	return c.logsJoomeo
   140  }
   141  
   142  // -------------------------------------------------------------------------------------------
   143  // ---------------------------------------- Lancement ----------------------------------------
   144  // -------------------------------------------------------------------------------------------
   145  
   146  func (c *MainController) parseCommandLine() bool {
   147  	local := flag.Bool("local", false, "utilise le serveur local, en version développement")
   148  	debug := flag.Bool("debug", false, "saute la vérification des mises à jour, l'authentification et le chargement de la base distante. Implique -local")
   149  
   150  	flag.Parse()
   151  	Server.actif = true
   152  	if *debug { // debug -> local
   153  		*local = true
   154  	}
   155  	// l'accès à Joomeo ne passe pas par le serveur
   156  	if *local {
   157  		Server.local = true
   158  		c.logsJoomeo = logs.JoomeoDev
   159  	} else {
   160  		c.logsJoomeo = logs.JoomeoProd
   161  	}
   162  	return *debug
   163  }
   164  
   165  func (c *MainController) checkLocalFolder() error {
   166  	_, err := os.Stat(LocalFolder)
   167  	if err == nil {
   168  		return nil
   169  	}
   170  	if err = os.Mkdir(LocalFolder, 0777); err != nil {
   171  		return fmt.Errorf("Impossible de créer le dossier de travail : %s", err)
   172  	}
   173  	return nil
   174  }
   175  
   176  func (c *MainController) InitControllers(modules rd.Modules) {
   177  	c.Controllers.Personnes = NewPersonnes(c, modules.Personnes)
   178  	c.Controllers.Aides = NewAides(c, modules.Aides)
   179  	c.Controllers.Camps = NewCamps(c, modules.Camps)
   180  	c.Controllers.SuiviCamps = NewSuiviCamps(c, modules.SuiviCamps)
   181  	c.Controllers.SuiviDossiers = NewSuiviDossiers(c, modules.SuiviDossiers)
   182  	c.Controllers.Equipiers = NewEquipiers(c, modules.Equipiers)
   183  	c.Controllers.Dons = NewDons(c, modules.Dons)
   184  	c.Controllers.Inscriptions = NewInscriptions(c, modules.Inscriptions)
   185  	c.Controllers.Paiements = NewPaiements(c, modules.Paiements)
   186  }
   187  
   188  func (c MainController) saveBaseToDisk() error {
   189  	f, err := os.Create(BasePath)
   190  	if err != nil {
   191  		return fmt.Errorf("Impossible d'<b>enregistrer</b> les données sur le disque : <br/> <i>%s</i>", err)
   192  	}
   193  	defer f.Close()
   194  	if err = dm.MarshalBaseLocale(c.Base, f); err != nil {
   195  		return fmt.Errorf("Impossible d'<b>enregistrer</b> les données sur le disque : <br/> <i>%s</i>", err)
   196  	}
   197  	return nil
   198  }
   199  
   200  // LoadBaseFromDisk charge et décode la sauvegarde du disque, au format .json
   201  // Le pointer `Base` est mis à jour.
   202  // En cas d'erreur, les dictionnaires de la base sont quand même initialisés.
   203  func (c *MainController) LoadBaseFromDisk() error {
   204  	*c.Base = dm.NewBaseLocale()
   205  	f, err := os.Open(BasePath)
   206  	if err != nil {
   207  		return fmt.Errorf("Impossible de <b>charger</b> les données depuis le <b>disque</b> : <br/> <i>%s</i>", err)
   208  	}
   209  	defer f.Close()
   210  	base, err := dm.UnMarshalBaseLocale(f)
   211  	if err != nil {
   212  		return fmt.Errorf("Impossible de <b>décrypter</b> les données du <b>disque</b> : <br/> <i>%s</i>", err)
   213  	}
   214  	*c.Base = base
   215  	c.ResetAllControllers()
   216  	c.ShowStandard("Données bien chargées depuis le disque.", false)
   217  	return nil
   218  }
   219  
   220  // LoadBaseFromServer requiert la base local sur le serveur, au format .json.gz
   221  // La réponse est décompressée et décodée.
   222  // Met à jour les controllers et enregistre sur le disque en cas de succès.
   223  func (c *MainController) LoadBaseFromServer(progressMonitor func(i uint8)) {
   224  	if progressMonitor == nil {
   225  		c.ShowError(errors.New("Fonction de suivi non fournie !"))
   226  		return
   227  	}
   228  	buf := new(bytes.Buffer)
   229  	err := requestDownload(apiserver.UrlDB, http.MethodGet, nil, progressMonitor, buf)
   230  	if err != nil {
   231  		c.ShowError(fmt.Errorf("Impossible de <b>charger</b> la base depuis le <b>serveur</b>. <br/> %s", err))
   232  		return
   233  	}
   234  	base, err := dm.DecompressUnMarshalBaseLocale(buf)
   235  	if err != nil {
   236  		c.ShowError(fmt.Errorf("Impossible de <b>décrypter</b> les données du <b>serveur</b>. <br/> %s", err))
   237  		return
   238  	}
   239  	*c.Base = base
   240  	c.ResetAllControllers()
   241  
   242  	// enregistre sur le disque
   243  	err = c.saveBaseToDisk()
   244  	if err != nil {
   245  		c.ShowError(err)
   246  		return
   247  	}
   248  
   249  	c.ShowStandard("Données mises à jour depuis le serveur et enregistrées sur le disque.", false)
   250  }
   251  
   252  // LoadPartialBaseFromServer demande le hash de la base au serveur,
   253  // le compare avec la base courante, et télécharge les différences,
   254  // Met à jour les controllers et enregistre sur le disque en cas de succès.
   255  func (c *MainController) LoadPartialBaseFromServer(progressMonitor func(i uint8)) {
   256  	if progressMonitor == nil {
   257  		c.ShowError(errors.New("Fonction de suivi non fournie !"))
   258  		return
   259  	}
   260  	rep, err := requeteResponseMultipart(apiserver.UrlDB, http.MethodPut, nil)
   261  	if err != nil {
   262  		c.ShowError(fmt.Errorf("Impossible de <b>charger</b> les données de hash depuis le <b>serveur</b>. <br/> %s", err))
   263  		return
   264  	}
   265  	serverHash, err := dm.NewBaseHash(rep[apiserver.DBHashHeaderJSONKey], rep[apiserver.DBHashBinaryKey])
   266  	if err != nil {
   267  		c.ShowError(fmt.Errorf("Impossible de <b>décrypter</b> les données de hash du <b>serveur</b>. <br/> %s", err))
   268  		return
   269  	}
   270  	clientHash, err := c.Base.Hash()
   271  	if err != nil {
   272  		c.ShowError(fmt.Errorf("Impossible de former le <b>hash</b> des données <b>locales</b>. <br/> %s", err))
   273  		return
   274  	}
   275  
   276  	diffs := clientHash.Compare(serverHash)
   277  	nbModified, nbDeleted := diffs.Total()
   278  	if nbModified+nbDeleted == 0 { // rien à faire, base déjà à jour
   279  		c.ShowStandard("Les données locales sont déjà à jour.", false)
   280  		return
   281  	}
   282  
   283  	var partialBase dm.BaseLocale
   284  	if nbModified > 0 { // on demande les différences au serveur
   285  		c.ShowStandard(fmt.Sprintf("Téléchargement des %d différences (suppressions: %d)", nbModified, nbDeleted), true)
   286  
   287  		buf := new(bytes.Buffer)
   288  		err := requestDownload(apiserver.UrlDB, http.MethodPost, diffs.Modified, progressMonitor, buf)
   289  		if err != nil {
   290  			c.ShowError(fmt.Errorf("Impossible de <b>charger</b> la base (partielle) depuis le <b>serveur</b>. <br/> %s", err))
   291  			return
   292  		}
   293  		partialBase, err = dm.DecompressUnMarshalBaseLocale(buf)
   294  		if err != nil {
   295  			c.ShowError(fmt.Errorf("Impossible de <b>décrypter</b> les données (partielles) du <b>serveur</b>. <br/> %s", err))
   296  			return
   297  		}
   298  	}
   299  
   300  	c.Base.MergeFrom(partialBase, diffs.Deleted)
   301  	c.ResetAllControllers()
   302  
   303  	// enregistre sur le disque
   304  	err = c.saveBaseToDisk()
   305  	if err != nil {
   306  		c.ShowError(err)
   307  		return
   308  	}
   309  
   310  	c.ShowStandard("Données bien mises à jour et enregistrées sur le disque.", false)
   311  }