Merge lp:~zeitgeist/zeitgeist/storagemonitor2 into lp:zeitgeist/0.1

Proposed by Mikkel Kamstrup Erlandsen
Status: Merged
Merged at revision: 1699
Proposed branch: lp:~zeitgeist/zeitgeist/storagemonitor2
Merge into: lp:zeitgeist/0.1
Diff against target: 476 lines (+420/-5)
6 files modified
_zeitgeist/engine/__init__.py (+1/-1)
_zeitgeist/engine/extensions/Makefile.am (+2/-1)
_zeitgeist/engine/extensions/storagemonitor.py (+385/-0)
_zeitgeist/engine/sql.py (+3/-1)
_zeitgeist/engine/upgrades/core_3_4.py (+23/-2)
doc/zeitgeist/source/dbus_api.rst (+6/-0)
To merge this branch: bzr merge lp:~zeitgeist/zeitgeist/storagemonitor2
Reviewer Review Type Date Requested Status
Siegfried Gevatter Approve
Review via email: mp+49212@code.launchpad.net

Description of the change

Woohoo, finally the storage monitor is ready. It supports uodating the storage table with the values from GIO volume monitors as well as the network state from Connman or NetworkManager which ever is available on the system.

The extension will also populate the 'storage' field of subjects that don't already have one.

So what's missing? Well ZG will raise NotImplementedError if you send it a StorageState that is different from StorageState.Any. So I'll propose another branch that properly supports this.

To post a comment you must log in.
Revision history for this message
Siegfried Gevatter (rainct) wrote :

+logging.basicConfig(level=logging.DEBUG)
WHY?

And why is this and the imports there twice (before and after the big comments)?

+# storgaemonitor extension. This is actually backwards compatible.
Storgae? Is that like 'deine mudda' in Danish?

review: Needs Fixing
Revision history for this message
Siegfried Gevatter (rainct) wrote :

> "lambda : "
The space before the colons looks ugly :P.

> "except:"
Please make this "except sqlite3.foobar" (or, worst-case, "except Exception").
Also, why do you need the rollback there?

> "A storage medium is indetified by a key"
How can I indetify you? :)

You could move most the the NM/Connman code into a common base class.

Looks great otherwise. Good job!!!

review: Needs Fixing
Revision history for this message
Mikkel Kamstrup Erlandsen (kamstrup) wrote :

> > "lambda : "
> The space before the colons looks ugly :P.

Fixed

> > "except:"
> Please make this "except sqlite3.foobar" (or, worst-case, "except Exception").
> Also, why do you need the rollback there?

Since sqlite3 doesn't have a common error super class I am just catching Exception for now. The rollback() has been changed to 'return' instead.

> > "A storage medium is indetified by a key"
> How can I indetify you? :)

Fixed

> You could move most the the NM/Connman code into a common base class.

Yeah, I was thinking that, but there is so little code in them that I think a common base class would almost give us more net lines :-)

1676. By Mikkel Kamstrup Erlandsen

Fixes from review by Siegfried Gevatter

1677. By Mikkel Kamstrup Erlandsen

Typo in docstring

1678. By Mikkel Kamstrup Erlandsen

Remove line logging.basicConfig(level=logging.DEBUG) from storagemonitor.py

Revision history for this message
Siegfried Gevatter (rainct) wrote :

Haven't tried it yet, but the code looks good to me (although I still think a baseclass would be neat :P).

Revision history for this message
Siegfried Gevatter (rainct) wrote :

