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