meta données pour cette page
Update de geonetwork
samedi 27 mai 2017, 09:08:59 (UTC+0200) cmartin
Contient un script à lancer pour faire les mises à jour de geonetwork. Tout ce qu'il faut c'est :
- connaître le dossier où le war courant est déployé
- connaître le chemin du war actuel
- avoir un nouveau war sous la main
Testé pour passage de geonetwork 3.0.4 à 3.2.1
SANS GARANTIE
Précautions
AVANT de lancer le script :
- avoir unecopie de la base de données
- avoir une copie du dossier de geonetwork (tar ou autre)
En cas d'échec ça permettra de remettre les choses comme il faut.
- S'assurer que le jre installé correspond à la nouvelle version du war de geonetwork. Ainsi pour geonetwork 3.2.1, il faut un JRE version 8, possiblement openjdk-8-jre-headless .
Avant de lancer le script
Arreter tomcat avant de lancer le script !!
fonctionnement du script
DOIT ÊTRE LANCÉ SOUS LE COMPTE ROOT
- inspection du war actuel
- détection des fichiers ajoutés
- détection des fichiers modifiés (comme connexion à une BDD)
- inspection du nouveau WAR
- mise en place du nouveau WAR dans le dossier
- déploiement du nouveau WAR
- suppression des fichiers obsoletes
- propose de remettre les fichiers modifiés (mais il faut vérifier manuellement que les modifs sont toujours valables, sinon, patatra)
- suppression du cache
Après utilisation du script
Redémarrage de tomcat à votre charge.
Un mot sur le script
C'était un machin en bash, fait par copier/coller de l'historique de mon shell lors des sessions de mise à jour. Évidemment, un tel machin est très instable et ne demande qu'à se casser la figure à la première occasion. Ce qui n'a pas manqué de se produire, lorsque que la locale était différente sur la machine de test par rapport à la machine de prod…
Tout réécrit en python. Le script n'est pas plus clair, AMHA, mais plus robuste certainement. Réécrit dans la nuit d'un vendredi à un samedi, d'où le copyright à Schplurtz.
NB pour moi même :
Le script à utiliser est update-geonetwork
. Il est généré
par le Makefile qui inclut le .mo en français. Le script
a éditer est enpython
Le script
note perso :
Ce script est dans le git IUEM des sysadmins.
Cliquez sur le titre pour télécharger.
- update-geonetwork
#! /usr/bin/env python # encoding: utf-8 # vim: se ts=2 sw=2 et ai: license=''' +-----------------------------------------------------------+ | | | Attention, ce logiciel est potentiellement dangereux ! | | Warting ! This software may be dangerous | | | | Veuillez prendre connaissance de la licence d'utilisation | | Please read the following license text | | | +-----------------------------------------------------------+ Software author : Christophe Martin, Schplurtz@laposte•net Copyright (C) Schplurtz le Déboulonné 2017 FRENCH | ENGLISH -------------------------------------+-------------------------------------- Ce logiciel est un programme | This software is a computer program informatique servant à mettre à | whose purpose is to update jour geonetwork. | geonetwork. | Ce logiciel est régi par la licence | This software is governed by the CeCILL soumise au droit français et | CeCILL license under French law and respectant les principes de | abiding by the rules of diffusion des logiciels libres. | distribution of free software. You Vous pouvez utiliser, modifier | can use, modify and/ or et/ou redistribuer ce programme | redistribute the software under the sous les conditions de la licence | terms of the CeCILL license as CeCILL telle que diffusée par le | circulated by CEA, CNRS and INRIA CEA, le CNRS et l'INRIA sur le site | at the following URL "http://www.cecill.info/". | "http://www.cecill.info/". | En contrepartie de l'accessibilité | As a counterpart to the access to au code source et des droits de | the source code and rights to copie, de modification et de | copy, modify and redistribute redistribution accordés par cette | granted by the license, users are licence, il n'est offert aux | provided only with a limited utilisateurs qu'une garantie | warranty and the software's limitée. Pour les mêmes raisons, | author, the holder of the economic seule une responsabilité restreinte | rights, and the successive pèse sur l'auteur du programme, le | licensors have only limited titulaire des droits patrimoniaux | liability. et les concédants successifs. | | A cet égard l'attention de | In this respect, the user's l'utilisateur est attirée sur les | attention is drawn to the risks risques associés au chargement, à | associated with loading, using, l'utilisation, à la modification | modifying and/or developing or et/ou au développement et à la | reproducing the software by the reproduction du logiciel par | user in light of its specific l'utilisateur étant donné sa | status of free software, that may spécificité de logiciel libre, qui | mean that it is complicated to peut le rendre complexe à manipuler | manipulate, and that also et qui le réserve donc à des | therefore means that it is développeurs et des professionnels | reserved for developers and avertis possédant des | experienced professionals having connaissances informatiques | in-depth computer knowledge. Users approfondies. Les utilisateurs | are therefore encouraged to load sont donc invités à charger et | and test the software's suitability tester l'adéquation du logiciel à | as regards their requirements in leurs besoins dans des conditions | conditions enabling the security of permettant d'assurer la sécurité de | their systems and/or data to be leurs systèmes et ou de leurs | ensured and, more generally, to données et, plus généralement, à | use and operate it in the same l'utiliser et l'exploiter dans les | conditions as regards security. mêmes conditions de sécurité. | | Le fait que vous puissiez accéder à | The fact that you are presently cet en-tête signifie que vous avez | reading this means that you have pris connaissance de la licence | had knowledge of the CeCILL license CeCILL, et que vous en avez accepté | and that you accept its terms. les termes. | ''' import glob import zipfile import filecmp import tempfile import atexit import os import sys import subprocess import shutil import re import gettext import base64 def error( *txt ): sys.stderr.write( "\n".join(txt) + "\n") # thanks to stackoverflow def which(program): import os def is_exe(fpath): return os.path.isfile(fpath) and os.access(fpath, os.X_OK) fpath, fname = os.path.split(program) if fpath: if is_exe(program): return program else: for path in os.environ['PATH'].split(os.pathsep): path = path.strip('"') exe_file = os.path.join(path, program) if is_exe(exe_file): return exe_file return None def cleanup(): os.chdir('/') # rmr(tmpdir) # Surtout pas ! Il peut y rester des fichiers importants rmr(os.path.join(tmpdir,'locale')) # C'est le moins qu'on puisse faire. def question( var, txt ): rep=raw_input( '{0} ({1}) '.format(txt, var) ).strip() if rep == 'exit': exit(0) if rep == '': return var return rep def yn(txt): while True: rep='' rep=question( rep, txt ).strip().lower() if re.match( '\A(o|oui|y|yes)\Z', rep ): return True if re.match( '\A(n|non|no)\Z', rep ): return False def iswar( fn ): ''' a good war file is an existing zip file that contains at least META-INF/MANIFEST.MF . ''' ret=True try: zip=zipfile.ZipFile( fn ) zip.getinfo('META-INF/MANIFEST.MF') except: ret=False return ret def rmr( *fname ): for filename in fname: if os.path.islink(filename) or os.path.isfile(filename): os.remove(filename) # print 'rm {0}'.format(filename) else: for root, dirs, files in os.walk(filename, False): for fn in files: os.remove(os.path.join(root,fn)) # print 'rm {0}'.format(os.path.join(root,fn)) for dn in dirs: os.rmdir(os.path.join(root,dn)) # print 'rmdir {0}'.format(os.path.join(root,dn)) os.rmdir(filename) # print 'rmdir {0}'.format(filename) def bindata(fn): lines='' with open(fn) as data: ok=False for line in data: if not ok and line == '# __DATA__\n': ok=True continue if ok: lines += line[1:] return base64.b64decode(lines) # # Main main Main # me=os.path.abspath(__file__.rstrip('co')) tmpdir=tempfile.mkdtemp() os.makedirs(os.path.join(tmpdir,'locale/fr_FR/LC_MESSAGES')) with open(os.path.join(tmpdir,'locale/fr_FR/LC_MESSAGES','cat.mo'),'w',0644) as cat: cat.write(bindata(me)) #gettext.install('enpython', os.path.join(os.path.dirname(__file__), 'locale')) gettext.install('cat', os.path.join(os.path.join(tmpdir,'locale'))) atexit.register( cleanup ) missing=False for cmd in ['less']: if not which( cmd ): error( _('missing ') + cmd ) missing=True if( missing ): error( '', _("There are missing commands"), _(" bye bye") ) exit( 1 ) # # Accept the license. Or not... # fnlic=os.path.join(tmpdir,'license') with open(fnlic,'w',0644) as f: f.write(license) subprocess.call(['less', '-E', '-X', '-K', _('-PSpace for next page'), fnlic ] ) os.remove(fnlic) if not yn( _("Do you accept the risks and the license ?") ): print _('Ok. exiting') exit(0) # # Now the real job # geonetdir='/var/lib/tomcat8/webapps/geonetwork' while True: geonetdir = question( geonetdir, _('Directory containing current geonetwork')) if os.path.isdir( geonetdir ): webinf=os.path.join( geonetdir, 'WEB-INF' ) if os.path.isdir( webinf ): break error( _('{0} not a dir or not a geonetwork dir...').format(geonetdir), _('try again.') ) oldwar=geonetdir + '.war' while True: oldwar = question( oldwar, _('current war file') ) if iswar(oldwar): break error( '{0} Not a valid/redeable war file.'.format(oldwar), 'try again.' ) newwar='geonetwork-3.2.1.war' while 1: newwar=question( newwar, _('New war file') ) if iswar(newwar): break error( '{0} Not a valid/redeable war file.'.format(newwar), 'try again.' ) geonetdir=os.path.abspath(geonetdir) oldwar=os.path.abspath(oldwar) newwar=os.path.abspath(newwar) print '' print _('geonetwork dir : '), geonetdir print _('current war file : '), oldwar print _('new war file : '), newwar oldcontent=os.path.join(tmpdir,'oldcontent') os.mkdir(oldcontent) os.chdir(oldcontent) print _('Extracting current war in temp dir') zipfile.ZipFile(oldwar, 'r').extractall() # Can't easily get what I want with filecmp.dircmp. It does not have # recursive capabilities. too bad. Let's do it the barbarian way : # get 2 full lists and compare. # diff_cur_old=filecmp.dircmp(geonetdir, oldcontent) # diff_cur_old.report_full_closure() #for file in diff_cur_old.diff_files: # print file #print '+++++++++++++++++++++++++++++++++++++++++++++' print _('Finding all files in current war') os.chdir(oldcontent) oldtree=set() for root, dirs, files in os.walk('.'): for name in files: oldtree.add(os.path.join(root,name)[2:]) #print '==============================================' #for x in oldtree: # print x print _('Finding all files in current installation') os.chdir(geonetdir) currenttree=set() for root, dirs, files in os.walk('.'): for name in files: currenttree.add(os.path.join(root,name)[2:]) #for name in dirs: # currenttree.add(os.path.join(root,name)) #for x in currenttree: # print x print _('Computing file diff between current war and current deployed app') currenttreeenplus=currenttree.copy() currenttreeenplus -= oldtree print _('{0} more files installed than files in current war').format(len(currenttreeenplus)) ''' for x in currenttreeenplus: print x ''' communstree=currenttree.copy() communstree -= currenttreeenplus print _('computed {0} files in common (should be {1})').format( len(communstree), len(oldtree)) print _('searching among {0} files, those that have been modified').format(len(communstree)) modifiedtree=set() for file in sorted(communstree): if not filecmp.cmp( os.path.join(geonetdir,file), os.path.join(oldcontent, file)): modifiedtree.add(file) print _('Removing temporary files') rmr(oldcontent) print _('saving {0} modified files for later use').format(len(modifiedtree)) modifiedfiles=os.path.join( tmpdir, 'modified' ) os.mkdir(modifiedfiles) for filename in modifiedtree: dir=os.path.join( modifiedfiles, os.path.dirname( filename )) if not os.path.exists( dir ): os.makedirs( dir ) shutil.copyfile(os.path.join(geonetdir, filename), os.path.join( modifiedfiles, filename )) print _('Analyzing new war') newzip=zipfile.ZipFile(newwar, 'r') newwartree=set() for filename in newzip.namelist(): newwartree.add(filename) print _('Finding obsolete files') obsoletetree=oldtree.copy() obsoletetree -= newwartree print _('Found {0} obsolete files').format(len(obsoletetree)) if yn( _('all checks are done. Start modifying ?') ): print _("Let's go!") else: print _('Bye bye.') exit(0) print _('extracting new war into {0}').format(geonetdir) os.chdir(geonetdir) newzip.extractall() print _('removing obsolete files') for filename in obsoletetree: os.remove(filename) print _('removing cache') rmr(*glob.glob('WEB-INF/data/data/resources/htmlcache/formatter-cache/public/*' )) rmr(*glob.glob('WEB-INF/data/data/resources/htmlcache/formatter-cache/private/*')) rmr(*glob.glob('WEB-INF/data/data/resources/htmlcache/formatter-cache/info-store.*.db')) rmr(*glob.glob('WEB-INF/data/wro4j-cache.*')) rep='all' while True: rep=question( rep, _(''' There are {0} modified files, what do you want to do ? 0 : restore all 1 : stop now and examine yourself the diffs 2 : ask for each file ''').format(len(modifiedtree)) ).strip().lower() if re.match( '\A[012]\Z', rep ): break error( _('did not understand, sorry...') ) os.chdir( modifiedfiles ) if '1' == rep: print _("OK. You have to examine the situation.\nmodified files are in {0}").format(modifiedfiles) elif '0' == rep: for filename in sorted(modifiedtree): print filename shutil.copyfile(filename, os.path.join(geonetdir,filename)) else: for filename in sorted(modifiedtree): if yn( _("reinstall modified this modified file to that location ?\n{0}\n{1}\n ?"). format(os.path.join(modifiedfiles,filename),os.path.join(geonetdir,filename))): shutil.copyfile(filename, os.path.join(geonetdir,filename)) os.remove(filename) for root, dirs, files in os.walk(modifiedfiles, False): for dn in dirs: try: os.rmdir(os.path.join(root,dn)) except: pass print _('you will find remaining files (if any) in {0}').format(modifiedfiles) print 'don des fichiers à tomcat8.' subprocess.call(['chown', '-hR', 'tomcat8:', geonetdir]) # __DATA__ #3hIElQAAAAAlAAAAHAAAAEQBAAA1AAAAbAIAAAAAAABAAwAAiwAAAEEDAAAKAAAAzQMAABUAAADY #AwAAEQAAAO4DAABAAAAAAAQAACcAAABBBAAAKQAAAGkEAAAiAAAAkwQAACkAAAC2BAAAIAAAAOAE #AAAWAAAAAQUAABgAAAAYBQAACQAAADEFAAAMAAAAOwUAAEAAAABIBQAACwAAAIkFAAAYAAAAlQUA #ABoAAACuBQAAJgAAAMkFAAAsAAAA8AUAABAAAAAdBgAAFAAAAC4GAAAcAAAAQwYAABsAAABgBgAA #EQAAAHwGAAAIAAAAjgYAAA8AAACXBgAAQwAAAKcGAAAOAAAA6wYAABcAAAD6BgAAJwAAABIHAAA4 #AAAAOgcAAAoAAABzBwAALQAAAH4HAAAyAAAArAcAACgAAADfBwAAewEAAAgIAAC+AAAAhAkAAAUA #AABDCgAAHAAAAEkKAAAZAAAAZgoAAGQAAACACgAAMwAAAOUKAAApAAAAGQsAADYAAABDCwAAPAAA #AHoLAAAvAAAAtwsAACQAAADnCwAAIAAAAAwMAAAJAAAALQwAABMAAAA3DAAATwAAAEsMAAAOAAAA #mwwAACcAAACqDAAAIAAAANIMAAA3AAAA8wwAAEEAAAArDQAAEgAAAG0NAAAVAAAAgA0AABkAAACW #DQAAIgAAALANAAAVAAAA0w0AAAcAAADpDQAAFgAAAPENAABBAAAACA4AABcAAABKDgAAJgAAAGIO #AABIAAAAiQ4AADoAAADSDgAADgAAAA0PAAA6AAAAHA8AAEoAAABXDwAAOAAAAKIPAAABAAAAIAAA #AAwAAAAlAAAAHgAAAAcAAAAcAAAAHQAAAAAAAAARAAAAAAAAABcAAAAbAAAAJAAAAAAAAAALAAAA #AAAAABYAAAACAAAAIwAAAAAAAAAGAAAACQAAAA0AAAASAAAAIgAAAAAAAAAIAAAADgAAAAAAAAAA #AAAABAAAAAAAAAAhAAAAGAAAAAAAAAAFAAAAAAAAAAMAAAAfAAAAFAAAAAAAAAAKAAAAFQAAABMA #AAAZAAAAGgAAAAAAAAAAAAAAAAAAAAAAAAAQAAAADwAAAAAKVGhlcmUgYXJlIHswfSBtb2RpZmll #ZCBmaWxlcywgd2hhdCBkbyB5b3Ugd2FudCB0byBkbyA/CgowIDogcmVzdG9yZSBhbGwKMSA6IHN0 #b3Agbm93IGFuZCBleGFtaW5lIHlvdXJzZWxmIHRoZSBkaWZmcwoyIDogYXNrIGZvciBlYWNoIGZp #bGUKACAgIGJ5ZSBieWUALVBTcGFjZSBmb3IgbmV4dCBwYWdlAEFuYWx5emluZyBuZXcgd2FyAENv #bXB1dGluZyBmaWxlIGRpZmYgYmV0d2VlbiBjdXJyZW50IHdhciBhbmQgY3VycmVudCBkZXBsb3ll #ZCBhcHAARGlyZWN0b3J5IGNvbnRhaW5pbmcgY3VycmVudCBnZW9uZXR3b3JrAERvIHlvdSBhY2Nl #cHQgdGhlIHJpc2tzIGFuZCB0aGUgbGljZW5zZSA/AEV4dHJhY3RpbmcgY3VycmVudCB3YXIgaW4g #dGVtcCBkaXIARmluZGluZyBhbGwgZmlsZXMgaW4gY3VycmVudCBpbnN0YWxsYXRpb24ARmluZGlu #ZyBhbGwgZmlsZXMgaW4gY3VycmVudCB3YXIARmluZGluZyBvYnNvbGV0ZSBmaWxlcwBGb3VuZCB7 #MH0gb2Jzb2xldGUgZmlsZXMATGV0J3MgZ28hAE5ldyB3YXIgZmlsZQBPSy4gWW91IGhhdmUgdG8g #ZXhhbWluZSB0aGUgc2l0dWF0aW9uLgptb2RpZmllZCBmaWxlcyBhcmUgaW4gezB9AE9rLiBleGl0 #aW5nAFJlbW92aW5nIHRlbXBvcmFyeSBmaWxlcwBUaGVyZSBhcmUgbWlzc2luZyBjb21tYW5kcwBh #bGwgY2hlY2tzIGFyZSBkb25lLiBTdGFydCBtb2RpZnlpbmcgPwBjb21wdXRlZCB7MH0gZmlsZXMg #aW4gY29tbW9uIChzaG91bGQgYmUgezF9KQBjdXJyZW50IHdhciBmaWxlAGN1cnJlbnQgd2FyIGZp #bGUgOiAgAGRpZCBub3QgdW5kZXJzdGFuZCwgc29ycnkuLi4AZXh0cmFjdGluZyBuZXcgd2FyIGlu #dG8gezB9AGdlb25ldHdvcmsgZGlyIDogAG1pc3NpbmcgAG5ldyB3YXIgZmlsZSA6IAByZWluc3Rh #bGwgbW9kaWZpZWQgdGhpcyBtb2RpZmllZCBmaWxlIHRvIHRoYXQgbG9jYXRpb24gPwp7MH0KezF9 #CiA/AHJlbW92aW5nIGNhY2hlAHJlbW92aW5nIG9ic29sZXRlIGZpbGVzAHNhdmluZyB7MH0gbW9k #aWZpZWQgZmlsZXMgZm9yIGxhdGVyIHVzZQBzZWFyY2hpbmcgYW1vbmcgezB9IGZpbGVzLCB0aG9z #ZSB0aGF0IGhhdmUgYmVlbiBtb2RpZmllZAB0cnkgYWdhaW4uAHlvdSB3aWxsIGZpbmQgcmVtYWlu #aW5nIGZpbGVzIChpZiBhbnkpIGluIHswfQB7MH0gbW9yZSBmaWxlcyBpbnN0YWxsZWQgdGhhbiBm #aWxlcyBpbiBjdXJyZW50IHdhcgB7MH0gbm90IGEgZGlyIG9yIG5vdCBhIGdlb25ldHdvcmsgZGly #Li4uAFByb2plY3QtSWQtVmVyc2lvbjogClJlcG9ydC1Nc2dpZC1CdWdzLVRvOiAKUE9ULUNyZWF0 #aW9uLURhdGU6IDIwMTctMDUtMjcgMDg6MTQrMDIwMApQTy1SZXZpc2lvbi1EYXRlOiAyMDE3LTA1 #LTI3IDA5OjExKzAyMDAKTGFzdC1UcmFuc2xhdG9yOiBDaHJpc3RvcGhlIE1hcnRpbiA8c2NocGx1 #cnR6QGxhcG9zdGUubmV0PgpMYW5ndWFnZS1UZWFtOiBGcmVuY2gKTGFuZ3VhZ2U6IGZyCk1JTUUt #VmVyc2lvbjogMS4wCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD1VVEYtOApDb250 #ZW50LVRyYW5zZmVyLUVuY29kaW5nOiA4Yml0ClBsdXJhbC1Gb3JtczogbnBsdXJhbHM9MjsgcGx1 #cmFsPShuID4gMSk7ClgtR2VuZXJhdG9yOiBQb2VkaXQgMS44LjcuMQoACklsIHkgYSB7MH0gZmlj #aGllcnMgbW9kaWZpw6lzLCBxdWUgdm91bGV6IHZvdXMgZmFpcmUgPwoKMCA6IExlcyByZXN0YXVy #ZXIgdG91cyBkJ3VuIGNvdXAuCjEgOiBBcnLDqnRlciBtYWludGVuYW50IGV0IGV4YW1pbmVyIHZv #dXMgbcOqbWUgbGVzIGRpZmbDqXJlbmNlcy4KMiA6IERlbWFuZGVyIHBvdXIgY2hhcXVlIGZpY2hp #ZXIuCgBTYWx1dAAtUEVzcGFjZSBwb3VyIGZhaXJlIGTDqWZpbGVyAEFuYWxpc2UgZHUgbm91dmVh #dSB3YXIuLi4AQ2FsY3VsIGRlIGxhIGRpZmbDqXJlbmNlIGRlIGZpY2hpZXJzIGVudHJlIGxlIHdh #ciBhY3R1ZWwgZXQgbCdhcHBsaWNhdGlvbiBhY3R1ZWxsZW1lbnQgZMOpcGxvecOpZS4uLgBEb3Nz #aWVyIGNvbnRlbmFudCBsYSB2ZXJzaW9uIGFjdHVlbGxlIGRlIGdlb25ldHdvcmsAQWNjZXB0ZXog #dm91cyBsZXMgcmlzcXVlcyBldCBsYSBsaWNlbmNlID8ARXh0cmFjdGlvbiBkdSB3YXIgYWN0dWVs #IGRhbnMgdW4gZG9zc2llciB0ZW1wb3JhaXJlLi4uAFJlY2hlcmNoZSBkZSB0b3VzIGxlcyBmaWNo #aWVycyBkZSBsJ2luc3RhbGxhdGlvbiBhY3R1ZWxsZS4uLgBSZWNoZXJjaGUgZGUgdG91cyBsZXMg #ZmljaGllcnMgZHUgd2FyIGFjdHVlbC4uLgBSZWNoZXJjaGUgZGVzIGZpY2hpZXJzIG9ic29sw6h0 #ZXMuLi4AVHJvdXbDqSB7MH0gZmljaGllcnMgb2Jzb2zDqHRlcy4AT24geSB2YSAhAE5vdXZlYXUg #ZmljaGllciB3YXIARCdhY2NvcmQuIFZvdXMgZGV2ZXogZXhhbWluZXIgbGEgc2l0dWF0aW9uCmxl #cyBmaWNoaWVycyBtb2RpZmnDqXMgc29udCBkYW5zIHswfQBEJ2FjY29yZC4gRmluLgBTdXBwcmVz #c2lvbiBkZXMgZmljaGllcnMgdGVtcG9yYWlyZXMuLi4ASWwgbWFucXVlIGRlcyBjb21tYW5kZXMg #ZXh0ZXJuZXMAVG91cyBsZXMgdGVzdHMgc29udCByw6lhbGlzw6lzLiBDb21tZW5jZXIgw6AgbW9k #aWZpZXIgPwBOb21icmUgZGUgZmljaGllcnMgZW4gY29tbXVuIGNhbGN1bMOpIDogezB9LiAoRGV2 #cmFpdCDDqnRyZSB7MX0uKQBGaWNoaWVyIHdhciBhY3R1ZWwARmljaGllciB3YXIgYWN0dWVsIDog #AFJpZW4gY29tcHJpcywgZMOpc29sw6kuLi4ARXh0cmFjdGlvbiBkdSBub3V2ZWF1IHdhciBkYW5z #IHswfQBEb3NzaWVyIGdlb25ldHdvcmsgOiAAbWFucXVlIABOb3V2ZWF1IGZpY2hpZXIgd2FyIDog #AFLDqWluc3RhbGxlciBjZSBmaWNoaWVyIG1vZGlmaWVyIMOgIGNldCBlbXBsYWNlbWVudCA6IAp7 #MH0KezF9CiA/AFN1cHByZXNzaW9uIGR1IGNhY2hlLi4uAFN1cHByZXNzaW9uIGRlcyBmaWNoaWVy #cyBvYnNvbMOodGVzLi4uAFNhdXZlZ2FyZGUgZGVzIHswfSBmaWNoaWVycyBtb2RpZmnDqXMgcG91 #ciByw6l1dGlsaXNhdGlvbiB1bHTDqXJpZXVyZS4uLgBSZWNoZXJjaGUgZGVzIGZpY2hpZXJzIG1v #ZGlmacOpcyBwYXJtaSBjZXMgezB9IGZpY2hpZXJzLi4uAEVzc2F5ZSBlbmNvcmUuAFZvdXMgdHJv #dXZlcmV6IGxlcyBmaWNoaWVycyByZXN0YW50LCBzJ2lsIHkgZW4gYSwgZGFucyB7MH0ASWwgeSBh #IHswfSBmaWNoaWVycyBkZSBwbHVzIGRhbnMgbCdhcHBsaWNhdGlvbiBkw6lwbG95w6llIHF1ZSBk #YW5zIGxlIHdhci4AIHswfSBuJ2VzdCBwYXMgdW4gZG9zc2llciBvdSBuZSBjb250aWVudCBwYXMg #Z2VvbmV0d29yay4A