├── COPYING ├── README.md ├── fuse.py └── gitfs.py /COPYING: -------------------------------------------------------------------------------- 1 | Licensed under FreeBSD License 2 | ------------------------------ 3 | 4 | Copyright (c) 2012, Sreejith Kesavan 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | The views and conclusions contained in the software and documentation are those 28 | of the authors and should not be interpreted as representing official policies, 29 | either expressed or implied, of the FreeBSD Project. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitFS 2 | A FUSE based filesystem for using Git(Hub) as a storage. 3 | 4 | # Usage 5 | To mount already existing git repo, run 6 | 7 | ```python gitfs.py git@github.com:/ ``` 8 | 9 | To unmount use 10 | 11 | ```fusermount -u ``` 12 | 13 | Licensed under FreeBSD license 14 | -------------------------------------------------------------------------------- /fuse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008 Giorgos Verigakis 2 | # 3 | # Permission to use, copy, modify, and distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | from __future__ import division 16 | 17 | from ctypes import * 18 | from ctypes.util import find_library 19 | from errno import * 20 | from functools import partial 21 | from os import strerror 22 | from platform import machine, system 23 | from stat import S_IFDIR 24 | from traceback import print_exc 25 | 26 | 27 | class c_timespec(Structure): 28 | _fields_ = [('tv_sec', c_long), ('tv_nsec', c_long)] 29 | 30 | class c_utimbuf(Structure): 31 | _fields_ = [('actime', c_timespec), ('modtime', c_timespec)] 32 | 33 | class c_stat(Structure): 34 | pass # Platform dependent 35 | 36 | _system = system() 37 | if _system in ('Darwin', 'FreeBSD'): 38 | _libiconv = CDLL(find_library("iconv"), RTLD_GLOBAL) # libfuse dependency 39 | ENOTSUP = 45 40 | c_dev_t = c_int32 41 | c_fsblkcnt_t = c_ulong 42 | c_fsfilcnt_t = c_ulong 43 | c_gid_t = c_uint32 44 | c_mode_t = c_uint16 45 | c_off_t = c_int64 46 | c_pid_t = c_int32 47 | c_uid_t = c_uint32 48 | setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), 49 | c_size_t, c_int, c_uint32) 50 | getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), 51 | c_size_t, c_uint32) 52 | c_stat._fields_ = [ 53 | ('st_dev', c_dev_t), 54 | ('st_ino', c_uint32), 55 | ('st_mode', c_mode_t), 56 | ('st_nlink', c_uint16), 57 | ('st_uid', c_uid_t), 58 | ('st_gid', c_gid_t), 59 | ('st_rdev', c_dev_t), 60 | ('st_atimespec', c_timespec), 61 | ('st_mtimespec', c_timespec), 62 | ('st_ctimespec', c_timespec), 63 | ('st_size', c_off_t), 64 | ('st_blocks', c_int64), 65 | ('st_blksize', c_int32)] 66 | elif _system == 'Linux': 67 | ENOTSUP = 95 68 | c_dev_t = c_ulonglong 69 | c_fsblkcnt_t = c_ulonglong 70 | c_fsfilcnt_t = c_ulonglong 71 | c_gid_t = c_uint 72 | c_mode_t = c_uint 73 | c_off_t = c_longlong 74 | c_pid_t = c_int 75 | c_uid_t = c_uint 76 | setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int) 77 | getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t) 78 | 79 | _machine = machine() 80 | if _machine == 'x86_64': 81 | c_stat._fields_ = [ 82 | ('st_dev', c_dev_t), 83 | ('st_ino', c_ulong), 84 | ('st_nlink', c_ulong), 85 | ('st_mode', c_mode_t), 86 | ('st_uid', c_uid_t), 87 | ('st_gid', c_gid_t), 88 | ('__pad0', c_int), 89 | ('st_rdev', c_dev_t), 90 | ('st_size', c_off_t), 91 | ('st_blksize', c_long), 92 | ('st_blocks', c_long), 93 | ('st_atimespec', c_timespec), 94 | ('st_mtimespec', c_timespec), 95 | ('st_ctimespec', c_timespec)] 96 | elif _machine == 'ppc': 97 | c_stat._fields_ = [ 98 | ('st_dev', c_dev_t), 99 | ('st_ino', c_ulonglong), 100 | ('st_mode', c_mode_t), 101 | ('st_nlink', c_uint), 102 | ('st_uid', c_uid_t), 103 | ('st_gid', c_gid_t), 104 | ('st_rdev', c_dev_t), 105 | ('__pad2', c_ushort), 106 | ('st_size', c_off_t), 107 | ('st_blksize', c_long), 108 | ('st_blocks', c_longlong), 109 | ('st_atimespec', c_timespec), 110 | ('st_mtimespec', c_timespec), 111 | ('st_ctimespec', c_timespec)] 112 | else: 113 | # i686, use as fallback for everything else 114 | c_stat._fields_ = [ 115 | ('st_dev', c_dev_t), 116 | ('__pad1', c_ushort), 117 | ('__st_ino', c_ulong), 118 | ('st_mode', c_mode_t), 119 | ('st_nlink', c_uint), 120 | ('st_uid', c_uid_t), 121 | ('st_gid', c_gid_t), 122 | ('st_rdev', c_dev_t), 123 | ('__pad2', c_ushort), 124 | ('st_size', c_off_t), 125 | ('st_blksize', c_long), 126 | ('st_blocks', c_longlong), 127 | ('st_atimespec', c_timespec), 128 | ('st_mtimespec', c_timespec), 129 | ('st_ctimespec', c_timespec), 130 | ('st_ino', c_ulonglong)] 131 | else: 132 | raise NotImplementedError('%s is not supported.' % _system) 133 | 134 | 135 | class c_statvfs(Structure): 136 | _fields_ = [ 137 | ('f_bsize', c_ulong), 138 | ('f_frsize', c_ulong), 139 | ('f_blocks', c_fsblkcnt_t), 140 | ('f_bfree', c_fsblkcnt_t), 141 | ('f_bavail', c_fsblkcnt_t), 142 | ('f_files', c_fsfilcnt_t), 143 | ('f_ffree', c_fsfilcnt_t), 144 | ('f_favail', c_fsfilcnt_t)] 145 | 146 | if _system == 'FreeBSD': 147 | c_fsblkcnt_t = c_uint64 148 | c_fsfilcnt_t = c_uint64 149 | setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int) 150 | getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t) 151 | class c_statvfs(Structure): 152 | _fields_ = [ 153 | ('f_bavail', c_fsblkcnt_t), 154 | ('f_bfree', c_fsblkcnt_t), 155 | ('f_blocks', c_fsblkcnt_t), 156 | ('f_favail', c_fsfilcnt_t), 157 | ('f_ffree', c_fsfilcnt_t), 158 | ('f_files', c_fsfilcnt_t), 159 | ('f_bsize', c_ulong), 160 | ('f_flag', c_ulong), 161 | ('f_frsize', c_ulong)] 162 | 163 | class fuse_file_info(Structure): 164 | _fields_ = [ 165 | ('flags', c_int), 166 | ('fh_old', c_ulong), 167 | ('writepage', c_int), 168 | ('direct_io', c_uint, 1), 169 | ('keep_cache', c_uint, 1), 170 | ('flush', c_uint, 1), 171 | ('padding', c_uint, 29), 172 | ('fh', c_uint64), 173 | ('lock_owner', c_uint64)] 174 | 175 | class fuse_context(Structure): 176 | _fields_ = [ 177 | ('fuse', c_voidp), 178 | ('uid', c_uid_t), 179 | ('gid', c_gid_t), 180 | ('pid', c_pid_t), 181 | ('private_data', c_voidp)] 182 | 183 | class fuse_operations(Structure): 184 | _fields_ = [ 185 | ('getattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat))), 186 | ('readlink', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)), 187 | ('getdir', c_voidp), # Deprecated, use readdir 188 | ('mknod', CFUNCTYPE(c_int, c_char_p, c_mode_t, c_dev_t)), 189 | ('mkdir', CFUNCTYPE(c_int, c_char_p, c_mode_t)), 190 | ('unlink', CFUNCTYPE(c_int, c_char_p)), 191 | ('rmdir', CFUNCTYPE(c_int, c_char_p)), 192 | ('symlink', CFUNCTYPE(c_int, c_char_p, c_char_p)), 193 | ('rename', CFUNCTYPE(c_int, c_char_p, c_char_p)), 194 | ('link', CFUNCTYPE(c_int, c_char_p, c_char_p)), 195 | ('chmod', CFUNCTYPE(c_int, c_char_p, c_mode_t)), 196 | ('chown', CFUNCTYPE(c_int, c_char_p, c_uid_t, c_gid_t)), 197 | ('truncate', CFUNCTYPE(c_int, c_char_p, c_off_t)), 198 | ('utime', c_voidp), # Deprecated, use utimens 199 | ('open', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), 200 | ('read', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t, 201 | POINTER(fuse_file_info))), 202 | ('write', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t, 203 | POINTER(fuse_file_info))), 204 | ('statfs', CFUNCTYPE(c_int, c_char_p, POINTER(c_statvfs))), 205 | ('flush', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), 206 | ('release', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), 207 | ('fsync', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))), 208 | ('setxattr', setxattr_t), 209 | ('getxattr', getxattr_t), 210 | ('listxattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)), 211 | ('removexattr', CFUNCTYPE(c_int, c_char_p, c_char_p)), 212 | ('opendir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), 213 | ('readdir', CFUNCTYPE(c_int, c_char_p, c_voidp, CFUNCTYPE(c_int, c_voidp, 214 | c_char_p, POINTER(c_stat), c_off_t), c_off_t, POINTER(fuse_file_info))), 215 | ('releasedir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), 216 | ('fsyncdir', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))), 217 | ('init', CFUNCTYPE(c_voidp, c_voidp)), 218 | ('destroy', CFUNCTYPE(c_voidp, c_voidp)), 219 | ('access', CFUNCTYPE(c_int, c_char_p, c_int)), 220 | ('create', CFUNCTYPE(c_int, c_char_p, c_mode_t, POINTER(fuse_file_info))), 221 | ('ftruncate', CFUNCTYPE(c_int, c_char_p, c_off_t, POINTER(fuse_file_info))), 222 | ('fgetattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat), 223 | POINTER(fuse_file_info))), 224 | ('lock', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info), c_int, c_voidp)), 225 | ('utimens', CFUNCTYPE(c_int, c_char_p, POINTER(c_utimbuf))), 226 | ('bmap', CFUNCTYPE(c_int, c_char_p, c_size_t, POINTER(c_ulonglong)))] 227 | 228 | 229 | def time_of_timespec(ts): 230 | return ts.tv_sec + ts.tv_nsec / 10 ** 9 231 | 232 | def set_st_attrs(st, attrs): 233 | for key, val in attrs.items(): 234 | if key in ('st_atime', 'st_mtime', 'st_ctime'): 235 | timespec = getattr(st, key + 'spec') 236 | timespec.tv_sec = int(val) 237 | timespec.tv_nsec = int((val - timespec.tv_sec) * 10 ** 9) 238 | elif hasattr(st, key): 239 | setattr(st, key, val) 240 | 241 | 242 | _libfuse_path = find_library('fuse') 243 | if not _libfuse_path: 244 | raise EnvironmentError('Unable to find libfuse') 245 | _libfuse = CDLL(_libfuse_path) 246 | _libfuse.fuse_get_context.restype = POINTER(fuse_context) 247 | 248 | 249 | def fuse_get_context(): 250 | """Returns a (uid, gid, pid) tuple""" 251 | ctxp = _libfuse.fuse_get_context() 252 | ctx = ctxp.contents 253 | return ctx.uid, ctx.gid, ctx.pid 254 | 255 | 256 | class FuseOSError(OSError): 257 | def __init__(self, errno): 258 | super(FuseOSError, self).__init__(errno, strerror(errno)) 259 | 260 | 261 | class FUSE(object): 262 | """This class is the lower level interface and should not be subclassed 263 | under normal use. Its methods are called by fuse. 264 | Assumes API version 2.6 or later.""" 265 | 266 | def __init__(self, operations, mountpoint, raw_fi=False, **kwargs): 267 | """Setting raw_fi to True will cause FUSE to pass the fuse_file_info 268 | class as is to Operations, instead of just the fh field. 269 | This gives you access to direct_io, keep_cache, etc.""" 270 | 271 | self.operations = operations 272 | self.raw_fi = raw_fi 273 | args = ['fuse'] 274 | if kwargs.pop('foreground', False): 275 | args.append('-f') 276 | if kwargs.pop('debug', False): 277 | args.append('-d') 278 | if kwargs.pop('nothreads', False): 279 | args.append('-s') 280 | kwargs.setdefault('fsname', operations.__class__.__name__) 281 | args.append('-o') 282 | args.append(','.join(key if val == True else '%s=%s' % (key, val) 283 | for key, val in kwargs.items())) 284 | args.append(mountpoint) 285 | argv = (c_char_p * len(args))(*args) 286 | 287 | fuse_ops = fuse_operations() 288 | for name, prototype in fuse_operations._fields_: 289 | if prototype != c_voidp and getattr(operations, name, None): 290 | op = partial(self._wrapper_, getattr(self, name)) 291 | setattr(fuse_ops, name, prototype(op)) 292 | err = _libfuse.fuse_main_real(len(args), argv, pointer(fuse_ops), 293 | sizeof(fuse_ops), None) 294 | del self.operations # Invoke the destructor 295 | if err: 296 | raise RuntimeError(err) 297 | 298 | def _wrapper_(self, func, *args, **kwargs): 299 | """Decorator for the methods that follow""" 300 | try: 301 | return func(*args, **kwargs) or 0 302 | except OSError, e: 303 | return -(e.errno or EFAULT) 304 | except: 305 | print_exc() 306 | return -EFAULT 307 | 308 | def getattr(self, path, buf): 309 | return self.fgetattr(path, buf, None) 310 | 311 | def readlink(self, path, buf, bufsize): 312 | ret = self.operations('readlink', path) 313 | data = create_string_buffer(ret[:bufsize - 1]) 314 | memmove(buf, data, len(data)) 315 | return 0 316 | 317 | def mknod(self, path, mode, dev): 318 | return self.operations('mknod', path, mode, dev) 319 | 320 | def mkdir(self, path, mode): 321 | return self.operations('mkdir', path, mode) 322 | 323 | def unlink(self, path): 324 | return self.operations('unlink', path) 325 | 326 | def rmdir(self, path): 327 | return self.operations('rmdir', path) 328 | 329 | def symlink(self, source, target): 330 | return self.operations('symlink', target, source) 331 | 332 | def rename(self, old, new): 333 | return self.operations('rename', old, new) 334 | 335 | def link(self, source, target): 336 | return self.operations('link', target, source) 337 | 338 | def chmod(self, path, mode): 339 | return self.operations('chmod', path, mode) 340 | 341 | def chown(self, path, uid, gid): 342 | # Check if any of the arguments is a -1 that has overflowed 343 | if c_uid_t(uid + 1).value == 0: 344 | uid = -1 345 | if c_gid_t(gid + 1).value == 0: 346 | gid = -1 347 | return self.operations('chown', path, uid, gid) 348 | 349 | def truncate(self, path, length): 350 | return self.operations('truncate', path, length) 351 | 352 | def open(self, path, fip): 353 | fi = fip.contents 354 | if self.raw_fi: 355 | return self.operations('open', path, fi) 356 | else: 357 | fi.fh = self.operations('open', path, fi.flags) 358 | return 0 359 | 360 | def read(self, path, buf, size, offset, fip): 361 | fh = fip.contents if self.raw_fi else fip.contents.fh 362 | ret = self.operations('read', path, size, offset, fh) 363 | if not ret: 364 | return 0 365 | data = create_string_buffer(ret[:size], size) 366 | memmove(buf, data, size) 367 | return size 368 | 369 | def write(self, path, buf, size, offset, fip): 370 | data = string_at(buf, size) 371 | fh = fip.contents if self.raw_fi else fip.contents.fh 372 | return self.operations('write', path, data, offset, fh) 373 | 374 | def statfs(self, path, buf): 375 | stv = buf.contents 376 | attrs = self.operations('statfs', path) 377 | for key, val in attrs.items(): 378 | if hasattr(stv, key): 379 | setattr(stv, key, val) 380 | return 0 381 | 382 | def flush(self, path, fip): 383 | fh = fip.contents if self.raw_fi else fip.contents.fh 384 | return self.operations('flush', path, fh) 385 | 386 | def release(self, path, fip): 387 | fh = fip.contents if self.raw_fi else fip.contents.fh 388 | return self.operations('release', path, fh) 389 | 390 | def fsync(self, path, datasync, fip): 391 | fh = fip.contents if self.raw_fi else fip.contents.fh 392 | return self.operations('fsync', path, datasync, fh) 393 | 394 | def setxattr(self, path, name, value, size, options, *args): 395 | data = string_at(value, size) 396 | return self.operations('setxattr', path, name, data, options, *args) 397 | 398 | def getxattr(self, path, name, value, size, *args): 399 | ret = self.operations('getxattr', path, name, *args) 400 | retsize = len(ret) 401 | buf = create_string_buffer(ret, retsize) # Does not add trailing 0 402 | if bool(value): 403 | if retsize > size: 404 | return -ERANGE 405 | memmove(value, buf, retsize) 406 | return retsize 407 | 408 | def listxattr(self, path, namebuf, size): 409 | ret = self.operations('listxattr', path) 410 | buf = create_string_buffer('\x00'.join(ret)) if ret else '' 411 | bufsize = len(buf) 412 | if bool(namebuf): 413 | if bufsize > size: 414 | return -ERANGE 415 | memmove(namebuf, buf, bufsize) 416 | return bufsize 417 | 418 | def removexattr(self, path, name): 419 | return self.operations('removexattr', path, name) 420 | 421 | def opendir(self, path, fip): 422 | # Ignore raw_fi 423 | fip.contents.fh = self.operations('opendir', path) 424 | return 0 425 | 426 | def readdir(self, path, buf, filler, offset, fip): 427 | # Ignore raw_fi 428 | for item in self.operations('readdir', path, fip.contents.fh): 429 | if isinstance(item, str): 430 | name, st, offset = item, None, 0 431 | else: 432 | name, attrs, offset = item 433 | if attrs: 434 | st = c_stat() 435 | set_st_attrs(st, attrs) 436 | else: 437 | st = None 438 | if filler(buf, name, st, offset) != 0: 439 | break 440 | return 0 441 | 442 | def releasedir(self, path, fip): 443 | # Ignore raw_fi 444 | return self.operations('releasedir', path, fip.contents.fh) 445 | 446 | def fsyncdir(self, path, datasync, fip): 447 | # Ignore raw_fi 448 | return self.operations('fsyncdir', path, datasync, fip.contents.fh) 449 | 450 | def init(self, conn): 451 | return self.operations('init', '/') 452 | 453 | def destroy(self, private_data): 454 | return self.operations('destroy', '/') 455 | 456 | def access(self, path, amode): 457 | return self.operations('access', path, amode) 458 | 459 | def create(self, path, mode, fip): 460 | fi = fip.contents 461 | if self.raw_fi: 462 | return self.operations('create', path, mode, fi) 463 | else: 464 | fi.fh = self.operations('create', path, mode) 465 | return 0 466 | 467 | def ftruncate(self, path, length, fip): 468 | fh = fip.contents if self.raw_fi else fip.contents.fh 469 | return self.operations('truncate', path, length, fh) 470 | 471 | def fgetattr(self, path, buf, fip): 472 | memset(buf, 0, sizeof(c_stat)) 473 | st = buf.contents 474 | fh = fip and (fip.contents if self.raw_fi else fip.contents.fh) 475 | attrs = self.operations('getattr', path, fh) 476 | set_st_attrs(st, attrs) 477 | return 0 478 | 479 | def lock(self, path, fip, cmd, lock): 480 | fh = fip.contents if self.raw_fi else fip.contents.fh 481 | return self.operations('lock', path, fh, cmd, lock) 482 | 483 | def utimens(self, path, buf): 484 | if buf: 485 | atime = time_of_timespec(buf.contents.actime) 486 | mtime = time_of_timespec(buf.contents.modtime) 487 | times = (atime, mtime) 488 | else: 489 | times = None 490 | return self.operations('utimens', path, times) 491 | 492 | def bmap(self, path, blocksize, idx): 493 | return self.operations('bmap', path, blocksize, idx) 494 | 495 | 496 | class Operations(object): 497 | """This class should be subclassed and passed as an argument to FUSE on 498 | initialization. All operations should raise a FuseOSError exception 499 | on error. 500 | 501 | When in doubt of what an operation should do, check the FUSE header 502 | file or the corresponding system call man page.""" 503 | 504 | def __call__(self, op, *args): 505 | if not hasattr(self, op): 506 | raise FuseOSError(EFAULT) 507 | return getattr(self, op)(*args) 508 | 509 | def access(self, path, amode): 510 | return 0 511 | 512 | bmap = None 513 | 514 | def chmod(self, path, mode): 515 | raise FuseOSError(EROFS) 516 | 517 | def chown(self, path, uid, gid): 518 | raise FuseOSError(EROFS) 519 | 520 | def create(self, path, mode, fi=None): 521 | """When raw_fi is False (default case), fi is None and create should 522 | return a numerical file handle. 523 | When raw_fi is True the file handle should be set directly by create 524 | and return 0.""" 525 | raise FuseOSError(EROFS) 526 | 527 | def destroy(self, path): 528 | """Called on filesystem destruction. Path is always /""" 529 | pass 530 | 531 | def flush(self, path, fh): 532 | return 0 533 | 534 | def fsync(self, path, datasync, fh): 535 | return 0 536 | 537 | def fsyncdir(self, path, datasync, fh): 538 | return 0 539 | 540 | def getattr(self, path, fh=None): 541 | """Returns a dictionary with keys identical to the stat C structure 542 | of stat(2). 543 | st_atime, st_mtime and st_ctime should be floats. 544 | NOTE: There is an incombatibility between Linux and Mac OS X concerning 545 | st_nlink of directories. Mac OS X counts all files inside the directory, 546 | while Linux counts only the subdirectories.""" 547 | 548 | if path != '/': 549 | raise FuseOSError(ENOENT) 550 | return dict(st_mode=(S_IFDIR | 0755), st_nlink=2) 551 | 552 | def getxattr(self, path, name, position=0): 553 | raise FuseOSError(ENOTSUP) 554 | 555 | def init(self, path): 556 | """Called on filesystem initialization. Path is always / 557 | Use it instead of __init__ if you start threads on initialization.""" 558 | pass 559 | 560 | def link(self, target, source): 561 | raise FuseOSError(EROFS) 562 | 563 | def listxattr(self, path): 564 | return [] 565 | 566 | lock = None 567 | 568 | def mkdir(self, path, mode): 569 | raise FuseOSError(EROFS) 570 | 571 | def mknod(self, path, mode, dev): 572 | raise FuseOSError(EROFS) 573 | 574 | def open(self, path, flags): 575 | """When raw_fi is False (default case), open should return a numerical 576 | file handle. 577 | When raw_fi is True the signature of open becomes: 578 | open(self, path, fi) 579 | and the file handle should be set directly.""" 580 | return 0 581 | 582 | def opendir(self, path): 583 | """Returns a numerical file handle.""" 584 | return 0 585 | 586 | def read(self, path, size, offset, fh): 587 | """Returns a string containing the data requested.""" 588 | raise FuseOSError(EIO) 589 | 590 | def readdir(self, path, fh): 591 | """Can return either a list of names, or a list of (name, attrs, offset) 592 | tuples. attrs is a dict as in getattr.""" 593 | return ['.', '..'] 594 | 595 | def readlink(self, path): 596 | raise FuseOSError(ENOENT) 597 | 598 | def release(self, path, fh): 599 | return 0 600 | 601 | def releasedir(self, path, fh): 602 | return 0 603 | 604 | def removexattr(self, path, name): 605 | raise FuseOSError(ENOTSUP) 606 | 607 | def rename(self, old, new): 608 | raise FuseOSError(EROFS) 609 | 610 | def rmdir(self, path): 611 | raise FuseOSError(EROFS) 612 | 613 | def setxattr(self, path, name, value, options, position=0): 614 | raise FuseOSError(ENOTSUP) 615 | 616 | def statfs(self, path): 617 | """Returns a dictionary with keys identical to the statvfs C structure 618 | of statvfs(3). 619 | On Mac OS X f_bsize and f_frsize must be a power of 2 (minimum 512).""" 620 | return {} 621 | 622 | def symlink(self, target, source): 623 | raise FuseOSError(EROFS) 624 | 625 | def truncate(self, path, length, fh=None): 626 | raise FuseOSError(EROFS) 627 | 628 | def unlink(self, path): 629 | raise FuseOSError(EROFS) 630 | 631 | def utimens(self, path, times=None): 632 | """Times is a (atime, mtime) tuple. If None use current time.""" 633 | return 0 634 | 635 | def write(self, path, data, offset, fh): 636 | raise FuseOSError(EROFS) 637 | 638 | 639 | class LoggingMixIn: 640 | def __call__(self, op, path, *args): 641 | print '->', op, path, repr(args) 642 | ret = '[Unhandled Exception]' 643 | try: 644 | ret = getattr(self, op)(path, *args) 645 | return ret 646 | except OSError, e: 647 | ret = str(e) 648 | raise 649 | finally: 650 | print '<-', op, repr(ret) 651 | -------------------------------------------------------------------------------- /gitfs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Use Git as a Storage Filesystem 4 | # 5 | # Copyright (c) 2011 Sreejith K 6 | # Licensed under FreeBSD License. Refer COPYING for more information 7 | # 8 | # http://foobarnbaz.com 9 | # Created on 11th Jan 2011 10 | 11 | 12 | from __future__ import with_statement 13 | 14 | from errno import EACCES 15 | from sys import argv, exit 16 | from time import time 17 | import logging 18 | import datetime 19 | import os 20 | from threading import Lock, Condition, Thread 21 | 22 | from fuse import FUSE, FuseOSError, Operations, LoggingMixIn 23 | 24 | class GitStatus(object): 25 | 26 | def __init__(self, path): 27 | if not os.getcwd() == path: 28 | os.chdir(path) 29 | self.status = {} 30 | 31 | def update(self): 32 | self.clear() 33 | logging.debug('getting status') 34 | for line in os.popen('git status').readlines(): 35 | line = line.strip() 36 | if line.startswith('#\t'): 37 | try: 38 | status, file = [l.strip() for l in line[2:].split(':')] 39 | if self.status.has_key(file): 40 | self.status[status].append(file) 41 | else: 42 | self.status[status] = [file] 43 | except ValueError: 44 | if self.status.has_key('untracked'): 45 | self.status['untracked'].append( line[2:].strip() ) 46 | else: 47 | self.status['untracked'] = [ line[2:].strip() ] 48 | logging.debug('current status: %r' %self.status) 49 | return self.status 50 | 51 | def stagedFiles(self): 52 | self.update() 53 | return self.status.get('renamed', []) + \ 54 | self.status.get('modified', []) + \ 55 | self.status.get('new file', []) + \ 56 | self.status.get('deleted', []) 57 | 58 | def unstagedFiles(self): 59 | self.update() 60 | return self.status.get('untracked', []) 61 | 62 | def clear(self): 63 | self.status.clear() 64 | 65 | class GitRepo(object): 66 | 67 | def __init__(self, path, origin, branch, sync=False): 68 | self.path = path 69 | self.origin = origin 70 | self.branch = branch 71 | self.status = GitStatus(path) 72 | # sync all the files with git 73 | if sync: 74 | self.synchronize() 75 | 76 | def synchronize(self): 77 | logging.debug('syncing') 78 | if self.syncNeeded(): 79 | unstaged = self.status.unstagedFiles() 80 | for file in unstaged: 81 | self.stage(file) 82 | self.commit('syncing files @ %s' %datetime.datetime.now()) 83 | self.push() 84 | 85 | def syncNeeded(self): 86 | return (self.status.stagedFiles() + self.status.unstagedFiles() and True or False) 87 | 88 | def stage(self, file): 89 | logging.debug('staging file %s' %file) 90 | os.system('git add %s' %file) 91 | 92 | def commit(self, msg): 93 | logging.debug('commiting file %s' %file) 94 | os.system('git commit -am \"%s\"' %msg) 95 | 96 | def push(self): 97 | logging.debug('pushing') 98 | os.system('git push origin %s' %self.branch) 99 | self.status.clear() 100 | 101 | class GitFS(LoggingMixIn, Operations): 102 | """A simple filesystem using Git and FUSE. 103 | """ 104 | def __init__(self, origin, branch='master', path='.'): 105 | self.origin = origin 106 | self.branch = branch 107 | self.root = os.path.realpath(path) 108 | self.repo = GitRepo(path, origin, branch, sync=True) 109 | self.halt = False 110 | self.rwlock = Lock() 111 | self.sync_c = Condition() 112 | self.sync_thread = Thread(target=self._sync, args=()) 113 | self.sync_thread.start() 114 | 115 | def _sync(self): 116 | while True: 117 | self.sync_c.acquire() 118 | if not self.halt: 119 | # wait till a sync request comes 120 | self.sync_c.wait() 121 | self.repo.synchronize() 122 | self.sync_c.release() 123 | else: 124 | self.sync_c.release() 125 | break 126 | 127 | def destroy(self, path): 128 | # stop sync thread 129 | self.sync_c.acquire() 130 | self.halt = True 131 | self.sync_c.notifyAll() 132 | self.sync_c.release() 133 | 134 | def __call__(self, op, path, *args): 135 | return super(GitFS, self).__call__(op, self.root + path, *args) 136 | 137 | def access(self, path, mode): 138 | if not os.access(path, mode): 139 | raise FuseOSError(EACCES) 140 | 141 | chmod = os.chmod 142 | chown = os.chown 143 | 144 | def create(self, path, mode): 145 | return os.open(path, os.O_WRONLY | os.O_CREAT, mode) 146 | 147 | def flush(self, path, fh): 148 | return os.fsync(fh) 149 | 150 | def fsync(self, path, datasync, fh): 151 | return os.fsync(fh) 152 | 153 | def getattr(self, path, fh=None): 154 | st = os.lstat(path) 155 | return dict((key, getattr(st, key)) for key in ('st_atime', 'st_ctime', 156 | 'st_gid', 'st_mode', 'st_mtime', 'st_nlink', 'st_size', 'st_uid')) 157 | 158 | getxattr = None 159 | 160 | def link(self, target, source): 161 | return os.link(source, target) 162 | 163 | listxattr = None 164 | mkdir = os.mkdir 165 | mknod = os.mknod 166 | open = os.open 167 | 168 | def read(self, path, size, offset, fh): 169 | with self.rwlock: 170 | os.lseek(fh, offset, 0) 171 | return os.read(fh, size) 172 | 173 | def readdir(self, path, fh): 174 | return ['.', '..'] + os.listdir(path) 175 | 176 | readlink = os.readlink 177 | 178 | def release(self, path, fh): 179 | return os.close(fh) 180 | 181 | def rename(self, old, new): 182 | return os.rename(old, self.root + new) 183 | 184 | rmdir = os.rmdir 185 | 186 | def statfs(self, path): 187 | stv = os.statvfs(path) 188 | return dict((key, getattr(stv, key)) for key in ('f_bavail', 'f_bfree', 189 | 'f_blocks', 'f_bsize', 'f_favail', 'f_ffree', 'f_files', 'f_flag', 190 | 'f_frsize', 'f_namemax')) 191 | 192 | def symlink(self, target, source): 193 | return os.symlink(source, target) 194 | 195 | def truncate(self, path, length, fh=None): 196 | with open(path, 'r+') as f: 197 | f.truncate(length) 198 | 199 | unlink = os.unlink 200 | utimens = os.utime 201 | 202 | def write(self, path, data, offset, fh): 203 | with self.rwlock: 204 | os.lseek(fh, offset, 0) 205 | return os.write(fh, data) 206 | 207 | 208 | if __name__ == "__main__": 209 | if len(argv) != 5: 210 | print 'usage: %s ' % argv[0] 211 | exit(1) 212 | fuse = FUSE(GitFS(argv[1], argv[2], argv[3]), argv[4], foreground=True) 213 | --------------------------------------------------------------------------------