patched_file_system.py revision a93a17c8d99d686bd4a1511e5504e5e6cc9fcadf
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 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, host_file_system, patcher):
42    self._host_file_system = host_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      raise FileNotFoundError('Files are removed from the patch.')
50    patched_files |= (set(added) | set(modified))
51    dir_paths = {path for path in paths if path.endswith('/')}
52    file_paths = set(paths) - dir_paths
53    patched_paths = file_paths & patched_files
54    unpatched_paths = file_paths - patched_files
55    return Future(delegate=_AsyncFetchFuture(
56        self._host_file_system.Read(unpatched_paths, binary),
57        self._patcher.Apply(patched_paths, self._host_file_system, binary),
58        self._TryReadDirectory(dir_paths, binary),
59        self))
60
61  ''' Given the list of patched files, it's not possible to determine whether
62  a directory to read exists in self._host_file_system. So try reading each one
63  and handle FileNotFoundError.
64  '''
65  def _TryReadDirectory(self, paths, binary):
66    value = {}
67    for path in paths:
68      assert path.endswith('/')
69      try:
70        value[path] = self._host_file_system.ReadSingle(path, binary)
71      except FileNotFoundError:
72        value[path] = None
73    return value
74
75  def _GetDirectoryListingFromPatch(self, path):
76    assert path.endswith('/')
77    def _FindChildrenInPath(files, path):
78      result = []
79      for f in files:
80        if f.startswith(path):
81          child_path = f[len(path):]
82          if '/' in child_path:
83            child_name = child_path[0:child_path.find('/') + 1]
84          else:
85            child_name = child_path
86          result.append(child_name)
87      return result
88
89    added, deleted, modified = (tuple(
90        _FindChildrenInPath(files, path)
91        for files in self._patcher.GetPatchedFiles()))
92
93    # A patch applies to files only. It cannot delete directories.
94    deleted_files = [child for child in deleted if not child.endswith('/')]
95    # However, these directories are actually modified because their children
96    # are patched.
97    modified += [child for child in deleted if child.endswith('/')]
98
99    return (added, deleted_files, modified)
100
101  def _PatchStat(self, stat_info, version, added, deleted, modified):
102    assert len(added) + len(deleted) + len(modified) > 0
103    assert stat_info.child_versions is not None
104
105    # Deep copy before patching to make sure it doesn't interfere with values
106    # cached in memory.
107    stat_info = deepcopy(stat_info)
108
109    stat_info.version = version
110    for child in added + modified:
111      stat_info.child_versions[child] = version
112    for child in deleted:
113      if stat_info.child_versions.get(child):
114        del stat_info.child_versions[child]
115
116    return stat_info
117
118  def Stat(self, path):
119    version = self._patcher.GetVersion()
120    assert version is not None
121    version = 'patched_%s' % version
122
123    directory, filename = path.rsplit('/', 1)
124    added, deleted, modified = self._GetDirectoryListingFromPatch(
125        directory + '/')
126
127    if len(added) > 0:
128      # There are new files added. It's possible (if |directory| is new) that
129      # self._host_file_system.Stat will throw an exception.
130      try:
131        stat_info = self._PatchStat(
132            self._host_file_system.Stat(directory + '/'),
133            version,
134            added,
135            deleted,
136            modified)
137      except FileNotFoundError:
138        stat_info = StatInfo(version, {child: version
139                                  for child in added + modified})
140    elif len(deleted) + len(modified) > 0:
141      # No files were added.
142      stat_info = self._PatchStat(self._host_file_system.Stat(directory + '/'),
143                                  version,
144                                  added,
145                                  deleted,
146                                  modified)
147    else:
148      # No changes are made in this directory.
149      return self._host_file_system.Stat(path)
150
151    if stat_info.child_versions is not None:
152      if filename:
153        if filename in stat_info.child_versions:
154          stat_info = StatInfo(stat_info.child_versions[filename])
155        else:
156          raise FileNotFoundError('%s was not in child versions' % filename)
157    return stat_info
158