ScolaSync  1.0
 Tout Classes Espaces de nommage Fichiers Fonctions Variables Pages
usbDisk.py
Aller à la documentation de ce fichier.
1 # -*- coding: utf-8 -*-
2 # $Id: usbDisk.py 36 2011-01-15 19:37:27Z georgesk $
3 
4 licence={}
5 licence_en="""
6  file usbDisk.py
7  this file is part of the project scolasync
8 
9  Copyright (C) 2010 Georges Khaznadar <georgesk@ofset.org>
10 
11  This program is free software: you can redistribute it and/or modify
12  it under the terms of the GNU General Public License as published by
13  the Free Software Foundation, either version3 of the License, or
14  (at your option) any later version.
15 
16  This program is distributed in the hope that it will be useful,
17  but WITHOUT ANY WARRANTY; without even the implied warranty of
18  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19  GNU General Public License for more details.
20 
21  You should have received a copy of the GNU General Public License
22  along with this program. If not, see <http://www.gnu.org/licenses/>.
23 """
24 
25 licence['en']=licence_en
26 
27 import dbus, subprocess, os.path, re, time
28 from PyQt4.QtGui import *
29 
30 
31 ##
32 #
33 # une classe pour représenter un disque ou une partition.
34 #
35 # les attributs publics sont :
36 # - \b path le chemin dans le système dbus
37 # - \b device l'objet dbus qui correspond à l'instance
38 # - \b device_prop un proxy pour questionner cet objet dbus
39 # - \b selected booléen vrai si on doit considérer cette instance comme sélectionnée. Vrai à l'initialisation
40 # - \b checkable booléen vrai si on veut que la sélection puisse être modifiée par l'utilisateur dans l'interface graphique
41 #
42 class uDisk:
43 
44  ##
45  #
46  # Le constructeur
47  # @param path un chemin dans le système dbus
48  # @param bus un objet dbus.BusSystem
49  # @param checkable vrai si on fera usage de self.selected
50  #
51  def __init__(self, path, bus, checkable=False):
52  self.path=path
53  self.device = bus.get_object("org.freedesktop.UDisks", self.path)
54  self.device_prop = dbus.Interface(self.device, "org.freedesktop.DBus.Properties")
55  self.selected=True
56  self.checkable=checkable
57  self.stickid=unicode(self.getProp("drive-serial"))
58  self.uuid=self.getProp("id-uuid")
59  self.fatuuid=None # pour l'uuid de la première partion vfat
60  self.firstFat=None # poignée de la première partition vfat
61  #
62 
63 
64  _itemNames={
65  "1device-mount-paths":QApplication.translate("uDisk","point de montage",None, QApplication.UnicodeUTF8),
66  "2device-size":QApplication.translate("uDisk","taille",None, QApplication.UnicodeUTF8),
67  "3drive-vendor":QApplication.translate("uDisk","marque",None, QApplication.UnicodeUTF8),
68  "4drive-model":QApplication.translate("uDisk","modèle de disque",None, QApplication.UnicodeUTF8),
69  "5drive-serial":QApplication.translate("uDisk","numéro de série",None, QApplication.UnicodeUTF8),
70  }
71 
72  _specialItems={"0Check":QApplication.translate("uDisk","cocher",None, QApplication.UnicodeUTF8)}
73 
74  _ItemPattern=re.compile("[0-9]?(.*)")
75 
76  ##
77  #
78  # renvoie l'uuid de la première partition FAT après que celle-ci aura été
79  # identifiée (utile pour les disques partitionnés)
80  # @return un uuid
81  #
82  def getFatUuid(self):
83  return "%s" %self.fatuuid
84 
85  ##
86  #
87  # renvoie un identifiant unique. Dans cette classe, cette fonction
88  # est synonyme de getFatUuid
89  # @return un identifiant supposément unique
90  #
91  def uniqueId(self):
92  return self.getFatUuid()
93 
94  ##
95  #
96  # Méthode statique, pour avoir des titres de colonne.
97  # renvoie des titres pour les items obtenus par __getitem__. Le
98  # résultat dépend du paramètre checkable.
99  # @param checkable vrai si le premier en-tête correspond à une colonne de cases à cocher
100  # @param locale la locale, pour traduire les titres éventuellement.
101  # Valeur par défaut : "C"
102  # @return une liste de titres de colonnes
103  #
104  def headers(checkable=False,locale="C"):
105  if checkable:
106  result=uDisk._specialItems.keys()+ uDisk._itemNames.keys()
107  return sorted(result)
108  else:
109  return sorted(uDisk._itemNames.keys())
110 
111  headers = staticmethod(headers)
112 
113  ##
114  #
115  # renvoie un proxy vers un navigateur de propriétés
116  # @param bus une instace de dbus.SystemBus
117  # @return l'objet proxy
118  #
119  def devicePropProxy(self, bus):
120  return self.device_prop
121 
122  ##
123  #
124  # Renvoie la valeur de vérité d'une propriété
125  # @param prop une propriété
126  # @param value
127  # @return vrai si la propriété est vraie (cas où value==None) ou vrai si la propriété a exactement la valeur value.
128  #
129  def isTrue(self,prop, value=None):
130  if value==None:
131  return bool(self.getProp(prop))
132  else:
133  return self.getProp(prop)==value
134 
135  ##
136  #
137  # Facilite le réprage des disques USB USB
138  # @return vrai dans le cas d'un disque USB
139  #
140  def isUsbDisk(self):
141  return self.isTrue("device-is-removable") and self.isTrue("drive-connection-interface","usb") and self.isTrue("device-size")
142 
143  ##
144  #
145  # Fournit une représentation imprimable
146  # @return une représentation imprimable de l'instance
147  #
148  def __str__(self):
149  return self.title()+self.valuableProperties()
150 
151  ##
152  #
153  # Permet d'obtenir un identifiant unique de disque
154  # @return le chemin dbus de l'instance
155  #
156  def title(self):
157  return self.path
158 
159  ##
160  #
161  # Permet d'accèder à l'instance par un nom de fichier
162  # @return un nom valide dans le système de fichiers, pour accéder
163  # à l'instance.
164  #
165  def file(self):
166  fileById=self.getProp("device-file-by-id")
167  if isinstance(fileById, dbus.Array): fileById=fileById[0]
168  return fileById
169 
170  ##
171  #
172  # Permet d'accèder à l'instance par un point de montage
173  # @return un point de montage, s'il en existe, sinon None
174  #
175  def mountPoint(self):
176  paths=self.getProp("device-mount-paths")
177  if isinstance(paths, dbus.Array) and len(paths)>0:
178  return paths[0]
179  else:
180  return None
181 
182  ##
183  #
184  # Facilite l'accès aux propriétés à l'aide des mots clés du module udisks
185  # @param name le nom d'une propriété
186  # @return une propriété dbus du disque ou de la partition, sinon None si le nom name est illégal
187  #
188  def getProp(self, name):
189  try:
190  return self.device_prop.Get("org.freedesktop.UDisks", name)
191  except:
192  return None
193 
194  ##
195  #
196  # Permet de reconnaitre les partitions DOS-FAT
197  # @return True dans le cas d'une partition FAT16 ou FAT32
198  #
199  def isDosFat(self):
200  return self.getProp("id-type")=="vfat"
201 
202  ##
203  #
204  # @return True si le disque ou la partion est montée
205  #
206  def isMounted(self):
207  return bool(self.getProp("device-is-mounted"))
208 
209  ##
210  #
211  # Facilite l'accès aux propriétés intéressantes d'une instance
212  # @return une chaîne indentée avec les propriétés intéressantes, une par ligne
213  #
214  def valuableProperties(self,indent=4):
215  prefix="\n"+" "*indent
216  r=""
217  props=["device-file-by-id",
218  "device-mount-paths",
219  "device-is-partition-table",
220  "partition-table-count",
221  "device-is-read-only",
222  "device-is-drive",
223  "device-is-optical-disc",
224  "device-is-mounted",
225  "drive-vendor",
226  "drive-model",
227  "drive-serial",
228  "id-uuid",
229  "partition-slave",
230  "partition-type",
231  "device-size",
232  "id-type"]
233  for prop in props:
234  p=self.getProp(prop)
235  if isinstance(p,dbus.Array):
236  if len(p)>0:
237  r+=prefix+"%s = array:" %(prop)
238  for s in p:
239  r+=prefix+" "*indent+s
240  elif isinstance(p,dbus.Boolean):
241  r+=prefix+"%s = %s" %(prop, bool(p))
242  elif isinstance(p,dbus.Int16) or isinstance(p,dbus.Int32) or isinstance(p,dbus.Int64) or isinstance(p,dbus.UInt16) or isinstance(p,dbus.UInt32) or isinstance(p,dbus.UInt64) or isinstance(p,int):
243  if p < 10*1024:
244  r+=prefix+"%s = %s" %(prop,p)
245  elif p < 10*1024*1024:
246  r+=prefix+"%s = %s k" %(prop,p/1024)
247  elif p < 10*1024*1024*1024:
248  r+=prefix+"%s = %s M" %(prop,p/1024/1024)
249  else:
250  r+=prefix+"%s = %s G" %(prop,p/1024/1024/1024)
251  else:
252  r+=prefix+"%s = %s" %(prop,p)
253  return r
254 
255  ##
256  #
257  # renvoie le chemin du disque, dans le cas où self est une partition
258  # @return le chemin dbus du disque maître, sinon "/"
259  #
260  def master(self):
261  return self.getProp("partition-slave")
262 
263  ##
264  #
265  # retire le numéro des en-têtes pour en faire un nom de propriété
266  # valide pour interroger dbus
267  # @param n un numéro de propriété qui se réfère aux headers
268  # @return une propriété renvoyée par dbus, dans un format imprimable
269  #
270  def unNumberProp(self,n):
271  m=uDisk._ItemPattern.match(self.headers()[n])
272  try:
273  prop=m.group(1)
274  result=self.showableProp(prop)
275  return result
276  except:
277  return ""
278 
279  ##
280  #
281  # Renvoie un élément de listage de données internes au disque
282  # @param n un nombre
283  # @param checkable vrai si on doit renvoyer une propriété supplémentaire pour n==0
284  # @return si checkable est vrai, un élément si n>0, et le drapeau self.selected si n==0 ; sinon un élément de façon ordinaire. Les noms des éléments sont dans la liste itemNames utilisée dans la fonction statique headers
285  #
286  def __getitem__(self,n):
287  propListe=self.headers()
288  if self.checkable:
289  if n==0:
290  return self.selected
291  elif n <= len(propListe):
292  return self.unNumberProp(n-1)
293  else:
294  if n < len(propListe):
295  return self.unNumberProp(n)
296 
297  ##
298  #
299  # Renvoie une propriété dans un type "montrable" par QT.
300  # les propriétés que renvoie dbus ont des types inconnus de Qt4,
301  # cette fonction les transtype pour que QVariant arrive à les
302  # prendre en compte.
303  # @param name le nom de la propriété
304  # @return une nombre ou une chaîne selon le type de propriété
305  #
306  def showableProp(self, name):
307  p=self.getProp(name)
308  if isinstance(p,dbus.Array):
309  if len(p)>0: return str(p[0])
310  else: return ""
311  elif isinstance(p,dbus.Boolean):
312  return "%s" %bool(p)
313  elif isinstance(p,dbus.Int16) or isinstance(p,dbus.Int32) or isinstance(p,dbus.Int64) or isinstance(p,dbus.UInt16) or isinstance(p,dbus.UInt32) or isinstance(p,dbus.UInt64) or isinstance(p,int):
314  return int(p)
315  else:
316  return "%s" %p
317 
318  ##
319  #
320  # Renvoie la première partition VFAT
321  # @result la première partition VFAT ou None s'il n'y en a pas
322  #
323  def getFirstFat(self):
324  if self.isDosFat(): return self
325  return self.firstFat
326 
327  ##
328  #
329  # Permet de s'assurer qu'une partition ou un disque sera bien monté
330  # @result le chemin du point de montage
331  #
332  def ensureMounted(self):
333  mount_paths=self.getProp("device-mount-paths")
334  if mount_paths==None: # le cas où la notion de montage est hors-sujet
335  return ""
336  leftTries=5
337  while len(mount_paths)==0 and leftTries >0:
338  leftTries = leftTries - 1
339  path=self.getProp("device-file-by-id")
340  if isinstance(path,dbus.Array) and len(path)>0:
341  path=path[0]
342  subprocess.call("udisks --mount %s > /dev/null" %path,shell=True)
343  paths=self.getProp("device-mount-paths")
344  if paths:
345  return self.getProp("device-mount-paths")[0]
346  else:
347  time.sleep(0.5)
348  else:
349  time.sleep(0.5)
350  if leftTries==0:
351  raise Exception, "Could not mount the VFAT after 5 tries."
352  else:
353  return mount_paths[0]
354 
355 
356 
357 ##
358 #
359 # une classe pour représenter la collection des disques USB connectés
360 #
361 # les attributs publics sont :
362 # - \b checkable booléen vrai si on veut gérer des sélections de disques
363 # - \b access le type d'accès qu'on veut pour les items
364 # - \b bus une instance de dbus.SystemBus
365 # - \b disks la collection de disques USB, organisée en un dictionnaire
366 # de disques : les clés sont les disques, qui renvoient à un ensemble
367 # de partitions du disque
368 # - \b enumdev une liste de chemins dbus vers les disques trouvés
369 # - \b firstFats une liste composée de la première partion DOS-FAT de chaque disque USB.
370 #
371 class Available:
372 
373  ##
374  #
375  # Le constructeur
376  # @param checkable : vrai si on veut pouvoir cocher les disques de la
377  # collection. Faux par défaut.
378  # @param access définit le type d'accès souhaité. Par défaut, c'est "disk"
379  # c'est à dire qu'on veut la liste des disques USB. Autres valeurs
380  # possibles : "firstFat" pour les premières partitions vfat.
381  # @param diskClass la classe de disques à créer
382  # @param diskDict un dictionnaire des disque maintenu par deviceListener
383  #
384  def __init__(self, checkable=False, access="disk", diskClass=uDisk, diskDict=None):
385  ## print "GRRRR should use diskDict=", diskDict
386  self.checkable=checkable
387  self.access=access
388  self.bus = dbus.SystemBus()
389  proxy = self.bus.get_object("org.freedesktop.UDisks",
390  "/org/freedesktop/UDisks")
391  iface = dbus.Interface(proxy, "org.freedesktop.UDisks")
392  self.disks={}
393  self.enumDev=iface.EnumerateDevices()
394  ### récupération des disques usb dans le dictionnaire self.disks
395  for path in self.enumDev:
396  ud=diskClass(path, self.bus, checkable)
397  if ud.isUsbDisk():
398  self.disks[ud]=[]
399  # cas des disques sans partitions
400  if bool(ud.getProp("device-is-partition-table")) == False:
401  # la propriété "device-is-partition-table" est fausse,
402  # probablement qu'il y a un système de fichiers
403  self.disks[ud].append(ud)
404  ### une deuxième passe pour récupérer et associer les partitions
405  for path in self.enumDev:
406  ud=diskClass(path, self.bus, checkable)
407  for d in self.disks.keys():
408  if ud.master() == d.path:
409  self.disks[d].append(ud)
410  self.finishInit()
411 
412  ##
413  #
414  # Fin de l'initialisation
415  #
416  def finishInit(self):
417  self.mountFirstFats()
418 
419  ##
420  #
421  # fabrique la liste des partitions FAT,
422  # monte les partitions FAT si elles ne le sont pas
423  #
424  def mountFirstFats(self):
425  self.firstFats = self.getFirstFats()
426  if self.access=="firstFat":
427  for p in self.firstFats:
428  p.ensureMounted()
429 
430  ##
431  #
432  # @return le nombre de medias connectés
433  #
434  def __trunc__(self):
435  return len(self.firstFats)
436 
437  ##
438  #
439  # Sert à comparer deux collections de disques, par exemple
440  # une collection passée et une collection présente.
441  # @param other une instance de Available
442  # @return vrai si other semble être la même collection de disques USB
443  #
444  def compare(self, other):
445  result=self.summary()==other.summary()
446  return result
447 
448  ##
449  #
450  # Permet de déterminer si un disque est dans la collection
451  # @param ud une instance de uDisk
452  # @return vrai si le uDisk ud est dans la collection
453  #
454  def contains(self, ud):
455  for k in self.disks.keys():
456  if k.getProp("device-file-by-id")==ud.getProp("device-file-by-id"): return True
457  return False
458 
459  ##
460  #
461  # Fournit une représentation imprimable d'un résumé
462  # @return une représentation imprimable d'un résumé de la collection
463  #
464  def summary(self):
465  r= "Available USB discs\n"
466  r+= "===================\n"
467  for d in sorted(self.disks.keys(), key=lambda disk: disk.getFatUuid()):
468  r+="%s\n" %(d.title(),)
469  if len(self.disks[d])>0:
470  r+=" Partitions :\n"
471  for part in sorted(self.disks[d], key=lambda disk: disk.getFatUuid()):
472  r+=" %s\n" %(part.path,)
473  return r
474 
475  ##
476  #
477  # Fournit une représentation imprimable
478  # @return une représentation imprimable de la collection
479  #
480  def __str__(self):
481  r= "Available USB discs\n"
482  r+= "===================\n"
483  for d in self.disks.keys():
484  r+="%s\n" %d
485  if len(self.disks[d])>0:
486  r+=" Partitions :\n"
487  for part in self.disks[d]:
488  r+=" %s\n" %(part.path)
489  r+=part.valuableProperties(12)+"\n"
490  return r
491 
492  ##
493  #
494  # Renvoye le nième disque. Le fonctionnement dépend du paramètre
495  # self.access
496  # @param n un numéro
497  # @return le nième disque USB connecté
498  #
499  def __getitem__(self, n):
500  if self.access=="disk":
501  return self.disks.keys()[n]
502  elif self.access=="firstFat":
503  return self.firstFats[n]
504 
505  ##
506  #
507  # Renseigne sur la longueur de la collection. Le fonctionnement
508  # dépend du paramètre self.access
509  # @return la longueur de la collection de disques renvoyée
510  #
511  def __len__(self):
512  if self.access=="disk":
513  return len(self.disks)
514  elif self.access=="firstFat":
515  return len(self.firstFats)
516 
517  ##
518  #
519  # Facilite l'accès aux partitions de type DOS-FAT, et a des effets
520  # de bord :
521  # * marque le disque avec l'uuid de la première partition FAT.
522  # * construit une liste des chemins uDisk des FATs
523  # @param setOwners si égale à True,
524  # signale que la liste devra comporter des attributs de propriétaire
525  # de medias.
526  # @return une liste de partitions, constituée de la première
527  # partition de type FAT de chaque disque USB connecté
528  #
529  def getFirstFats(self, setOwners=False):
530  result=[]
531  self.fatPaths=[]
532  for d in self.disks.keys():
533  for p in self.disks[d]:
534  if p.isDosFat() or p==d :
535  # le cas p == d correspond aux disques non partitionnés
536  # on va supposer que dans ce cas la partition ne peut
537  # être que de type DOS !!!
538  result.append(p)
539  self.fatPaths.append(p.title())
540  # on marque le disque père et la partition elle-même
541  d.fatuuid=p.uuid
542  d.firstFat=p
543  p.fatuuid=p.uuid
544  if setOwners:
545  p.owner=d.owner
546  break
547  return result
548 
549  ##
550  #
551  # @param dev un chemin comme /org/freedesktop/UDisks/devices/sdb3
552  # @return True si la partition est dans la liste des partions disponibles
553  #
554  def hasDev(self, dev):
555  s="%s" %dev
556  s=s.replace("/org/freedesktop/UDisks/devices/","")
557  for p in self.fatPaths:
558  if p.split("/")[-1]==s:
559  return True
560  return False
561 
562 
563 if __name__=="__main__":
564  machin=Available()
565  print machin
566 
567