193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik# Copyright (c) 2014 The Chromium Authors. All rights reserved. 293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik# Use of this source code is governed by a BSD-style license that can be 393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik# found in the LICENSE file. 4b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik 593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik"""ResourceFinder is a helper class for finding resources given their name.""" 693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 7b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craikimport codecs 893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craikimport os 9b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik 1093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craikfrom tvcm import module 1193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craikfrom tvcm import style_sheet as style_sheet_module 1293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craikfrom tvcm import resource as resource_module 1393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craikfrom tvcm import html_module 1493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craikfrom tvcm import strip_js_comments 1593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 16b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik 1793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craikclass ResourceLoader(object): 1893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik """Manges loading modules and their dependencies from files. 1993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 2093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik Modules handle parsing and the construction of their individual dependency 2193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik pointers. The loader deals with bookkeeping of what has been loaded, and 2293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik mapping names to file resources. 2393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik """ 2493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik def __init__(self, project): 2593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik self.project = project 2693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik self.stripped_js_by_filename = {} 2793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik self.loaded_modules = {} 2893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik self.loaded_raw_scripts = {} 2993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik self.loaded_style_sheets = {} 3093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik self.loaded_images = {} 3193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 3293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik @property 3393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik def source_paths(self): 3493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik """A list of base directories to search for modules under.""" 3593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return self.project.source_paths 3693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 37f516a629824f0019315b9841dc2df76dc7862ddeChris Craik def FindResource(self, some_path, binary=False): 3893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik """Finds a Resource for the given path. 3993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 4093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik Args: 4193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik some_path: A relative or absolute path to a file. 4293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 4393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik Returns: 4493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik A Resource or None. 4593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik """ 4693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if os.path.isabs(some_path): 47f516a629824f0019315b9841dc2df76dc7862ddeChris Craik return self.FindResourceGivenAbsolutePath(some_path, binary) 4893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik else: 49f516a629824f0019315b9841dc2df76dc7862ddeChris Craik return self.FindResourceGivenRelativePath(some_path, binary) 5093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 51f516a629824f0019315b9841dc2df76dc7862ddeChris Craik def FindResourceGivenAbsolutePath(self, absolute_path, binary=False): 5293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik """Returns a Resource for the given absolute path.""" 5393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik candidate_paths = [] 5493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik for source_path in self.source_paths: 5593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if absolute_path.startswith(source_path): 5693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik candidate_paths.append(source_path) 5793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if len(candidate_paths) == 0: 5893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return None 5993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 6093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik # Sort by length. Longest match wins. 6193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik candidate_paths.sort(lambda x, y: len(x) - len(y)) 6293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik longest_candidate = candidate_paths[-1] 63f516a629824f0019315b9841dc2df76dc7862ddeChris Craik return resource_module.Resource(longest_candidate, absolute_path, binary) 6493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 65f516a629824f0019315b9841dc2df76dc7862ddeChris Craik def FindResourceGivenRelativePath(self, relative_path, binary=False): 6693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik """Returns a Resource for the given relative path.""" 6793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik absolute_path = None 6893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik for script_path in self.source_paths: 6993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik absolute_path = os.path.join(script_path, relative_path) 7093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if os.path.exists(absolute_path): 71f516a629824f0019315b9841dc2df76dc7862ddeChris Craik return resource_module.Resource(script_path, absolute_path, binary) 7293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return None 7393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 74b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik def _FindResourceGivenNameAndSuffix( 75b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik self, requested_name, extension, return_resource=False): 7693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik """Searches for a file and reads its contents. 7793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 7893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik Args: 7993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik requested_name: The name of the resource that was requested. 8093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik extension: The extension for this requested resource. 8193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 8293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik Returns: 8393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik A (path, contents) pair. 8493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik """ 8593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik pathy_name = requested_name.replace('.', os.sep) 8693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik filename = pathy_name + extension 8793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 8893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik resource = self.FindResourceGivenRelativePath(filename) 8993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if return_resource: 9093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return resource 9193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if not resource: 9293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return None, None 9393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return _read_file(resource.absolute_path) 9493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 9593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik def FindModuleResource(self, requested_module_name): 9693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik """Finds a module javascript file and returns a Resource, or none.""" 97b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik js_resource = self._FindResourceGivenNameAndSuffix( 98b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik requested_module_name, '.js', return_resource=True) 99b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik html_resource = self._FindResourceGivenNameAndSuffix( 100b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik requested_module_name, '.html', return_resource=True) 10193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if js_resource and html_resource: 102b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik if html_module.IsHTMLResourceTheModuleGivenConflictingResourceNames( 103b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik js_resource, html_resource): 10493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return html_resource 10593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return js_resource 10693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik elif js_resource: 10793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return js_resource 10893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return html_resource 10993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 11093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik def LoadModule(self, module_name=None, module_filename=None): 111b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik assert bool(module_name) ^ bool(module_filename), ( 112b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik 'Must provide either module_name or module_filename.') 11393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if module_filename: 11493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik resource = self.FindResource(module_filename) 11593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if not resource: 11693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik raise Exception('Could not find %s in %s' % ( 11793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik module_filename, repr(self.source_paths))) 11893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik module_name = resource.name 11993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik else: 120b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik resource = None # Will be set if we end up needing to load. 12193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 12293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if module_name in self.loaded_modules: 12393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik assert self.loaded_modules[module_name].contents 12493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return self.loaded_modules[module_name] 12593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 126b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik if not resource: # happens when module_name was given 12793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik resource = self.FindModuleResource(module_name) 12893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if not resource: 12993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik raise module.DepsException('No resource for module "%s"' % module_name) 13093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 13193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik m = html_module.HTMLModule(self, module_name, resource) 13293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik self.loaded_modules[module_name] = m 13393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 134f516a629824f0019315b9841dc2df76dc7862ddeChris Craik # Fake it, this is probably either polymer.min.js or platform.js which are 13593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik # actually .js files.... 13693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if resource.absolute_path.endswith('.js'): 13793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return m 13893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 13993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik m.Parse() 14093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik m.Load() 14193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return m 14293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 14393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik def LoadRawScript(self, relative_raw_script_path): 14493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik resource = None 14593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik for source_path in self.source_paths: 146b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik possible_absolute_path = os.path.join( 147b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik source_path, os.path.normpath(relative_raw_script_path)) 14893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if os.path.exists(possible_absolute_path): 14993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik resource = resource_module.Resource(source_path, possible_absolute_path) 15093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik break 15193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if not resource: 152b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik raise module.DepsException( 153b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik 'Could not find a file for raw script %s in %s' % 154b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik (relative_raw_script_path, self.source_paths)) 155b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik assert relative_raw_script_path == resource.unix_style_relative_path, ( 156b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik 'Expected %s == %s' % (relative_raw_script_path, 157b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik resource.unix_style_relative_path)) 15893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 15993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if resource.absolute_path in self.loaded_raw_scripts: 16093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return self.loaded_raw_scripts[resource.absolute_path] 16193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 16293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik raw_script = module.RawScript(resource) 16393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik self.loaded_raw_scripts[resource.absolute_path] = raw_script 16493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return raw_script 16593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 16693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik def LoadStyleSheet(self, name): 16793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if name in self.loaded_style_sheets: 16893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return self.loaded_style_sheets[name] 16993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 170b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik resource = self._FindResourceGivenNameAndSuffix( 171b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik name, '.css', return_resource=True) 17293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if not resource: 173b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik raise module.DepsException( 174b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik 'Could not find a file for stylesheet %s' % name) 17593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 17693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik style_sheet = style_sheet_module.StyleSheet(self, name, resource) 17793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik style_sheet.load() 17893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik self.loaded_style_sheets[name] = style_sheet 17993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return style_sheet 18093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 18193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik def LoadImage(self, abs_path): 18293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if abs_path in self.loaded_images: 18393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return self.loaded_images[abs_path] 18493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 18593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if not os.path.exists(abs_path): 186b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik raise module.DepsException("url('%s') did not exist" % abs_path) 18793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 188b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik res = self.FindResourceGivenAbsolutePath(abs_path, binary=True) 189b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik if res is None: 190b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik raise module.DepsException("url('%s') was not in search path" % abs_path) 19193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 19293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik image = style_sheet_module.Image(res) 19393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik self.loaded_images[abs_path] = image 19493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return image 19593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 19693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik def GetStrippedJSForFilename(self, filename, early_out_if_no_tvcm): 19793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if filename in self.stripped_js_by_filename: 19893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return self.stripped_js_by_filename[filename] 19993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 20093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik with open(filename, 'r') as f: 20193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik contents = f.read(4096) 20293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if early_out_if_no_tvcm and ('tvcm' not in contents): 20393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return None 20493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 20593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik s = strip_js_comments.StripJSComments(contents) 20693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik self.stripped_js_by_filename[filename] = s 20793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return s 20893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 20993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 21093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craikdef _read_file(absolute_path): 21193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik """Reads a file and returns a (path, contents) pair. 21293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 21393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik Args: 21493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik absolute_path: Absolute path to a file. 21593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik 21693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik Raises: 21793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik Exception: The given file doesn't exist. 21893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik IOError: There was a problem opening or reading the file. 21993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik """ 22093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik if not os.path.exists(absolute_path): 22193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik raise Exception('%s not found.' % absolute_path) 222f516a629824f0019315b9841dc2df76dc7862ddeChris Craik f = codecs.open(absolute_path, mode='r', encoding='utf-8') 22393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik contents = f.read() 22493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik f.close() 22593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik return absolute_path, contents 226