Skip to content

Commit

Permalink
chmod +w is not necessary anymore.
Browse files Browse the repository at this point in the history
  • Loading branch information
chmduquesne committed Mar 20, 2011
1 parent 327d7ef commit ab38430
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 52 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
*.pyc
15 changes: 15 additions & 0 deletions Makefile
@@ -0,0 +1,15 @@
test: dirs
python sharebox.py test/local/mnt -o gitdir=test/local/git

unmount:
fusermount -u test/local/mnt
fusermount -u test/remote/mnt

dirs:
mkdir -p test/local/mnt
mkdir -p test/local/git
mkdir -p test/remote/mnt
mkdir -p test/remote/git

clean:
rm -rf *.pyc
187 changes: 135 additions & 52 deletions sharebox.py
@@ -1,20 +1,42 @@
#!/usr/bin/env python #!/usr/bin/env python

"""
Filesystem based on git-annex.
Usage
About git-annex: git-annex allows to keep only links to files and to keep
their content out of git. Once a file is added to git annex, it is
replaced by a symbolic link to the content of the file. The content of the
file is made read-only so that you don't modify it by mistake.
What does this file system:
- It automatically adds new files to git-annex.
- It resolves git-annex symlinks so that you see them as regular writable
files.
- If the content of a file is not present on the file system, it is
requested on the fly from one of the replicated copies.
- When you access a file, it does copy on write: if you don't modify it,
you read the git-annex copy. However, if you change it, the copy is
unlocked on the fly and commited to git-annex when closed. Depending on
the mount option, the previous copy can be kept in git-annex.
- It pulls at regular intervals the other replicated copies and launches a
merge program if there are conflicts.
"""
from __future__ import with_statement from __future__ import with_statement


from errno import EACCES from errno import EACCES
from os.path import realpath
from sys import argv, exit
import threading import threading


import os import os
import os.path


from fuse import FUSE, FuseOSError, Operations, LoggingMixIn from fuse import FUSE, FuseOSError, Operations, LoggingMixIn


import shlex import shlex
import subprocess import subprocess
import logging
import time import time
import sys
import getopt


def ignored(path): def ignored(path):
""" """
Expand Down Expand Up @@ -45,7 +67,6 @@ def shell_do(cmd):
""" """
calls the given shell command calls the given shell command
""" """
logging.debug(cmd)
p = subprocess.Popen(shlex.split(cmd)) p = subprocess.Popen(shlex.split(cmd))
p.wait() p.wait()


Expand All @@ -71,7 +92,6 @@ def __enter__(self):
def __exit__(self, type, value, traceback): def __exit__(self, type, value, traceback):
if not self.ignored: if not self.ignored:
shell_do('git annex add %s' % self.path) shell_do('git annex add %s' % self.path)
shell_do('chmod +w %s' % os.readlink(self.path))
shell_do('git commit -m "changed %s"' % self.path) shell_do('git commit -m "changed %s"' % self.path)


class CopyOnWrite: class CopyOnWrite:
Expand Down Expand Up @@ -118,27 +138,28 @@ def __enter__(self):
if self.opened_copies.get(self.fh, None) == None: if self.opened_copies.get(self.fh, None) == None:
if not self.ignored: if not self.ignored:
shell_do('git annex unlock %s' % self.path) shell_do('git annex unlock %s' % self.path)
self.opened_copies[self.fh] = os.open(self.path, self.opened_copies[self.fh] = os.open(self.path,
os.O_WRONLY | os.O_CREAT) os.O_WRONLY | os.O_CREAT)
return self.opened_copies.get(self.fh, self.fh) return self.opened_copies.get(self.fh, self.fh)


def __exit__(self, type, value, traceback): def __exit__(self, type, value, traceback):
if self.commit: if self.commit:
if not self.ignored: if self.opened_copies.get(self.fh, None) != None:
try: if not self.ignored:
os.close(self.opened_copies[self.fh]) try:
del self.opened_copies[self.fh] os.close(self.opened_copies[self.fh])
except KeyError: del self.opened_copies[self.fh]
pass except KeyError:
shell_do('git annex add %s' % self.path) pass
shell_do('chmod +w %s' % os.readlink(self.path)) shell_do('git annex add %s' % self.path)
shell_do('git commit -m "changed %s"' % self.path) shell_do('git commit -m "changed %s"' % self.path)


class ShareBox(LoggingMixIn, Operations): class ShareBox(LoggingMixIn, Operations):
""" """
Assumes operating from the root of the managed git directory Assumes operating from the root of the managed git directory
""" """
def __init__(self): def __init__(self, gitdir):
self.gitdir = gitdir
self.rwlock = threading.Lock() self.rwlock = threading.Lock()
self.opened_copies = {} self.opened_copies = {}
with self.rwlock: with self.rwlock:
Expand All @@ -153,17 +174,24 @@ def __call__(self, op, path, *args):
""" """
redirects self.op('/foo', ...) to self.op('./foo', ...) redirects self.op('/foo', ...) to self.op('./foo', ...)
""" """
if os.path.realpath(os.curdir) != self.gitdir:
os.chdir(self.gitdir)
return super(ShareBox, self).__call__(op, "." + path, *args) return super(ShareBox, self).__call__(op, "." + path, *args)


getxattr = None getxattr = None
listxattr = None listxattr = None
link = os.link link = os.link
mknod = os.mknod mknod = os.mknod
mkdir = os.mkdir mkdir = os.mkdir
open = os.open
readlink = os.readlink readlink = os.readlink
rmdir = os.rmdir rmdir = os.rmdir


