Web · Wiki · Activities · Blog · Lists · Chat · Meeting · Bugs · Git · Translate · Archive · People · Donate
1
#!/usr/bin/env python
2
# Author: Sascha Silbe <sascha-pgp@silbe.org> (PGP signed emails only)
3
#
4
# This program is free software: you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License version 3
6
# as published by the Free Software Foundation.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
14
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
"""datastore-fuse: access the Sugar data store using FUSE
16
17
Mounting this "file system" allows "legacy" applications to access the Sugar
18
data store.
19
"""
20
21
import errno
22
import fuse
23
import logging
24
import os
25
import os.path
26
import shutil
27
import stat
28
import sys
29
import tempfile
30
31
from sugar.logger import trace
32
import sugar.logger
33
34
import fsemulation
35
36
37
fuse.fuse_python_api = (0, 2)
38
39
40
DS_DBUS_SERVICE = "org.laptop.sugar.DataStore"
41
DS_DBUS_INTERFACE = "org.laptop.sugar.DataStore"
42
DS_DBUS_PATH = "/org/laptop/sugar/DataStore"
43
44
XATTR_CREATE = 1
45
XATTR_REPLACE = 2
46
47
# DBus still has no way to indicate an infinite timeout :-/
48
DBUS_TIMEOUT_MAX = 2**31 / 1000
49
50
51
class FSEntryStat(fuse.Stat):
52
53
    def __init__(self, fs_entry, inode):
54
        if isinstance(fs_entry, fsemulation.Directory):
55
            n_links = 2
56
        else:
57
            n_links = 1
58
        fuse.Stat.__init__(self, st_mode=fs_entry.mode, st_ino=inode,
59
            st_uid=os.getuid(), st_gid=os.getgid(),
60
            st_size=fs_entry.get_size(), st_nlink=n_links,
61
            st_mtime=fs_entry.get_mtime(), st_ctime=fs_entry.get_ctime())
62
63
64
class DataStoreFile(object):
65
66
    _ACCESS_MASK = os.O_RDONLY | os.O_RDWR | os.O_WRONLY
67
    direct_io = False
68
    keep_cache = False
69
70
    @trace()
71
    def __init__(self, filesystem, path, flags, mode=None):
72
        self._filesystem = filesystem
73
        self._flags = flags
74
        self._read_only = False
75
        self._is_temporary = False
76
        self._dirty = False
77
        self._path = path
78
79
        # Contrary to what's documented in the wiki, we'll get passed O_CREAT
80
        # and mknod() won't get called automatically, so we'll have to take
81
        # care of all possible cases ourselves.
82
        if flags & os.O_EXCL:
83
            filesystem.mknod(path)
84
            entry = filesystem.resolve(path, follow_links=True)
85
        else:
86
            try:
87
                entry = filesystem.resolve(path, follow_links=True)
88
89
            except IOError, exception:
90
                if exception.errno != errno.ENOENT:
91
                    raise
92
93
                if not flags & os.O_CREAT:
94
                    raise
95
96
                filesystem.mknod(path, flags, mode)
97
                entry = filesystem.resolve(path, follow_links=True)
98
99
        self._entry = entry
100
        self._read_only = flags & self._ACCESS_MASK == os.O_RDONLY
101
102
        if self._filesystem.should_truncate(entry.object_id) or \
103
           (flags & os.O_TRUNC):
104
            self._file = self._create()
105
            self._filesystem.reset_truncate(entry.object_id)
106
        else:
107
            self._file = self._checkout()
108
109
    def _create(self):
110
        self._is_temporary = True
111
        return tempfile.NamedTemporaryFile(prefix='datastore-fuse')
112
113
    def _checkout(self):
114
        name = self._entry.get_data()
115
        if not name:
116
            # existing, but empty entry
117
            return self._create()
118
119
        if self._read_only:
120
            return file(name)
121
122
        try:
123
            copy = self._create()
124
            shutil.copyfileobj(file(name), copy)
125
            copy.seek(0)
126
            return copy
127
128
        finally:
129
            os.remove(name)
130
131
    @trace()
132
    def read(self, length, offset):
133
        self._file.seek(offset)
134
        return self._file.read(length)
135
136
    @trace()
137
    def write(self, buf, offset):
138
        if self._read_only:
139
            raise IOError(errno.EBADF, os.strerror(errno.EBADF))
140
141
        if self._flags & os.O_APPEND:
142
            self._file.seek(0, os.SEEK_END)
143
        else:
144
            self._file.seek(offset)
145
146
        self._file.write(buf)
147
        self._dirty = True
