patched_file_system.py revision f2477e01787aa58f445919b809d89e252beef54f
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.
4
5from copy import deepcopy
6
7from file_system import FileSystem, StatInfo, FileNotFoundError
8from future import Gettable, Future
9
10class _AsyncFetchFuture(object):
11  def __init__(self,
12               unpatched_files_future,
13               patched_files_future,
14               dirs_value,
15               patched_file_system):
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
20
21  def Get(self):
22    files = self._unpatched_files_future.Get()
23    files.update(self._patched_files_future.Get())
24    files.update(
25        dict((path, self._PatchDirectoryListing(path, self._dirs_value[path]))
26             for path in self._dirs_value))
27    return files
28
29  def _PatchDirectoryListing(self, path, original_listing):
30    added, deleted, modified = (
31        self._patched_file_system._GetDirectoryListingFromPatch(path))
32    if original_listing is None:
33      if len(added) == 0:
34        raise FileNotFoundError('Directory %s not found in the patch.' % path)
35      return added
36    return list((set(original_listing) | set(added)) - set(deleted))
37
38class PatchedFileSystem(FileSystem):
39  ''' Class to fetch resources with a patch applied.
40  '''
41  def __init__(self, base_file_system, patcher):
42    self._base_file_system = base_file_system
43    self._patcher = patcher
44
45  def Read(self, paths, binary=False):
46    patched_files = set()
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, binary),
59        self._patcher.Apply(patched_paths, self._base_file_system, binary),
60        self._TryReadDirectory(dir_paths, binary),
61        self))
62
63  def Refresh(self):
64    return self._base_file_system.Refresh()
65
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.
69  '''
70  def _TryReadDirectory(self, paths, binary):
71    value = {}
72    for path in paths:
73      assert path.endswith('/')
74      try:
75        value[path] = self._base_file_system.ReadSingle(path, binary).Get()
76      except FileNotFoundError:
77        value[path] = None
78    return value
79
80  def _GetDirectoryListingFromPatch(self, path):
81    assert path.endswith('/')
82    def _FindChildrenInPath(files, path):
83      result = []
84      for f in files:
85        if f.startswith(path):
86          child_path = f[len(path):]
87          if '/' in child_path:
88            child_name = child_path[0:child_path.find('/') + 1]
89          else:
90            child_name = child_path
91          result.append(child_name)
92      return result
93
94    added, deleted, modified = (tuple(
95        _FindChildrenInPath(files, path)
96        for files in self._patcher.GetPatchedFiles()))
97
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
101    # are patched.
102    modified += [child for child in deleted if child.endswith('/')]
103
104    return (added, deleted_files, modified)
105
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
109
110    # Deep copy before patching to make sure it doesn't interfere with values
111    # cached in memory.
112    stat_info = deepcopy(stat_info)
113
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]
120
121    return stat_info
122
123  def Stat(self, path):
124    version = self._patcher.GetVersion()
125    assert version is not None
126    version = 'patched_%s' % version
127
128    directory, filename = path.rsplit('/', 1)
129    added, deleted, modified = self._GetDirectoryListingFromPatch(
130        directory + '/')
131
132    if len(added) > 0:
133      # There are new files added. It's possible (if |directory| is new) that
134      # self._base_file_system.Stat will throw an exception.
135      try:
136        stat_info = self._PatchStat(
137            self._base_file_system.Stat(directory + '/'),
138            version,
139            added,
140            deleted,
141            modified)
142      except FileNotFoundError:
143        stat_info = StatInfo(
144            version,
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 + '/'),
149                                  version,
150                                  added,
151                                  deleted,
152                                  modified)
153    else:
154      # No changes are made in this directory.
155      return self._base_file_system.Stat(path)
156
157    if stat_info.child_versions is not None:
158      if filename:
159        if filename in stat_info.child_versions:
160          stat_info = StatInfo(stat_info.child_versions[filename])
161        else:
162          raise FileNotFoundError('%s was not in child versions' % filename)
163    return stat_info
164
165  def GetIdentity(self):
166    return '%s(%s,%s)' % (self.__class__.__name__,
167                          self._base_file_system.GetIdentity(),
168                          self._patcher.GetIdentity())
169