Add a few items to the TODO list.
[cumulus.git] / contrib / cumulus-fuse
1 #!/usr/bin/python
2 #
3 # FUSE interface to Cumulus, allowing snapshots to be mounted as a virtual
4 # filesystem.
5 #
6 # Copyright (C) 2006-2008  The Regents of the University of California
7 # Written by Michael Vrable <mvrable@cs.ucsd.edu>
8 #
9 # This program can be distributed under the terms of the GNU GPL, either
10 # version 2 of the License, or (at your option) any later version.  See the
11 # file COPYING.
12
13 import itertools, os, stat, errno
14 import fuse
15 from fuse import Fuse
16 import cumulus
17 import cumulus.metadata
18
19 fuse.fuse_python_api = (0, 2)
20
21 # TODO: Figure out FUSE option parsing
22 lowlevel = cumulus.LowlevelDataStore('/backups/lbs/corinth')
23 store = cumulus.ObjectStore(lowlevel)
24
25 def _printable(ptr):
26     if ptr is None: return None
27     return tuple(x[1] for x in ptr)
28
29 def parse_path(path):
30     """Strip leading slashe from path, and split apart into components."""
31     if not path.startswith('/'):
32         return None
33     if path == '/':
34         return []
35     else:
36         return path[1:].split('/')
37
38 lookup_cache = {}
39 def load_metadata(path):
40     if type(path) != type([]):
41         path = parse_path(path)
42
43     if path is None or len(path) < 2:
44         return None
45
46     path = tuple(path)
47     if path in lookup_cache:
48         return lookup_cache[path]
49
50     snapshot = cumulus.parse_full(store.load_snapshot(path[0]))
51     metadata = cumulus.metadata.Metadata(store, snapshot['Root'])
52     ptr = metadata.search(lambda x: cmp(x, path[1:]))
53     item = metadata._read(ptr)
54     if metadata._get_path(item) != path[1:]:
55         lookup_cache[path] = None
56         return None
57     lookup_cache[path] = cumulus.MetadataItem(item, store)
58     return lookup_cache[path]
59
60 class MyStat(fuse.Stat):
61     def __init__(self):
62         self.st_mode = 0
63         self.st_ino = 0
64         self.st_dev = 0
65         self.st_nlink = 0
66         self.st_uid = 0
67         self.st_gid = 0
68         self.st_size = 0
69         self.st_atime = 0
70         self.st_mtime = 0
71         self.st_ctime = 0
72
73 class CumulusFS(Fuse):
74     def getattr(self, path):
75         st = MyStat()
76         path = parse_path(path)
77
78         if path is None: return -errno.ENOENT
79         if path == []:
80             # Root directory
81             st.st_mode = stat.S_IFDIR | 0755
82             st.st_nlink = 2
83             return st
84
85         snapshot = cumulus.parse_full(store.load_snapshot(path[0]))
86         if len(path) == 1:
87             # Snapshot directory
88             st.st_mode = stat.S_IFDIR | 0755
89             st.st_nlink = 2
90         else:
91             # File contained within a snapshot
92             m = load_metadata(path)
93             if m is None:
94                 return -errno.ENOENT
95
96             st.st_nlink = 1
97             st.st_uid = m.items.user[0]
98             st.st_gid = m.items.group[0]
99             st.st_mtime = m.items.mtime
100             st.st_ctime = m.items.ctime
101             st.st_atime = m.items.mtime
102             if m.items.type == 'd':
103                 st.st_mode = stat.S_IFDIR | m.items.mode
104                 st.st_nlink = 2
105             elif m.items.type == 'l':
106                 st.st_mode = stat.S_IFLNK | m.items.mode
107             else:
108                 st.st_mode = stat.S_IFREG | m.items.mode
109                 st.st_size = m.items.size
110
111         return st
112
113     def _cumulus_readdir(self, metadata, snapshot, path):
114         # Find pointer to base directory in metadata
115         ptr1 = metadata.search(lambda x: cmp(x, path))
116
117         # Find pointer to end of directory contents
118         def endcmp(p1):
119             def _cmp(p2):
120                 if len(p2) > len(p1): p2 = p2[0:len(p1)]
121                 if p2 > p1:
122                     return 1
123                 else:
124                     return -1
125             return _cmp
126         ptr2 = metadata.search(endcmp(path), ptr1)
127
128         # Scan through looking for top-level files and directories.  Skip over
129         # data for files in subdirectories.
130         while metadata._cmp(ptr1, ptr2) < 0:
131             item = metadata._read(ptr1)
132             m = cumulus.MetadataItem(item, store)
133             if m.items.name == '.':
134                 itempath = []
135             else:
136                 itempath = m.items.name.split('/')
137             assert itempath[0:len(path)] == path
138
139             if len(itempath) == len(path):
140                 ptr1 = metadata._advance(ptr1)
141                 continue
142
143             if len(itempath) > len(path) + 1:
144                 ptr1 = metadata.search(endcmp(itempath[0:len(path)+1]),
145                                        ptr1, ptr2)
146                 continue
147
148             lookup_cache[(snapshot,) + tuple(itempath)] = m
149             yield itempath[len(path)]
150             ptr1 = metadata._advance(ptr1)
151
152     def readdir(self, path, offset):
153         if path == '/':
154             for r in itertools.chain(('.', '..'), lowlevel.list_snapshots()):
155                 yield fuse.Direntry(r)
156         else:
157             path = parse_path(path)
158             if path is None:
159                 return
160             snapshot = cumulus.parse_full(store.load_snapshot(path[0]))
161             metadata = cumulus.metadata.Metadata(store, snapshot['Root'])
162             for r in itertools.chain(('.', '..'),
163                                      self._cumulus_readdir(metadata,
164                                                            path[0],
165                                                            path[1:])):
166                 yield fuse.Direntry(r)
167
168     def readlink(self, path):
169         m = load_metadata(path)
170         if m is None:
171             return -errno.ENOENT
172         else:
173             return m.items.target
174
175     def open(self, path, flags):
176         m = load_metadata(path)
177         if m is None:
178             return -errno.ENOENT
179         accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
180         if (flags & accmode) != os.O_RDONLY:
181             return -errno.EACCES
182
183     def read(self, path, size, offset):
184         m = load_metadata(path)
185         if m is None:
186             return -errno.ENOENT
187
188         buf = ''
189         for b in m.data():
190             if size == 0: break
191
192             # Skip over this data block if we can, based on remaining data
193             # offset and block size.
194             (bseg, boff, bcsum, bslice) = store.parse_ref(b)
195             if bslice is not None:
196                 bsize = bslice[1]
197                 if offset >= bsize:
198                     offset -= bsize
199                     continue
200
201             # Otherwise, load the data block and read any data out of it we
202             # can.
203             data = store.get(b)
204             if offset >= len(data):
205                 offset -= len(data)
206                 continue
207             if offset > 0:
208                 data = data[offset:]
209                 offset = 0
210             if size < len(data):
211                 data = data[0:size]
212
213             buf += data
214             size -= len(data)
215
216         return buf
217
218 def main():
219     usage="""
220 cumulus-fuse: Mount cumulus snapshots as a filesystem
221
222 """ + Fuse.fusage
223     server = CumulusFS(version="%prog " + fuse.__version__,
224                        usage=usage,
225                        dash_s_do='setsingle')
226
227     server.parser.add_option(mountopt="root", metavar="PATH", default='/',
228                              help="read snapshots from PATH [default: %default]")
229
230     server.parse(errex=1)
231     print server.fuse_args
232     print server.fuse_args.assemble()
233     server.main()
234     store.cleanup()
235
236 if __name__ == '__main__':
237     main()