148
        return len(buf)
149
150
    @trace()
151
    def release(self, flags_):
152
        self.fsync()
153
        self._file.close()
154
        if not self._is_temporary:
155
            os.remove(self._file.name)
156
157
    @trace()
158
    def fsync(self, isfsyncfile_=None):
159
        self.flush()
160
        if self._read_only:
161
            return
162
163
        if self._dirty:
164
            self._entry.write_data(self._file.name)
165
166
    @trace()
167
    def flush(self):
168
        self._file.flush()
169
170
    @trace()
171
    def fgetattr(self):
172
        return self._filesystem.getattr(self._path)
173
174
    @trace()
175
    def ftruncate(self, length):
176
        self._file.truncate(length)
177
178
    @trace()
179
    def lock(self, cmd_, owner_, **kwargs_):
180
        raise IOError(errno.EPERM, os.strerror(errno.EPERM))
181
182
183
class DataStoreFS(fuse.Fuse):
184
185
    # FUSE API
186
187
    def __init__(self_fs, *args, **kw):
188
        # pylint: disable-msg=E0213
189
        class WrappedDataStoreFile(DataStoreFile):
190
            def __init__(self_file, *args, **kwargs):
191
                # pylint: disable-msg=E0213
192
                DataStoreFile.__init__(self_file, self_fs, *args, **kwargs)
193
194
        self_fs.file_class = WrappedDataStoreFile
195
        self_fs._truncate_object_ids = set()
196
        self_fs._max_inode_number = 1
197
        self_fs._object_id_to_inode_number = {}
198
199
        fuse.Fuse.__init__(self_fs, *args, **kw)
200
201
        self_fs._fs_emu = fsemulation.FSEmulation()
202
        # TODO: listen to DS signals to update name mapping
203
204
    @trace()
205
    def getattr(self, path):
206
        entry = self.resolve(path)
207
        return FSEntryStat(entry, self._get_inode_number(path))
208
209
    def readdir(self, path, offset=None):
210
        logging.debug('readdir(): begin')
211
        for name_unicode, entry in self.resolve(path).readdir():
212
            logging.debug('readdir(): name_unicode=%r, entry=%r', name_unicode, entry)
213
            name_utf8 = name_unicode.encode('utf-8')
214
            entry_path = os.path.join(path, name_utf8)
215
            if isinstance(entry, fsemulation.Directory):
216
                yield fuse.Direntry(name_utf8, type=stat.S_IFDIR,
217
                    ino=self._get_inode_number(entry_path))
218
            elif isinstance(entry, fsemulation.Symlink):
219
                yield fuse.Direntry(name_utf8, type=stat.S_IFLNK,
220
                    ino=self._get_inode_number(entry_path))
221
            elif isinstance(entry, fsemulation.DSObject):
222
                yield fuse.Direntry(name_utf8, type=stat.S_IFREG,
223
                    ino=self._get_inode_number(entry_path))
224
            else:
225
                logging.error('readdir(): FS object of unknown type: %r',
226
                              entry)
227
        logging.debug('readdir(): end')
228
229
    def readlink(self, path):
230
        return self.resolve(path).readlink().encode('utf-8')
231
232
    def mknod(self, path, mode_=None, dev_=None):
233
        # called by FUSE for open(O_CREAT) before instantiating the file
234
        directory, name = os.path.split(path)
235
        return self.resolve(directory).mknod(unicode(name, 'utf-8'))
236
237
    def truncate(self, path, mode_=None, dev_=None):
238
        # Documented to be called by FUSE when opening files with O_TRUNC,
239
        # unless -o o_trunc_atomic is passed as a CLI option
240
        entry = self.resolve(path, follow_links=True)
241
        if isinstance(entry, fsemulation.Directory):
242
            raise IOError(errno.EISDIR, os.strerror(errno.EISDIR))
243
        elif not isinstance(entry, fsemulation.DSObject):
244
            logging.error('Trying to truncate an object of unknown type: %r',
245
                          entry)
246
            raise IOError(errno.EINVAL, os.strerror(errno.EINVAL))
247
248
        self._truncate_object_ids.add(entry.object_id)
249
250
    @trace()
251
    def unlink(self, path):
252
        entry = self.resolve(path)
253
        if isinstance(entry, fsemulation.Directory):
254
            raise IOError(errno.EISDIR, os.strerror(errno.EISDIR))
255
        entry.remove()
256
257
    @trace()
258
    def utime(self, path_, times_):
259
        # TODO: update timestamp property
260
        return
261
262
    def mkdir(self, path, mode_):
