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 python3safe=True
25 
26 import subprocess, threading, re, os, os.path, shutil, time, glob, shlex
27 from PyQt4.QtCore import *
28 
29 _threadNumber=0
30 
31 ##
32 #
33 # force l'existence d'un répertoire, récursivement si nécessaire
34 # @param destpath le chemin de ce répertoire
35 #
36 def ensureDirExists(destpath):
37  os.path.isdir(destpath) or os.makedirs(destpath, mode=0o755)
38  return
39 
40 ##
41 #
42 # Une classe pour tenir un registre des threads concernant les baladeurs.
43 #
45 
46  ##
47  #
48  # Le constructure met en place un dictionnaire
49  #
50  def __init__(self):
51  self.dico={}
52 
53  def __str__(self):
54  return "ThreadRegister: %s" %self.dico
55 
56  ##
57  #
58  # @param ud un disque
59  # @param thread un thread
60  # Empile un thread pour le baladeur ud
61  #
62  def push(self, ud, thread):
63  if ud.owner not in self.dico.keys():
64  self.dico[ud.owner]=[thread]
65  else:
66  self.dico[ud.owner].append(thread)
67 
68  ##
69  #
70  # @param ud un disque
71  # @param thread un thread
72  # Dépile un thread pour le baladeur ud
73  #
74  def pop(self, ud, thread):
75  self.dico[ud.owner].remove(thread)
76 
77  ##
78  #
79  # Indique si le disque est occupé par des threads
80  # @param owner le propriétaire du disque
81  # @return les données associées par le dictionnaire
82  #
83  def busy(self, owner):
84  if owner in self.dico.keys():
85  return self.dico[owner]
86  return []
87 
88  ##
89  #
90  # renvoie l'ensemble des threads actifs
91  #
92  def threadSet(self):
93  result=set()
94  for o in self.dico.keys():
95  for t in self.dico[o]:
96  result.add(t)
97  return result
98 
99 ##
100 #
101 # Évite d'avoir des <i>slashes</i> dans un nom de thread
102 # @return la fin du nom de chemin, après le dernier <i>slash</i> ;
103 # si le chemin ne finit pas bien, remplace les <i>slashes</i> par
104 # des sous-tirets "_".
105 #
106 def _sanitizePath(path):
107  pattern=re.compile(".*([^/]+)")
108  m=pattern.match(str(path))
109  if m:
110  return m.group(1)
111  else:
112  return str(path).replace('/','_')
113 
114 ##
115 #
116 # fabrique un nom de thread commençant par th_, suivi d'un nombre unique,
117 # suivi d'une chaîne relative à la clé USB
118 # @param ud une instance de uDisk
119 # @return un nom de thread unique
120 #
121 def _threadName(ud):
122  global _threadNumber
123  name="th_%04d_%s" %(_threadNumber,_sanitizePath(ud.path))
124  _threadNumber+=1
125  return name
126 
127 ##
128 #
129 # Renvoie la date et l'heure dans un format court
130 # @return une chaîne donnée par strftime et le format %Y/%m/%d-%H:%M:%S
131 #
132 def _date():
133  return time.strftime("%Y/%m/%d-%H:%M:%S")
134 
135 ##
136 #
137 # Une classe abstraite
138 # Cette classe sert de creuset pour les classe servant aux copies
139 # et aux effacement.
140 #
141 class abstractThreadUSB(threading.Thread):
142  ##
143  #
144  # Constructeur
145  # Crée un thread pour copier une liste de fichiers vers une clé USB.
146  # @param ud l'instance uDisk correspondant à une partition de clé USB
147  # @param fileList la liste des fichiers à traiter
148  # @param subdir un sous-répertoire de la clé USB
149  # @param dest un répertoire de destination si nécessaire, None par défaut
150  # @param logfile un fichier de journalisation, /dev/null par défaut
151  # @param parent un widget qui recevra de signaux en début et en fin
152  # d'exécution
153  #
154  def __init__(self,ud, fileList, subdir, dest=None, logfile="/dev/null",
155  parent=None):
156  threading.Thread.__init__(self,target=self.toDo,
157  args=(ud, fileList, subdir, dest, logfile),
158  name=_threadName(ud))
159  self.cmd="echo This is an abstract method, don't call it"
160  self.ud=ud
161  ud.threadRunning=True
162  self.fileList=fileList
163  self.subdir=subdir
164  self.dest=dest
165  self.logfile=logfile
166  self.parent=parent
167 
168  ##
169  #
170  # Écrit un message dans le fichier de journalisation
171  # @param msg le message
172  #
173  def writeToLog(self, msg):
174  open(os.path.expanduser(self.logfile),"a").write(msg+"\n")
175  return
176 
177  ##
178  #
179  # Une version modifiée de shutil.copytree qui accepte que les
180  # repertoires destination soient déjà existants. Cette source dérive
181  # de la documentation fournie avec Python 2.7
182  # @param src un nom de fichier ou de répertoire
183  # @param dst un nom de de répertoire (déjà existant ou à créer)
184  # @param symlinks vrai si on veut recopier les liens tels quels
185  # @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)
186  # @param erase s'il est vrai la source est effacée après copie réussie
187  # @param errors la liste d'erreurs déjà relevées jusque là
188  # @return une liste d'erreurs éventuellement relevées, sinon une liste vide
189  #
190  def copytree(self,src, dst, symlinks=False, ignore=None, erase=False, errors=[]):
191  names = os.listdir(src)
192  if ignore is not None:
193  ignored_names = ignore(src, names)
194  else:
195  ignored_names = set()
196 
197  try:
198  os.makedirs(dst)
199  except OSError as err:
200  pass
201  for name in names:
202  if name in ignored_names:
203  continue
204  srcname = os.path.join(src, name)
205  dstname = os.path.join(dst, name)
206  try:
207  if symlinks and os.path.islink(srcname):
208  linkto = os.readlink(srcname)
209  os.symlink(linkto, dstname)
210  if not errors and erase:
211  os.unlink(srcname)
212  elif os.path.isdir(srcname):
213  errors=self.copytree(srcname, dstname,
214  symlinks=symlinks, ignore=ignore,
215  erase=erase, errors=errors)
216  if not errors and erase:
217  os.rmdir(srcname)
218  else:
219  shutil.copy2(srcname, dstname)
220  if not errors and erase:
221  os.unlink(srcname)
222  # XXX What about devices, sockets etc.?
223  except IOError as why:
224  errors.append((srcname, dstname, str(why)))
225  # catch the Error from the recursive copytree so that we can
226  # continue with other files
227  except os.error as why:
228  errors.append((srcname, dstname, str(why)))
229  # catch the Error from the recursive copytree so that we can
230  # continue with other files
231  except Exception as err:
232  errors.extend(err.args[0])
233  return errors
234 
235  ##
236  #
237  # Renvoie une chaîne informative sur le thread
238  # @return une chaine donnant des informations sur ce qui va
239  # se passer dans le thread qui a été créé.
240  #
241  def __str__(self):
242  result="%s(\n" %self.threadType()
243  result+=" ud = %s\n" %self.ud
244  result+=" fileList = %s\n" %self.fileList
245  result+=" subdir = %s\n" %self.subdir
246  result+=" dest = %s\n" %self.dest
247  result+=" logfile = %s\n" %self.logfile
248  result+=" cmd = %s\n" %self.cmd
249  result+="\n"
250  return result
251 
252  ##
253  #
254  # @return une chaîne courte qui informe sur le type de thread
255  #
256  def threadType(self):
257  return "abstractThreadUSB"
258 
259  ##
260  #
261  # La fonction abstraite pour les choses à faire
262  # @param ud l'instance uDisk correspondant à une partition de clé USB
263  # @param fileList la liste des fichiers à traiter
264  # @param subdir un sous-répertoire de la clé USB
265  # @param dest un répertoire de destination
266  # @param logfile un fichier de journalisation
267  #
268  def toDo(self, ud, fileList, subdir, dest, logfile):
269  # ça ne fait rien du tout pour un thread abstrait
270  pass
271 
272 ##
273 #
274 # Classe pour les threads copiant vers les clés USB
275 #
276 class threadCopyToUSB(abstractThreadUSB):
277  ##
278  #
279  # Constructeur
280  # Crée un thread pour copier une liste de fichiers vers une clé USB.
281  # @param ud l'instance uDisk correspondant à une partition de clé USB
282  # @param fileList la liste des fichiers à copier
283  # @param subdir le sous-répertoire de la clé USB où faire la copie
284  # @param logfile un fichier de journalisation, /dev/null par défaut
285  # @param parent un widget qui recevra de signaux en début et en fin
286  # d'exécution
287  #
288  def __init__(self,ud, fileList, subdir, logfile="/dev/null",
289  parent=None):
290  abstractThreadUSB.__init__(self,ud, fileList, subdir, dest=None, logfile=logfile, parent=parent)
291  self.cmd='mkdir -p "{toDir}"; cp -R {fromFile} "{toDir}"'
292 
293  ##
294  #
295  # @return une chaîne courte qui informe sur le type de thread
296  #
297  def threadType(self):
298  return "threadCopyToUSB"
299 
300  ##
301  #
302  # Copie une liste de fichiers vers une clé USB sous un répertoire donné.
303  # Ce répertoire est composé de ud.visibleDir() joint au
304  # sous-répertoire subdir.
305  # À chaque fichier ou répertoire copié, une ligne est journalisée dans le
306  # fichier de journal de l'application.
307  # @param ud l'instance uDisk correspondant à une partition de clé USB
308  # @param fileList la liste des fichiers à copier
309  # @param logfile un fichier de journalisation
310  # @param subdir le sous-répertoire de la clé USB où faire la copie
311  #
312  def toDo(self, ud, fileList, subdir, dest, logfile):
313  while subdir[0]=='/':
314  subdir=subdir[1:]
315  destpath=os.path.join(ud.ensureMounted(),ud.visibleDir(),subdir)
316  ensureDirExists(destpath)
317  # boucle de copie
318  for f in fileList:
319  cmd="copying %s to %s" %(f, destpath)
320  if self.parent:
321  self.parent.emit(SIGNAL("pushCmd(QString, QString)"), ud.owner, cmd)
322  destpath1=os.path.join(destpath, os.path.basename(f))
323  # copie d'arbre si on copie un répertoire, ou de simple fichier
324  if os.path.isdir(f):
325  errors=self.copytree(f, destpath1)
326  else:
327  errors=[]
328  try:
329  shutil.copy2(f, destpath1)
330  except Exception as err:
331  errors.extend((f, destpath1, str(err)))
332 
333  msg="[%s] " %_date()
334  if not errors:
335  msg += "Success: "
336  else:
337  msg += "Error: "
338  msg += cmd
339  for e in errors:
340  msg += " <%s>" %e
341  if self.parent:
342  self.parent.emit(SIGNAL("popCmd(QString, QString)"), ud.owner, msg)
343  self.writeToLog(msg)
344 
345 ##
346 #
347 # Classe pour les threads copiant depuis les clés USB
348 #
350  ##
351  #
352  # Constructeur
353  # Crée un thread pour copier une liste de fichiers depuis une clé USB
354  # vers un répertoire de disque.
355  # @param ud l'instance uDisk correspondant à une partition de clé USB
356  # @param fileList la liste des fichiers à copier
357  # @param subdir le sous-répertoire de la clé USB d'où faire la copie
358  # @param dest un répertoire de destination
359  # @param logfile un fichier de journalisation, /dev/null par défaut
360  # @param parent un widget qui recevra de signaux en début et en fin
361  # d'exécution
362  #
363  def __init__(self,ud, fileList, subdir=".", dest="/tmp",
364  rootPath="/", logfile="/dev/null", parent=None):
365  abstractThreadUSB.__init__(self,ud, fileList, subdir, dest=dest,
366  logfile=logfile, parent=parent)
367  self.rootPath=rootPath
368 
369  ##
370  #
371  # Copie une liste de fichiers d'une clé USB sous un répertoire donné.
372  # À chaque fichier ou répertoire copié, une ligne est journalisée
373  # dans le fichier de journal de l'application.
374  # @param ud l'instance uDisk correspondant à une partition de clé USB
375  # @param fileList la liste des fichiers à copier, qui peut contenir des jokers
376  # @param dest un répertoire de destination
377  # @param logfile un fichier de journalisation
378  # @param subdir le sous-répertoire de la clé USB où faire la copie
379  #
380  def toDo(self, ud, fileList, subdir, dest, logfile):
381  for f in fileList:
382  ## prend le fichier ou le répertoire sur le disque courant
383  fromPath=os.path.join(ud.ensureMounted(), f)
384  owner=ud.ownerByDb()
385  ## personnalise le nom de la destination
386  newName="%s_%s" %(owner,os.path.dirname(f))
387  ## calcule le point de copie et le répertoire à créer s'il le faut
388  toPath=os.path.join(dest,newName)
389  # crée le répertoire cible si nécessaire
390  ensureDirExists(toPath)
391  cmd="copying %s to %s" %(fromPath, toPath)
392  if self.parent:
393  self.parent.emit(SIGNAL("pushCmd(QString, QString)"), ud.owner, cmd)
394  destpath1=os.path.join(toPath, os.path.basename(f))
395  if os.path.isdir(fromPath):
396  errors=self.copytree(fromPath, destpath1)
397  else:
398  errors=[]
399  try:
400  shutil.copy2(fromPath, destpath1)
401  except Exception as err:
402  errors.extend((fromPath, destpath1, str(err)))
403 
404  msg="[%s] " %_date()
405  if not errors:
406  msg += "Success: "
407  else:
408  msg += "Error: "
409  msg += cmd
410  for e in errors:
411  msg += " <%s>" %e
412  if self.parent:
413  self.parent.emit(SIGNAL("popCmd(QString, QString)"), ud.owner, msg)
414  self.writeToLog(msg)
415 
416 ##
417 #
418 # Classe pour les threads déplaçant des fichiers depuis les clés USB
419 #
421  ##
422  #
423  # Constructeur
424  # Crée un thread pour déplacer une liste de fichiers depuis une clé USB
425  # vers un répertoire de disque.
426  # @param ud l'instance uDisk correspondant à une partition de clé USB
427  # @param fileList la liste des fichiers à copier
428  # @param subdir le sous-répertoire de la clé USB d'où faire la copie
429  # @param dest un répertoire de destination
430  # @param logfile un fichier de journalisation, /dev/null par défaut
431  # @param parent un widget qui recevra de signaux en début et en fin
432  # d'exécution
433  #
434  def __init__(self,ud, fileList, subdir=".", dest="/tmp",
435  rootPath="/", logfile="/dev/null", parent=None):
436  abstractThreadUSB.__init__(self,ud, fileList, subdir, dest=dest,
437  logfile=logfile, parent=parent)
438  self.rootPath=rootPath
439 
440  ##
441  #
442  # Copie une liste de fichiers d'une clé USB sous un répertoire donné.
443  # Après chaque copie réussie la source est effacée.
444  # À chaque fichier ou répertoire copié, une ligne est journalisée
445  # dans le fichier de journal de l'application.
446  # @param ud l'instance uDisk correspondant à une partition de clé USB
447  # @param fileList la liste des fichiers à copier
448  # @param dest un répertoire de destination
449  # @param logfile un fichier de journalisation
450  # @param subdir le sous-répertoire de la clé USB où faire la copie
451  #
452  def toDo(self, ud, fileList, subdir, dest, logfile):
453  for f in fileList:
454  ## prend le fichier ou le répertoire sur le disque courant
455  fromPath=os.path.join(ud.ensureMounted(), f)
456  owner=ud.ownerByDb()
457  ## personnalise le nom de la destination
458  newName="%s_%s" %(owner,os.path.dirname(f))
459  ## calcule le point de copie et le répertoire à créer s'il le faut
460  toPath=os.path.join(dest,newName)
461  # crée le répertoire cible si nécessaire
462  ensureDirExists(toPath)
463  cmd="copying %s to %s" %(fromPath, toPath)
464  if self.parent:
465  self.parent.emit(SIGNAL("pushCmd(QString, QString)"), ud.owner, cmd)
466  destpath1=os.path.join(toPath, os.path.basename(f))
467  if os.path.isdir(fromPath):
468  errors=self.copytree(fromPath, destpath1, erase=True)
469  try:
470  os.rmdir(fromPath)
471  except Exception as err:
472  errors.extend((fromPath, destpath1, str(err)))
473  else:
474  errors=[]
475  try:
476  shutil.copy2(fromPath, destpath1)
477  os.unlink(fromPath)
478  except Exception as err:
479  errors.extend((fromPath, destpath1, str(err)))
480 
481  msg="[%s] " %_date()
482  if not errors:
483  msg += "Success: "
484  else:
485  msg += "Error: "
486  msg += cmd
487  for e in errors:
488  msg += " <%s>" %e
489  if self.parent:
490  self.parent.emit(SIGNAL("popCmd(QString, QString)"), ud.owner, msg)
491  self.writeToLog(msg)
492 
493 ##
494 #
495 # Classe pour les threads effaçant des sous-arbres dans les clés USB
496 #
498  ##
499  #
500  # Constructeur
501  # Crée un thread pour supprimer une liste de fichiers dans une clé USB.
502  # @param ud l'instance uDisk correspondant à une partition de clé USB
503  # @param fileList la liste des fichiers à supprimer
504  # @param subdir le sous-répertoire de la clé USB où faire les suppressions
505  # @param logfile un fichier de journalisation, /dev/null par défaut
506  # @param parent un widget qui recevra de signaux en début et en fin
507  # d'exécution
508  #
509  def __init__(self,ud, fileList, subdir, logfile="/dev/null",
510  parent=None):
511  abstractThreadUSB.__init__(self,ud, fileList, subdir, dest=None,
512  logfile=logfile, parent=parent)
513 
514  ##
515  #
516  # Supprime une liste de fichiers dans une clé USB.
517  # La liste est prise sous un répertoire donné. Le répertoire visible
518  # qui dépend du constructuer d ela clé est pris en compte.
519  # À chaque fichier ou répertoire supprimé, une ligne est
520  # journalisée dans le fichier de journal de l'application.
521  # @param l'instance uDisk correspondant à une partition de clé USB
522  # @param fileList la liste des fichiers à copier
523  # @param dest un répertoire de destination
524  # @param logfile un fichier de journalisation
525  # @param subdir le sous-répertoire de la clé USB où faire la copie
526  #
527  def toDo(self, ud, fileList, subdir, dest, logfile):
528  for f in fileList:
529  toDel=os.path.join(ud.ensureMounted(), f)
530  cmd="Deleting %s" %toDel
531  errors=[]
532  if self.parent:
533  self.parent.emit(SIGNAL("pushCmd(QString, QString)"), ud.owner, cmd)
534  if os.path.isdir(toDel):
535  try:
536  for root, dirs, files in os.walk(toDel, topdown=False):
537  for name in files:
538  os.remove(os.path.join(root, name))
539  for name in dirs:
540  os.rmdir(os.path.join(root, name))
541  os.rmdir(toDel)
542  except Exception as err:
543  errors.expand((toDel,str(err)))
544  else:
545  try:
546  os.unlink(toDel)
547  except Exception as err:
548  errors.expand((toDel,str(err)))
549  msg="[%s] " %_date()
550  if not errors:
551  msg += "Success: "
552  else:
553  msg += "Error: "
554  msg += cmd
555  for e in errors:
556  msg += " <%s>" %e
557  if self.parent:
558  self.parent.emit(SIGNAL("popCmd(QString, QString)"), ud.owner, msg)
559  self.writeToLog(msg)
560