如果你是我的長期讀者,那麼你應該知道我在尋找一個完美備份程式,最後我寫了一個基於bup的我自己的加密層。
在寫encbup的時候,我對僅僅恢復一個檔案就必須要下載整個巨大的檔案檔案的做法不甚滿意,但仍然希望能將EncFS和 rdiff-backup一起使用來實現可遠端掛載、加密、去重、版本化備份的功能。
再次試用obnam 後(囉嗦一句:它還是慢的出奇),我注意到了它有一個mount命令。深入研究後,我發現了fuse-python和fusepy,感覺用Python寫一個FUSE檔案系統應該挺簡單的。
聰明的讀者可能已經意識到了我接下來要做的事情:我決定用Python寫一個加密檔案系統層!它與EncFS會非常相似,但也有一些重要的區別:
- 它預設以反向模式執行,接收正常的檔案並且暴露一個被加密的目錄。任何備份程式會發現(並且備份)這些加密的目錄,不需要任何其它的儲存。
- 它也能接受由一個目錄列表組成的配置檔案,並且在掛載點將這些目錄暴露出來。這樣的話,所有的備份指令碼就需要將掛載點備份,各種不同的目錄會立刻得以備份。
- 它會更偏重於備份,而不是加密儲存。寫起來應該會挺有意思的。
一個FUSE檔案系統示例
寫這個指令碼的第一步是寫出一個純粹的傳遞式的檔案系統。它僅僅是接受一個目錄,並在掛載點將其暴露出來,確保任何在掛載點的修改都會映象到源資料中。
fusepy 要求你寫一個類,裡面定義了各種作業系統級別的方法。你可以選擇定義那些你的檔案系統想要支援的方法,其他的可以暫時不予定義,但是我需要定義全部的方法,因為我的檔案系統是一個傳遞式的檔案系統,它應該表現的與原有的檔案系統儘可能一致。
寫這段程式碼會非常簡單有趣,因為大部分的方法只是對os模組的一些簡單封裝(確實,你可以直接給它們賦值,比如 open=os.open 等等,但是我的模組需要一些路徑擴充套件)。不幸的是,fuse-python有一個bug(據我所知)是當開啟和讀檔案的時候,它無法將檔案控制程式碼回傳給檔案系統。因而我的指令碼不知道某個應用執行讀寫操作時對應的是哪個檔案控制程式碼,從而導致了失敗。只需要對fusepy做極少的改動,它就可以很好地執行。它只有一個檔案,所以你可以把它直接放到你的工程裡。
程式碼
在這裡,我很樂意給出這段程式碼,當你打算自己實現檔案系統的時候可以拿來參考。這段程式碼提供了一個很好的起點,你可以直接把這個類複製到你的工程中並且根據需要重寫裡面的一些方法。
接下來是真正的程式碼了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
#!/usr/bin/env python from __future__ import with_statement import os import sys import errno from fuse import FUSE, FuseOSError, Operations class Passthrough(Operations): def __init__(self, root): self.root = root # Helpers # ======= def _full_path(self, partial): if partial.startswith("/"): partial = partial[1:] path = os.path.join(self.root, partial) return path # Filesystem methods # ================== def access(self, path, mode): full_path = self._full_path(path) if not os.access(full_path, mode): raise FuseOSError(errno.EACCES) def chmod(self, path, mode): full_path = self._full_path(path) return os.chmod(full_path, mode) def chown(self, path, uid, gid): full_path = self._full_path(path) return os.chown(full_path, uid, gid) def getattr(self, path, fh=None): full_path = self._full_path(path) st = os.lstat(full_path) return dict((key, getattr(st, key)) for key in ('st_atime', 'st_ctime', 'st_gid', 'st_mode', 'st_mtime', 'st_nlink', 'st_size', 'st_uid')) def readdir(self, path, fh): full_path = self._full_path(path) dirents = ['.', '..'] if os.path.isdir(full_path): dirents.extend(os.listdir(full_path)) for r in dirents: yield r def readlink(self, path): pathname = os.readlink(self._full_path(path)) if pathname.startswith("/"): # Path name is absolute, sanitize it. return os.path.relpath(pathname, self.root) else: return pathname def mknod(self, path, mode, dev): return os.mknod(self._full_path(path), mode, dev) def rmdir(self, path): full_path = self._full_path(path) return os.rmdir(full_path) def mkdir(self, path, mode): return os.mkdir(self._full_path(path), mode) def statfs(self, path): full_path = self._full_path(path) stv = os.statvfs(full_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 unlink(self, path): return os.unlink(self._full_path(path)) def symlink(self, target, name): return os.symlink(self._full_path(target), self._full_path(name)) def rename(self, old, new): return os.rename(self._full_path(old), self._full_path(new)) def link(self, target, name): return os.link(self._full_path(target), self._full_path(name)) def utimens(self, path, times=None): return os.utime(self._full_path(path), times) # File methods # ============ def open(self, path, flags): full_path = self._full_path(path) return os.open(full_path, flags) def create(self, path, mode, fi=None): full_path = self._full_path(path) return os.open(full_path, os.O_WRONLY | os.O_CREAT, mode) def read(self, path, length, offset, fh): os.lseek(fh, offset, os.SEEK_SET) return os.read(fh, length) def write(self, path, buf, offset, fh): os.lseek(fh, offset, os.SEEK_SET) return os.write(fh, buf) def truncate(self, path, length, fh=None): full_path = self._full_path(path) with open(full_path, 'r+') as f: f.truncate(length) def flush(self, path, fh): return os.fsync(fh) def release(self, path, fh): return os.close(fh) def fsync(self, path, fdatasync, fh): return self.flush(path, fh) def main(mountpoint, root): FUSE(Passthrough(root), mountpoint, foreground=True) if __name__ == '__main__': main(sys.argv[2], sys.argv[1]) |
如果你想要執行它,只需要安裝fusepy,把這段程式碼放進一個檔案(比如myfuse.py)然後執行 python myfuse.py /你的目錄 /掛載點目錄 。你會發現 “/你的目錄” 路徑下的所有檔案都跑到”/掛載點目錄”,還能像用原生檔案系統一樣操作它們。
結語
總的來說,我並不認為寫一個檔案系統就這麼簡單。接下來要做的是在指令碼里新增加密/解密的功能,以及一些幫助類的方法。我的目標是能讓它除了有更好的擴充套件性(因為是用Python寫的),以及包含一些針對備份檔案的額外特性外,可以成為一個EncFS的完全替代品。
如果你想跟進這個指令碼的開發過程,請在下面訂閱我的郵件列表,或者在Twitter上關注我。一如既往的歡迎反饋(在下面評論就很好)。