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