1# Copyright (c) 2008 Giorgos Verigakis <verigak@gmail.com> 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 15from ctypes import * 16from ctypes.util import find_library 17from errno import * 18from functools import partial 19from platform import machine, system 20from stat import S_IFDIR 21from traceback import print_exc 22 23import logging 24 25 26class c_timespec(Structure): 27 _fields_ = [('tv_sec', c_long), ('tv_nsec', c_long)] 28 29class c_utimbuf(Structure): 30 _fields_ = [('actime', c_timespec), ('modtime', c_timespec)] 31 32class c_stat(Structure): 33 pass # Platform dependent 34 35_system = system() 36if _system in ('Darwin', 'FreeBSD'): 37 _libiconv = CDLL(find_library("iconv"), RTLD_GLOBAL) # libfuse dependency 38 ENOTSUP = 45 39 c_dev_t = c_int32 40 c_fsblkcnt_t = c_ulong 41 c_fsfilcnt_t = c_ulong 42 c_gid_t = c_uint32 43 c_mode_t = c_uint16 44 c_off_t = c_int64 45 c_pid_t = c_int32 46 c_uid_t = c_uint32 47 setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), 48 c_size_t, c_int, c_uint32) 49 getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), 50 c_size_t, c_uint32) 51 c_stat._fields_ = [ 52 ('st_dev', c_dev_t), 53 ('st_ino', c_uint32), 54 ('st_mode', c_mode_t), 55 ('st_nlink', c_uint16), 56 ('st_uid', c_uid_t), 57 ('st_gid', c_gid_t), 58 ('st_rdev', c_dev_t), 59 ('st_atimespec', c_timespec), 60 ('st_mtimespec', c_timespec), 61 ('st_ctimespec', c_timespec), 62 ('st_size', c_off_t), 63 ('st_blocks', c_int64), 64 ('st_blksize', c_int32)] 65elif _system == 'Linux': 66 ENOTSUP = 95 67 c_dev_t = c_ulonglong 68 c_fsblkcnt_t = c_ulonglong 69 c_fsfilcnt_t = c_ulonglong 70 c_gid_t = c_uint 71 c_mode_t = c_uint 72 c_off_t = c_longlong 73 c_pid_t = c_int 74 c_uid_t = c_uint 75 setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int) 76 getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t) 77 78 _machine = machine() 79 if _machine == 'x86_64': 80 c_stat._fields_ = [ 81 ('st_dev', c_dev_t), 82 ('st_ino', c_ulong), 83 ('st_nlink', c_ulong), 84 ('st_mode', c_mode_t), 85 ('st_uid', c_uid_t), 86 ('st_gid', c_gid_t), 87 ('__pad0', c_int), 88 ('st_rdev', c_dev_t), 89 ('st_size', c_off_t), 90 ('st_blksize', c_long), 91 ('st_blocks', c_long), 92 ('st_atimespec', c_timespec), 93 ('st_mtimespec', c_timespec), 94 ('st_ctimespec', c_timespec)] 95 elif _machine == 'ppc': 96 c_stat._fields_ = [ 97 ('st_dev', c_dev_t), 98 ('st_ino', c_ulonglong), 99 ('st_mode', c_mode_t), 100 ('st_nlink', c_uint), 101 ('st_uid', c_uid_t), 102 ('st_gid', c_gid_t), 103 ('st_rdev', c_dev_t), 104 ('__pad2', c_ushort), 105 ('st_size', c_off_t), 106 ('st_blksize', c_long), 107 ('st_blocks', c_longlong), 108 ('st_atimespec', c_timespec), 109 ('st_mtimespec', c_timespec), 110 ('st_ctimespec', c_timespec)] 111 else: 112 # i686, use as fallback for everything else 113 c_stat._fields_ = [ 114 ('st_dev', c_dev_t), 115 ('__pad1', c_ushort), 116 ('__st_ino', c_ulong), 117 ('st_mode', c_mode_t), 118 ('st_nlink', c_uint), 119 ('st_uid', c_uid_t), 120 ('st_gid', c_gid_t), 121 ('st_rdev', c_dev_t), 122 ('__pad2', c_ushort), 123 ('st_size', c_off_t), 124 ('st_blksize', c_long), 125 ('st_blocks', c_longlong), 126 ('st_atimespec', c_timespec), 127 ('st_mtimespec', c_timespec), 128 ('st_ctimespec', c_timespec), 129 ('st_ino', c_ulonglong)] 130else: 131 raise NotImplementedError('%s is not supported.' % _system) 132 133 134class c_statvfs(Structure): 135 _fields_ = [ 136 ('f_bsize', c_ulong), 137 ('f_frsize', c_ulong), 138 ('f_blocks', c_fsblkcnt_t), 139 ('f_bfree', c_fsblkcnt_t), 140 ('f_bavail', c_fsblkcnt_t), 141 ('f_files', c_fsfilcnt_t), 142 ('f_ffree', c_fsfilcnt_t), 143 ('f_favail', c_fsfilcnt_t)] 144 145if _system == 'FreeBSD': 146 c_fsblkcnt_t = c_uint64 147 c_fsfilcnt_t = c_uint64 148 setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int) 149 getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t) 150 class c_statvfs(Structure): 151 _fields_ = [ 152 ('f_bavail', c_fsblkcnt_t), 153 ('f_bfree', c_fsblkcnt_t), 154 ('f_blocks', c_fsblkcnt_t), 155 ('f_favail', c_fsfilcnt_t), 156 ('f_ffree', c_fsfilcnt_t), 157 ('f_files', c_fsfilcnt_t), 158 ('f_bsize', c_ulong), 159 ('f_flag', c_ulong), 160 ('f_frsize', c_ulong)] 161 162class fuse_file_info(Structure): 163 _fields_ = [ 164 ('flags', c_int), 165 ('fh_old', c_ulong), 166 ('writepage', c_int), 167 ('direct_io', c_uint, 1), 168 ('keep_cache', c_uint, 1), 169 ('flush', c_uint, 1), 170 ('padding', c_uint, 29), 171 ('fh', c_uint64), 172 ('lock_owner', c_uint64)] 173 174class fuse_context(Structure): 175 _fields_ = [ 176 ('fuse', c_voidp), 177 ('uid', c_uid_t), 178 ('gid', c_gid_t), 179 ('pid', c_pid_t), 180 ('private_data', c_voidp)] 181 182class fuse_operations(Structure): 183 _fields_ = [ 184 ('getattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat))), 185 ('readlink', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)), 186 ('getdir', c_voidp), # Deprecated, use readdir 187 ('mknod', CFUNCTYPE(c_int, c_char_p, c_mode_t, c_dev_t)), 188 ('mkdir', CFUNCTYPE(c_int, c_char_p, c_mode_t)), 189 ('unlink', CFUNCTYPE(c_int, c_char_p)), 190 ('rmdir', CFUNCTYPE(c_int, c_char_p)), 191 ('symlink', CFUNCTYPE(c_int, c_char_p, c_char_p)), 192 ('rename', CFUNCTYPE(c_int, c_char_p, c_char_p)), 193 ('link', CFUNCTYPE(c_int, c_char_p, c_char_p)), 194 ('chmod', CFUNCTYPE(c_int, c_char_p, c_mode_t)), 195 ('chown', CFUNCTYPE(c_int, c_char_p, c_uid_t, c_gid_t)), 196 ('truncate', CFUNCTYPE(c_int, c_char_p, c_off_t)), 197 ('utime', c_voidp), # Deprecated, use utimens 198 ('open', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), 199 ('read', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t, 200 POINTER(fuse_file_info))), 201 ('write', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t, 202 POINTER(fuse_file_info))), 203 ('statfs', CFUNCTYPE(c_int, c_char_p, POINTER(c_statvfs))), 204 ('flush', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), 205 ('release', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), 206 ('fsync', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))), 207 ('setxattr', setxattr_t), 208 ('getxattr', getxattr_t), 209 ('listxattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)), 210 ('removexattr', CFUNCTYPE(c_int, c_char_p, c_char_p)), 211 ('opendir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), 212 ('readdir', CFUNCTYPE(c_int, c_char_p, c_voidp, CFUNCTYPE(c_int, c_voidp, 213 c_char_p, POINTER(c_stat), c_off_t), c_off_t, POINTER(fuse_file_info))), 214 ('releasedir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), 215 ('fsyncdir', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))), 216 ('init', CFUNCTYPE(c_voidp, c_voidp)), 217 ('destroy', CFUNCTYPE(c_voidp, c_voidp)), 218 ('access', CFUNCTYPE(c_int, c_char_p, c_int)), 219 ('create', CFUNCTYPE(c_int, c_char_p, c_mode_t, POINTER(fuse_file_info))), 220 ('ftruncate', CFUNCTYPE(c_int, c_char_p, c_off_t, POINTER(fuse_file_info))), 221 ('fgetattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat), 222 POINTER(fuse_file_info))), 223 ('lock', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info), c_int, c_voidp)), 224 ('utimens', CFUNCTYPE(c_int, c_char_p, POINTER(c_utimbuf))), 225 ('bmap', CFUNCTYPE(c_int, c_char_p, c_size_t, POINTER(c_ulonglong)))] 226 227 228def time_of_timespec(ts): 229 return ts.tv_sec + ts.tv_nsec / 10 ** 9 230 231def set_st_attrs(st, attrs): 232 for key, val in attrs.items(): 233 if key in ('st_atime', 'st_mtime', 'st_ctime'): 234 timespec = getattr(st, key + 'spec') 235 timespec.tv_sec = int(val) 236 timespec.tv_nsec = int((val - timespec.tv_sec) * 10 ** 9) 237 elif hasattr(st, key): 238 setattr(st, key, val) 239 240 241_libfuse_path = find_library('fuse') 242if not _libfuse_path: 243 raise EnvironmentError('Unable to find libfuse') 244_libfuse = CDLL(_libfuse_path) 245_libfuse.fuse_get_context.restype = POINTER(fuse_context) 246 247 248def fuse_get_context(): 249 """Returns a (uid, gid, pid) tuple""" 250 ctxp = _libfuse.fuse_get_context() 251 ctx = ctxp.contents 252 return ctx.uid, ctx.gid, ctx.pid 253 254 255class FUSE(object): 256 """This class is the lower level interface and should not be subclassed 257 under normal use. Its methods are called by fuse. 258 Assumes API version 2.6 or later.""" 259 260 def __init__(self, operations, mountpoint, raw_fi=False, **kwargs): 261 """Setting raw_fi to True will cause FUSE to pass the fuse_file_info 262 class as is to Operations, instead of just the fh field. 263 This gives you access to direct_io, keep_cache, etc.""" 264 265 self.operations = operations 266 self.raw_fi = raw_fi 267 args = ['fuse'] 268 if kwargs.pop('foreground', False): 269 args.append('-f') 270 if kwargs.pop('debug', False): 271 args.append('-d') 272 if kwargs.pop('nothreads', False): 273 args.append('-s') 274 kwargs.setdefault('fsname', operations.__class__.__name__) 275 args.append('-o') 276 args.append(','.join(key if val == True else '%s=%s' % (key, val) 277 for key, val in kwargs.items())) 278 args.append(mountpoint) 279 argv = (c_char_p * len(args))(*args) 280 281 fuse_ops = fuse_operations() 282 for name, prototype in fuse_operations._fields_: 283 if prototype != c_voidp and getattr(operations, name, None): 284 op = partial(self._wrapper_, getattr(self, name)) 285 setattr(fuse_ops, name, prototype(op)) 286 _libfuse.fuse_main_real(len(args), argv, pointer(fuse_ops), 287 sizeof(fuse_ops), None) 288 del self.operations # Invoke the destructor 289 290 def _wrapper_(self, func, *args, **kwargs): 291 """Decorator for the methods that follow""" 292 try: 293 return func(*args, **kwargs) or 0 294 except OSError as e: 295 return -(e.errno or EFAULT) 296 except: 297 print_exc() 298 return -EFAULT 299 300 def getattr(self, path, buf): 301 return self.fgetattr(path, buf, None) 302 303 def readlink(self, path, buf, bufsize): 304 ret = self.operations('readlink', path).encode('utf-8') 305 data = create_string_buffer(ret[:bufsize - 1]) 306 memmove(buf, data, len(data)) 307 return 0 308 309 def mknod(self, path, mode, dev): 310 return self.operations('mknod', path, mode, dev) 311 312 def mkdir(self, path, mode): 313 return self.operations('mkdir', path, mode) 314 315 def unlink(self, path): 316 return self.operations('unlink', path) 317 318 def rmdir(self, path): 319 return self.operations('rmdir', path) 320 321 def symlink(self, source, target): 322 return self.operations('symlink', target, source) 323 324 def rename(self, old, new): 325 return self.operations('rename', old, new) 326 327 def link(self, source, target): 328 return self.operations('link', target, source) 329 330 def chmod(self, path, mode): 331 return self.operations('chmod', path, mode) 332 333 def chown(self, path, uid, gid): 334 return self.operations('chown', path, uid, gid) 335 336 def truncate(self, path, length): 337 return self.operations('truncate', path, length) 338 339 def open(self, path, fip): 340 fi = fip.contents 341 if self.raw_fi: 342 return self.operations('open', path, fi) 343 else: 344 fi.fh = self.operations('open', path, fi.flags) 345 return 0 346 347 def read(self, path, buf, size, offset, fip): 348 fh = fip.contents if self.raw_fi else fip.contents.fh 349 ret = self.operations('read', path, size, offset, fh) 350 if not ret: 351 return 0 352 data = create_string_buffer(ret[:size], size) 353 memmove(buf, data, size) 354 return size 355 356 def write(self, path, buf, size, offset, fip): 357 data = string_at(buf, size) 358 fh = fip.contents if self.raw_fi else fip.contents.fh 359 return self.operations('write', path, data, offset, fh) 360 361 def statfs(self, path, buf): 362 stv = buf.contents 363 attrs = self.operations('statfs', path) 364 for key, val in attrs.items(): 365 if hasattr(stv, key): 366 setattr(stv, key, val) 367 return 0 368 369 def flush(self, path, fip): 370 fh = fip.contents if self.raw_fi else fip.contents.fh 371 return self.operations('flush', path, fh) 372 373 def release(self, path, fip): 374 fh = fip.contents if self.raw_fi else fip.contents.fh 375 return self.operations('release', path, fh) 376 377 def fsync(self, path, datasync, fip): 378 fh = fip.contents if self.raw_fi else fip.contents.fh 379 return self.operations('fsync', path, datasync, fh) 380 381 def setxattr(self, path, name, value, size, options, *args): 382 data = string_at(value, size) 383 return self.operations('setxattr', path, name, data, options, *args) 384 385 def getxattr(self, path, name, value, size, *args): 386 ret = self.operations('getxattr', path, name, *args) 387 retsize = len(ret) 388 buf = create_string_buffer(ret, retsize) # Does not add trailing 0 389 if bool(value): 390 if retsize > size: 391 return -ERANGE 392 memmove(value, buf, retsize) 393 return retsize 394 395 def listxattr(self, path, namebuf, size): 396 ret = self.operations('listxattr', path) 397 buf = create_string_buffer('\x00'.join(ret)) if ret else '' 398 bufsize = len(buf) 399 if bool(namebuf): 400 if bufsize > size: 401 return -ERANGE 402 memmove(namebuf, buf, bufsize) 403 return bufsize 404 405 def removexattr(self, path, name): 406 return self.operations('removexattr', path, name) 407 408 def opendir(self, path, fip): 409 # Ignore raw_fi 410 fip.contents.fh = self.operations('opendir', path) 411 return 0 412 413 def readdir(self, path, buf, filler, offset, fip): 414 # Ignore raw_fi 415 for item in self.operations('readdir', path, fip.contents.fh): 416 if isinstance(item, str): 417 name, st, offset = item, None, 0 418 name = name.encode('utf-8') 419 else: 420 name, attrs, offset = item 421 if attrs: 422 st = c_stat() 423 set_st_attrs(st, attrs) 424 else: 425 st = None 426 if filler(buf, name, st, offset) != 0: 427 break 428 return 0 429 430 def releasedir(self, path, fip): 431 # Ignore raw_fi 432 return self.operations('releasedir', path, fip.contents.fh) 433 434 def fsyncdir(self, path, datasync, fip): 435 # Ignore raw_fi 436 return self.operations('fsyncdir', path, datasync, fip.contents.fh) 437 438 def init(self, conn): 439 return self.operations('init', '/') 440 441 def destroy(self, private_data): 442 return self.operations('destroy', '/') 443 444 def access(self, path, amode): 445 return self.operations('access', path, amode) 446 447 def create(self, path, mode, fip): 448 fi = fip.contents 449 if self.raw_fi: 450 return self.operations('create', path, mode, fi) 451 else: 452 fi.fh = self.operations('create', path, mode) 453 return 0 454 455 def ftruncate(self, path, length, fip): 456 fh = fip.contents if self.raw_fi else fip.contents.fh 457 return self.operations('truncate', path, length, fh) 458 459 def fgetattr(self, path, buf, fip): 460 memset(buf, 0, sizeof(c_stat)) 461 st = buf.contents 462 fh = fip and (fip.contents if self.raw_fi else fip.contents.fh) 463 attrs = self.operations('getattr', path, fh) 464 set_st_attrs(st, attrs) 465 return 0 466 467 def lock(self, path, fip, cmd, lock): 468 fh = fip.contents if self.raw_fi else fip.contents.fh 469 return self.operations('lock', path, fh, cmd, lock) 470 471 def utimens(self, path, buf): 472 if buf: 473 atime = time_of_timespec(buf.contents.actime) 474 mtime = time_of_timespec(buf.contents.modtime) 475 times = (atime, mtime) 476 else: 477 times = None 478 return self.operations('utimens', path, times) 479 480 def bmap(self, path, blocksize, idx): 481 return self.operations('bmap', path, blocksize, idx) 482 483 484class Operations(object): 485 """This class should be subclassed and passed as an argument to FUSE on 486 initialization. All operations should raise an OSError exception on 487 error. 488 489 When in doubt of what an operation should do, check the FUSE header 490 file or the corresponding system call man page.""" 491 492 def __call__(self, op, *args): 493 if not hasattr(self, op): 494 raise OSError(EFAULT, '') 495 return getattr(self, op)(*args) 496 497 def access(self, path, amode): 498 return 0 499 500 bmap = None 501 502 def chmod(self, path, mode): 503 raise OSError(EROFS, '') 504 505 def chown(self, path, uid, gid): 506 raise OSError(EROFS, '') 507 508 def create(self, path, mode, fi=None): 509 """When raw_fi is False (default case), fi is None and create should 510 return a numerical file handle. 511 When raw_fi is True the file handle should be set directly by create 512 and return 0.""" 513 raise OSError(EROFS, '') 514 515 def destroy(self, path): 516 """Called on filesystem destruction. Path is always /""" 517 pass 518 519 def flush(self, path, fh): 520 return 0 521 522 def fsync(self, path, datasync, fh): 523 return 0 524 525 def fsyncdir(self, path, datasync, fh): 526 return 0 527 528 def getattr(self, path, fh=None): 529 """Returns a dictionary with keys identical to the stat C structure 530 of stat(2). 531 st_atime, st_mtime and st_ctime should be floats. 532 NOTE: There is an incombatibility between Linux and Mac OS X concerning 533 st_nlink of directories. Mac OS X counts all files inside the directory, 534 while Linux counts only the subdirectories.""" 535 536 if path != '/': 537 raise OSError(ENOENT, '') 538 return dict(st_mode=(S_IFDIR | 0o755), st_nlink=2) 539 540 def getxattr(self, path, name, position=0): 541 raise OSError(ENOTSUP, '') 542 543 def init(self, path): 544 """Called on filesystem initialization. Path is always / 545 Use it instead of __init__ if you start threads on initialization.""" 546 pass 547 548 def link(self, target, source): 549 raise OSError(EROFS, '') 550 551 def listxattr(self, path): 552 return [] 553 554 lock = None 555 556 def mkdir(self, path, mode): 557 raise OSError(EROFS, '') 558 559 def mknod(self, path, mode, dev): 560 raise OSError(EROFS, '') 561 562 def open(self, path, flags): 563 """When raw_fi is False (default case), open should return a numerical 564 file handle. 565 When raw_fi is True the signature of open becomes: 566 open(self, path, fi) 567 and the file handle should be set directly.""" 568 return 0 569 570 def opendir(self, path): 571 """Returns a numerical file handle.""" 572 return 0 573 574 def read(self, path, size, offset, fh): 575 """Returns a string containing the data requested.""" 576 raise OSError(ENOENT, '') 577 578 def readdir(self, path, fh): 579 """Can return either a list of names, or a list of (name, attrs, offset) 580 tuples. attrs is a dict as in getattr.""" 581 return ['.', '..'] 582 583 def readlink(self, path): 584 raise OSError(ENOENT, '') 585 586 def release(self, path, fh): 587 return 0 588 589 def releasedir(self, path, fh): 590 return 0 591 592 def removexattr(self, path, name): 593 raise OSError(ENOTSUP, '') 594 595 def rename(self, old, new): 596 raise OSError(EROFS, '') 597 598 def rmdir(self, path): 599 raise OSError(EROFS, '') 600 601 def setxattr(self, path, name, value, options, position=0): 602 raise OSError(ENOTSUP, '') 603 604 def statfs(self, path): 605 """Returns a dictionary with keys identical to the statvfs C structure 606 of statvfs(3). 607 On Mac OS X f_bsize and f_frsize must be a power of 2 (minimum 512).""" 608 return {} 609 610 def symlink(self, target, source): 611 raise OSError(EROFS, '') 612 613 def truncate(self, path, length, fh=None): 614 raise OSError(EROFS, '') 615 616 def unlink(self, path): 617 raise OSError(EROFS, '') 618 619 def utimens(self, path, times=None): 620 """Times is a (atime, mtime) tuple. If None use current time.""" 621 return 0 622 623 def write(self, path, data, offset, fh): 624 raise OSError(EROFS, '') 625 626 627class LoggingMixIn: 628 def __call__(self, op, path, *args): 629 logging.debug('-> %s %s %s', op, path, repr(args)) 630 ret = '[Unknown Error]' 631 try: 632 ret = getattr(self, op)(path, *args) 633 return ret 634 except OSError as e: 635 ret = str(e) 636 raise 637 finally: 638 logging.debug('<- %s %s', op, repr(ret)) 639