def statfs(self, path):
stv = os.statvfs(path)
return dict((key, getattr(stv, key)) for key in ('f_bavail',
'f_bfree', 'f_blocks', 'f_bsize', 'f_favail', 'f_ffree',
'f_files', 'f_flag', 'f_frsize', 'f_namemax'))

def create(self, path, mode): def create(self, path, mode):
return os.open(path, os.O_WRONLY | os.O_CREAT, mode) return os.open(path, os.O_WRONLY | os.O_CREAT, mode)


Expand All @@ -174,32 +202,52 @@ def readdir(self, path, fh):
return ['.', '..'] + os.listdir(path) return ['.', '..'] + os.listdir(path)


def access(self, path, mode): def access(self, path, mode):
if not os.access(path, mode): if annexed(path):
raise FuseOSError(EACCES) if not os.path.exists(path):

raise FuseOSError(EACCES)
def getattr(self, path, fh=None): else:
if not os.access(path, mode):
raise FuseOSError(EACCES)

def open(self, path, flags):
""" """
symlinks to annexed files are dereferenced When an annexed file is requested, if it is not present on the
system we first try to get it. If it fails, we refuse the access.
Since we do copy on write, we do not need to try to open in write
mode annexed files.
""" """
path_ = path res = None
if annexed(path) and os.path.exists(path): if annexed(path):
path_ = os.readlink(path) if not os.path.exists(path):
st = os.lstat(path_) shell_do('git annex get %s' % path)
return dict((key, getattr(st, key)) for key in ('st_atime', if not os.path.exists(path):
'st_ctime', 'st_gid', 'st_mode', 'st_mtime', 'st_nlink', raise FuseOSError(EACCES)
'st_size', 'st_uid')) res = os.open(path, 32768) # magic to open read only
else:
res = os.open(path, flags)
return res


def statfs(self, path): def getattr(self, path, fh=None):
""" """
symlinks to annexed files are dereferenced When an annexed file is requested, we fake some of its attributes,
making it look like a conventional file (of size 0 if if is not
present on the system).
""" """
path_ = path path_ = path
if annexed(path) and os.path.exists(path): faked_attr = {}
path_ = os.readlink(path) if annexed(path):
stv = os.statvfs(path_) faked_attr ['st_mode'] = 33188 # we fake a 644 regular file
return dict((key, getattr(stv, key)) for key in ('f_bavail', if os.path.exists(path):
'f_bfree', 'f_blocks', 'f_bsize', 'f_favail', 'f_ffree', path_ = os.readlink(path)
'f_files', 'f_flag', 'f_frsize', 'f_namemax')) else:
faked_attr ['st_size'] = 0
st = os.lstat(path_)
res = dict((key, getattr(st, key)) for key in ('st_atime',
'st_ctime', 'st_gid', 'st_mode', 'st_mtime',
'st_nlink', 'st_size', 'st_uid'))
for attr, value in faked_attr.items():
res [attr] = value
return res


def chmod(self, path, mode): def chmod(self, path, mode):
with self.rwlock: with self.rwlock:
Expand Down Expand Up @@ -253,8 +301,8 @@ def release(self, path, fh):
os.close(fh) os.close(fh)


def rename(self, old, new): def rename(self, old, new):
# Make sure to lock the file (and to annex it if it was not)
with self.rwlock: with self.rwlock:
# Make sure to lock the file (and to annex it if it was not)
if not ignored(old): if not ignored(old):
shell_do('git annex add %s' % old) shell_do('git annex add %s' % old)
os.rename(old, '.' + new) os.rename(old, '.' + new)
Expand Down Expand Up @@ -292,19 +340,54 @@ def synchronize():
""" """
i = 0 i = 0
while 1: while 1:
logging.debug("synchronizing %s" % i)
time.sleep(1) time.sleep(1)
i += 1 i += 1


if __name__ == "__main__": if __name__ == "__main__":
if len(argv) != 3: try:
print 'usage: %s <gitdir> <mountpoint>' % argv[0] opts, args = getopt.gnu_getopt(sys.argv[1:], "ho:", ["help"])
exit(1) except getopt.GetoptError, err:
logging.basicConfig(filename="sharebox.log", level=logging.DEBUG) print str(err)
t = threading.Thread(target=synchronize) print 'usage: %s <mountpoint> [-o <option>]' % sys.argv[0]
t.daemon = True sys.exit(1)
#t.start()
mountpoint = realpath(argv[2]) gitdir = None
gitdir = realpath(argv[1]) logfile = 'sharebox.log'
os.chdir(gitdir) foreground = False
fuse = FUSE(ShareBox(), mountpoint, foreground=True) sync = 0

for opt, arg in opts:
if opt in ("-h", "--help"):
print 'usage: %s <mountpoint> [-o <option>]' % sys.argv[0]
sys.exit(0)
if opt == "-o":
if '=' in arg:
option = arg.split('=')[0]
value = arg.replace( option + '=', '', 1)
if option == 'gitdir':
gitdir = value
if option == 'logfile':
logfile = value
if option == 'sync':
sync = value
else:
if arg == 'foreground':
foreground=True

if not gitdir:
print 'missing the gitdir option'
sys.exit(1)

mountpoint = "".join(args)
if mountpoint == "":
print 'invalid mountpoint'
sys.exit(1)

if sync:
t = threading.Thread(target=synchronize)
t.daemon = True
t.start()

mountpoint = os.path.realpath(mountpoint)
gitdir = os.path.realpath(gitdir)
fuse = FUSE(ShareBox(gitdir), mountpoint, foreground=foreground)

0 comments on commit ab38430

Please sign in to comment.