]> arthur.barton.de Git - bup.git/blob - cmd-fuse.py
cmd-ls and cmd-fuse: toys for browsing your available backups.
[bup.git] / cmd-fuse.py
1 #!/usr/bin/env python
2 import sys, os, stat, errno, fuse, re, time, tempfile
3 import options, git
4 from helpers import *
5
6
7 def namesplit(path):
8     l = path.split('/', 3)
9     ref = None
10     date = None
11     dir = None
12     assert(l[0] == '')
13     if len(l) > 1:
14         ref = l[1] or None
15     if len(l) > 2:
16         date = l[2]
17     if len(l) > 3:
18         dir = l[3]
19     return (ref, date, dir)
20
21
22 # FIXME: iterating through a file just to check its size is super slow!
23 def sz(it):
24     count = 0
25     for d in it:
26         count += len(d)
27     return count
28
29
30 def date_to_commit(ref, datestr):
31     dates = dates_for_ref(ref)
32     dates.sort(reverse=True)
33     try:
34         dp = time.strptime(datestr, '%Y-%m-%d-%H%M%S')
35     except ValueError:
36         dp = time.strptime(datestr, '%Y-%m-%d')
37     dt = time.mktime(dp)
38     commit = None
39     for (d,commit) in dates:
40         if d <= dt: break
41     assert(commit)
42     return commit
43
44
45 refdates = {}
46 def dates_for_ref(ref):
47     dates = refdates.get(ref)
48     if not dates:
49         dates = refdates[ref] = list(git.rev_list(ref))
50         dates.sort()
51     return dates
52
53
54 class Stat(fuse.Stat):
55     def __init__(self):
56         self.st_mode = 0
57         self.st_ino = 0
58         self.st_dev = 0
59         self.st_nlink = 0
60         self.st_uid = 0
61         self.st_gid = 0
62         self.st_size = 0
63         self.st_atime = 0
64         self.st_mtime = 0
65         self.st_ctime = 0
66
67
68 statcache = {}
69 filecache = {}
70
71
72 class BupFs(fuse.Fuse):
73     def getattr(self, path):
74         log('--getattr(%r)\n' % path)
75         sc = statcache.get(path)
76         if sc:
77             return sc
78         (ref,date,filename) = namesplit(path)
79         if not ref:
80             st = Stat()
81             st.st_mode = stat.S_IFDIR | 0755
82             st.st_nlink = 1  # FIXME
83             statcache[path] = st
84             return st
85         elif not date or not filename:
86             st = Stat()
87             try:
88                 git.read_ref(ref)
89             except git.GitError:
90                 pass
91             st.st_mode = stat.S_IFDIR | 0755
92             st.st_nlink = 1  # FIXME
93             statcache[path] = st
94             return st
95         else:
96             st = Stat()
97             commit = date_to_commit(ref, date)
98             (dir,name) = os.path.split(filename)
99             it = cp.get('%s:%s' % (commit.encode('hex'), dir))
100             type = it.next()
101             if type == 'tree':
102                 for (mode,n,sha) in git._treeparse(''.join(it)):
103                     if n == name:
104                         st.st_mode = int(mode, 8)
105                         st.st_nlink = 1  # FIXME
106                         if stat.S_ISDIR(st.st_mode):
107                             st.st_size = 1024
108                         else:
109                             fileid = '%s:%s' % (commit.encode('hex'), filename)
110                             st.st_size = sz(cp.join(fileid))
111                         statcache[path] = st
112                         return st
113         return -errno.ENOENT
114
115     def readdir(self, path, offset):
116         log('--readdir(%r)\n' % path)
117         yield fuse.Direntry('.')
118         yield fuse.Direntry('..')
119         (ref,date,dir) = namesplit(path)
120         if not ref:
121             for (name,sha) in git.list_refs():
122                 name = re.sub('^refs/heads/', '', name)
123                 yield fuse.Direntry(name)
124         elif not date:
125             dates = dates_for_ref(ref)
126             for (date,commit) in dates:
127                 l = time.localtime(date)
128                 yield fuse.Direntry(time.strftime('%Y-%m-%d-%H%M%S', l))
129         else:
130             commit = date_to_commit(ref, date)
131             it = cp.get('%s:%s' % (commit.encode('hex'), dir or ''))
132             type = it.next()
133             if type == 'tree':
134                 for (mode,n,sha) in git._treeparse(''.join(it)):
135                     yield fuse.Direntry(n)
136
137     def readlink(self, path):
138         log('--readlink(%r)\n' % path)
139         self.open(path, os.O_RDONLY)  # FIXME: never released
140         return self.read(path, 10000, 0)
141
142     def open(self, path, flags):
143         log('--open(%r)\n' % path)
144         (ref,date,dir) = namesplit(path)
145         if not dir:
146             return -errno.ENOENT
147         commit = date_to_commit(ref, date)
148         try:
149             it = cp.get('%s:%s' % (commit.encode('hex'), dir or ''))
150         except KeyError:
151             return -errno.ENOENT
152         type = it.next()
153         if type != 'blob':
154             return -errno.EINVAL
155         accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
156         if (flags & accmode) != os.O_RDONLY:
157             return -errno.EACCES
158
159         f = tempfile.TemporaryFile()
160         for blob in it:
161             f.write(blob)
162         f.flush()
163         filecache[path] = f
164
165     def release(self, path, flags):
166         log('--release(%r)\n' % path)
167         del filecache[path]
168
169     def read(self, path, size, offset):
170         log('--read(%r)\n' % path)
171         f = filecache.get(path)
172         if not f:
173             return -errno.ENOENT
174         f.seek(offset)
175         return f.read(size)
176
177
178 if not hasattr(fuse, '__version__'):
179     raise RuntimeError, "your fuse module is too old for fuse.__version__"
180 fuse.fuse_python_api = (0, 2)
181
182 optspec = """
183 bup fuse [mountpoint]
184 --
185 d,debug   increase debug level
186 f,foreground  run in foreground
187 """
188 o = options.Options('bup fuse', optspec)
189 (opt, flags, extra) = o.parse(sys.argv[1:])
190
191 if len(extra) != 1:
192     log("bup fuse: exactly one argument expected\n")
193     o.usage()
194
195 f = BupFs()
196 f.fuse_args.mountpoint = extra[0]
197 if opt.debug:
198     f.fuse_args.add('debug')
199 if opt.foreground:
200     f.fuse_args.setmod('foreground')
201 f.fuse_args.add('allow_other')
202
203 git.check_repo_or_die()
204 cp = git.CatPipe()
205 f.main()