1#!/bin/sh
2"""": # -*-python-*-
3# https://sourceware.org/bugzilla/show_bug.cgi?id=26034
4export "BUP_ARGV_0"="$0"
5arg_i=1
6for arg in "$@"; do
7    export "BUP_ARGV_${arg_i}"="$arg"
8    shift
9    arg_i=$((arg_i + 1))
10done
11# Here to end of preamble replaced during install
12bup_python="$(dirname "$0")/../../config/bin/python" || exit $?
13exec "$bup_python" "$0"
14"""
15# end of bup preamble
16
17from __future__ import absolute_import, print_function
18import errno, os, sys
19
20try:
21    import fuse
22except ImportError:
23    print('error: cannot find the python "fuse" module; please install it',
24          file=sys.stderr)
25    sys.exit(2)
26if not hasattr(fuse, '__version__'):
27    print('error: fuse module is too old for fuse.__version__', file=sys.stderr)
28    sys.exit(2)
29fuse.fuse_python_api = (0, 2)
30
31if sys.version_info[0] > 2:
32    try:
33        fuse_ver = fuse.__version__.split('.')
34        fuse_ver_maj = int(fuse_ver[0])
35    except:
36        log('error: cannot determine the fuse major version; please report',
37            file=sys.stderr)
38        sys.exit(2)
39    if len(fuse_ver) < 3 or fuse_ver_maj < 1:
40        print("error: fuse module can't handle binary data; please upgrade to 1.0+\n",
41              file=sys.stderr)
42        sys.exit(2)
43
44sys.path[:0] = [os.path.dirname(os.path.realpath(__file__)) + '/..']
45
46from bup import compat, options, git, vfs, xstat
47from bup.compat import argv_bytes, fsdecode, py_maj
48from bup.helpers import log
49from bup.repo import LocalRepo
50
51
52# FIXME: self.meta and want_meta?
53
54# The path handling is just wrong, but the current fuse module can't
55# handle bytes paths.
56
57class BupFs(fuse.Fuse):
58    def __init__(self, repo, verbose=0, fake_metadata=False):
59        fuse.Fuse.__init__(self)
60        self.repo = repo
61        self.verbose = verbose
62        self.fake_metadata = fake_metadata
63
64    def getattr(self, path):
65        path = argv_bytes(path)
66        global opt
67        if self.verbose > 0:
68            log('--getattr(%r)\n' % path)
69        res = vfs.resolve(self.repo, path, want_meta=(not self.fake_metadata),
70                          follow=False)
71        name, item = res[-1]
72        if not item:
73            return -errno.ENOENT
74        if self.fake_metadata:
75            item = vfs.augment_item_meta(self.repo, item, include_size=True)
76        else:
77            item = vfs.ensure_item_has_metadata(self.repo, item,
78                                                include_size=True)
79        meta = item.meta
80        # FIXME: do we want/need to do anything more with nlink?
81        st = fuse.Stat(st_mode=meta.mode, st_nlink=1, st_size=meta.size)
82        st.st_mode = meta.mode
83        st.st_uid = meta.uid or 0
84        st.st_gid = meta.gid or 0
85        st.st_atime = max(0, xstat.fstime_floor_secs(meta.atime))
86        st.st_mtime = max(0, xstat.fstime_floor_secs(meta.mtime))
87        st.st_ctime = max(0, xstat.fstime_floor_secs(meta.ctime))
88        return st
89
90    def readdir(self, path, offset):
91        path = argv_bytes(path)
92        assert not offset  # We don't return offsets, so offset should be unused
93        res = vfs.resolve(self.repo, path, follow=False)
94        dir_name, dir_item = res[-1]
95        if not dir_item:
96            yield -errno.ENOENT
97        yield fuse.Direntry('..')
98        # FIXME: make sure want_meta=False is being completely respected
99        for ent_name, ent_item in vfs.contents(repo, dir_item, want_meta=False):
100            fusename = fsdecode(ent_name.replace(b'/', b'-'))
101            yield fuse.Direntry(fusename)
102
103    def readlink(self, path):
104        path = argv_bytes(path)
105        if self.verbose > 0:
106            log('--readlink(%r)\n' % path)
107        res = vfs.resolve(self.repo, path, follow=False)
108        name, item = res[-1]
109        if not item:
110            return -errno.ENOENT
111        return fsdecode(vfs.readlink(repo, item))
112
113    def open(self, path, flags):
114        path = argv_bytes(path)
115        if self.verbose > 0:
116            log('--open(%r)\n' % path)
117        res = vfs.resolve(self.repo, path, follow=False)
118        name, item = res[-1]
119        if not item:
120            return -errno.ENOENT
121        accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
122        if (flags & accmode) != os.O_RDONLY:
123            return -errno.EACCES
124        # Return None since read doesn't need the file atm...
125        # If we *do* return the file, it'll show up as the last argument
126        #return vfs.fopen(repo, item)
127
128    def read(self, path, size, offset):
129        path = argv_bytes(path)
130        if self.verbose > 0:
131            log('--read(%r)\n' % path)
132        res = vfs.resolve(self.repo, path, follow=False)
133        name, item = res[-1]
134        if not item:
135            return -errno.ENOENT
136        with vfs.fopen(repo, item) as f:
137            f.seek(offset)
138            return f.read(size)
139
140
141optspec = """
142bup fuse [-d] [-f] <mountpoint>
143--
144f,foreground  run in foreground
145d,debug       run in the foreground and display FUSE debug information
146o,allow-other allow other users to access the filesystem
147meta          report original metadata for paths when available
148v,verbose     increase log output (can be used more than once)
149"""
150o = options.Options(optspec)
151opt, flags, extra = o.parse(compat.argv[1:])
152if not opt.verbose:
153    opt.verbose = 0
154
155# Set stderr to be line buffered, even if it's not connected to the console
156# so that we'll be able to see diagnostics in a timely fashion.
157errfd = sys.stderr.fileno()
158sys.stderr.flush()
159sys.stderr = os.fdopen(errfd, 'w', 1)
160
161if len(extra) != 1:
162    o.fatal('only one mount point argument expected')
163
164git.check_repo_or_die()
165repo = LocalRepo()
166f = BupFs(repo=repo, verbose=opt.verbose, fake_metadata=(not opt.meta))
167
168# This is likely wrong, but the fuse module doesn't currently accept bytes
169f.fuse_args.mountpoint = extra[0]
170
171if opt.debug:
172    f.fuse_args.add('debug')
173if opt.foreground:
174    f.fuse_args.setmod('foreground')
175f.multithreaded = False
176if opt.allow_other:
177    f.fuse_args.add('allow_other')
178f.main()
179