/// <summary> /// This is only exposed for unit testing. This will throw an exception if unable to generate the content, it will never return null or a blank string. /// </summary> public static string GetFileContentRepresentation(TextFileContents contents, long millisecondsTakenToGenerate) { if (contents == null) { throw new ArgumentNullException("contents"); } if (millisecondsTakenToGenerate < 0) { throw new ArgumentOutOfRangeException("millisecondsTakenToGenerate", "may not be a negative value"); } var contentBuilder = new StringBuilder(); contentBuilder.AppendFormat( "/*{0}:{1}:{2}:{3}ms*/{4}", contents.RelativePath.Length.ToString( new string('0', int.MaxValue.ToString().Length) // Pad out the length to the number of digits required to display int.MaxValue ), contents.RelativePath, contents.LastModified.ToString(LastModifiedDateFormat), Math.Min(millisecondsTakenToGenerate, 99999).ToString("00000"), Environment.NewLine ); contentBuilder.Append(contents.Content); return(contentBuilder.ToString()); }
/// <summary> /// This will never return null, it will throw an exception for a null or empty relativePath - it is up to the particular implementation whether or not to throw /// an exception for invalid / inaccessible filenames (if no exception is thrown, the issue should be logged). It is up the the implementation to handle mapping /// the relative path to a full file path. /// </summary> public TextFileContents Load(string relativePath) { if (string.IsNullOrWhiteSpace(relativePath)) { throw new ArgumentException("Null/blank relativePath specified"); } var file = new FileInfo(_relativePathMapper.MapPath(relativePath)); if (!file.Exists) { throw new ArgumentException("Requested file does not exist: " + relativePath); } try { var fileContents = new TextFileContents( relativePath, file.LastWriteTime, File.ReadAllText(file.FullName) ); return(fileContents); } catch (Exception e) { throw new ArgumentException("Unable to load requested file: " + relativePath, e); } }
private TextFileContents RemoveCSSComments(TextFileContents content) { if (content == null) { throw new ArgumentNullException("content"); } return(new TextFileContents( content.RelativePath, content.LastModified, CSSCommentRemover.Replace(content.Content + "/**/", "") // Ensure that any unclosed comments are handled )); }
/// <summary> /// This will never return null, it will throw an exception for a null or empty relativePath - it is up to the particular implementation whether or not to throw /// an exception for invalid / inaccessible filenames (if no exception is thrown, the issue should be logged). It is up the the implementation to handle mapping /// the relative path to a full file path. /// </summary> public TextFileContents Load(string relativePath) { if (string.IsNullOrWhiteSpace(relativePath)) { throw new ArgumentException("Null/blank relativePath specified"); } // Try to retrieve cached data var lastModifiedDateOfSource = _lastModifiedDateRetriever.GetLastModifiedDate(relativePath); var cacheFile = _cacheFileLocationRetriever(relativePath); cacheFile.Refresh(); if (cacheFile.Exists) { try { using (var stream = cacheFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read)) { using (var reader = new StreamReader(stream)) { var cachedData = GetFileContents(reader); if (cachedData.LastModified >= lastModifiedDateOfSource) { return(cachedData); } } } } catch (Exception e) { _logger.LogIgnoringAnyError(LogLevel.Error, () => "DiskCachingTextFileLoader.Load: Error loading content - " + e.Message); if (e is InvalidCacheFileFormatException) { if (_invalidContentBehaviour == InvalidContentBehaviourOptions.Delete) { try { cacheFile.Delete(); } catch (Exception invalidFileContentDeleteException) { _logger.LogIgnoringAnyError( LogLevel.Warning, () => "DiskCachingTextFileLoader.Add: Unable to delete cache file with invalid contents - " + invalidFileContentDeleteException.Message, invalidFileContentDeleteException ); if (_errorBehaviour == ErrorBehaviourOptions.LogAndRaiseException) { throw; } } } } else { if (_errorBehaviour == ErrorBehaviourOptions.LogAndRaiseException) { throw; } } } } // Do the work and cache the result var timer = new Stopwatch(); timer.Start(); var content = _contentLoader.Load(relativePath); timer.Stop(); try { // 2015-12-22 DWR: The contentLoader may have a sensible-seeming LastModified-retrieval mechanism of take-largest-last-modified-date-from-the-source- // files but we want to ignore that value if it is less recent than the value that the lastModifiedDateRetriever reference gave us, otherwise the // cached data will be useless. It's easier to explain with an example - if Style1.css, Style11.css, Style12.css and Style2.css are all in the same // folder and Style1.css imports Style11.css and Style12.css while Style11.css, Style12.css and Style.css import nothing, the disk-cache data for // Style1 will be useless if Style2 is updated and the lastModifiedDateRetriever is a SingleFolderLastModifiedDateRetriever. This is because the // lastModifiedDateRetriever will return the last-modified date of Style2, which will always be more recent than any of Style1, Style11, Style12 // last-modified values but the last-modified of the flattened-Style1.css content will only consider the last-modified dates of the three files // that it pulls data from. It is not as accurate to claim that the flattened-Style1.css content has the last-modified date of Style2 but it's // the only way to get consistent dates, which are required for the caching to work effectively. if (lastModifiedDateOfSource > content.LastModified) { content = new TextFileContents(content.RelativePath, lastModifiedDateOfSource, content.Content); } File.WriteAllText( cacheFile.FullName, GetFileContentRepresentation(content, timer.ElapsedMilliseconds) ); } catch (Exception e) { _logger.LogIgnoringAnyError(LogLevel.Warning, () => "DiskCachingTextFileLoader.Add: Error writing file - " + e.Message, e); if (_errorBehaviour == ErrorBehaviourOptions.LogAndRaiseException) { throw; } } return(content); }
/// <summary> /// This will return a TextFileContents instance creating by removing all comments and flattening all of the import declarations in a stylesheet - nested /// imports are handled recursively. Only imports in the same folder are supported - the imports may not have relative or absolute paths specified, nor /// may they be external urls - breaking these conditions will result in an UnsupportedStylesheetImportException being raised. If there are any circular /// references defined by the imports, a CircularStylesheetImportException will be raised. The LastModified value on the returned data will be the most /// recent date taken from all of the considered files. /// </summary> private TextFileContents GetCombinedContent(string relativePath, IEnumerable <string> importChain) { if (string.IsNullOrWhiteSpace(relativePath)) { throw new ArgumentException("Null/blank relativePath specified"); } if (importChain == null) { throw new ArgumentNullException("importChain"); } var importChainArray = importChain.ToList(); if (importChainArray.Any(i => string.IsNullOrWhiteSpace(i))) { throw new ArgumentException("Null/blank entry encountered in importChain set"); } var combinedContentFile = _contentLoader.Load(relativePath); if (_contentLoaderCommentRemovalBehaviour == ContentLoaderCommentRemovalBehaviourOptions.ContentIsUnprocessed) { combinedContentFile = RemoveCSSComments(combinedContentFile); } foreach (var importDeclaration in GetImportDeclarations(combinedContentFile.Content)) { if (importDeclaration == null) { throw new Exception("Null reference encountered in data returned from GetImportDeclarations"); } // Ensure that the imported stylesheet is not a relative or absolute path or an external url var removeImport = false; if (importDeclaration.RelativePath.Contains("\\") || importDeclaration.RelativePath.Contains("/")) { if (_unsupportedImportBehaviour == ErrorBehaviourOptions.LogAndContinue) { _logger.LogIgnoringAnyError(LogLevel.Warning, () => "Unsupported import specified: " + importDeclaration.RelativePath + " (it has been removed)"); removeImport = true; } else { _logger.LogIgnoringAnyError(LogLevel.Warning, () => "Unsupported import specified: " + importDeclaration.RelativePath); throw new UnsupportedStylesheetImportException("Imported stylesheets may not specify relative or absolute paths nor external urls: " + importDeclaration.RelativePath); } } // If the original file has a relative path (eg. "styles/Test1.css") then we'll need to include that path in the import filename (eg. "Test2.css" // must be transformed for "styles/Test2.css") otherwise the circular reference detection won't work and the file won't be loaded from the right // location when GetCombinedContent is called recursively for the import. We can use this approach to include the path on the import filename // since we are only supporting imports in the same location as the containing stylesheet (see above; relative or absolute paths are not // allowed in imports) StylesheetImportDeclaration importDeclarationWithConsistentFilename; var breakPoint = relativePath.LastIndexOfAny(new[] { '\\', '/' }); if (breakPoint == -1) { importDeclarationWithConsistentFilename = importDeclaration; } else { importDeclarationWithConsistentFilename = new StylesheetImportDeclaration( importDeclaration.Declaration, relativePath.Substring(0, breakPoint + 1) + importDeclaration.RelativePath, importDeclaration.MediaOverride ); } // Ensure that the requested stylesheet has not been requested further up the chain - if so, throw a CircularStylesheetImportException rather than // waiting for a StackOverflowException to occur (or log a warning and remove the import, depending upon specified behaviour options) if (importChainArray.Any(f => f.Equals(importDeclarationWithConsistentFilename.RelativePath, StringComparison.InvariantCultureIgnoreCase))) { if (_circularReferenceImportBehaviour == ErrorBehaviourOptions.LogAndContinue) { _logger.LogIgnoringAnyError( LogLevel.Warning, () => string.Format( "Circular import encountered: {0} (it has been removed from {1})", importDeclarationWithConsistentFilename.RelativePath, relativePath ) ); removeImport = true; } else { throw new CircularStylesheetImportException("Circular stylesheet import detected for file: " + importDeclarationWithConsistentFilename.RelativePath); } } // Retrieve the content from imported file, wrap it in a media query if required and replace the import declaration with the content TextFileContents importedFileContent; if (removeImport) { // If we want to ignore this import (meaning it's invalid and DifferentFolderImportBehaviourOptions is to log and proceed instead of throw an // exception) then we just want to replace the dodgy import with blank content importedFileContent = new TextFileContents(importDeclarationWithConsistentFilename.RelativePath, DateTime.MinValue, ""); } else { importedFileContent = GetCombinedContent( importDeclarationWithConsistentFilename.RelativePath, importChainArray.Concat(new[] { combinedContentFile.RelativePath }) ); } if ((importDeclarationWithConsistentFilename.MediaOverride != null) && !removeImport) // Don't bother wrapping an import that will be ignored in any media query content { importedFileContent = new TextFileContents( importedFileContent.RelativePath, importedFileContent.LastModified, String.Format( "@media {0} {{{1}{2}{1}}}{1}", importDeclarationWithConsistentFilename.MediaOverride, Environment.NewLine, importedFileContent.Content ) ); } combinedContentFile = new TextFileContents( combinedContentFile.RelativePath, combinedContentFile.LastModified > importedFileContent.LastModified ? combinedContentFile.LastModified : importedFileContent.LastModified, combinedContentFile.Content.Replace( importDeclarationWithConsistentFilename.Declaration, importedFileContent.Content ) ); } return(combinedContentFile); }