/// <summary> /// Attempts to load a <see cref="DnaConfiguration"/> from a set of configuration files /// The order of priority in the list is first is least priority, last is highest. /// /// Any values coming after will override the previous values, to create a final /// combined <see cref="DnaConfiguration"/> /// </summary> /// <param name="filePaths">A list of all paths to the configuration files</param> /// <param name="currentConfiguration">The current configuration to merge the settings with</param> /// <param name="defaultConfigurationIndex">If specified, it will treat the file path at the index as the default configuration, and so use the environments current directory for relative paths</param> /// <param name="currentFolder">The current folder used to resolve relative paths if needed</param> /// <param name="globalSettingsOnly">If true, only deal with/merge global settings from the files. For example extracting any LiveServers information</param> public static DnaConfiguration LoadFromFiles(string[] filePaths, string currentFolder, DnaConfiguration currentConfiguration = null, int defaultConfigurationIndex = -1, bool globalSettingsOnly = false) { // Create final setting as default var finalSetting = new DnaConfiguration(); // Copy current settings if they exist if (currentConfiguration != null) { finalSetting = JsonConvert.DeserializeObject <DnaConfiguration>(JsonConvert.SerializeObject(currentConfiguration)); } // For each file for (var i = 0; i < filePaths.Length; i++) { // Get file path var filePath = filePaths[i]; // Default configuration uses current directory as relative path source var configFolder = i == defaultConfigurationIndex ? Environment.CurrentDirectory : Path.GetDirectoryName(filePath); // Try and load the settings var settings = LoadFromFile(filePath); // TODO: Update to use reflection on properties and set the finalSettings // if the settings properties are not null // Make sure we got settings if (settings == null) { continue; } // Merge the settings MergeSettings(settings, finalSetting, filePath, configFolder, globalSettingsOnly); } // If output path is not specified, set it to the callers file path if (string.IsNullOrEmpty(finalSetting.OutputPath)) { finalSetting.OutputPath = currentFolder; } // Return the result return(finalSetting); }
/// <summary> /// Merges the current settings into the final settings if the values are present (not null or not empty strings) /// </summary> /// <param name="currentSettings">The settings loaded from the current configuration file</param> /// <param name="finalSettings">The final merged configuration settings</param> /// <param name="configurationFilePath">The file path to the current configuration settings (for logging purposes)</param> /// <param name="currentPath">The current folder that any string path values should be resolved to absolute paths from</param> /// <param name="globalSettingsOnly">If true, only deal with/merge global settings from the files. For example extracting any LiveServers information</param> private static void MergeSettings(DnaConfiguration currentSettings, DnaConfiguration finalSettings, string configurationFilePath, string currentPath, bool globalSettingsOnly) { CoreLogger.Log($"{(globalSettingsOnly ? "Global" : "")} Configuration: {configurationFilePath}"); // If this is not a global load, then load these folder specific settings if (!globalSettingsOnly) { // Monitor Path TryGetSetting(() => currentSettings.MonitorPath, () => finalSettings.MonitorPath, resolvePath: true, currentPath: currentPath); // Generate On Start TryGetSetting(() => currentSettings.GenerateOnStart, () => finalSettings.GenerateOnStart); // Process And Close TryGetSetting(() => currentSettings.ProcessAndClose, () => finalSettings.ProcessAndClose); // Log Level TryGetSetting(() => currentSettings.LogLevel, () => finalSettings.LogLevel); // Output Path TryGetSetting(() => currentSettings.OutputPath, () => finalSettings.OutputPath, resolvePath: true, currentPath: currentPath); // Scss Output Style TryGetSetting(() => currentSettings.ScssOutputStyle, () => finalSettings.ScssOutputStyle, resolvePath: true, currentPath: currentPath); // Scss Generate Source Map TryGetSetting(() => currentSettings.ScssGenerateSourceMaps, () => finalSettings.ScssGenerateSourceMaps, resolvePath: true, currentPath: currentPath); } // Open Vs Code TryGetSetting(() => currentSettings.OpenVsCode, () => finalSettings.OpenVsCode, resolvePath: true, currentPath: currentPath); // Static Folders // TODO: Improve this and automate it with attributes or something on the properties... // For now manually resolve them currentSettings.StaticFolders?.ForEach(staticFolder => { staticFolder.SourceFolder = ResolveFullPath(currentPath, staticFolder.SourceFolder, true, out bool wasRelative); staticFolder.DestinationFolder = ResolveFullPath(currentPath, staticFolder.DestinationFolder, true, out wasRelative); }); TryGetSettingList(() => currentSettings.StaticFolders, () => finalSettings.StaticFolders, resolvePath: true, currentPath: currentPath, logDetails: (item) => $"{item.SourceFolder} > {item.DestinationFolder}"); // Live Server Directories TryGetSettingList(() => currentSettings.LiveServerDirectories, () => finalSettings.LiveServerDirectories, resolvePath: true, currentPath: currentPath); // Live Data Sources TryGetSettingList( () => currentSettings.LiveDataSources, () => finalSettings.LiveDataSources, resolvePath: true, currentPath: currentPath, logDetails: (item) => item.ConfigurationFileSource ); // Cache Path TryGetSetting(() => currentSettings.CachePath, () => finalSettings.CachePath, resolvePath: true, currentPath: currentPath); // Space between each configuration details for console log niceness CoreLogger.Log(""); }
/// <summary> /// Downloads and extracts any files from the provided Live Data Sources /// </summary> /// <param name="sourceConfigurations">The list of sources</param> /// <param name="force">Forces installing any download versions regardless of what version exists in the cache</param> /// <returns></returns> public void DownloadSourcesAsync(List <DnaConfigurationLiveDataSource> sourceConfigurations, bool force = false) { // Flag if we end up downloading anything var somethingDownloaded = false; // Log it CoreLogger.LogInformation("Updating Live Data Sources..."); if (sourceConfigurations != null) { // Keep track of sources we add in this loop var addedConfigurations = new List <LiveDataSource>(); // Loop each source provided... foreach (var sourceConfiguration in sourceConfigurations) { CoreLogger.Log($"LiveData: Processing source {sourceConfiguration.ConfigurationFileSource}..."); var liveDataSource = new LiveDataSource(); #region Get Source Configuration // Is source a web link? var isWeb = sourceConfiguration.ConfigurationFileSource.ToLower().StartsWith("http"); // Is source a local configuration file var isLocal = sourceConfiguration.ConfigurationFileSource.ToLower().EndsWith(DnaSettings.LiveDataConfigurationFileName.ToLower()); // Is source folder? (used directly, not downloaded to cache folder) // Detected by being a folder link that inside that folder exists a dna.live.config file var isDirectSource = !isWeb && !isLocal && File.Exists(Path.Combine(sourceConfiguration.ConfigurationFileSource, DnaSettings.LiveDataConfigurationFileName)); // If this is a web source... if (isWeb) { #region Download Configuration File // Download its information var informationString = WebHelpers.DownloadString(sourceConfiguration.ConfigurationFileSource); // If it is null, it failed if (string.IsNullOrEmpty(informationString)) { // Log it CoreLogger.Log($"LiveData: Failed to download configuration {sourceConfiguration.ConfigurationFileSource}", type: LogType.Warning); // Stop continue; } #endregion #region Deserialize // Try to deserialize the Json try { liveDataSource = JsonConvert.DeserializeObject <LiveDataSource>(informationString); } catch (Exception ex) { // If we failed, log it CoreLogger.Log($"LiveData: Failed to deserialize configuration {sourceConfiguration.ConfigurationFileSource}. {ex.Message}", type: LogType.Warning); // Stop continue; } #endregion } // If it ends with dna.live.config and the local file exists else if (isLocal) { if (!File.Exists(sourceConfiguration.ConfigurationFileSource)) { // Log it CoreLogger.Log($"LiveData: Local configuration file not found {sourceConfiguration.ConfigurationFileSource}", type: LogType.Warning); // Stop continue; } #region Read Configuration File // Read its information var informationString = File.ReadAllText(sourceConfiguration.ConfigurationFileSource); // If it is null, it failed if (string.IsNullOrEmpty(informationString)) { // Log it CoreLogger.Log($"LiveData: Failed to read local configuration {sourceConfiguration.ConfigurationFileSource}", type: LogType.Warning); // Stop continue; } #endregion #region Deserialize // Try to deserialize the Json try { liveDataSource = JsonConvert.DeserializeObject <LiveDataSource>(informationString); } catch (Exception ex) { // If we failed, log it CoreLogger.Log($"LiveData: Failed to deserialize configuration {sourceConfiguration.ConfigurationFileSource}. {ex.Message}", type: LogType.Warning); // Stop continue; } #endregion } // Otherwise... else { // If it is a folder that exists and contains the dna.live.config file // specifying it this way means it should be treated as a direct access // local file (not copied to the cache folder) // // So, ignore it for this step either way but if it doesn't contain // a configuration file, warn it is an unknown source if (!isDirectSource) { // Log it CoreLogger.Log($"LiveData: Unknown source type {sourceConfiguration.ConfigurationFileSource}", type: LogType.Warning); } else { // Log it CoreLogger.Log($"LiveData: Skipping local source folder (will be used directly not copied to cache) {sourceConfiguration.ConfigurationFileSource}"); } // Stop either way continue; } #endregion #region Newer Version Check // If we are forcing an update ignore this step if (!force) { // Check if we have a newer version... var newerVersion = Sources.FirstOrDefault(localSource => // Has the same name... localSource.Name.EqualsIgnoreCase(liveDataSource.Name) && // And a higher version localSource.Version >= liveDataSource.Version); if (newerVersion != null) { // Log it CoreLogger.Log($"LiveData: Skipping download as same or newer version exists {newerVersion.Name} ({newerVersion.CachedFilePath})"); // Stop continue; } } #endregion #region Delete Old Versions // Find any older version and delete it var olderVersions = Sources.Where(localSource => // Has the same name... localSource.Name.EqualsIgnoreCase(liveDataSource.Name) && // And a lower version (or we are forcing an update) (force || localSource.Version < liveDataSource.Version)).ToList(); // If we got any lower versions... if (olderVersions?.Count > 0) { // Loop each older version... foreach (var olderVersion in olderVersions) { try { // Try and delete the folder Directory.Delete(olderVersion.CachedFilePath, true); } catch (Exception ex) { // Log it CoreLogger.Log($"LiveData: Failed to delete older version {olderVersion.CachedFilePath}. {ex.Message}", type: LogType.Warning); // Stop continue; } } } #endregion #region Download Source var zipFile = isWeb ? // If Web: New unique filename to download to FileHelpers.GetUnusedPath(Path.Combine(CachePath, $"{liveDataSource.Name}.zip")) : // Otherwise source should point to zip file relative to current path DnaConfiguration.ResolveFullPath(Path.GetDirectoryName(sourceConfiguration.ConfigurationFileSource), liveDataSource.Source, true, out bool wasRelative); if (isWeb) { // If Url is relative... if (!liveDataSource.Source.Contains("://")) { // Get URL folder var urlFolder = sourceConfiguration.ConfigurationFileSource.Substring(0, sourceConfiguration.ConfigurationFileSource.LastIndexOf('/')); // Prepend the current sources path liveDataSource.Source = $"{urlFolder}/{liveDataSource.Source}"; } // Now attempt to download the source zip file CoreLogger.Log($"LiveData: Downloading source contents... {liveDataSource.Source}"); // Download to folder var downloaded = WebHelpers.DownloadFile(liveDataSource.Source, zipFile); // If it failed to download... if (!downloaded) { // Log it CoreLogger.Log($"LiveData: Failed to download source file {liveDataSource.Source}", type: LogType.Warning); // Stop continue; } } else { // Make sure zip exists if (!File.Exists(zipFile)) { // Log it CoreLogger.Log($"LiveData: Local source zip file does not exist {zipFile}", type: LogType.Warning); // Stop continue; } } // Get unused folder to extract to var saveFolder = FileHelpers.GetUnusedPath(Path.Combine(CachePath, liveDataSource.Name)); #endregion // Flag if we succeeded so the local sources get refreshed after we are done somethingDownloaded = true; // Whatever happens now, fail or succeed, we should clean up the downloaded zip try { #region Extract Source // Try and extract the zip var unzipSuccessful = ZipHelpers.Unzip(zipFile, saveFolder); if (!unzipSuccessful) { // Log it CoreLogger.Log($"LiveData: Failed to unzip downloaded file {zipFile}", type: LogType.Warning); // Clean up folder try { // If save folder exists... if (Directory.Exists(saveFolder)) { // Delete it Directory.Delete(saveFolder, true); } } catch (Exception ex) { // Log it CoreLogger.Log($"LiveData: Failed to delete failed extraction folder {saveFolder}. {ex.Message}", type: LogType.Warning); } // Stop continue; } #endregion #region Verify Valid Configuration // Verify the zip has valid dna.live.config file in and it successfully parses // Get expected configuration path var configFilePath = Path.Combine(saveFolder, DnaSettings.LiveDataConfigurationFileName); // Flag if it is a valid source var validSource = true; // If the file does not exist or it fails to parse if (!File.Exists(configFilePath)) { // Log it CoreLogger.Log($"LiveData: Live Data configuration file missing {configFilePath}.", type: LogType.Warning); // Flag it validSource = false; } else { // Try and parse the file try { // Try and parse var result = JsonConvert.DeserializeObject <LiveDataSource>(File.ReadAllText(configFilePath)); #region Already Added Check // Make sure we don't already have this name if (addedConfigurations.Any(source => source.Name.EqualsIgnoreCase(result.Name))) { // Log it CoreLogger.Log($"LiveData: Ignoring source as another exists with same name {result.Name}. {result.CachedFilePath}", type: LogType.Warning); // Flag it validSource = false; } #endregion // If it is a valid source... if (validSource) { // Add to already added list addedConfigurations.Add(result); // Log successful install CoreLogger.Log($"Installed new Live Data Source {result.Name} v{result.Version}, from {sourceConfiguration.ConfigurationFileSource}", type: LogType.Success); } } catch (Exception ex) { // Log it CoreLogger.Log($"LiveData: Failed to parse Live Data configuration file {configFilePath}. {ex.Message}", type: LogType.Error); // Flag it validSource = false; } } // If it is not a valid file... if (!validSource) { // Log it CoreLogger.Log($"LiveData: Cleaning invalid source folder {saveFolder}.", type: LogType.Warning); // Delete source folder DeleteSource(saveFolder); } #endregion } finally { // If it was a downloaded file... if (isWeb) { // Log it CoreLogger.Log($"LiveData: Cleaning up downloaded file {zipFile}"); try { // Try and delete it File.Delete(zipFile); } catch (Exception ex) { // Log it CoreLogger.Log($"LiveData: Failed to delete downloaded file {zipFile}. {ex.Message}", type: LogType.Error); } } } } } // Rescan if we downloaded anything if (somethingDownloaded) { // Refresh local sources RefreshLocalSources(sourceConfigurations); } CoreLogger.Log($"LiveData: Finished downloading sources"); }
/// <summary> /// Finds the first occurrence Sass @import statement in the file contents /// </summary> /// <param name="fileContents">The path of the file to look in</param> /// <param name="fileContents">The contents of the file</param> /// <param name="match">The <see cref="Match"/> that found the include statement</param> /// <param name="includePaths">The include path(s) found</param> /// <returns></returns> protected override bool GetIncludeTag(string filePath, string fileContents, ref Match match, out List <string> includePaths) { // Blank list to start with includePaths = new List <string>(); // Find any of the following: // // @import "x"; // @import "_x"; // @import "x.scss"; // @import "_x.scss"; // @import "../x.scss"; // @import "x", "y", "z"; // // Also match all of the above replacing " with ' // // Partial _ in filename // ======================== // If import excludes _ at the start of the name, add it and then // if no file is found with an _ then resort to looking for one without the _ // // If both are found, only the file with the _ is used // // Try and find match of @import ... ; match = Regex.Match(fileContents, mSassImportLineRegex, RegexOptions.Multiline); // Make sure we have enough groups if (match.Groups.Count < 2) { return(false); } // Get the area between @import and ; for example // @import "a"; "a" // @import "a", "b"; "a", "b" var innerImport = match.Groups[1].Value.Trim(); // Make sure it starts and ends with a " or ' to ignore things like CSS @import url()... var normalizedInnerImport = innerImport.Replace("'", "\""); if (!(normalizedInnerImport.StartsWith("\"") && normalizedInnerImport.EndsWith("\""))) { return(false); } // Now get the values between the comma's and quotes var innerMatches = Regex.Matches(innerImport, mSassImportSplitRegex, RegexOptions.Singleline); // For each match... foreach (Match innerMatch in innerMatches) { // Make sure we have enough groups if (innerMatch.Groups.Count < 2) { continue; } // Get include path value var includePath = innerMatch.Groups[1].Value; // Add extension if not added if (!includePath.EndsWith(ScssExtension)) { includePath += ScssExtension; } // Resolve any relative aspects of the path includePath = DnaConfiguration.ResolveFullPath(Path.GetDirectoryName(filePath), includePath, false, out bool wasRelative); // Sass rules (from testing other Sass compilers) show that if an include doesn't start with an underscore // but both the underscore file and file without an underscore exist (such as a.scss and _a.scss) // then the _ file will be the one that get's included // // So check for that if (!Path.GetFileName(includePath).StartsWith("_")) { var underscoredPath = Path.Combine(Path.GetDirectoryName(includePath), $"_{Path.GetFileName(includePath)}"); if (File.Exists(underscoredPath)) { includePath = underscoredPath; } } // Add this path includePaths.Add(includePath); } // Return successful return(true); }
/// <summary> /// Processes the HTTP request /// </summary> /// <param name="context">The Http Context</param> private void Process(HttpListenerContext context) { // Log it CoreLogger.Log($"LiveServer Processing request {context.Request.Url.OriginalString}..."); // Get the URL information after the hostname // i.e. http://localhost:8080/ would be / // http://localhost:8080/some/path would be /some/path var url = context.Request.Url.AbsolutePath; // Get query string var query = context.Request.Url.Query; // If this is a request for the auto-reload script... if (query.EqualsIgnoreCase(AutoReloadRequestQueryUrl)) { // Serve the Javascript script ServeString(AutoReloadJavascript, MimeTypes.GetExtension("file.js"), context); // Done return; } // If this is a request to return once there are changes... if (query.EqualsIgnoreCase(SignalNewContentQuery)) { // Pass off this request to simply return successful once it get's told there is a file change HangUntilFileChange(context); return; } else { // If the URL is just / (root) if (string.IsNullOrWhiteSpace(url) || url == "/") { // Look for index by default url = "index"; } // Otherwise... else { // Remove leading slash url = url.Substring(1); } // Now look in the watch directory for a file with this name... var filePath = DnaConfiguration.ResolveFullPath(ServingDirectory, url, false, out bool wasRelative); // If this file exists... if (File.Exists(filePath)) { // Serve it ServeFile(filePath, context); // Done return; } // If the file has no extension, try adding .htm if (!Path.HasExtension(filePath) && File.Exists(filePath + ".htm")) { // Serve it ServeFile(filePath + ".htm", context); // Done return; } // If the file has no extension, try adding .html if (!Path.HasExtension(filePath) && File.Exists(filePath + ".html")) { // Serve it ServeFile(filePath + ".html", context); // Done return; } // Let client know the file is not found context.Response.StatusCode = (int)HttpStatusCode.NotFound; // Close the response context.Response.OutputStream.Close(); } }