[2011-03-04 22:48:11,119] - ERROR - zeitgeist.extension - Failed loading the 'StorageMonitor' extension
Traceback (most recent call last):
  File "/home/rainct/Desenvolupament/Python/zeitgeist-project/storagemonitor2/zeitgeist/../_zeitgeist/engine/extension.py", line 265, in load
    obj = extension(self.__engine)
  File "/home/rainct/Desenvolupament/Python/zeitgeist-project/storagemonitor2/zeitgeist/../_zeitgeist/engine/extensions/storagemonitor.py", line 125, in __init__
    lambda: self.remove_storage_medium("net"))
  File "/home/rainct/Desenvolupament/Python/zeitgeist-project/storagemonitor2/zeitgeist/../_zeitgeist/engine/extensions/storagemonitor.py", line 326, in __init__
    proxy = dbus.SystemBus().get_object(NetworkMonitor.NM_BUS_NAME,
NameError: global name 'NetworkMonitor' is not defined

review: Needs Fixing
1679. By Mikkel Kamstrup Erlandsen

Fix ref to class variables NetworkMonitor.BLAH -> NMNetworkMonitor.BLAH

Revision history for this message
Siegfried Gevatter (rainct) wrote :

Alright, I've tested it with Network Manager and it seems to work fine. Awesome work!

Since I love nitpicking so much, "self._up ()" could be changed to "self._up()" :P. But anyway, I think this is ready to merge as soon as dbschema4 goes in (outstanding issues for that are deciding what to do with 0.7 compatibility, merging Seif's move tracking and deciding if we like Markus' cache deletion workaround - am I forgetting anything?).

review: Approve
1680. By Siegfried Gevatter

Merge with trunk.

1681. By Markus Korn

merged changes from lp:zeitgeist

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '_zeitgeist/engine/__init__.py'
2--- _zeitgeist/engine/__init__.py 2011-01-17 15:54:47 +0000
3+++ _zeitgeist/engine/__init__.py 2011-04-04 09:02:24 +0000
4@@ -57,7 +57,7 @@
5
6 # Required version of DB schema
7 CORE_SCHEMA="core"
8- CORE_SCHEMA_VERSION = 3
9+ CORE_SCHEMA_VERSION = 4
10
11 USER_EXTENSION_PATH = os.path.join(DATA_PATH, "extensions")
12
13
14=== modified file '_zeitgeist/engine/extensions/Makefile.am'
15--- _zeitgeist/engine/extensions/Makefile.am 2010-06-17 20:43:34 +0000
16+++ _zeitgeist/engine/extensions/Makefile.am 2011-04-04 09:02:24 +0000
17@@ -3,4 +3,5 @@
18 app_PYTHON = \
19 __init__.py \
20 blacklist.py \
21- datasource_registry.py
22+ datasource_registry.py \
23+ storagemonitor.py
24
25=== added file '_zeitgeist/engine/extensions/storagemonitor.py'
26--- _zeitgeist/engine/extensions/storagemonitor.py 1970-01-01 00:00:00 +0000
27+++ _zeitgeist/engine/extensions/storagemonitor.py 2011-04-04 09:02:24 +0000
28@@ -0,0 +1,385 @@
29+# -.- coding: utf-8 -.-
30+
31+# Zeitgeist
32+#
33+# Copyright © 2009 Mikkel Kamstrup Erlandsen <mikkel.kamstrup@gmail.com>
34+# Copyright © 2011 Canonical Ltd
35+#
36+# This program is free software: you can redistribute it and/or modify
37+# it under the terms of the GNU Lesser General Public License as published by
38+# the Free Software Foundation, either version 2.1 of the License, or
39+# (at your option) any later version.
40+#
41+# This program is distributed in the hope that it will be useful,
42+# but WITHOUT ANY WARRANTY; without even the implied warranty of
43+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
44+# GNU Lesser General Public License for more details.
45+#
46+# You should have received a copy of the GNU Lesser General Public License
47+# along with this program. If not, see <http://www.gnu.org/licenses/>.
48+
49+import os
50+import dbus
51+import dbus.service
52+import sqlite3
53+import gio
54+import logging
55+
56+from zeitgeist.datamodel import Event
57+from _zeitgeist.engine.extension import Extension
58+from _zeitgeist.engine import constants
59+
60+from zeitgeist.datamodel import StorageState
61+from _zeitgeist.engine.sql import get_default_cursor
62+
63+log = logging.getLogger("zeitgeist.storagemonitor")
64+
65+#
66+# Storage mediums we need to handle:
67+#
68+# - USB drives
69+# - Data CD/DVDs
70+# - Audio CDs
71+# - Video DVD
72+# - Networked file systems
73+# - Online resources
74+#
75+# A storage medium is gio.Volume (since this is a physical entity for the user)
76+# or a network interface - how ever NetworkManager/ConnMan model these
77+#
78+# We can not obtain UUIDs for all of the listed gio.Volumes, so we need a
79+# fallback chain of identifiers
80+#
81+# DB schema:
82+# It may be handy for app authors to have the human-readable
83+# description at hand. We can not currently easily do this in the
84+# current db... We may be able to do this in a new table, not
85+# breaking compat with the log db. We might also want a formal type
86+# associated with the storage so apps can use an icon for it.
87+# A new table and a new object+interface on DBus could facilitate this
88+#
89+# 'storage' table
90+# id
91+# name
92+# state
93+# +type
94+# +display_name
95+#
96+# FIXME: We can not guess what the correct ID of CDs and DVDs were when they
97+# are ejected, and also guess "unknown"
98+#
99+
100+STORAGE_MONITOR_DBUS_OBJECT_PATH = "/org/gnome/zeitgeist/storagemonitor"
101+STORAGE_MONITOR_DBUS_INTERFACE = "org.gnome.zeitgeist.StorageMonitor"
102+
103+class StorageMonitor(Extension, dbus.service.Object):
104+ """
105+ The Storage Monitor monitors the availability of network interfaces and
106+ storage devices and updates the Zeitgeist database with this information so
107+ clients can efficiently query based on the storage identifier and availability
108+ of the storage media the event subjects reside on.
109+
110+ For storage devices the monitor will use the UUID of the partition that a
111+ subject reside on as storage id. For network URIs the storage monitor will
112+ use the fixed identifier :const:`net`. For subjects residing on persistent,
113+ but unidentifiable, media attached to the computer the id :const:`local`
114+ will be used. For URIs that can't be handled the storage id will be set
115+ to :const:`unknown`. The :const:`local` and :const:`unknown` storage media
116+ are considered to be always in an available state. To determine the
117+ availability of the :const:`net` media the monitor will use either Connman
118+ or NetworkManager - what ever is available on the host system.
119+
120+ For subjects being inserted into the log that doesn't have a storage id set
121+ on them this extension will try and figure it out on the fly and update
122+ the subject appropriately before its inserted into the log.
123+
124+ The storage monitor of the Zeitgeist engine has DBus object path
125+ :const:`/org/gnome/zeitgeist/storagemonitor` under the bus name
126+ :const:`org.gnome.zeitgeist.Engine`.
127+ """
128+ PUBLIC_METHODS = []
129+
130+ def __init__ (self, engine):
131+ Extension.__init__(self, engine)
132+ dbus.service.Object.__init__(self, dbus.SessionBus(),
133+ STORAGE_MONITOR_DBUS_OBJECT_PATH)
134+
135+ self._db = get_default_cursor()
136+ mon = gio.VolumeMonitor()
137+
138+ # Update DB with all current states
139+ for vol in mon.get_volumes():
140+ self.add_storage_medium(self._get_volume_id(vol), vol.get_icon().to_string(), vol.get_name())
141+
142+ # React to volumes comming and going
143+ mon.connect("volume-added", self._on_volume_added)
144+ mon.connect("volume-removed", self._on_volume_removed)
145+
146+ # Write connectivity to the DB. Dynamically decide whether to use
147+ # Connman or NetworkManager
148+ if dbus.SystemBus().name_has_owner ("net.connman"):
149+ self._network = ConnmanNetworkMonitor(lambda: self.add_storage_medium("net", "stock_internet", "Internet"),
150+ lambda: self.remove_storage_medium("net"))
151+ elif dbus.SystemBus().name_has_owner ("org.freedesktop.NetworkManager"):
152+ self._network = NMNetworkMonitor(lambda: self.add_storage_medium("net", "stock_internet", "Internet"),
153+ lambda: self.remove_storage_medium("net"))
154+ else:
155+ log.info("No network monitoring system found (Connman or NetworkManager)."
156+ "Network monitoring disabled")
157+
158+ def pre_insert_event (self, event, dbus_sender):
159+ """
160+ On-the-fly add subject.storage to events if it is not set
161+ """
162+ for subj in event.subjects:
163+ if not subj.storage:
164+ storage = self._find_storage(subj.uri)
165+ #log.debug("Subject %s resides on %s" % (subj.uri, storage))
166+ subj.storage = storage
167+ return event
168+
169+ def _find_storage (self, uri):
170+ """
171+ Given a URI find the name of the storage medium it resides on
172+ """
173+ uri_scheme = uri.rpartition("://")[0]
174+ if uri_scheme in ["http", "https", "ftp", "sftp", "ssh", "mailto"]:
175+ return "net"
176+ elif uri_scheme == "file":
177+ # Note: gio.File.find_enclosing_mount() does not behave
178+ # as documented, but throws errors when no
179+ # gio.Mount is found.
180+ # Cases where we have no mount often happens when
181+ # we are on a non-removable drive , and this is
182+ # the assumption here. We use the stora medium
183+ # 'local' for this situation
184+ try:
185+ mount = gio.File(uri=uri).find_enclosing_mount()
186+ except gio.Error:
187+ return "local"
188+ if mount is None: return "unknown"
189+ return self._get_volume_id(mount.get_volume())
190+
191+ def _on_volume_added (self, mon, volume):
192+ icon = volume.get_icon()
193+ if isinstance(icon, gio.ThemedIcon):
194+ icon_name = icon.get_names()[0]
195+ else:
196+ icon_name = ""
197+ self.add_storage_medium (self._get_volume_id(volume), icon_name, volume.get_name())
198+
199+ def _on_volume_removed (self, mon, volume):
200+ self.remove_storage_medium (self._get_volume_id(volume))
201+
202+ def _get_volume_id (self, volume):
203+ """
204+ Get a string identifier for a gio.Volume. The id is constructed
205+ as a "best effort" since we can not always uniquely identify
206+ volumes, especially audio- and data CDs are problematic.
207+ """
208+ volume_id = volume.get_uuid()
209+ if volume_id : return volume_id
210+
211+ volume_id = volume.get_identifier("uuid")
212+ if volume_id : return volume_id
213+
214+ volume_id = volume.get_identifier("label")
215+ if volume_id : return volume_id
216+
217+ volume_id = volume.get_name()
218+ if volume_id : return volume_id
219+
220+ return "unknown"
221+
222+ def add_storage_medium (self, medium_name, icon, display_name):
223+ """
224+ Mark storage medium as available in the Zeitgeist DB
225+ """
226+ if isinstance(icon,gio.Icon):
227+ icon = icon.to_string()
228+ elif not isinstance(icon, basestring):
229+ raise TypeError, "The 'icon' argument must be a gio.Icon or a string"
230+
231+ log.debug("Setting storage medium %s '%s' as available" % (medium_name, display_name))
232+
233+ try:
234+ self._db.execute("INSERT INTO storage (value, state, icon, display_name) VALUES (?, ?, ?, ?)", (medium_name, StorageState.Available, icon, display_name))
235+ except sqlite3.IntegrityError, e:
236+ try:
237+ self._db.execute("UPDATE storage SET state=?, icon=?, display_name=? WHERE value=?", (StorageState.Available, icon, display_name, medium_name))
238+ except Exception, e:
239+ log.warn("Error updating storage state for '%s': %s" % (medium_name, e))
240+ return
241+
242+ self._db.connection.commit()
243+
244+ # Notify DBus that the storage is available
245+ self.StorageAvailable(medium_name, { "available" : True,
246+ "icon" : icon or "",
247+ "display-name" : display_name or ""})
248+
249+ def remove_storage_medium (self, medium_name):
250+ """
251+ Mark storage medium as `not` available in the Zeitgeist DB
252+ """
253+
254+ log.debug("Setting storage medium %s as not available" % medium_name)
255+
256+ try:
257+ self._db.execute("INSERT INTO storage (value, state) VALUES (?, ?)", (medium_name, StorageState.NotAvailable))
258+ except sqlite3.IntegrityError, e:
259+ try:
260+ self._db.execute("UPDATE storage SET state=? WHERE value=?", (StorageState.NotAvailable, medium_name))
261+ except Exception, e:
262+ log.warn("Error updating storage state for '%s': %s" % (medium_name, e))
263+ return
264+
265+ self._db.connection.commit()
266+
267+ # Notify DBus that the storage is unavailable
268+ self.StorageUnavailable(medium_name)
269+
270+ @dbus.service.method(STORAGE_MONITOR_DBUS_INTERFACE,
271+ out_signature="a(sa{sv})")
272+ def GetStorages (self):
273+ """
274+ Retrieve a list describing all storage media known by the Zeitgeist daemon.
275+ A storage medium is identified by a key - as set in the subject
276+ :const:`storage` field. For each storage id there is a dict of properties
277+ that will minimally include the following: :const:`available` with a boolean
278+ value, :const:`icon` a string with the name of the icon to use for the
279+ storage medium, and :const:`display-name` a string with a human readable
280+ name for the storage medium.
281+
282+ The DBus signature of the return value of this method is :const:`a(sa{sv})`.
283+ """
284+ storage_mediums = []
285+ storage_data = self._db.execute("SELECT value, state, icon, display_name FROM storage").fetchall()
286+
287+ for row in storage_data:
288+ if not row[0] : continue
289+ storage_mediums.append((row[0],
290+ { "available" : bool(row[1]),
291+ "icon" : row[2] or "",
292+ "display-name" : row[3] or ""}))
293+
294+ return storage_mediums
295+
296+ @dbus.service.signal(STORAGE_MONITOR_DBUS_INTERFACE,
297+ signature="sa{sv}")
298+ def StorageAvailable (self, storage_id, storage_description):
299+ """
300+ The Zeitgeist daemon emits this signal when the storage medium with id
301+ :const:`storage_id` has become available.
302+
303+ The second parameter for this signal is a dictionary containing string
304+ keys and variant values. The keys that are guaranteed to be there are
305+ :const:`available` with a boolean value, :const:`icon` a string with the
306+ name of the icon to use for the storage medium, and :const:`display-name`
307+ a string with a human readable name for the storage medium.
308+
309+ The DBus signature of this signal is :const:`sa{sv}`.
310+ """
311+ pass
312+
313+ @dbus.service.signal(STORAGE_MONITOR_DBUS_INTERFACE,
314+ signature="s")
315+ def StorageUnavailable (self, storage_id):
316+ """
317+ The Zeitgeist daemon emits this signal when the storage medium with id
318+ :const:`storage_id` is no longer available.
319+
320+ The DBus signature of this signal is :const:`s`.
321+ """
322+ pass
323+
324+class NMNetworkMonitor:
325+ """
326+ Checks whether there is a funtioning network interface via
327+ NetworkManager (requires NM >= 0.8).
328+ See http://projects.gnome.org/NetworkManager/developers/spec-08.html
329+ """
330+ NM_BUS_NAME = "org.freedesktop.NetworkManager"
331+ NM_IFACE = "org.freedesktop.NetworkManager"
332+ NM_OBJECT_PATH = "/org/freedesktop/NetworkManager"
333+
334+ NM_STATE_UNKNOWN = 0
335+ NM_STATE_ASLEEP = 1
336+ NM_STATE_CONNECTING = 2
337+ NM_STATE_CONNECTED = 3
338+ NM_STATE_DISCONNECTED = 4
339+
340+ def __init__ (self, on_network_up, on_network_down):
341+ log.debug("Creating NetworkManager network monitor")
342+ if not callable(on_network_up):
343+ raise TypeError((
344+ "First argument to NMNetworkMonitor constructor "
345+ "must be callable, found %s" % on_network_up))
346+ if not callable(on_network_down):
347+ raise TypeError((
348+ "Second argument to NMNetworkMonitor constructor "
349+ "must be callable, found %s" % on_network_up))
350+
351+ self._up = on_network_up
352+ self._down = on_network_down
353+
354+ proxy = dbus.SystemBus().get_object(NMNetworkMonitor.NM_BUS_NAME,
355+ NMNetworkMonitor.NM_OBJECT_PATH)
356+ self._props = dbus.Interface(proxy, dbus.PROPERTIES_IFACE)
357+ self._nm = dbus.Interface(proxy, NMNetworkMonitor.NM_IFACE)
358+ self._nm.connect_to_signal("StateChanged", self._on_state_changed)
359+
360+ # Register the initial state
361+ state = self._props.Get(NMNetworkMonitor.NM_IFACE, "State")
362+ self._on_state_changed(state)
363+
364+ def _on_state_changed(self, state):
365+ log.debug("NetworkManager network state: %s" % state)
366+ if state == NMNetworkMonitor.NM_STATE_CONNECTED:
367+ self._up ()
368+ else:
369+ self._down()
370+
371+class ConnmanNetworkMonitor:
372+ """
373+ Checks whether there is a funtioning network interface via Connman
374+ """
375+ CM_BUS_NAME = "net.connman"
376+ CM_IFACE = "net.connman.Manager"
377+ CM_OBJECT_PATH = "/"
378+
379+ def __init__ (self, on_network_up, on_network_down):
380+ log.debug("Creating Connman network monitor")
381+ if not callable(on_network_up):
382+ raise TypeError((
383+ "First argument to ConnmanNetworkMonitor constructor "
384+ "must be callable, found %s" % on_network_up))
385+ if not callable(on_network_down):
386+ raise TypeError((
387+ "Second argument to ConnmanNetworkMonitor constructor "
388+ "must be callable, found %s" % on_network_up))
389+
390+ self._up = on_network_up
391+ self._down = on_network_down
392+
393+ proxy = dbus.SystemBus().get_object(ConnmanNetworkMonitor.CM_BUS_NAME,
394+ ConnmanNetworkMonitor.CM_OBJECT_PATH)
395+ self._cm = dbus.Interface(proxy, ConnmanNetworkMonitor.CM_IFACE)
396+ self._cm.connect_to_signal("StateChanged", self._on_state_changed)
397+ #
398+ # ^^ There is a bug in some Connman versions causing it to not emit the
399+ # net.connman.Manager.StateChanged signal. We take our chances this
400+ # instance is working properly :-)
401+ #
402+
403+
404+ # Register the initial state
405+ state = self._cm.GetState()
406+ self._on_state_changed(state)
407+
408+ def _on_state_changed(self, state):
409+ log.debug("Connman network state is '%s'" % state)
410+ if state == "online":
411+ self._up ()
412+ else:
413+ self._down()
414
415=== modified file '_zeitgeist/engine/sql.py'
416--- _zeitgeist/engine/sql.py 2011-01-17 15:54:47 +0000
417+++ _zeitgeist/engine/sql.py 2011-04-04 09:02:24 +0000
418@@ -242,7 +242,9 @@
419 CREATE TABLE IF NOT EXISTS storage
420 (id INTEGER PRIMARY KEY,
421 value VARCHAR UNIQUE,
422- state INTEGER)
423+ state INTEGER,
424+ icon VARCHAR,
425+ display_name VARCHAR)
426 """)
427 cursor.execute("""
428 CREATE UNIQUE INDEX IF NOT EXISTS storage_value
429
430=== modified file '_zeitgeist/engine/upgrades/core_3_4.py'
431--- _zeitgeist/engine/upgrades/core_3_4.py 2011-02-09 14:00:20 +0000
432+++ _zeitgeist/engine/upgrades/core_3_4.py 2011-04-04 09:02:24 +0000
433@@ -1,6 +1,27 @@
434 # upgrading from db version 3 to 4
435
436-# FIXME: Add description of upgrade changes
437+# Changes:
438+#
439+# * Appends to new rows to the 'storage' table that is needed by the new
440+# storagemonitor extension. This is actually backwards compatible.
441+
442+from zeitgeist.datamodel import StorageState
443
444 def run(cursor):
445- pass
446+ # Add the new columns for the storage table
447+ cursor.execute ("ALTER TABLE storage ADD COLUMN icon VARCHAR")
448+ cursor.execute ("ALTER TABLE storage ADD COLUMN display_name VARCHAR")
449+
450+ # Add the default storage mediums 'UNKNOWN' and 'local' and set them
451+ # as always available
452+ cursor.execute("INSERT INTO storage (value, state) VALUES ('unknown', ?)", (StorageState.Available,))
453+ unknown_storage_rowid = cursor.lastrowid
454+ cursor.execute("INSERT INTO storage (value, state) VALUES ('local', ?)", (StorageState.Available,))
455+
456+ # Set all subjects that are already in the DB to have 'unknown' storage
457+ # That way they will always be marked as available. We don't have a chance
458+ # of properly backtracking all items, so we use this as a clutch
459+ cursor.execute("UPDATE event SET subj_storage=? WHERE subj_storage IS NULL", (unknown_storage_rowid, ))
460+
461+ cursor.connection.commit()
462+
463
464=== modified file 'doc/zeitgeist/source/dbus_api.rst'
465--- doc/zeitgeist/source/dbus_api.rst 2010-03-04 19:24:04 +0000
466+++ doc/zeitgeist/source/dbus_api.rst 2011-04-04 09:02:24 +0000
467@@ -107,3 +107,9 @@
468
469 .. autoclass:: _zeitgeist.engine.extensions.datasource_registry.DataSourceRegistry
470 :members: RegisterDataSource, GetDataSources, SetDataSourceEnabled, DataSourceEnabled, DataSourceRegistered, DataSourceDisconnected
471+
472+org.gnome.zeitgeist.StorageMonitor
473++++++++++++++++++++++++++++++
474+
475+.. autoclass:: _zeitgeist.engine.extensions.storagemonitor.StorageMonitor
476+ :members: GetStorages, StorageAvailable, StorageUnavailable