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