]> git.uio.no Git - u/mrichter/AliRoot.git/blob - PWGPP/QA/scripts/alienSync.sh
update docs
[u/mrichter/AliRoot.git] / PWGPP / QA / scripts / alienSync.sh
1 #!/bin/bash
2 #
3 #  - script to sync a group of files on alien with a local cache
4 #    downloads only new and updated files
5 #  - by default it mirrors the directory structure in a specified local location
6 #    (the local chache location and paths can be manipulated.)
7 #  - needs a configured config file (by default alienSync.config)
8 #    and a working alien environment (token and at least $ALIEN_DIR or $ALIEN_ROOT set)
9 #
10 #  origin: Mikolaj Krzewicki, mikolaj.krzewicki@cern.ch
11 #
12 if [ ${BASH_VERSINFO} -lt 4 ]; then
13   echo "bash version >= 4 needed, you have ${BASH_VERSION}, exiting..."
14   exit 1
15 fi
16
17 main()
18 {
19   if [[ $# -lt 1 ]]; then
20     echo "Usage:  ${0##*/} configFile=/path/to/config"
21     echo "expert: ${0##*/} alienFindCommand=\"alien_find /some/path/ file\" [opt=value]"
22     echo "        ${0##*/} alienFindCommand=\"alien_find /some/path/ file\" localPathPrefix=\${PWD}"
23     echo
24     echo "by default files are downloaded to current dir, or \${alienSync_localPathPrefix}, if set."
25     echo "At least specify alienFindCommand, either on command line or in the configFile."
26     echo "the logs go by default to localPathPrefix/alienSyncLogs"
27     return
28   fi
29   
30   # try to load the config file
31   #[[ ! -f $1 ]] && echo "config file $1 not found, exiting..." | tee -a $logFile && exit 1
32   if ! parseConfig "$@"; then return 1; fi
33
34   [[ -z ${alienFindCommand} ]] && echo "alienFindCommand not defined!" && return 1
35
36   #if not set, use the default group
37   [[ -z ${alienSyncFilesGroupOwnership} ]] && alienSyncFilesGroupOwnership=$(id -gn)
38
39   # do some accounting
40   [[ ! -d $logOutputPath ]] && echo "logOutputPath not available, creating..." && mkdir -p $logOutputPath && chgrp ${alienSyncFilesGroupOwnership} ${logOutputPath}
41   [[ ! -d $logOutputPath ]] && echo "could not create log dir, exiting..." && exit 1
42   dateString=$(date +%Y-%m-%d-%H-%M)
43   logFile=$logOutputPath/alienSync-$dateString.log
44   echo "$0 $@"|tee -a $logFile
45   echo ""|tee -a $logFile
46   echo log: $logFile
47   
48   #be nice and allow group members access as well (002 will create dirs with 775 and files with 664)
49   umask 0002
50
51   #lock
52   lockFile=$logOutputPath/runningNow.lock
53   [[ -f $lockFile && ${allowConcurrent} -ne 1 ]] && echo "locked. Another process running? ($lockFile)" | tee -a $logFile && exit 1
54   touch $lockFile
55   [[ ! -f $lockFile ]] && echo "unable to create lock. exiting..." | tee -a $logFile && exit 1
56
57   #redirect all output to a log
58   if [[ $allOutputToLog -eq 1 ]]; then
59     exec 6>&1
60     exec 1>$logFile 2>&1
61   fi
62   
63   newFilesList=$logOutputPath/"newFiles.list"
64   rm -f $newFilesList
65   touch $newFilesList
66   redoneFilesList=$logOutputPath/"redoneFiles.list"
67   rm -f $redoneFilesList
68   touch $redoneFilesList
69   updatedFilesList="${logOutputPath}/updatedFiles.list"
70   failedDownloadList="${logOutputPath}/failedDownload.list"
71   touch ${failedDownloadList}
72
73
74   # check the config
75   [[ -z $alienFindCommand ]] && echo "alienFindCommand not defined, exiting..." && exitScript 1
76   [[ -z ${localPathPrefix} ]] && echo "localPathPrefix not defined, exiting..." && exitScript 1
77   [[ -z $logOutputPath ]] && echo "logOutputPath not defined, exiting..." && exitScript 1
78   [[ -z $secondsToSuicide ]] && echo "setting default secondsToSuicide of 10 hrs..." && secondsToSuicide=$(( 10*3600 ))
79
80   # init alien 
81   [[ -z $ALIEN_ROOT && -n $ALIEN_DIR ]] && ALIEN_ROOT=$ALIEN_DIR
82   #if ! haveAlienToken; then
83   #  $ALIEN_ROOT/api/bin/alien-token-destroy
84     $ALIEN_ROOT/api/bin/alien-token-init $alienUserName
85   #fi
86   #if ! haveAlienToken; then
87   #  if [[ $allOutputToLog -eq 1 ]]; then
88   #    exec 1>&6 6>&-
89   #  fi
90   #  echo "problems getting token! exiting..." | tee -a $logFile
91   #  exitScript 1
92   #fi
93   #ls -ltr /tmp/gclient_env_$UID
94   #cat /tmp/gclient_env_$UID
95   source /tmp/gclient_env_$UID
96
97   #set a default timeout for grid access
98   [[ -z $copyTimeout ]] && copyTimeout=600
99   export GCLIENT_COMMAND_MAXWAIT=$copyTimeout
100
101   localAlienDatabase=$logOutputPath/localAlienDatabase.list
102   localFileList=$logOutputPath/localFile.list
103   
104   alienFileListCurrent=$logOutputPath/alienFileDatabase.list
105   [[ ! -f $localFileList ]] && touch $localFileList
106   candidateLocalFileDatabase=$logOutputPath/candidateLocalFileDatabase.list
107
108   #here we produce the current alien file list
109   if [[ -n ${useExistingAlienFileDatabase} && -f ${localAlienDatabase} ]]; then
110     #we use the old one
111     echo "using ${localAlienDatabase} instead of full alien search"
112     echo cp -f ${localAlienDatabase} ${alienFileListCurrent}
113     cp -f ${localAlienDatabase} ${alienFileListCurrent}
114   else
115     #we make a new one
116     echo "eval $alienFindCommand > $alienFileListCurrent"
117     eval "$alienFindCommand" > $alienFileListCurrent
118   fi
119
120   echo "number of files in the collection: $(wc -l $alienFileListCurrent)"
121   #create a list of candidate destination locations
122   #this is in case there are more files on alien trying to get to the same local destination
123   #in which case we take the one with the youngest ctime (later in code)
124   if [[ -n ${destinationModifyCommand} ]]; then
125     echo eval "cat $alienFileListCurrent | ${destinationModifyCommand} | sed \"s,^,${localPathPrefix},\"  > ${candidateLocalFileDatabase}"
126     eval "cat $alienFileListCurrent | ${destinationModifyCommand} | sed \"s,^,${localPathPrefix},\"  > ${candidateLocalFileDatabase}"
127   fi
128
129   #logic is: if file list is missing we force the md5 recalculation
130   [[ ! -f $localAlienDatabase ]] && forceLocalMD5recalculation=1 && echo "forcing local MD5 sum recalculation" && cp -f $alienFileListCurrent $localAlienDatabase
131
132   #since we grep through the files frequently, copy some stuff to tmpfs for fast access
133   tmp=$(mktemp -d 2>/dev/null)
134   if [[ -d $tmp ]]; then
135     cp $localAlienDatabase $tmp
136     cp $localFileList $tmp
137     cp $alienFileListCurrent $tmp
138     [[ -f ${candidateLocalFileDatabase} ]] && cp ${candidateLocalFileDatabase} ${tmp}
139   else
140     tmp=$logOutputPath
141   fi
142
143   echo "starting downloading:"
144   lineNumber=0
145   alienFileCounter=0
146   localFileCounter=0
147   downloadedFileCounter=0
148   while read -r alienFile md5alien timestamp size
149   do
150     ((lineNumber++))
151     
152     #sometimes the md5 turns out empty and is then stored as a "." to avoid problems parsing
153     [[ "$md5alien" =~ "." ]] && md5alien=""
154     
155     [[ -n $timeStampInLog ]] && date
156     [[ $SECONDS -ge $secondsToSuicide ]] && echo "$SECONDS seconds passed, exiting by suicide..." && break
157     [[ "$alienFile" != "/"*"/"?* ]] && echo "WARNING: read line not path-like: $alienFile" && continue
158     ((alienFileCounter++))
159     destination=${localPathPrefix}/${alienFile}
160     destination=${destination//\/\///} #remove double slashes
161     [[ -n ${destinationModifyCommand} ]] && destination=$( eval "echo ${destination} | ${destinationModifyCommand}" )
162     destinationdir=${destination%/*}
163     [[ -n $softLinkName ]] && softlinktodestination=${destinationdir}/${softLinkName}
164     tmpdestination="${destination}.aliensyncTMP"
165     
166     #if we allow concurrent running (DANGEROUS) check if somebody is already trying to process this file
167     if [[ -f ${tmpdestination} && ${allowConcurrent} -eq 1 ]]; then 
168       echo "$tmpdestination exists - concurrent donwload? skipping..."
169       continue
170     fi
171
172     if [[ -n ${destinationModifyCommand} ]]; then
173       #find the candidate in the database, in case there are more files trying to go to the same
174       #place due to $destinationModifyCommand which alters the final path, find the one
175       #with the largest ctime (3rd field in the database list) and check if that is the current one
176       #if not - skip
177       #echo grep -n ${destination} $candidateLocalFileDatabase | sed "s/:/ /"  | sort -rk4
178       #grep -n ${destination} $candidateLocalFileDatabase| sed "s/:/ /"  | sort -rk4
179       #this guy contains: index of the original entry, local file name, md5, ctime
180       candidateDBrecord=($(grep -n ${destination} $tmp/${candidateLocalFileDatabase##*/}| sed "s/:/ /"  | sort -rk4|head -n1 ))
181       originalEntryIndex=${candidateDBrecord[0]}
182       [[ $lineNumber -ne $originalEntryIndex ]] && continue
183     fi
184     
185     redownloading=""
186     if [[ -f ${destination} ]]; then
187       #soft link the downloaded file (maybe to provide a consistent link to the latest version)
188       if [[ -n $softlinktodestination ]]; then
189         echo ln -sf ${destination} ${softlinktodestination}
190         ln -sf ${destination} ${softlinktodestination}
191       fi
192       ((localFileCounter++))
193       
194       localDBrecord=($(grep $alienFile $tmp/${localAlienDatabase##*/}))
195       md5local=${localDBrecord[1]}
196
197       #sometimes the md5 turns out empty and is then stored as a "." to avoid problems parsing
198       [[ "$md5local" =~ "." ]] && md5local=""
199
200       if [[ $forceLocalMD5recalculation -eq 1 || -z $md5local ]]; then
201         md5recalculated=$(checkMD5sum ${destination})
202         [[ "$md5local" != "$md5recalculated" ]] && echo "WARNING: local copy change ${destination}"
203         md5local=${md5recalculated}
204       fi
205       if [[ "$md5local" == "$md5alien" && -n $md5alien ]]; then
206         echo "OK ${destination} $md5alien"
207         if ! grep -q ${destination} $tmp/${localFileList##*/}; then
208           echo ${destination} >> $localFileList
209         fi
210         continue
211       fi
212       if [[ -z $md5alien ]]; then
213         if ! grep -q ${destination} $tmp/${localFileList##*/}; then
214           echo ${destination} >> $localFileList
215         fi
216         echo "WARNING: missing alien md5, leaving the local file as it is"
217         continue
218       fi
219       echo "WARNING: md5 mismatch ${destination}"
220       echo "  $md5local $md5alien"
221       redownloading=1
222     fi
223     
224     [[ -f $tmpdestination ]] && echo "WARNING: stale $tmpdestination, removing" && rm $tmpdestination
225     
226     mkdir -p ${destinationdir} && chgrp ${alienSyncFilesGroupOwnership} ${destinationdir}
227     [[ ! -d $destinationdir ]] && echo cannot access $destinationdir && continue
228
229     #check token
230     #if ! haveAlienToken; then
231     #  $ALIEN_ROOT/api/bin/alien-token-init $alienUserName
232     #  #source /tmp/gclient_env_$UID
233     #fi
234     
235     export copyMethod
236     export copyScript
237     export copyTimeout
238     export copyTimeoutHard
239     echo copyFromAlien "$alienFile" "$tmpdestination"
240     [[ $pretend -eq 1 ]] && continue
241     copyFromAlien $alienFile $tmpdestination
242     chgrp ${alienSyncFilesGroupOwnership} $tmpdestination
243
244     # if we didn't download remove the destination in case we tried to redownload 
245     # a corrupted file
246     [[ ! -f $tmpdestination ]] && echo "file not downloaded" && rm -f ${destination} && continue
247
248     downloadOK=0
249     #verify the downloaded md5 if available, validate otherwise...
250     if [[ -n $md5alien ]]; then
251       md5recalculated=$(checkMD5sum ${tmpdestination})
252       if [[ ${md5alien} == ${md5recalculated} ]]; then
253         echo "OK md5 after download"
254         downloadOK=1
255       else
256         echo "failed verifying md5 $md5alien of $tmpdestination"
257       fi
258     else
259       downloadOK=1
260     fi
261
262     #handle zip files - check the checksums
263     if [[ $alienFile =~ '.zip' && $downloadOK -eq 1 ]]; then
264       echo "checking integrity of zip archive $tmpdestination"
265       if unzip -t $tmpdestination; then
266         downloadOK=1
267       else
268         downloadOK=0
269       fi
270     fi
271
272     if [[ $downloadOK -eq 1 ]]; then
273       echo mv $tmpdestination ${destination}
274       mv $tmpdestination ${destination}
275       chgrp ${alienSyncFilesGroupOwnership} ${destination}
276       ((downloadedFileCounter++))
277       if [[ -n $softlinktodestination ]]; then
278         echo ln -s ${destination} $softlinktodestination
279         ln -s ${destination} $softlinktodestination
280       fi
281       [[ -z $redownloading ]] && echo ${destination} >> $newFilesList
282       [[ -n $redownloading ]] && echo ${destination} >> $redoneFilesList
283       if ! grep -q ${destination} $tmp/${localFileList##*/}; then
284         echo ${destination} >> $localFileList
285       fi
286       [[ -n ${postCommand} ]] && ( cd ${destinationdir}; eval "${postCommand}" )
287       if grep -q ${alienFile} ${failedDownloadList}; then
288         echo "removing ${alienFile} from ${failedDownloadList}"
289         grep -v ${alienFile} ${failedDownloadList} >tmpUpdatedFailed
290         mv tmpUpdatedFailed ${failedDownloadList}
291       fi
292     else
293       echo "download not validated, NOT moving to ${destination}..."
294       echo "removing $tmpdestination"
295       rm -f $tmpdestination
296       echo ${alienFile} >> ${failedDownloadList}
297       continue
298     fi
299
300     [[ -f $tmpdestination ]] && echo "WARNING: tmpdestination should not still be here! removing..." && rm -r ${tmpdestination}
301
302     if [[ $unzipFiles -eq 1 ]]; then
303       echo unzip -o ${destination} -d ${destinationdir}
304       unzip -o ${destination} -d ${destinationdir}
305     fi
306
307     echo
308   done < ${alienFileListCurrent}
309
310   [[ $alienFileCounter -gt 0 ]] && mv -f $alienFileListCurrent $localAlienDatabase
311
312   echo "${0##*/} DONE"
313  
314   if [[ $allOutputToLog -eq 1 ]]; then
315     exec 1>&6 6>&-
316   fi
317  
318   cat ${newFilesList} ${redoneFilesList} > ${updatedFilesList}
319   
320   echo alienFindCommand:
321   echo "  $alienFindCommand"
322   echo
323   echo "files on alien: $alienFileCounter"
324   echo "local files before: $localFileCounter"
325   echo "files downloaded: $downloadedFileCounter"
326   echo
327   echo "new files:"
328   echo
329   cat $newFilesList
330   echo
331   echo "redone files:"
332   echo
333   cat $redoneFilesList
334   echo
335   echo
336   
337   #output the list of failed files to stdout, so the cronjob can mail it
338   echo '###############################'
339   echo "failed to download from alien:"
340   echo
341   local tmpfailed=$(mktemp)
342   [[ "$(cat ${failedDownloadList} | wc -l)" -gt 0 ]] && sort ${failedDownloadList} | uniq -c | awk 'BEGIN{print "#tries\t file" }{print $1 "\t " $2}' | tee ${tmpfailed}
343   
344   [[ -n ${MAILTO} ]] && echo $logFile | mail -s "alienSync ${alienFindCommand} done" ${MAILTO}
345
346   echo
347   echo
348   echo '###############################'
349   echo "eval ${executeEnd}"
350   eval "${executeEnd}"
351
352   exitScript 0
353 }
354
355 exitScript()
356 {
357   echo
358   echo removing $lockFile
359   rm -f $lockFile
360   echo removing $tmp
361   rm -rf $tmp
362   exit $1
363 }
364
365 alien_find()
366 {
367   # like a regular alien_find command
368   # output is a list with md5 sums and ctimes
369   executable="$ALIEN_ROOT/api/bin/gbbox find"
370   [[ ! -x ${executable% *} ]] && echo "### error, no $executable..." && return 1
371   [[ -z $logOutputPath ]] && logOutputPath="./"
372
373   maxCollectionLength=10000
374
375   export GCLIENT_COMMAND_MAXWAIT=600
376   export GCLIENT_COMMAND_RETRY=20
377   export GCLIENT_SERVER_RESELECT=4
378   export GCLIENT_SERVER_RECONNECT=2
379   export GCLIENT_RETRY_DAMPING=1.2
380   export GCLIENT_RETRY_SLEEPTIME=2
381
382   iterationNumber=0
383   numberOfFiles=$maxCollectionLength
384   rm -f $logOutputPath/alien_find.err
385   while [[ $numberOfFiles -ge $maxCollectionLength && $iterationNumber -lt 100 ]]; do
386     numberOfFiles=0
387     offset=$((maxCollectionLength*iterationNumber-1)); 
388     [[ $offset -lt 0 ]] && offset=0; 
389     $executable -x coll -l ${maxCollectionLength} -o ${offset} "$@" 2>>$logOutputPath/alien_find.err \
390     | while read -a fields;
391     do
392       nfields=${#fields[*]}
393       turl=""
394       md5=""
395       ctime=""
396       size=""
397       for ((x=1;x<=${nfields};x++)); do
398         field=${fields[${x}]}
399         if [[ "${field}" == "md5="* ]]; then
400           eval ${field}
401         fi
402         if [[ "${field}" == "turl="* ]]; then
403           eval ${field}
404         fi
405         if [[ "${field}" == "ctime="* ]]; then
406           eval ${field}" "${fields[((x+1))]}
407         fi
408         if [[ "${field}" == "size="* ]]; then
409           eval ${field}" "${fields[((x+1))]}
410         fi
411       done
412       ctime=$( date -d "${ctime}" +%s 2>/dev/null)
413       [[ -z $md5 ]] && md5="."
414       [[ -n "$turl" ]] && echo "${turl//"alien://"/} ${md5} ${ctime} ${size}" && ((numberOfFiles++))
415     done
416     ((iterationNumber++))
417   done
418   return 0
419 }
420
421 alien_find_split()
422 {
423   #split the search in sub searches in the subdirectories of the base path
424   basePath=${1}
425   searchTerm=${2}
426   subPathSelection=${3}
427   [[ -z ${subPathSelection} ]] && subPathSelection=".*"
428   gbbox ls ${basePath} 2>/dev/null | \
429   while read subPath; do
430     [[ ! ${subPath} =~ ${subPathSelection} ]] && continue
431     alien_find ${basePath}/${subPath} ${searchTerm}
432   done 
433 }
434
435 listCollectionContents()
436 {
437   #find the xml collections and print the list of filenames and hashes
438   while read -a fields; do
439     nfields=${#fields[*]}
440     turl=""
441     md5=""
442     ctime=""
443     for ((x=1;x<=${nfields};x++)); do
444       field=${fields[${x}]}
445       if [[ "${field}" == "md5="* ]]; then
446         eval ${field}
447       fi
448       if [[ "${field}" == "turl="* ]]; then
449         eval ${field}
450       fi
451       if [[ "${field}" == "ctime="* ]]; then
452         eval "${field} ${fields[((x+1))]}"
453       fi
454     done
455     ctime=$( date -d "${ctime}" +%s 2>/dev/null)
456     [[ -n "$turl" ]] && echo "${turl//"alien://"/} ${md5} ${ctime}"
457   done < <(catCollections $1 $2 2>/dev/null)
458 }
459
460 catCollections()
461 {
462   #print the contents of collection(s)
463   if [[ $# -eq 2 ]]; then
464     while read collection; do
465       [[ $collection != "/"*"/"?* ]] && continue
466       gbbox cat $collection
467     done < <(alien_find $1 $2)
468   elif [[ $# -eq 1 ]]; then
469     gbbox cat $1
470   fi
471 }
472
473 haveAlienToken()
474 {
475   #only get a new token if the old one expires soon
476   maxExpireTime=$1
477   [[ -z $maxExpireTime ]] && maxExpireTime=4000
478   [[ -z $ALIEN_ROOT ]] && echo "no ALIEN_ROOT!" && return 1
479   now=$(date "+%s")
480   tokenExpirationTime=$($ALIEN_ROOT/api/bin/alien-token-info|grep Expires)
481   tokenExpirationTime=$(date -d "${tokenExpirationTime#*:}" "+%s")
482   secondsToExpire=$(( tokenExpirationTime-now ))
483   if [[ $secondsToExpire -lt $maxExpireTime ]]; then
484     return 1
485   else
486     echo "token valid for another $secondsToExpire seconds"
487     return 0
488   fi
489 }
490
491 copyFromAlien()
492 {
493   #copy the file $1 to $2 using a specified method
494   #uses the "timeout" command to make sure the 
495   #download processes will not hang forever.
496   #
497   [[ -z $copyTimeout ]] && copyTimeout=600
498   [[ -z $copyTimeoutHard ]] && copyTimeoutHard=1200
499   src=${1//"alien://"/}
500   src="alien://${src}"
501   dst=$2
502   if [[ "$copyMethod" == "tfilecp" ]]; then
503     if which timeout &>/dev/null; then
504       echo timeout $copyTimeout root -b -q "$copyScript(\"$src\",\"$dst\")"
505       timeout $copyTimeout root -b -q "$copyScript(\"$src\",\"$dst\")"
506     else
507       echo root -b -q "$copyScript(\"$src\",\"$dst\")"
508       root -b -q "$copyScript(\"$src\",\"$dst\")"
509     fi
510   else
511     if which timeout &>/dev/null; then
512       echo timeout $copyTimeout $ALIEN_ROOT/api/bin/alien_cp $src $dst
513       timeout $copyTimeout $ALIEN_ROOT/api/bin/alien_cp $src $dst
514     else
515       echo $ALIEN_ROOT/api/bin/alien_cp $src $dst
516       $ALIEN_ROOT/api/bin/alien_cp $src $dst
517     fi
518   fi
519 }
520
521 parseConfig()
522 {
523   #config file
524   configFile=""
525   alienFindCommand=""
526   secondsToSuicide=$(( 10*3600 ))
527   localPathPrefix="${PWD}"
528   #define alienSync_localPathPrefix in your env to have a default central location
529   [[ -n ${alienSync_localPathPrefix} ]] && localPathPrefix=${alienSync_localPathPrefix}
530   unzipFiles=0
531   allOutputToLog=0
532
533   args=("$@")
534
535   #first, check if the config file is configured
536   #is yes - source it so that other options can override it
537   #if any
538   for opt in "${args[@]}"; do
539     if [[ ${opt} =~ configFile=.* ]]; then
540       eval "${opt}"
541       [[ ! -f ${configFile} ]] && echo "configFile ${configFile} not found, exiting..." && return 1
542       echo "using config file: ${configFile}"
543       source "${configFile}"
544       break
545     fi
546   done
547
548   #then, parse the options as they override the options from file
549   for opt in "${args[@]}"; do
550     if [[ ! "${opt}" =~ .*=.* ]]; then
551       echo "badly formatted option ${var}, should be: option=value, stopping..."
552       return 1
553     fi
554     local var="${opt%%=*}"
555     local value="${opt#*=}"
556     echo "${var} = ${value}"
557     export ${var}="${value}"
558   done
559
560   #things that by default depend on other variables should be set here, after the dependencies
561   [[ -z ${logOutputPath} ]] && logOutputPath="${localPathPrefix}/alienSyncLogs"
562 }
563
564 checkMD5sum()
565 {
566   local file="${1}"
567   local md5=""
568   [[ ! -f ${file} ]] && return 1
569   if which md5sum &>/dev/null; then
570     local tmp=($(md5sum ${file}))
571     md5=${tmp[0]}
572   elif which md5 &>/dev/null; then
573     local tmp=($(md5 ${file}))
574     md5=${tmp[3]}
575   fi
576   echo ${md5}
577 }
578
579 main "$@"