This repository was archived by the owner on Nov 5, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathonedrivefs
executable file
·319 lines (260 loc) · 10.2 KB
/
onedrivefs
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
#!/usr/bin/env python2
from __future__ import unicode_literals, print_function
import itertools as it, operator as op, functools as ft
from os.path import basename, dirname
from collections import OrderedDict, namedtuple
from tempfile import NamedTemporaryFile
import os, sys, types, errno, stat, re, logging
from onedrive import api_v5, conf
import fuse
from datetime import datetime, timedelta
from calendar import timegm
try:
import iso8601
def parse_ts(ts):
return timegm(iso8601.parse_date(ts).utctimetuple())
except ImportError:
def parse_ts(ts):
try: (ts, tz), tz_mul = ts.rsplit('+', 1), 1
except ValueError: (ts, tz), tz_mul = ts.rsplit('-', 1), -1
ts_utc = datetime.strptime(ts, '%Y-%m-%dT%H:%M:%S')
ts_utc += timedelta(0, (int(tz[:2])*3600 + int(tz[2:])*60) * tz_mul)
return timegm(ts_utc.utctimetuple())
class CacheLRU(object):
CE = namedtuple('CachedException', 'err')
hits = misses = 0
def __init__(self, func, cache_exceptions=True, maxsize=100):
self.func, self.maxsize, self.ce = func, maxsize, cache_exceptions
self.cache = OrderedDict()
@staticmethod
def _key(argz, kwz):
return kwz.pop('_cache_key', False)\
or (argz + tuple(sorted(kwz.viewitems())))
def __call__(self, *argz, **kwz):
key = self._key(argz, kwz)
try:
res = self.cache.pop(key)
self.hits += 1
except KeyError:
try: res = self.func(*argz, **kwz)
except Exception as err:
if not self.ce: raise
res = self.CE(err)
self.misses += 1
while len(self.cache) >= self.maxsize:
self.cache.popitem(0)
self.cache[key] = res
if isinstance(res, self.CE): raise res.err
else: return res
def purge(self, *argz, **kwz):
self.cache.pop(self._key(argz, kwz), None)
class PerPathCache(CacheLRU):
@staticmethod
def _key(argz, kwz): return argz[0]
def __call__(self, path):
return super(PerPathCache, self).__call__(path)
def purge(self, path):
return super(PerPathCache, self).purge(path)
class OneDriveFS(fuse.LoggingMixIn, fuse.Operations):
def __init__( self,
api, root='me/skydrive', log=None,
api_cache=True, api_cache_scale=1.0 ):
self.api, self.root = api, root
self.log = log or logging.getLogger(
'{}.{}'.format(__name__, self.__class__.__name__) )
if api_cache and api_cache_scale > 0:
self._resolve = PerPathCache(
self._resolve_path, maxsize=int(200 * api_cache_scale) )
self._info = PerPathCache(
self.api.info, maxsize=int(100 * api_cache_scale) )
self._listdir = PerPathCache(
self.api.listdir, maxsize=int(50 * api_cache_scale) )
else:
self._resolve, self._info, self._listdir =\
self._resolve_path, self.api.info, self.api.listdir
self.created = set()
def _resolve_path(self, path):
return self.api.resolve_path(path, root_id=self.root)
def _purge(self, *entries, **kwz):
caches = kwz.keys() or ['info', 'listdir']
for entry in entries:
if isinstance(entry, types.StringTypes): ec = caches
else: entry, ec = entry
if isinstance(ec, types.StringTypes): ec = [ec]
for cache in ec:
# print('PURGE', entry, cache)
op.attrgetter('_{}.purge'.format(cache))(self)(entry)
def __call__(self, op, *args):
try: res = super(OneDriveFS, self).__call__(op, *args)
except api_v5.DoesNotExists:
raise fuse.FuseOSError(errno.ENOENT)
except api_v5.ProtocolError as err:
if err.code in [401, 403]: raise fuse.FuseOSError(errno.EACCES)
elif err.code == 404: raise fuse.FuseOSError(errno.ENOENT)
raise
return 0 if res is None else res
def _isdir(self, obj_id):
return self._info(obj_id).get('type') in ['folder', 'album']
def getattr(self, path, fh=None):
obj = self._info(self._resolve(path))\
if path not in self.created else dict(type='file', size=0)
obj_mtime = obj.get('updated_time') or obj.get('created_time')
return dict( (k,v) for k,v in [
( 'st_mode', (stat.S_IFDIR | 0755)
if obj['type'] in ['folder', 'album'] else (stat.S_IFREG | 0644) ),
('st_mtime', obj_mtime and parse_ts(obj_mtime)),
('st_nlink', 2), ('st_size', obj.get('size')) ] if v is not None )
def access(self, path, mode):
self._resolve(path)
def readdir(self, path, fh):
return ['.', '..'] + map( op.itemgetter('name'),
self._listdir(self._resolve(path)) )
def statfs(self, path):
free, quota = self.api.get_quota()
return dict( f_bavail=quota-free, f_bfree=free,
f_blocks=quota, f_bsize=1, f_favail=-1, f_ffree=-1,
f_files=-1, f_flag=0, f_frsize=1, f_namemax=256 )
def mkdir(self, path, mode):
name, parent = basename(path),\
self._resolve(dirname(path)) or self.root
self.api.mkdir(name, parent)
self._purge(parent)
def rename(self, old, new):
if old == new: return
new_dir, old_dir = dirname(new), dirname(old)
old_id = self._resolve(old)
if new_dir != old_dir:
self.api.move(old_id, self._resolve(new_dir))
self._purge(old_dir, (old, 'resolve'))
# If remote path with same name exists, delete it
try: self.unlink(path) # to raise EISDIR, if necessary
except api_v5.DoesNotExists: pass
self.api.info_update(old_id, dict(name=basename(new)))
self._purge(new_dir)
def rmdir(self, path):
path_id = self._resolve(path)
if not self._isdir(path_id):
raise fuse.FuseOSError(errno.ENOTDIR)
elif self._listdir(path_id):
raise fuse.FuseOSError(errno.ENOTEMPTY)
self.api.delete(path_id)
self._purge(dirname(path), (path, 'resolve'))
def unlink(self, path):
path_id = self._resolve(path)
if self._isdir(path_id):
raise fuse.FuseOSError(errno.EISDIR)
self.api.delete(path_id)
self.created.discard(path)
self._purge(dirname(path), (path, ['resolve', 'info']))
def read(self, path, size, offset, fh):
path_id = self._resolve(path)
return self.api.get(path_id, byte_range='{}-{}'.format(offset, offset+size))
def create(self, path, mode, fi=None):
if not self._isdir(self._resolve(dirname(path))):
raise fuse.FuseOSError(errno.ENOTDIR)
try: path_id = self._resolve(path)
except api_v5.DoesNotExists: pass
else:
if self._isdir(path_id):
raise fuse.FuseOSError(errno.EISDIR)
if path_id and mode & (os.O_CREAT and os.O_EXCL):
raise fuse.FuseOSError(errno.EEXIST)
self.created.add(path)
def write(self, path, data, offset, fh):
path_dir = dirname(path)
try: path_id = self._resolve(path)
except api_v5.DoesNotExists: path_id = None
# Create/modify file, upload it with tmp name
with NamedTemporaryFile() as tmp:
if path_id: tmp.write(self.api.get(path_id))
if offset: tmp.seek(offset)
tmp.write(data)
tmp.flush()
res = self.api.put( tmp.name,
self._resolve(path_dir), overwrite='ChooseNewName' )
# Set proper name
try:
if path_id: self.api.delete(path_id)
self.api.info_update(res['id'], dict(name=basename(path)))
finally:
self.created.discard(path)
# Always drop path_dir caches, just to be safe
self._purge((path, ['resolve', 'info', 'listdir']), (path_dir, ['info', 'listdir']))
return len(data)
def truncate(self, path, length, fh=None):
if path in self.created: return
path_id = self._resolve(path)
# Modify file, upload it with tmp name
with NamedTemporaryFile() as tmp:
tmp.write(self.api.get(path_id))
tmp.truncate(length)
res = self.api.put( tmp.name,
self._resolve(path_dir), overwrite='ChooseNewName' )
# Set proper name
try:
self.api.delete(path_id)
self.api.info_update(res['id'], dict(name=basename(path)))
finally:
# Always drop path_dir caches, just to be safe
self._purge((path, ['resolve', 'info']), (path_dir, ['info', 'listdir']))
def main():
opts_fs = dict(api_cache=True, api_cache_scale=1.0)
import argparse
parser = argparse.ArgumentParser(
description='Mount OneDrive as a FUSE filesystem.')
parser.add_argument('config',
metavar='config_path[:onedrive_path]',
nargs='?', default=conf.ConfigMixin.conf_path_default,
help='Writable configuration state-file (yaml).'
' Used to store authorization_code, access and refresh tokens.'
' Should initially contain at least something like "{client: {id: xxx, secret: yyy}}".'
' Path might be appended to it after colon to make only that folder accessible.'
' Default: %(default)s')
parser.add_argument('mountpoint', metavar='path', help='Path to mount OneDrive to.')
parser.add_argument('-o', '--mount-options',
help=''.join([ 'Comma-separated list of mount options.',
' Use "someflag=no", or "someflag=false" to disable binary flags.',
' OneDriveFS options: {};'\
.format( ', '.join('{} (default: {})'.format(k, v)
for k,v in sorted(opts_fs.viewitems())) ),
' the rest is passed to fuse.',
' See "man mount.fuse" for the list of fuse-specific options.' ]))
parser.add_argument('-f', '--foreground', action='store_true',
help='Dont fork into background after mount succeeds.')
parser.add_argument('--debug', action='store_true',
help='Verbose operation mode. Implies --foreground.')
optz = parser.parse_args()
log = logging.getLogger()
logging.basicConfig(level=logging.WARNING
if not optz.debug else logging.DEBUG)
opts_fuse = dict(foreground=optz.foreground or optz.debug)
if optz.mount_options:
for opt in optz.mount_options.strip().strip(',').split(','):
if '=' in opt: opt, val = opt.split('=', 1)
# elif opt.startswith('no_'): opt, val = opt[3:], False
else: val = True
if opt in ['api_cache']: # binary flags
if isinstance(val, types.StringTypes):
if val.lower() in ['yes', 'true', 'y']: val = True
elif val.lower() in ['no', 'false', 'n']: val = False
else: parser.error('Unrecognized flag ({!r}) value: {!r}'.format(opt, val))
opts_fs[opt] = val
elif opt in ['api_cache_scale']: # numeric options
opts_fs[opt] = float(val)
elif opt in []: # strings
opts_fs[opt] = val
else: opts_fuse[opt] = val
log.debug('FS options: {}; FUSE: {}'.format(opts_fs, opts_fuse))
if ':' in optz.config:
optz.config, onedrive_path = optz.config.split(':', 1)
if not optz.config: optz.config = conf.ConfigMixin.conf_path_default
else: onedrive_path = None
api = api_v5.PersistentOneDriveAPI.from_conf(optz.config)
if not onedrive_path or not re.search(
r'^(file|folder)\.[0-9a-f]{16}\.[0-9A-F]{16}!\d+'
r'|folder\.[0-9a-f]{16}$', onedrive_path ):
onedrive_path = api.resolve_path(onedrive_path or 'me/skydrive')
fuse.FUSE(
OneDriveFS(api, onedrive_path, log=log, **opts_fs),
optz.mountpoint, **opts_fuse )
if __name__ == '__main__': main()