예제 #1
0
        /// <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);
        }
예제 #2
0
        /// <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);
        }
예제 #5
0
        /// <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();
            }
        }