OLD | NEW |
---|---|
1 # Copyright 2013 The Chromium Authors. All rights reserved. | 1 # Copyright 2013 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 from file_system import FileSystem, FileNotFoundError, StatInfo | 5 from file_system import FileSystem, FileNotFoundError, StatInfo |
6 from future import Future | 6 from future import Future |
7 | 7 |
8 | 8 |
9 def MoveTo(base, obj): | 9 def MoveTo(base, obj): |
10 '''Returns an object as |obj| moved to |base|. That is, | 10 '''Returns an object as |obj| moved to |base|. That is, |
(...skipping 10 matching lines...) Expand all Loading... | |
21 | 21 |
22 def MoveAllTo(base, obj): | 22 def MoveAllTo(base, obj): |
23 '''Moves every value in |obj| to |base|. See MoveTo. | 23 '''Moves every value in |obj| to |base|. See MoveTo. |
24 ''' | 24 ''' |
25 result = {} | 25 result = {} |
26 for key, value in obj.iteritems(): | 26 for key, value in obj.iteritems(): |
27 result[key] = MoveTo(base, value) | 27 result[key] = MoveTo(base, value) |
28 return result | 28 return result |
29 | 29 |
30 | 30 |
31 def _List(file_system): | |
32 '''Returns a list of '/' separated paths derived from |file_system|. | |
33 For example, {'index.html': '', 'www': {'file.txt': ''}} would return | |
34 ['index.html', 'www/file.txt']. | |
35 ''' | |
36 assert isinstance(file_system, dict) | |
37 result = {} | |
38 def update_result(item, path): | |
39 if isinstance(item, dict): | |
40 if path != '': | |
41 path += '/' | |
42 file_listing = [p if isinstance(content, basestring) else (p + '/') | |
43 for p, content in item.iteritems()] | |
44 # Support non-canonical paths until the server has purged all | |
Yoyo Zhou
2014/02/05 00:32:33
Sounds like there's a TODO in here.
not at google - send to devlin
2014/02/05 03:55:53
Done.
| |
45 # non-canonical paths. | |
46 result[path], result['/' + path] = file_listing, file_listing | |
47 for subpath, subitem in item.iteritems(): | |
48 update_result(subitem, path + subpath) | |
49 elif isinstance(item, basestring): | |
50 result[path] = item | |
51 # See above comment. | |
52 result['/' + path] = result[path] | |
Yoyo Zhou
2014/02/05 00:32:33
nit: this is written differently from the above
not at google - send to devlin
2014/02/05 03:55:53
Done.
| |
53 else: | |
54 raise ValueError('Unsupported item type: %s' % type(item)) | |
55 update_result(file_system, '') | |
56 return result | |
57 | |
58 | |
59 class _StatTracker(object): | |
60 '''Maintains the versions of paths in a file system. The versions of files | |
61 are changed either by |Increment| or |SetVersion|. The versions of | |
62 directories are derived from the versions of files within it. | |
63 ''' | |
64 | |
65 def __init__(self): | |
66 self._path_stats = {} | |
67 self._global_stat = 0 | |
68 | |
69 def Increment(self, path=None, by=1): | |
70 if path is None: | |
71 self._global_stat += by | |
72 else: | |
73 self.SetVersion(path, self._path_stats.get(path, 0) + by) | |
74 | |
75 def SetVersion(self, path, new_version): | |
76 if path == '' or path.endswith('/'): | |
Yoyo Zhou
2014/02/05 00:32:33
path_util.IsDirectory
not at google - send to devlin
2014/02/05 03:55:53
Ah! Yeah so I did this and in fact it mooted the T
| |
77 raise ValueError('Only files have an incrementable stat, ' | |
78 'but "%s" is a directory' % path) | |
79 | |
80 # Update version of that file. | |
81 self._path_stats[path] = new_version | |
82 | |
83 # Update all parent directory versions as well. | |
84 slash_index = 0 # (deliberately including '' in the dir paths) | |
85 while slash_index != -1: | |
86 dir_path = path[:slash_index] + '/' | |
87 self._path_stats[dir_path] = max(self._path_stats.get(dir_path, 0), | |
88 new_version) | |
89 if dir_path == '/': | |
90 # Legacy support for '/' being the root of the file system rather | |
91 # than ''. Eventually when the path normalisation logic is complete | |
92 # this will be impossible and this logic will change slightly. | |
93 self._path_stats[''] = self._path_stats['/'] | |
94 slash_index = path.find('/', slash_index + 1) | |
95 | |
96 def GetVersion(self, path): | |
97 return self._global_stat + self._path_stats.get(path, 0) | |
98 | |
99 | |
31 class TestFileSystem(FileSystem): | 100 class TestFileSystem(FileSystem): |
32 '''A FileSystem backed by an object. Create with an object representing file | 101 '''A FileSystem backed by an object. Create with an object representing file |
33 paths such that {'a': {'b': 'hello'}} will resolve Read('a/b') as 'hello', | 102 paths such that {'a': {'b': 'hello'}} will resolve Read('a/b') as 'hello', |
34 Read('a/') as ['b'], and Stat determined by a value incremented via | 103 Read('a/') as ['b'], and Stat determined by a value incremented via |
35 IncrementStat. | 104 IncrementStat. |
36 ''' | 105 ''' |
37 | 106 |
38 def __init__(self, obj, relative_to=None, identity=None): | 107 def __init__(self, obj, relative_to=None, identity=None): |
39 assert obj is not None | 108 assert obj is not None |
40 self._obj = obj if relative_to is None else MoveTo(relative_to, obj) | 109 if relative_to is not None: |
110 obj = MoveTo(relative_to, obj) | |
41 self._identity = identity or type(self).__name__ | 111 self._identity = identity or type(self).__name__ |
42 self._path_stats = {} | 112 self._path_values = _List(obj) |
43 self._global_stat = 0 | 113 self._stat_tracker = _StatTracker() |
44 | 114 |
45 # | 115 # |
46 # FileSystem implementation. | 116 # FileSystem implementation. |
47 # | 117 # |
48 | 118 |
49 def Read(self, paths): | 119 def Read(self, paths): |
50 test_fs = self | 120 for path in paths: |
51 class Delegate(object): | 121 if path not in self._path_values: |
52 def Get(self): | 122 return FileNotFoundError.RaiseInFuture(path) |
53 return dict((path, test_fs._ResolvePath(path)) for path in paths) | 123 return Future(value=dict((k, v) for k, v in self._path_values.iteritems() |
54 return Future(delegate=Delegate()) | 124 if k in paths)) |
55 | 125 |
56 def Refresh(self): | 126 def Refresh(self): |
57 return Future(value=()) | 127 return Future(value=()) |
58 | 128 |
59 def _ResolvePath(self, path): | |
60 def Resolve(parts): | |
61 '''Resolves |parts| of a path info |self._obj|. | |
62 ''' | |
63 result = self._obj.get(parts[0]) | |
64 for part in parts[1:]: | |
65 if not isinstance(result, dict): | |
66 raise FileNotFoundError( | |
67 '%s at %s did not resolve to a dict, instead %s' % | |
68 (path, part, result)) | |
69 result = result.get(part) | |
70 return result | |
71 | |
72 def GetPaths(obj): | |
73 '''Lists the paths within |obj|; this is basially keys() but with | |
74 directory paths (i.e. dicts) with a trailing /. | |
75 ''' | |
76 def ToPath(k, v): | |
77 if isinstance(v, basestring): | |
78 return k | |
79 if isinstance(v, dict): | |
80 return '%s/' % k | |
81 raise ValueError('Cannot convert type %s to path', type(v)) | |
82 return [ToPath(k, v) for k, v in obj.items()] | |
83 | |
84 path = path.lstrip('/') | |
85 | |
86 if path == '': | |
87 return GetPaths(self._obj) | |
88 | |
89 parts = path.split('/') | |
90 if parts[-1] != '': | |
91 file_contents = Resolve(parts) | |
92 if not isinstance(file_contents, basestring): | |
93 raise FileNotFoundError( | |
94 '%s (%s) did not resolve to a string, instead %s' % | |
95 (path, parts, file_contents)) | |
96 return file_contents | |
97 | |
98 dir_contents = Resolve(parts[:-1]) | |
99 if not isinstance(dir_contents, dict): | |
100 raise FileNotFoundError( | |
101 '%s (%s) did not resolve to a dict, instead %s' % | |
102 (path, parts, dir_contents)) | |
103 | |
104 return GetPaths(dir_contents) | |
105 | |
106 def Stat(self, path): | 129 def Stat(self, path): |
107 read_result = self.Read([path]).Get().get(path) | 130 read_result = self.ReadSingle(path).Get() |
108 stat_result = StatInfo(self._SinglePathStat(path)) | 131 stat_result = StatInfo(str(self._stat_tracker.GetVersion(path))) |
109 if isinstance(read_result, list): | 132 if isinstance(read_result, list): |
110 stat_result.child_versions = dict( | 133 stat_result.child_versions = dict( |
111 (file_result, self._SinglePathStat('%s%s' % (path, file_result))) | 134 (file_result, |
135 str(self._stat_tracker.GetVersion('%s%s' % (path, file_result)))) | |
112 for file_result in read_result) | 136 for file_result in read_result) |
113 return stat_result | 137 return stat_result |
114 | 138 |
115 def _SinglePathStat(self, path): | |
116 return str(self._global_stat + self._path_stats.get(path, 0)) | |
117 | |
118 # | 139 # |
119 # Testing methods. | 140 # Testing methods. |
120 # | 141 # |
121 | 142 |
122 def IncrementStat(self, path=None, by=1): | 143 def IncrementStat(self, path=None, by=1): |
123 if path is not None: | 144 self._stat_tracker.Increment(path, by=by) |
124 self._path_stats[path] = self._path_stats.get(path, 0) + by | |
125 else: | |
126 self._global_stat += by | |
127 | 145 |
128 def GetIdentity(self): | 146 def GetIdentity(self): |
129 return self._identity | 147 return self._identity |
OLD | NEW |