263
        directory, name = os.path.split(path)
264
        return self.resolve(directory).mkdir(unicode(name, 'utf-8'))
265
266
    @trace()
267
    def rmdir(self, path):
268
        entry = self.resolve(path)
269
        if not isinstance(entry, fsemulation.Directory):
270
            raise IOError(errno.ENOTDIR, os.strerror(errno.ENOTDIR))
271
        entry.remove()
272
273
    def rename(self, pathfrom, pathto):
274
        # FIXME
275
        raise IOError(errno.EPERM, os.strerror(errno.EPERM))
276
        #self._delegate(pathfrom, 'rename', pathto)
277
278
    @trace()
279
    def symlink(self, destination_, path_):
280
        # TODO for tags?
281
        raise IOError(errno.EPERM, os.strerror(errno.EPERM))
282
283
    @trace()
284
    def link(self, destination_, path_):
285
        raise IOError(errno.EPERM, os.strerror(errno.EPERM))
286
287
    @trace()
288
    def chmod(self, path_, mode_):
289
        raise IOError(errno.EPERM, os.strerror(errno.EPERM))
290
291
    @trace()
292
    def chown(self, path_, user, group):
293
        if (user != os.getuid()) or (group != os.getgid()):
294
            raise IOError(errno.EPERM, os.strerror(errno.EPERM))
295
296
    def getxattr(self, path, name, size):
297
        if not name.startswith('user.'):
298
            raise IOError(errno.ENODATA, os.strerror(errno.ENODATA))
299
300
        attr_name_unicode = unicode(name[5:], 'utf-8')
301
        props = self.resolve(path).get_properties([attr_name_unicode])
302
        #log.debug('getxattr(): props=%r', props)
303
        if attr_name_unicode not in props:
304
            raise IOError(errno.ENODATA, os.strerror(errno.ENODATA))
305
306
        value_unicode = props[attr_name_unicode]
307
        if isinstance(value_unicode, unicode):
308
            value_utf8 = value_unicode.encode('utf-8')
309
        else:
310
            # binary data
311
            value_utf8 = str(value_unicode)
312
313
        if not size:
314
            # We are asked for size of the value.
315
            return len(value_utf8)
316
317
        return value_utf8
318
319
    def listxattr(self, path, size):
320
        props = self.resolve(path).list_properties()
321
        attribute_names = ['user.' + name.encode('utf-8') for name in props]
322
        if not size:
323
            # We are asked for the size of the \0-separated list.
324
            return sum([len(name) + 1 for name in attribute_names])
325
326
        return attribute_names
327
328
    def setxattr(self, path, name, value, flags):
329
        if not name.startswith('user.'):
330
            raise IOError(errno.EOPNOTSUPP, os.strerror(errno.EOPNOTSUPP))
331
332
        attr_name_unicode = unicode(name[5:], 'utf-8')
333
        value_unicode = unicode(value, 'utf-8')
334
        entry = self.resolve(path)
335
        if flags & XATTR_CREATE:
336
            entry.create_property(name, value_unicode)
337
        elif flags & XATTR_REPLACE:
338
            entry.replace_property(name, value_unicode)
339
        else:
340
            entry.set_properties({name: value_unicode})
341
342
    # internal API (for DataStoreFile)
343
344
    def should_truncate(self, object_id):
345
        return object_id in self._truncate_object_ids
346
347
    def reset_truncate(self, object_id):
348
        self._truncate_object_ids.discard(object_id)
349
350
    def resolve(self, path, follow_links=False):
351
        """Look up emulated FS object located at path (UTF-8 encoded string)
352
        """
353
        return self._fs_emu.resolve(unicode(path, 'utf-8'),
354
                                    follow_links=follow_links)
355
356
    # private methods
357
358
    def _get_inode_number(self, key):
359
        if key not in self._object_id_to_inode_number:
360
            inode_number = self._max_inode_number
361
            self._max_inode_number += 1
362
            self._object_id_to_inode_number[key] = inode_number
363
364
        return self._object_id_to_inode_number[key]
365
366
367
def main():
368
    usage = "datastore-fuse: access the Sugar data store using FUSE\n"
369
    usage += fuse.Fuse.fusage
370
371
    sugar.logger.start()
372
373
    # FIXME: figure out how to force options to on, properly.
374
    sys.argv += ['-o', 'use_ino']
375
    server = DataStoreFS(version="%prog " + fuse.__version__, usage=usage,
376
        dash_s_do='setsingle')
377
    server.parse(errex=1)
378
    server.main()
379
380
if __name__ == '__main__':
381
    main()