ScolaSync  4.0
 Tout Classes Espaces de nommage Fichiers Fonctions Variables Pages
usbThread.py
Aller à la documentation de ce fichier.
1 # -*- coding: utf-8 -*-
2 # $Id: usbThread.py 47 2011-06-13 10:20:14Z georgesk $
3 
4 licenceEn="""
5  file usbThread.py
6  this file is part of the project scolasync
7 
8  Copyright (C) 2010-2012 Georges Khaznadar <georgesk@ofset.org>
9 
10  This program is free software: you can redistribute it and/or modify
11  it under the terms of the GNU General Public License as published by
12  the Free Software Foundation, either version3 of the License, or
13  (at your option) any later version.
14 
15  This program is distributed in the hope that it will be useful,
16  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18  GNU General Public License for more details.
19 
20  You should have received a copy of the GNU General Public License
21  along with this program. If not, see <http://www.gnu.org/licenses/>.
22 """
23 
24 import subprocess, threading, re, os, os.path, shutil
25 import time, glob, shlex, io
26 from PyQt4.QtCore import *
27 
28 _threadNumber=0
29 
30 ##
31 #
32 # force l'existence d'un répertoire, récursivement si nécessaire
33 # @param destpath le chemin de ce répertoire
34 #
35 def ensureDirExists(destpath):
36  os.path.isdir(destpath) or os.makedirs(destpath, mode=0o755)
37  return
38 
39 ##
40 #
41 # Une classe pour tenir un registre des threads concernant les baladeurs.
42 #
44 
45  ##
46  #
47  # Le constructure met en place un dictionnaire
48  #
49  def __init__(self):
50  self.dico={}
51 
52  def __str__(self):
53  return "ThreadRegister: %s" %self.dico
54 
55  ##
56  #
57  # @param ud un disque
58  # @param thread un thread
59  # Empile un thread pour le baladeur ud
60  #
61  def push(self, ud, thread):
62  if ud.owner not in self.dico.keys():
63  self.dico[ud.owner]=[thread]
64  else:
65  self.dico[ud.owner].append(thread)
66 
67  ##
68  #
69  # @param ud un disque
70  # @param thread un thread
71  # Dépile un thread pour le baladeur ud
72  #
73  def pop(self, ud, thread):
74  self.dico[ud.owner].remove(thread)
75 
76  ##
77  #
78  # Indique si le disque est occupé par des threads
79  # @param owner le propriétaire du disque
80  # @return les données associées par le dictionnaire
81  #
82  def busy(self, owner):
83  if owner in self.dico.keys():
84  return self.dico[owner]
85  return []
86 
87  ##
88  #
89  # renvoie l'ensemble des threads actifs
90  #
91  def threadSet(self):
92  result=set()
93  for o in self.dico.keys():
94  for t in self.dico[o]:
95  result.add(t)
96  return result
97 
98 ##
99 #
100 # Évite d'avoir des <i>slashes</i> dans un nom de thread
101 # @return la fin du nom de chemin, après le dernier <i>slash</i> ;
102 # si le chemin ne finit pas bien, remplace les <i>slashes</i> par
103 # des sous-tirets "_".
104 #
105 def _sanitizePath(path):
106  pattern=re.compile(".*([^/]+)")
107  m=pattern.match(str(path))
108  if m:
109  return m.group(1)
110  else:
111  return str(path).replace('/','_')
112 
113 ##
114 #
115 # fabrique un nom de thread commençant par th_, suivi d'un nombre unique,
116 # suivi d'une chaîne relative à la clé USB
117 # @param ud une instance de uDisk
118 # @return un nom de thread unique
119 #
120 def _threadName(ud):
121  global _threadNumber
122  if hasattr(ud, "path"):
123  name="th_%04d_%s" %(_threadNumber,_sanitizePath(ud.path))
124  else:
125  name="th_%04d_%s" %(_threadNumber,"dummy")
126  _threadNumber+=1
127  return name
128 
129 ##
130 #
131 # Renvoie la date et l'heure dans un format court
132 # @return une chaîne donnée par strftime et le format %Y/%m/%d-%H:%M:%S
133 #
134 def _date():
135  return time.strftime("%Y/%m/%d-%H:%M:%S")
136 
137 ##
138 #
139 # Une classe abstraite, qui sert de creuset pour les classe servant
140 # aux copies et aux effacements.
141 #
142 # Les classes filles doivent redéfinir la méthode \b toDo : c'est celle qui
143 # est démarrée quand le thread est lancé. Cette méthode est appelée dans
144 # le contexte « \b with ud.rlock », qui évite que deux threads en même temps
145 # ne cherchent à accéder au même média.
146 #
147 # Une méthode \b copytree est définie pour remplacer shutils.copytree
148 # qui ne fait pas tout à fait l'affaire.
149 #
150 class abstractThreadUSB(threading.Thread):
151  ##
152  #
153  # Constructeur
154  # Crée un thread pour copier une liste de fichiers vers une clé USB.
155  # @param ud l'instance uDisk correspondant à une partition de clé USB
156  # @param fileList la liste des fichiers à traiter
157  # @param subdir un sous-répertoire de la clé USB
158  # @param dest un répertoire de destination si nécessaire, None par défaut
159  # @param logfile un fichier de journalisation, /dev/null par défaut
160  # @param parent un widget qui recevra de signaux en début et en fin
161  # d'exécution
162  #
163  def __init__(self,ud, fileList, subdir, dest=None, logfile="/dev/null",
164  parent=None):
165  threading.Thread.__init__(self, name=_threadName(ud))
166  self._args=(ud, fileList, subdir, dest, logfile)
167  self.ud=ud
168  if hasattr(ud,"threadRunning"): ud.threadRunning=True
169  self.fileList=fileList
170  self.subdir=subdir
171  self.dest=dest
172  self.logfile=logfile
173  self.parent=parent
174 
175  def run(self):
176  with self.ud.rlock:
177  self.toDo(*self._args)
178 
179  ##
180  #
181  # Écrit un message dans le fichier de journalisation
182  # @param msg le message
183  #
184  def writeToLog(self, msg):
185  open(os.path.expanduser(self.logfile),"a").write(msg+"\n")
186  return
187 
188  ##
189  #
190  # Une version modifiée de shutil.copytree qui accepte que les
191  # repertoires destination soient déjà existants. Cette source dérive
192  # de la documentation fournie avec Python 2.7
193  # @param src un nom de fichier ou de répertoire
194  # @param dst un nom de de répertoire (déjà existant ou à créer)
195  # @param symlinks vrai si on veut recopier les liens tels quels
196  # @param ignore une fonction qui construit une liste de fichiers à ignorer (profil : répertoire, liste de noms de fichiers -> liste de noms de fichiers à ignorer)
197  # @param erase s'il est vrai la source est effacée après copie réussie
198  # @param errors la liste d'erreurs déjà relevées jusque là
199  # @return une liste d'erreurs éventuellement relevées, sinon une liste vide
200  #
201  def copytree(self,src, dst, symlinks=False, ignore=None, erase=False, errors=[]):
202  names = os.listdir(src)
203  if ignore is not None:
204  ignored_names = ignore(src, names)
205  else:
206  ignored_names = set()
207 
208  try:
209  os.makedirs(dst)
210  except OSError as err:
211  pass
212  for name in names:
213  if name in ignored_names:
214  continue
215  srcname = os.path.join(src, name)
216  dstname = os.path.join(dst, name)
217  try:
218  if symlinks and os.path.islink(srcname):
219  linkto = os.readlink(srcname)
220  os.symlink(linkto, dstname)
221  if not errors and erase:
222  os.unlink(srcname)
223  elif os.path.isdir(srcname):
224  errors=self.copytree(srcname, dstname,
225  symlinks=symlinks, ignore=ignore,
226  erase=erase, errors=errors)
227  if not errors and erase:
228  os.rmdir(srcname)
229  else:
230  shutil.copy2(srcname, dstname)
231  if not errors and erase:
232  os.unlink(srcname)
233  # XXX What about devices, sockets etc.?
234  except IOError as why:
235  errors.append((srcname, dstname, str(why)))
236  # catch the Error from the recursive copytree so that we can
237  # continue with other files
238  except os.error as why:
239  errors.append((srcname, dstname, str(why)))
240  # catch the Error from the recursive copytree so that we can
241  # continue with other files
242  except Exception as err:
243  errors.extend(err.args[0])
244  return errors
245 
246  ##
247  #
248  # Renvoie une chaîne informative sur le thread
249  # @return une chaine donnant des informations sur ce qui va
250  # se passer dans le thread qui a été créé.
251  #
252  def __str__(self):
253  result="%s(\n" %self.threadType()
254  result+=" ud = %s\n" %self.ud
255  result+=" fileList = %s\n" %self.fileList
256  result+=" subdir = %s\n" %self.subdir
257  result+=" dest = %s\n" %self.dest
258  result+=" logfile = %s\n" %self.logfile
259  result+="\n"
260  return result
261 
262  ##
263  #
264  # information sur le thread.
265  # @return une chaîne courte qui informe sur le type de thread
266  #
267  def threadType(self):
268  return "abstractThreadUSB"
269 
270  ##
271  #
272  # La fonction abstraite pour les choses à faire
273  # @param ud l'instance uDisk correspondant à une partition de clé USB
274  # @param fileList la liste des fichiers à traiter
275  # @param subdir un sous-répertoire de la clé USB
276  # @param dest un répertoire de destination
277  # @param logfile un fichier de journalisation
278  #
279  def toDo(self, ud, fileList, subdir, dest, logfile):
280  # ça ne fait rien du tout pour un thread abstrait
281  pass
282 
283 ##
284 #
285 # Classe pour les threads copiant vers les clés USB
286 #
287 class threadCopyToUSB(abstractThreadUSB):
288  ##
289  #
290  # Constructeur
291  # Crée un thread pour copier une liste de fichiers vers une clé USB.
292  # @param ud l'instance uDisk correspondant à une partition de clé USB
293  # @param fileList la liste des fichiers à copier
294  # @param subdir le sous-répertoire de la clé USB où faire la copie
295  # @param logfile un fichier de journalisation, /dev/null par défaut
296  # @param parent un widget qui recevra de signaux en début et en fin
297  # d'exécution
298  #
299  def __init__(self,ud, fileList, subdir, logfile="/dev/null",
300  parent=None):
301  abstractThreadUSB.__init__(self,ud, fileList, subdir, dest=None, logfile=logfile, parent=parent)
302 
303  ##
304  #
305  # @return une chaîne courte qui informe sur le type de thread
306  #
307  def threadType(self):
308  return "threadCopyToUSB"
309 
310  ##
311  #
312  # Copie une liste de fichiers vers une clé USB sous un répertoire donné.
313  # Ce répertoire est composé de ud.visibleDir() joint au
314  # sous-répertoire subdir.
315  # À chaque fichier ou répertoire copié, une ligne est journalisée dans le
316  # fichier de journal de l'application.
317  # @param ud l'instance uDisk correspondant à une partition de clé USB
318  # @param fileList la liste des fichiers à copier
319  # @param logfile un fichier de journalisation
320  # @param subdir le sous-répertoire de la clé USB où faire la copie
321  #
322  def toDo(self, ud, fileList, subdir, dest, logfile):
323  while subdir[0]=='/':
324  subdir=subdir[1:]
325  destpath=os.path.join(ud.ensureMounted(),ud.visibleDir(),subdir)
326  ensureDirExists(destpath)
327  # boucle de copie
328  for f in fileList:
329  cmd="copying %s to %s" %(f, destpath)
330  if self.parent:
331  self.parent.emit(SIGNAL("pushCmd(QString, QString)"), ud.owner, cmd)
332  destpath1=os.path.join(destpath, os.path.basename(f))
333  # copie d'arbre si on copie un répertoire, ou de simple fichier
334  if os.path.isdir(f):
335  errors=self.copytree(f, destpath1)
336  else:
337  errors=[]
338  try:
339  shutil.copy2(f, destpath1)
340  except Exception as err:
341  errors.append([f, destpath1, str(err)])
342 
343  print ("GRRRR il faut lire le fichier TODO")
344  msg="[%s] " %_date()
345  if not errors:
346  msg+="Success: "
347  else:
348  msg+="Error: "
349  msg+=cmd
350  for e in errors:
351  msg+= " <%s>" %str(e)
352  if self.parent:
353  self.parent.emit(SIGNAL("popCmd(QString, QString)"), ud.owner, cmd)
354  self.writeToLog(msg)
355 
356 ##
357 #
358 # Classe pour les threads copiant depuis les clés USB
359 #
361  ##
362  #
363  # Constructeur
364  # Crée un thread pour copier une liste de fichiers depuis une clé USB
365  # vers un répertoire de disque.
366  # @param ud l'instance uDisk correspondant à une partition de clé USB
367  # @param fileList la liste des fichiers à copier
368  # @param subdir le sous-répertoire de la clé USB d'où faire la copie
369  # @param dest un répertoire de destination
370  # @param logfile un fichier de journalisation, /dev/null par défaut
371  # @param parent un widget qui recevra de signaux en début et en fin
372  # d'exécution
373  #
374  def __init__(self,ud, fileList, subdir=".", dest="/tmp",
375  rootPath="/", logfile="/dev/null", parent=None):
376  abstractThreadUSB.__init__(self,ud, fileList, subdir, dest=dest,
377  logfile=logfile, parent=parent)
378  self.rootPath=rootPath
379 
380  ##
381  #
382  # Copie une liste de fichiers d'une clé USB sous un répertoire donné.
383  # À chaque fichier ou répertoire copié, une ligne est journalisée
384  # dans le fichier de journal de l'application.
385  # @param ud l'instance uDisk correspondant à une partition de clé USB
386  # @param fileList la liste des fichiers à copier, qui peut contenir des jokers
387  # @param dest un répertoire de destination
388  # @param logfile un fichier de journalisation
389  # @param subdir le sous-répertoire de la clé USB où faire la copie
390  #
391  def toDo(self, ud, fileList, subdir, dest, logfile):
392  for f in fileList:
393  ## prend le fichier ou le répertoire sur le disque courant
394  fromPath=os.path.join(ud.ensureMounted(), f)
395  owner=ud.ownerByDb()
396  ## personnalise le nom de la destination
397  newName="%s_%s" %(owner,os.path.dirname(f))
398  ## calcule le point de copie et le répertoire à créer s'il le faut
399  toPath=os.path.join(dest,newName)
400  # crée le répertoire cible si nécessaire
401  ensureDirExists(toPath)
402  cmd="copying %s to %s" %(fromPath, toPath)
403  if self.parent:
404  self.parent.emit(SIGNAL("pushCmd(QString, QString)"), ud.owner, cmd)
405  destpath1=os.path.join(toPath, os.path.basename(f))
406  if os.path.isdir(fromPath):
407  errors=self.copytree(fromPath, destpath1)
408  else:
409  errors=[]
410  try:
411  shutil.copy2(fromPath, destpath1)
412  except Exception as err:
413  errors.extend((fromPath, destpath1, str(err)))
414 
415  msg="[%s] " %_date()
416  if not errors:
417  msg += "Success: "
418  else:
419  msg += "Error: "
420  msg += cmd
421  for e in errors:
422  msg += " <%s>" %e
423  if self.parent:
424  self.parent.emit(SIGNAL("popCmd(QString, QString)"), ud.owner, msg)
425  self.writeToLog(msg)
426 
427 ##
428 #
429 # Classe pour les threads déplaçant des fichiers depuis les clés USB
430 #
432  ##
433  #
434  # Constructeur
435  # Crée un thread pour déplacer une liste de fichiers depuis une clé USB
436  # vers un répertoire de disque.
437  # @param ud l'instance uDisk correspondant à une partition de clé USB
438  # @param fileList la liste des fichiers à copier
439  # @param subdir le sous-répertoire de la clé USB d'où faire la copie
440  # @param dest un répertoire de destination
441  # @param logfile un fichier de journalisation, /dev/null par défaut
442  # @param parent un widget qui recevra de signaux en début et en fin
443  # d'exécution
444  #
445  def __init__(self,ud, fileList, subdir=".", dest="/tmp",
446  rootPath="/", logfile="/dev/null", parent=None):
447  abstractThreadUSB.__init__(self,ud, fileList, subdir, dest=dest,
448  logfile=logfile, parent=parent)
449  self.rootPath=rootPath
450 
451  ##
452  #
453  # Copie une liste de fichiers d'une clé USB sous un répertoire donné.
454  # Après chaque copie réussie la source est effacée.
455  # À chaque fichier ou répertoire copié, une ligne est journalisée
456  # dans le fichier de journal de l'application.
457  # @param ud l'instance uDisk correspondant à une partition de clé USB
458  # @param fileList la liste des fichiers à copier
459  # @param dest un répertoire de destination
460  # @param logfile un fichier de journalisation
461  # @param subdir le sous-répertoire de la clé USB où faire la copie
462  #
463  def toDo(self, ud, fileList, subdir, dest, logfile):
464  for f in fileList:
465  ## prend le fichier ou le répertoire sur le disque courant
466  fromPath=os.path.join(ud.ensureMounted(), f)
467  owner=ud.ownerByDb()
468  ## personnalise le nom de la destination
469  newName="%s_%s" %(owner,os.path.dirname(f))
470  ## calcule le point de copie et le répertoire à créer s'il le faut
471  toPath=os.path.join(dest,newName)
472  # crée le répertoire cible si nécessaire
473  ensureDirExists(toPath)
474  cmd="copying %s to %s" %(fromPath, toPath)
475  if self.parent:
476  self.parent.emit(SIGNAL("pushCmd(QString, QString)"), ud.owner, cmd)
477  destpath1=os.path.join(toPath, os.path.basename(f))
478  if os.path.isdir(fromPath):
479  errors=self.copytree(fromPath, destpath1, erase=True)
480  try:
481  os.rmdir(fromPath)
482  except Exception as err:
483  errors.extend((fromPath, destpath1, str(err)))
484  else:
485  errors=[]
486  try:
487  shutil.copy2(fromPath, destpath1)
488  os.unlink(fromPath)
489  except Exception as err:
490  errors.extend((fromPath, destpath1, str(err)))
491 
492  msg="[%s] " %_date()
493  if not errors:
494  msg += "Success: "
495  else:
496  msg += "Error: "
497  msg += cmd
498  for e in errors:
499  msg += " <%s>" %e
500  if self.parent:
501  self.parent.emit(SIGNAL("popCmd(QString, QString)"), ud.owner, msg)
502  self.writeToLog(msg)
503 
504 ##
505 #
506 # Classe pour les threads effaçant des sous-arbres dans les clés USB
507 #
509  ##
510  #
511  # Constructeur
512  # Crée un thread pour supprimer une liste de fichiers dans une clé USB.
513  # @param ud l'instance uDisk correspondant à une partition de clé USB
514  # @param fileList la liste des fichiers à supprimer
515  # @param subdir le sous-répertoire de la clé USB où faire les suppressions
516  # @param logfile un fichier de journalisation, /dev/null par défaut
517  # @param parent un widget qui recevra de signaux en début et en fin
518  # d'exécution
519  #
520  def __init__(self,ud, fileList, subdir, logfile="/dev/null",
521  parent=None):
522  abstractThreadUSB.__init__(self,ud, fileList, subdir, dest=None,
523  logfile=logfile, parent=parent)
524 
525  ##
526  #
527  # Supprime une liste de fichiers dans une clé USB.
528  # La liste est prise sous un répertoire donné. Le répertoire visible
529  # qui dépend du constructuer d ela clé est pris en compte.
530  # À chaque fichier ou répertoire supprimé, une ligne est
531  # journalisée dans le fichier de journal de l'application.
532  # @param l'instance uDisk correspondant à une partition de clé USB
533  # @param fileList la liste des fichiers à copier
534  # @param dest un répertoire de destination
535  # @param logfile un fichier de journalisation
536  # @param subdir le sous-répertoire de la clé USB où faire la copie
537  #
538  def toDo(self, ud, fileList, subdir, dest, logfile):
539  for f in fileList:
540  toDel=os.path.join(ud.ensureMounted(), f)
541  cmd="Deleting %s" %toDel
542  errors=[]
543  if self.parent:
544  self.parent.emit(SIGNAL("pushCmd(QString, QString)"), ud.owner, cmd)
545  if os.path.isdir(toDel):
546  try:
547  for root, dirs, files in os.walk(toDel, topdown=False):
548  for name in files:
549  os.remove(os.path.join(root, name))
550  for name in dirs:
551  os.rmdir(os.path.join(root, name))
552  os.rmdir(toDel)
553  except Exception as err:
554  errors.expand((toDel,str(err)))
555  else:
556  try:
557  os.unlink(toDel)
558  except Exception as err:
559  errors.expand((toDel,str(err)))
560  msg="[%s] " %_date()
561  if not errors:
562  msg += "Success: "
563  else:
564  msg += "Error: "
565  msg += cmd
566  for e in errors:
567  msg += " <%s>" %e
568  if self.parent:
569  self.parent.emit(SIGNAL("popCmd(string, string)"), ud.owner, msg)
570  self.writeToLog(msg)
571 
572 if __name__=="__main__":
573  import sys, ownedUsbDisk, subprocess
574 
575  ##
576  # Teste la fonction copytree
578  t=abstractThreadUSB(None, sys.argv[1:-1], sys.argv[-1])
579  if len(sys.argv) < 3:
580  print("Usage : %s répertoire_source répertoire_destination" %sys.argv[0])
581  print("Ça doit créer sous répertoire_destination la même arborescence que sous répertoire_source")
582  print("et ça crée répertoire_destination à la volée si nécessaire.")
583  sys.exit(-1)
584  errors=t.copytree(sys.argv[1],sys.argv[2])
585  print("Erreurs = %s" %errors)
586  subprocess.call ("diff -ruN %s %s" %(sys.argv[1],sys.argv[2]), shell=True)
587  print ("Ne pas oublier d'effacer %s si nécessaire" %sys.argv[2])
588 
589  ##
590  #
591  # Teste la copie d'un fichier vers une destination telle qu'elle est pratiquée
592  # dans la méthode copytree de abstractThreadUSB
593  #
594  def test_copy2():
595  if len(sys.argv) < 3:
596  print("Usage : %s fichier répertoire_destination" %sys.argv[0])
597  print("Ça doit créer sous répertoire_destination une copie du fichier")
598  print("et ça crée répertoire_destination à la volée si nécessaire.")
599  sys.exit(-1)
600  srcname=sys.argv[1]
601  dstname=os.path.join(sys.argv[2],sys.argv[1])
602  shutil.copy2(srcname, dstname)
603  print ("fin de la copie de %s vers %s, listing de %s" %(sys.argv[1],sys.argv[2],sys.argv[2]))
604  subprocess.call("ls %s" %sys.argv[2], shell=True)
605 
606  #test_copytree()
607  test_copy2()
608