如果你是我的長(zhǎng)期讀者,那么你應(yīng)該知道我在尋找一個(gè)完美備份程序,最后我寫(xiě)了一個(gè)基于bup的我自己的加密層。
在寫(xiě)encbup的時(shí)候,我對(duì)僅僅恢復(fù)一個(gè)文件就必須要下載整個(gè)巨大的檔案文件的做法不甚滿意,但仍然希望能將EncFS和 rdiff-backup一起使用來(lái)實(shí)現(xiàn)可遠(yuǎn)程掛載、加密、去重、版本化備份的功能。
再次試用obnam 后(啰嗦一句:它還是慢的出奇),我注意到了它有一個(gè)mount命令。深入研究后,我發(fā)現(xiàn)了fuse-python和fusepy,感覺(jué)用Python寫(xiě)一個(gè)FUSE文件系統(tǒng)應(yīng)該挺簡(jiǎn)單的。
聰明的讀者可能已經(jīng)意識(shí)到了我接下來(lái)要做的事情:我決定用Python寫(xiě)一個(gè)加密文件系統(tǒng)層!它與EncFS會(huì)非常相似,但也有一些重要的區(qū)別:
- 它默認(rèn)以反向模式運(yùn)行,接收正常的文件并且暴露一個(gè)被加密的目錄。任何備份程序會(huì)發(fā)現(xiàn)(并且備份)這些加密的目錄,不需要任何其它的存儲(chǔ)。
- 它也能接受由一個(gè)目錄列表組成的配置文件,并且在掛載點(diǎn)將這些目錄暴露出來(lái)。這樣的話,所有的備份腳本就需要將掛載點(diǎn)備份,各種不同的目錄會(huì)立刻得以備份。
- 它會(huì)更偏重于備份,而不是加密存儲(chǔ)。寫(xiě)起來(lái)應(yīng)該會(huì)挺有意思的。
一個(gè)FUSE文件系統(tǒng)示例
寫(xiě)這個(gè)腳本的第一步是寫(xiě)出一個(gè)純粹的傳遞式的文件系統(tǒng)。它僅僅是接受一個(gè)目錄,并在掛載點(diǎn)將其暴露出來(lái),確保任何在掛載點(diǎn)的修改都會(huì)鏡像到源數(shù)據(jù)中。
fusepy 要求你寫(xiě)一個(gè)類,里面定義了各種操作系統(tǒng)級(jí)別的方法。你可以選擇定義那些你的文件系統(tǒng)想要支持的方法,其他的可以暫時(shí)不予定義,但是我需要定義全部的方法,因?yàn)槲业奈募到y(tǒng)是一個(gè)傳遞式的文件系統(tǒng),它應(yīng)該表現(xiàn)的與原有的文件系統(tǒng)盡可能一致。
寫(xiě)這段代碼會(huì)非常簡(jiǎn)單有趣,因?yàn)榇蟛糠值姆椒ㄖ皇菍?duì)os模塊的一些簡(jiǎn)單封裝(確實(shí),你可以直接給它們賦值,比如 open=os.open 等等,但是我的模塊需要一些路徑擴(kuò)展)。不幸的是,fuse-python有一個(gè)bug(據(jù)我所知)是當(dāng)打開(kāi)和讀文件的時(shí)候,它無(wú)法將文件句柄回傳給文件系統(tǒng)。因而我的腳本不知道某個(gè)應(yīng)用執(zhí)行讀寫(xiě)操作時(shí)對(duì)應(yīng)的是哪個(gè)文件句柄,從而導(dǎo)致了失敗。只需要對(duì)fusepy做極少的改動(dòng),它就可以很好地運(yùn)行。它只有一個(gè)文件,所以你可以把它直接放到你的工程里。
代碼
在這里,我很樂(lè)意給出這段代碼,當(dāng)你打算自己實(shí)現(xiàn)文件系統(tǒng)的時(shí)候可以拿來(lái)參考。這段代碼提供了一個(gè)很好的起點(diǎn),你可以直接把這個(gè)類復(fù)制到你的工程中并且根據(jù)需要重寫(xiě)里面的一些方法。
接下來(lái)是真正的代碼了:
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 ]) |
如果你想要運(yùn)行它,只需要安裝fusepy,把這段代碼放進(jìn)一個(gè)文件(比如myfuse.py)然后運(yùn)行 python myfuse.py /你的目錄 /掛載點(diǎn)目錄 。你會(huì)發(fā)現(xiàn) “/你的目錄” 路徑下的所有文件都跑到”/掛載點(diǎn)目錄”,還能像用原生文件系統(tǒng)一樣操作它們。
結(jié)語(yǔ)
總的來(lái)說(shuō),我并不認(rèn)為寫(xiě)一個(gè)文件系統(tǒng)就這么簡(jiǎn)單。接下來(lái)要做的是在腳本里添加加密/解密的功能,以及一些幫助類的方法。我的目標(biāo)是能讓它除了有更好的擴(kuò)展性(因?yàn)槭怯肞ython寫(xiě)的),以及包含一些針對(duì)備份文件的額外特性外,可以成為一個(gè)EncFS的完全替代品。
如果你想跟進(jìn)這個(gè)腳本的開(kāi)發(fā)過(guò)程,請(qǐng)?jiān)谙旅嬗嗛單业泥]件列表,或者在Twitter上關(guān)注我。一如既往的歡迎反饋(在下面評(píng)論就很好)。