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
3 # found in the LICENSE file.
5 from copy
import deepcopy
7 from file_system
import FileSystem
, StatInfo
, FileNotFoundError
8 from future
import Gettable
, Future
10 class _AsyncFetchFuture(object):
12 unpatched_files_future
,
16 self
._unpatched
_files
_future
= unpatched_files_future
17 self
._patched
_files
_future
= patched_files_future
18 self
._dirs
_value
= dirs_value
19 self
._patched
_file
_system
= patched_file_system
22 files
= self
._unpatched
_files
_future
.Get()
23 files
.update(self
._patched
_files
_future
.Get())
25 dict((path
, self
._PatchDirectoryListing
(path
, self
._dirs
_value
[path
]))
26 for path
in self
._dirs
_value
))
29 def _PatchDirectoryListing(self
, path
, original_listing
):
30 added
, deleted
, modified
= (
31 self
._patched
_file
_system
._GetDirectoryListingFromPatch
(path
))
32 if original_listing
is None:
34 raise FileNotFoundError('Directory %s not found in the patch.' % path
)
36 return list((set(original_listing
) |
set(added
)) - set(deleted
))
38 class PatchedFileSystem(FileSystem
):
39 ''' Class to fetch resources with a patch applied.
41 def __init__(self
, base_file_system
, patcher
):
42 self
._base
_file
_system
= base_file_system
43 self
._patcher
= patcher
45 def Read(self
, paths
):
47 added
, deleted
, modified
= self
._patcher
.GetPatchedFiles()
48 if set(paths
) & set(deleted
):
49 def raise_file_not_found():
50 raise FileNotFoundError('Files are removed from the patch.')
51 return Future(delegate
=Gettable(raise_file_not_found
))
52 patched_files |
= (set(added
) |
set(modified
))
53 dir_paths
= set(path
for path
in paths
if path
.endswith('/'))
54 file_paths
= set(paths
) - dir_paths
55 patched_paths
= file_paths
& patched_files
56 unpatched_paths
= file_paths
- patched_files
57 return Future(delegate
=_AsyncFetchFuture(
58 self
._base
_file
_system
.Read(unpatched_paths
),
59 self
._patcher
.Apply(patched_paths
, self
._base
_file
_system
),
60 self
._TryReadDirectory
(dir_paths
),
64 return self
._base
_file
_system
.Refresh()
66 ''' Given the list of patched files, it's not possible to determine whether
67 a directory to read exists in self._base_file_system. So try reading each one
68 and handle FileNotFoundError.
70 def _TryReadDirectory(self
, paths
):
73 assert path
.endswith('/')
75 value
[path
] = self
._base
_file
_system
.ReadSingle(path
).Get()
76 except FileNotFoundError
:
80 def _GetDirectoryListingFromPatch(self
, path
):
81 assert path
.endswith('/')
82 def _FindChildrenInPath(files
, path
):
85 if f
.startswith(path
):
86 child_path
= f
[len(path
):]
88 child_name
= child_path
[0:child_path
.find('/') + 1]
90 child_name
= child_path
91 result
.append(child_name
)
94 added
, deleted
, modified
= (tuple(
95 _FindChildrenInPath(files
, path
)
96 for files
in self
._patcher
.GetPatchedFiles()))
98 # A patch applies to files only. It cannot delete directories.
99 deleted_files
= [child
for child
in deleted
if not child
.endswith('/')]
100 # However, these directories are actually modified because their children
102 modified
+= [child
for child
in deleted
if child
.endswith('/')]
104 return (added
, deleted_files
, modified
)
106 def _PatchStat(self
, stat_info
, version
, added
, deleted
, modified
):
107 assert len(added
) + len(deleted
) + len(modified
) > 0
108 assert stat_info
.child_versions
is not None
110 # Deep copy before patching to make sure it doesn't interfere with values
112 stat_info
= deepcopy(stat_info
)
114 stat_info
.version
= version
115 for child
in added
+ modified
:
116 stat_info
.child_versions
[child
] = version
117 for child
in deleted
:
118 if stat_info
.child_versions
.get(child
):
119 del stat_info
.child_versions
[child
]
123 def Stat(self
, path
):
124 version
= self
._patcher
.GetVersion()
125 assert version
is not None
126 version
= 'patched_%s' % version
128 directory
, filename
= path
.rsplit('/', 1)
129 added
, deleted
, modified
= self
._GetDirectoryListingFromPatch
(
133 # There are new files added. It's possible (if |directory| is new) that
134 # self._base_file_system.Stat will throw an exception.
136 stat_info
= self
._PatchStat
(
137 self
._base
_file
_system
.Stat(directory
+ '/'),
142 except FileNotFoundError
:
143 stat_info
= StatInfo(
145 dict((child
, version
) for child
in added
+ modified
))
146 elif len(deleted
) + len(modified
) > 0:
147 # No files were added.
148 stat_info
= self
._PatchStat
(self
._base
_file
_system
.Stat(directory
+ '/'),
154 # No changes are made in this directory.
155 return self
._base
_file
_system
.Stat(path
)
157 if stat_info
.child_versions
is not None:
159 if filename
in stat_info
.child_versions
:
160 stat_info
= StatInfo(stat_info
.child_versions
[filename
])
162 raise FileNotFoundError('%s was not in child versions' % filename
)
165 def GetIdentity(self
):
166 return '%s(%s,%s)' % (self
.__class
__.__name
__,
167 self
._base
_file
_system
.GetIdentity(),
168 self
._patcher
.GetIdentity())