diff --git a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/DisasterRecoveryFileValidator.cs b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/DisasterRecoveryFileValidator.cs new file mode 100644 index 00000000..f9cb1663 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/DisasterRecoveryFileValidator.cs @@ -0,0 +1,162 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Data; +using System.Data.Common; +using System.Data.SqlClient; +using System.Globalization; +using System.IO; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection; +using Microsoft.SqlTools.ServiceLayer.FileBrowser; + +namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery +{ + /// + /// Validate selected file paths for backup/restore operations + /// + public static class DisasterRecoveryFileValidator + { + internal const string LocalSqlServer = "(local)"; + internal const string LocalMachineName = "."; + + public static bool ValidatePaths(FileBrowserValidateEventArgs args, out string errorMessage) + { + errorMessage = string.Empty; + bool result = true; + SqlConnection connection = null; + + if (args != null) + { + ConnectionInfo connInfo; + ConnectionService.Instance.TryFindConnection(args.OwnerUri, out connInfo); + if (connInfo != null) + { + DbConnection dbConnection = null; + connInfo.TryGetConnection(Connection.ConnectionType.Default, out dbConnection); + if (dbConnection != null) + { + connection = ReliableConnectionHelper.GetAsSqlConnection(dbConnection); + } + } + + if (connection != null) + { + bool isLocal = false; + if (string.Compare(GetMachineName(connection.DataSource), Environment.MachineName, StringComparison.OrdinalIgnoreCase) == 0) + { + isLocal = true; + } + + foreach (string filePath in args.FilePaths) + { + bool isFolder; + bool existing = IsPathExisting(connection, filePath, out isFolder); + + if (existing) + { + if (isFolder) + { + errorMessage = string.Format(SR.BackupPathIsFolderError, filePath); + result = false; + break; + } + } + else + { + // If the file path doesn't exist, check if the folder exists + string folderPath = PathWrapper.GetDirectoryName(filePath); + if (isLocal) + { + if (!string.IsNullOrEmpty(folderPath) && !Directory.Exists(folderPath)) + { + errorMessage = string.Format(SR.InvalidBackupPathError, folderPath); + result = false; + break; + } + } + else + { + bool isFolderOnRemote; + bool existsOnRemote = IsPathExisting(connection, folderPath, out isFolderOnRemote); + if (!existsOnRemote) + { + errorMessage = string.Format(SR.InvalidBackupPathError, folderPath); + result = false; + break; + } + } + } + } + } + else + { + result = false; + } + } + else + { + result = false; + } + + return result; + } + + #region private methods + + internal static bool IsPathExisting(SqlConnection connection, string path, out bool isFolder) + { + Request req = new Request + { + Urn = "Server/File[@FullName='" + Urn.EscapeString(path) + "']", + Fields = new[] { "IsFile" } + }; + + Enumerator en = new Enumerator(); + bool isExisting = false; + isFolder = false; + + using (DataSet ds = en.Process(connection, req)) + { + if (FileBrowserBase.IsValidDataSet(ds)) + { + isFolder = !(Convert.ToBoolean(ds.Tables[0].Rows[0]["IsFile"], CultureInfo.InvariantCulture)); + isExisting = true; + } + } + + return isExisting; + } + + internal static string GetMachineName(string sqlServerName) + { + string machineName = string.Empty; + if (sqlServerName != null) + { + string serverName = sqlServerName.ToLowerInvariant().Trim(); + if ((serverName == LocalSqlServer) || (serverName == LocalMachineName)) + { + machineName = System.Environment.MachineName; + } + else + { + machineName = sqlServerName; + if (sqlServerName.Trim().Length != 0) + { + // [0] = machine, [1] = instance + return sqlServerName.Split('\\')[0]; + } + } + } + + return machineName; + } + + #endregion + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/DisasterRecoveryService.cs b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/DisasterRecoveryService.cs index b33df78f..4bae7958 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/DisasterRecoveryService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/DisasterRecoveryService.cs @@ -9,12 +9,11 @@ using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Admin; using Microsoft.SqlTools.ServiceLayer.Admin.Contracts; using Microsoft.SqlTools.ServiceLayer.Connection; -using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts; -using Microsoft.SqlTools.ServiceLayer.TaskServices; -using System.Threading; -using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation; -using System.Globalization; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts; +using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation; +using Microsoft.SqlTools.ServiceLayer.FileBrowser; +using Microsoft.SqlTools.ServiceLayer.TaskServices; namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery { @@ -26,6 +25,7 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery private static readonly Lazy instance = new Lazy(() => new DisasterRecoveryService()); private static ConnectionService connectionService = null; private SqlTaskManager sqlTaskManagerInstance = null; + private FileBrowserService fileBrowserService = null; private RestoreDatabaseHelper restoreDatabaseService = new RestoreDatabaseHelper(); /// @@ -78,6 +78,25 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery } } + /// + /// Gets or sets the current filebrowser service instance + /// + internal FileBrowserService FileBrowserServiceInstance + { + get + { + if (fileBrowserService == null) + { + fileBrowserService = FileBrowserService.Instance; + } + return fileBrowserService; + } + set + { + fileBrowserService = value; + } + } + /// /// Initializes the service instance /// @@ -89,14 +108,18 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery // Create backup serviceHost.SetRequestHandler(BackupRequest.Type, HandleBackupRequest); - // Create respore task + // Create restore task serviceHost.SetRequestHandler(RestoreRequest.Type, HandleRestoreRequest); - // Create respore plan + // Create restore plan serviceHost.SetRequestHandler(RestorePlanRequest.Type, HandleRestorePlanRequest); - // Create respore config + // Create restore config serviceHost.SetRequestHandler(RestoreConfigInfoRequest.Type, HandleRestoreConfigInfoRequest); + + // Register file path validation callbacks + FileBrowserServiceInstance.RegisterValidatePathsCallback(FileValidationServiceConstants.Backup, DisasterRecoveryFileValidator.ValidatePaths); + FileBrowserServiceInstance.RegisterValidatePathsCallback(FileValidationServiceConstants.Restore, DisasterRecoveryFileValidator.ValidatePaths); } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserCloseRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserCloseRequest.cs new file mode 100644 index 00000000..28dd3d55 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserCloseRequest.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.FileBrowser.Contracts +{ + /// + /// Parameters to pass to close file browser + /// + public class FileBrowserCloseParams + { + /// + /// Connection uri + /// + public string OwnerUri; + } + + /// + /// Response for closing the browser + /// + public class FileBrowserCloseResponse + { + /// + /// Result of the operation + /// + public bool Succeeded; + + /// + /// Error message if any + /// + public string Message; + } + + /// + /// Requst to close the file browser + /// + class FileBrowserCloseRequest + { + public static readonly + RequestType Type = + RequestType.Create("filebrowser/close"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserExpandCompleteNotification.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserExpandCompleteNotification.cs new file mode 100644 index 00000000..ce246831 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserExpandCompleteNotification.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.FileBrowser.Contracts +{ + /// + /// Event params for expanding a node + /// + public class FileBrowserExpandCompleteParams + { + /// + /// Expanded node + /// + public FileTreeNode ExpandedNode; + + /// + /// Result of the operation + /// + public bool Succeeded; + + /// + /// Error message if any + /// + public string Message; + } + + /// + /// Notification for expand completion + /// + public class FileBrowserExpandCompleteNotification + { + public static readonly + EventType Type = + EventType.Create("filebrowser/expandcomplete"); + } + +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserExpandRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserExpandRequest.cs new file mode 100644 index 00000000..5a90d981 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserExpandRequest.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.FileBrowser.Contracts +{ + /// + /// Parameters for expanding a folder node + /// + public class FileBrowserExpandParams + { + /// + /// Connection uri + /// + public string OwnerUri; + + /// + /// The path to expand the nodes for + /// + public string ExpandPath; + } + + /// + /// Request to expand a node in the file browser + /// + public class FileBrowserExpandRequest + { + public static readonly + RequestType Type = + RequestType.Create("filebrowser/expand"); + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserOpenCompleteNotification.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserOpenCompleteNotification.cs new file mode 100644 index 00000000..39854c1d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserOpenCompleteNotification.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.FileBrowser.Contracts +{ + /// + /// Event params for opening a file browser + /// Returns full directory structure on the server side + /// + public class FileBrowserOpenCompleteParams + { + /// + /// Entire file/folder tree + /// + public FileTree FileTree; + + /// + /// Result of the operation + /// + public bool Succeeded; + + /// + /// Error message + /// + public string Message; + } + + /// + /// Notification for completing file browser opening + /// + public class FileBrowserOpenCompleteNotification + { + public static readonly + EventType Type = + EventType.Create("filebrowser/opencomplete"); + } + +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserOpenRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserOpenRequest.cs new file mode 100644 index 00000000..52cb2af5 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserOpenRequest.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.FileBrowser.Contracts +{ + /// + /// Parameters for opening file browser + /// + public class FileBrowserOpenParams + { + /// + /// Connection uri + /// + public string OwnerUri; + + /// + /// The initial path to expand the nodes for (e.g. Backup will set this path to default backup folder) + /// + public string ExpandPath; + + /// + /// File extension filter (e.g. *.bak) + /// + public string[] FileFilters; + } + + /// + /// Request to open a file browser + /// + public class FileBrowserOpenRequest + { + public static readonly + RequestType Type = + RequestType.Create("filebrowser/open"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserValidateCompleteNotification.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserValidateCompleteNotification.cs new file mode 100644 index 00000000..2e23cf43 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserValidateCompleteNotification.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.FileBrowser.Contracts +{ + /// + /// Event params for validation completion + /// + public class FileBrowserValidateCompleteParams + { + /// + /// Result of the operation + /// + public bool Succeeded; + + /// + /// Error message if any + /// + public string Message; + } + + /// + /// Notification for validation completion + /// + public class FileBrowserValidateCompleteNotification + { + public static readonly + EventType Type = + EventType.Create("filebrowser/validatecomplete"); + } + +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserValidateRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserValidateRequest.cs new file mode 100644 index 00000000..f10ec1bf --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileBrowserValidateRequest.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.FileBrowser.Contracts +{ + /// + /// Parameters for validating selected file paths + /// + public class FileBrowserValidateParams + { + /// + /// Connection uri + /// + public string OwnerUri; + + /// + /// Type of service that uses the file browser + /// + public string ServiceType; + + /// + /// Selected files + /// + public string[] SelectedFiles; + } + + /// + /// Requst to validate the selected file paths + /// + class FileBrowserValidateRequest + { + public static readonly + RequestType Type = + RequestType.Create("filebrowser/validate"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileTree.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileTree.cs new file mode 100644 index 00000000..72ac4f2e --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileTree.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.FileBrowser.Contracts +{ + /// + /// Tree to represent file/folder structure + /// + public class FileTree + { + /// + /// Root node of the tree + /// + public FileTreeNode RootNode { get; private set; } + + /// + /// Selected node of the tree + /// + public FileTreeNode SelectedNode { get; set; } + + /// + /// Constructor + /// + public FileTree() + { + this.RootNode = new FileTreeNode(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileTreeNode.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileTreeNode.cs new file mode 100644 index 00000000..da0d78d6 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/Contracts/FileTreeNode.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; + +namespace Microsoft.SqlTools.ServiceLayer.FileBrowser.Contracts +{ + /// + /// Tree node to represent file or folder + /// + public class FileTreeNode + { + /// + /// Constructor + /// + public FileTreeNode() + { + this.Children = new List(); + } + + /// + /// Parent node + /// + public FileTreeNode Parent { get; private set; } + + /// + /// List of children nodes + /// + public List Children { get; private set; } + + // Indicates if the node is expanded, applicable to a folder. + public bool IsExpanded { get; set; } + + // Indicates if the node is file or folder + public bool IsFile { get; set; } + + /// + /// File or folder name + /// + public string Name { get; set; } + + /// + /// Full path + /// + public string FullPath { get; set; } + + public void AddChildNode(FileTreeNode item) + { + item.Parent = this; + this.Children.Add(item); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserBase.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserBase.cs new file mode 100644 index 00000000..0152d307 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserBase.cs @@ -0,0 +1,195 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.Linq; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using System.Data.SqlClient; + +namespace Microsoft.SqlTools.ServiceLayer.FileBrowser +{ + public struct FileInfo + { + // Empty for folder + public string fileName; + public string folderName; + public string path; + // Includes file name in the path. Empty for folder. + public string fullPath; + } + + /// + /// Base class for file browser + /// + public abstract class FileBrowserBase + { + private Enumerator enumerator = null; + protected SqlConnection sqlConnection = null; + + protected Enumerator Enumerator + { + get + { + return this.enumerator = (this.enumerator ?? new Enumerator()); + } + } + + /// + /// Separator string for components of the file path. Defaults to \ for Windows and / for Linux + /// + internal char PathSeparator { get; set; } + + /// + /// Returns the PathSeparator values of the Server. + /// + /// PathSeparator + internal static char GetPathSeparator(Enumerator enumerator, SqlConnection connection) + { + var req = new Request(); + req.Urn = "Server"; + req.Fields = new[] { "PathSeparator" }; + string pathSeparator = string.Empty; + + using (DataSet ds = enumerator.Process(connection, req)) + { + if (IsValidDataSet(ds)) + { + pathSeparator = Convert.ToString(ds.Tables[0].Rows[0][0], System.Globalization.CultureInfo.InvariantCulture); + } + } + + if (string.IsNullOrEmpty(pathSeparator)) + { + pathSeparator = @"\"; + } + + return pathSeparator[0]; + } + + /// + /// Enumerates the FileInfo objects associated with drives + /// + /// + /// + /// + internal static IEnumerable EnumerateDrives(Enumerator enumerator, SqlConnection connection) + { + // if not supplied, server name will be obtained from urn + Request req = new Request(); + bool clustered = false; + + req.Urn = "Server/Information"; + req.Fields = new string[] { "IsClustered", "PathSeparator", "HostPlatform" }; + + var pathSeparator = @"\"; + var hostPlatform = HostPlatformNames.Windows; + try + { + using (DataSet ds = enumerator.Process(connection, req)) + { + if (IsValidDataSet(ds)) + { + clustered = Convert.ToBoolean(ds.Tables[0].Rows[0][0], + CultureInfo.InvariantCulture); + pathSeparator = Convert.ToString(ds.Tables[0].Rows[0][1], CultureInfo.InvariantCulture); + hostPlatform = Convert.ToString(ds.Tables[0].Rows[0][2], CultureInfo.InvariantCulture); + } + } + } + catch (UnknownPropertyEnumeratorException) + { + //there can be no clusters on 7.0 server + } + + // we need to issue different queries to get all fixed drives on a normal server, and + // shared drives on a cluster + req.Urn = clustered ? "Server/AvailableMedia[@SharedDrive=true()]" : "Server/Drive"; + req.Fields = new[] { "Name" }; + + using (DataSet ds = enumerator.Process(connection, req)) + { + if (IsValidDataSet(ds)) + { + for (int i = 0; i < ds.Tables[0].Rows.Count; i++) + { + var fileInfo = new FileInfo + { + fileName = string.Empty, + path = Convert.ToString(ds.Tables[0].Rows[i][0], System.Globalization.CultureInfo.InvariantCulture) + }; + + // if we're looking at shared devices on a clustered server + // they already have \ on the drive + // sys.dm_os_enumerate_fixed_drives appends a \ on Windows for sql17+ + if (!clustered && hostPlatform == HostPlatformNames.Windows && !fileInfo.path.EndsWith(pathSeparator)) + { + fileInfo.path += pathSeparator; + } + + yield return fileInfo; + } + } + } + } + + /// + /// Enumerates files and folders that are immediate children of the given path on the server + /// + /// + /// + /// + /// + internal static IEnumerable EnumerateFilesInFolder(Enumerator enumerator, SqlConnection connection, string path) + { + var request = new Request + { + Urn = "Server/File[@Path='" + Urn.EscapeString(path) + "']", + Fields = new[] { "Name", "IsFile", "FullName" }, + OrderByList = new[] + { + new OrderBy + { + Field = "IsFile" + }, + new OrderBy + { + Field = "Name" + } + } + }; + + using (DataSet ds = enumerator.Process(connection, request)) + { + if (IsValidDataSet(ds)) + { + foreach (DataRow row in ds.Tables[0].Rows) + { + bool isFile = Convert.ToBoolean((object)row[1], CultureInfo.InvariantCulture); + yield return new FileInfo + { + path = isFile ? path : Convert.ToString((object)row[2], CultureInfo.InvariantCulture), + fileName = isFile ? Convert.ToString((object)row[0], CultureInfo.InvariantCulture) : String.Empty, + folderName = isFile ? String.Empty : Convert.ToString((object)row[0], CultureInfo.InvariantCulture), + fullPath = isFile ? Convert.ToString((object)row[2], CultureInfo.InvariantCulture) : String.Empty + }; + } + } + } + } + + internal static bool IsValidDataSet(DataSet ds) + { + return (ds != null + && ds.Tables != null + && ds.Tables.Count > 0 + && ds.Tables[0].Rows != null + && ds.Tables[0].Rows.Count > 0) ; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserException.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserException.cs new file mode 100644 index 00000000..f92684bc --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserException.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.SqlTools.ServiceLayer.FileBrowser +{ + /// + /// Exception raised from file browser operation + /// + internal sealed class FileBrowserException : Exception + { + internal FileBrowserException(string m) : base(m) + { + } + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserOperation.cs new file mode 100644 index 00000000..1e1dc3c7 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserOperation.cs @@ -0,0 +1,239 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Globalization; +using System.Text.RegularExpressions; +using Microsoft.SqlTools.ServiceLayer.FileBrowser.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.FileBrowser +{ + /// + /// Implementation for file browser operation + /// + internal class FileBrowserOperation : FileBrowserBase + { + private FileTree fileTree; + private string expandPath; + private string[] fileFilters; + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public FileBrowserOperation() + { + this.fileTree = new FileTree(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The connection info + /// The file extension filters + public FileBrowserOperation(SqlConnection connectionInfo, string expandPath, string[] fileFilters = null): this() + { + this.sqlConnection = connectionInfo; + this.expandPath = expandPath; + if (fileFilters == null) + { + this.fileFilters = new string[1] { "*" }; + } + else + { + this.fileFilters = fileFilters; + } + } + + #endregion + + #region public properties and methods + + public FileTree FileTree + { + get + { + return this.fileTree; + } + } + + internal string[] FileFilters + { + get + { + return this.fileFilters; + } + } + + public void PopulateFileTree() + { + this.PathSeparator = GetPathSeparator(this.Enumerator, this.sqlConnection); + PopulateDrives(); + ExpandSelectedNode(this.expandPath); + } + + /// + /// Expand nodes for the selected path. + /// + public void ExpandSelectedNode(string expandPath) + { + this.expandPath = expandPath; + if (!string.IsNullOrEmpty(this.expandPath)) + { + var dirs = this.expandPath.TrimEnd(this.PathSeparator).Split(this.PathSeparator); + List currentChildren = this.fileTree.RootNode.Children; + FileTreeNode lastNode = null; + string pathSeparatorString = Convert.ToString(this.PathSeparator); + + foreach (string dir in dirs) + { + FileTreeNode currentNode = null; + foreach (FileTreeNode node in currentChildren) + { + if (node.Name == pathSeparatorString || string.Equals(node.Name, dir, StringComparison.OrdinalIgnoreCase)) + { + currentNode = node; + break; + } + } + + if (currentNode != null) + { + currentNode.IsExpanded = true; + if (!currentNode.IsFile) + { + PopulateFileNode(currentNode); + } + currentChildren = currentNode.Children; + lastNode = currentNode; + } + else + { + if (lastNode != null) + { + this.fileTree.SelectedNode = lastNode; + } + throw new FileBrowserException(string.Format(SR.InvalidPathError, this.expandPath)); + } + } + + if (lastNode != null) + { + this.fileTree.SelectedNode = lastNode; + } + } + } + + #endregion + + private void PopulateDrives() + { + bool first = true; + foreach (var fileInfo in EnumerateDrives(Enumerator, sqlConnection)) + { + // Windows drive letter paths have a '\' at the end. Linux drive paths won't have a '\'. + var node = new FileTreeNode + { + Name = Convert.ToString(fileInfo.path, CultureInfo.InvariantCulture).TrimEnd('\\'), + FullPath = fileInfo.path + }; + + this.fileTree.RootNode.AddChildNode(node); + + if (first) + { + this.fileTree.SelectedNode = node; + first = false; + } + + PopulateFileNode(node); + } + } + + private void PopulateFileNode(FileTreeNode parentNode) + { + string parentPath = parentNode.FullPath; + parentNode.Children.Clear(); + + foreach (var file in EnumerateFilesInFolder(Enumerator, sqlConnection, parentPath)) + { + bool isFile = !string.IsNullOrEmpty(file.fileName); + FileTreeNode treeNode = new FileTreeNode(); + if (isFile) + { + treeNode.Name = file.fileName; + treeNode.FullPath = file.fullPath; + } + else + { + treeNode.Name = file.folderName; + treeNode.FullPath = file.path; + } + treeNode.IsFile = isFile; + + // if the node is a directory, or if we are browsing for files and the file name is allowed, + // add the node to the tree + if (!isFile || (this.FilterFile(treeNode.Name, this.fileFilters))) + { + parentNode.AddChildNode(treeNode); + } + } + } + + /// + /// Filter a filename based on the full mask provide. The full mask may be a collection a masks seperated by semi-colons. + /// For example: *; *.txt + /// + internal bool FilterFile(string fileName, string[] masks) + { + for (int index = 0; index < masks.Length; index++) + { + if (MatchFileToSubMask(fileName, masks[index])) + { + return true; + } + } + return false; + } + + /// + /// Compares a file name to the user specified mask using a regular expression + /// + /// + /// + /// + private bool MatchFileToSubMask(string fileName, string mask) + { + Regex regex; + + // If this mask is for all files (*) then just return true. + if (mask == "*") + { + return true; + } + + mask = mask.Replace(".", "\\."); + mask = mask.Replace("*", ".*"); + mask = mask.Replace("?", "."); + + // Perform case insensitive RegEx + { + regex = new Regex(mask, RegexOptions.IgnoreCase); + } + + if (!regex.IsMatch(fileName)) + { + return false; + } + else + { + return true; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserService.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserService.cs new file mode 100644 index 00000000..468d0b27 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserService.cs @@ -0,0 +1,282 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Data.SqlClient; +using System.Threading.Tasks; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection; +using Microsoft.SqlTools.ServiceLayer.FileBrowser.Contracts; +using Microsoft.SqlTools.ServiceLayer.Hosting; + +namespace Microsoft.SqlTools.ServiceLayer.FileBrowser +{ + /// + /// Main class for file browser service + /// + public sealed class FileBrowserService + { + private static readonly Lazy LazyInstance = new Lazy(() => new FileBrowserService()); + public static FileBrowserService Instance => LazyInstance.Value; + + // Cache file browser operations for expanding node request + private Dictionary ownerToFileBrowserMap = new Dictionary(); + private Dictionary validatePathsCallbackMap = new Dictionary(); + private ConnectionService connectionService = null; + + /// + /// Signature for callback method that validates the selected file paths + /// + /// + public delegate bool ValidatePathsCallback(FileBrowserValidateEventArgs eventArgs, out string errorMessage); + + internal ConnectionService ConnectionServiceInstance + { + get + { + if (connectionService == null) + { + connectionService = ConnectionService.Instance; + } + return connectionService; + } + set + { + connectionService = value; + } + } + + /// + /// Service host object for sending/receiving requests/events. + /// Internal for testing purposes. + /// + internal IProtocolEndpoint ServiceHost + { + get; + set; + } + + /// + /// Constructor + /// + public FileBrowserService() + { + } + + /// + /// Register validate path callback + /// + /// + /// + public void RegisterValidatePathsCallback(string service, ValidatePathsCallback callback) + { + if (this.validatePathsCallbackMap.ContainsKey(service)) + { + this.validatePathsCallbackMap.Remove(service); + } + + this.validatePathsCallbackMap.Add(service, callback); + } + + /// + /// Initializes the service instance + /// + /// + /// + public void InitializeService(ServiceHost serviceHost) + { + this.ServiceHost = serviceHost; + + // Open a file browser + serviceHost.SetRequestHandler(FileBrowserOpenRequest.Type, HandleFileBrowserOpenRequest); + + // Expand a folder node + serviceHost.SetRequestHandler(FileBrowserExpandRequest.Type, HandleFileBrowserExpandRequest); + + // Validate the selected files + serviceHost.SetRequestHandler(FileBrowserValidateRequest.Type, HandleFileBrowserValidateRequest); + + // Close the file browser + serviceHost.SetRequestHandler(FileBrowserCloseRequest.Type, HandleFileBrowserCloseRequest); + } + + #region request handlers + + internal async Task HandleFileBrowserOpenRequest( + FileBrowserOpenParams fileBrowserParams, + RequestContext requestContext) + { + try + { + Task.Run(() => RunFileBrowserOpenTask(fileBrowserParams)); + await requestContext.SendResult(true); + } + catch + { + await requestContext.SendResult(false); + } + } + + internal async Task HandleFileBrowserExpandRequest( + FileBrowserExpandParams fileBrowserParams, + RequestContext requestContext) + { + try + { + Task.Run(() => RunFileBrowserExpandTask(fileBrowserParams)); + await requestContext.SendResult(true); + } + catch + { + await requestContext.SendResult(false); + } + } + + internal async Task HandleFileBrowserValidateRequest( + FileBrowserValidateParams fileBrowserParams, + RequestContext requestContext) + { + try + { + Task.Run(() => RunFileBrowserValidateTask(fileBrowserParams)); + await requestContext.SendResult(true); + } + catch + { + await requestContext.SendResult(false); + } + } + + internal async Task HandleFileBrowserCloseRequest( + FileBrowserCloseParams fileBrowserParams, + RequestContext requestContext) + { + FileBrowserCloseResponse response = new FileBrowserCloseResponse(); + if (this.ownerToFileBrowserMap.ContainsKey(fileBrowserParams.OwnerUri)) + { + this.ownerToFileBrowserMap.Remove(fileBrowserParams.OwnerUri); + response.Succeeded = true; + } + else + { + response.Succeeded = false; + } + + await requestContext.SendResult(response); + } + + #endregion + + internal async Task RunFileBrowserOpenTask(FileBrowserOpenParams fileBrowserParams) + { + FileBrowserOpenCompleteParams result = new FileBrowserOpenCompleteParams(); + + try + { + ConnectionInfo connInfo; + this.ConnectionServiceInstance.TryFindConnection(fileBrowserParams.OwnerUri, out connInfo); + SqlConnection conn = null; + + if (connInfo != null) + { + DbConnection dbConn; + connInfo.TryGetConnection(ConnectionType.Default, out dbConn); + if (dbConn != null) + { + conn = ReliableConnectionHelper.GetAsSqlConnection((IDbConnection)dbConn); + } + } + + if (conn != null) + { + FileBrowserOperation browser = new FileBrowserOperation(conn, fileBrowserParams.ExpandPath, fileBrowserParams.FileFilters); + browser.PopulateFileTree(); + this.ownerToFileBrowserMap.Add(fileBrowserParams.OwnerUri, browser); + result.FileTree = browser.FileTree; + result.Succeeded = true; + } + else + { + result.Succeeded = false; + } + } + catch (Exception ex) + { + result.Succeeded = false; + result.Message = ex.Message; + } + + await ServiceHost.SendEvent(FileBrowserOpenCompleteNotification.Type, result); + } + + internal async Task RunFileBrowserExpandTask(FileBrowserExpandParams fileBrowserParams) + { + FileBrowserExpandCompleteParams result = new FileBrowserExpandCompleteParams(); + try + { + if (this.ownerToFileBrowserMap.ContainsKey(fileBrowserParams.OwnerUri)) + { + FileBrowserOperation browser = this.ownerToFileBrowserMap[fileBrowserParams.OwnerUri]; + browser.ExpandSelectedNode(fileBrowserParams.ExpandPath); + result.ExpandedNode = browser.FileTree.SelectedNode; + result.Succeeded = true; + } + else + { + result.Succeeded = false; + } + } + catch (Exception ex) + { + result.Succeeded = false; + result.Message = ex.Message; + } + + await ServiceHost.SendEvent(FileBrowserExpandCompleteNotification.Type, result); + } + + internal async Task RunFileBrowserValidateTask(FileBrowserValidateParams fileBrowserParams) + { + FileBrowserValidateCompleteParams result = new FileBrowserValidateCompleteParams(); + + try + { + if (this.validatePathsCallbackMap.ContainsKey(fileBrowserParams.ServiceType) + && this.validatePathsCallbackMap[fileBrowserParams.ServiceType] != null + && fileBrowserParams.SelectedFiles != null + && fileBrowserParams.SelectedFiles.Length > 0) + { + string errorMessage; + result.Succeeded = this.validatePathsCallbackMap[fileBrowserParams.ServiceType](new FileBrowserValidateEventArgs + { + ServiceType = fileBrowserParams.ServiceType, + OwnerUri = fileBrowserParams.OwnerUri, + FilePaths = fileBrowserParams.SelectedFiles + }, out errorMessage); + + if (!string.IsNullOrEmpty(errorMessage)) + { + result.Message = errorMessage; + } + } + else + { + result.Succeeded = true; + } + } + catch (Exception ex) + { + result.Succeeded = false; + result.Message = ex.Message; + } + + await ServiceHost.SendEvent(FileBrowserValidateCompleteNotification.Type, result); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserValidateEventArgs.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserValidateEventArgs.cs new file mode 100644 index 00000000..442e0dcd --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserValidateEventArgs.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.SqlTools.ServiceLayer.FileBrowser +{ + /// + /// Event arguments for validating selected files in file browser + /// + public sealed class FileBrowserValidateEventArgs : EventArgs + { + /// + /// Connection uri + /// + public string OwnerUri { get; set; } + + /// + /// Service which provide validation callback + /// + public string ServiceType { get; set; } + + /// + /// Selected files + /// + public string[] FilePaths { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/ServiceConstants.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/ServiceConstants.cs new file mode 100644 index 00000000..54cc96f1 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/ServiceConstants.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.FileBrowser +{ + /// + /// List of services that provide file validation callback to file browser service + /// + public static class FileValidationServiceConstants + { + /// + /// Backup + /// + public const string Backup = "Backup"; + + /// + /// Restore + /// + public const string Restore = "Restore"; + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs index 7e974337..26c23f71 100755 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs @@ -3285,6 +3285,22 @@ namespace Microsoft.SqlTools.ServiceLayer } } + public static string BackupPathIsFolderError + { + get + { + return Keys.GetString(Keys.BackupPathIsFolderError); + } + } + + public static string InvalidBackupPathError + { + get + { + return Keys.GetString(Keys.InvalidBackupPathError); + } + } + public static string TaskInProgress { get @@ -3493,6 +3509,14 @@ namespace Microsoft.SqlTools.ServiceLayer } } + public static string InvalidPathError + { + get + { + return Keys.GetString(Keys.InvalidPathError); + } + } + public static string ConnectionServiceListDbErrorNotConnected(string uri) { return Keys.GetString(Keys.ConnectionServiceListDbErrorNotConnected, uri); @@ -4834,6 +4858,12 @@ namespace Microsoft.SqlTools.ServiceLayer public const string BackupTaskName = "BackupTaskName"; + public const string BackupPathIsFolderError = "BackupPathIsFolderError"; + + + public const string InvalidBackupPathError = "InvalidBackupPathError"; + + public const string TaskInProgress = "TaskInProgress"; @@ -4912,6 +4942,9 @@ namespace Microsoft.SqlTools.ServiceLayer public const string ScriptTaskName = "ScriptTaskName"; + public const string InvalidPathError = "InvalidPathError"; + + private Keys() { } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx index 0822ea24..0f97e781 100755 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx @@ -1815,6 +1815,14 @@ Backup Database + + The provided path specifies a directory but a file path is required: {0} + + + + Cannot verify the existence of the backup file location: {0} + + In progress @@ -1919,4 +1927,8 @@ scripting + + Cannot access the specified path on the server: {0} + + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings index 01122b85..3515ef10 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings @@ -807,6 +807,10 @@ prototype_file_noApplicableFileGroup = No Applicable Filegroup # Backup Service BackupTaskName = Backup Database +# Backup File Validation Errors +BackupPathIsFolderError = The provided path specifies a directory but a file path is required: {0} +InvalidBackupPathError = Cannot verify the existence of the backup file location: {0} + ############################################################################ # Task Service TaskInProgress = In progress @@ -840,4 +844,8 @@ TheLastBackupTaken = The last backup taken ({0}) ############################################################################ # Generate Script -ScriptTaskName = scripting \ No newline at end of file +ScriptTaskName = scripting + +############################################################################ +# File Browser Validation Errors +InvalidPathError = Cannot access the specified path on the server: {0} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf index 734bb2e8..f4312ad7 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf @@ -2250,6 +2250,21 @@ scripting + + The provided path specifies a directory but a file path is required: {0} + The file name specified is also a directory name: {0} + + + + Cannot verify the existence of the backup file location: {0} + Cannot verify the existence of the backup file location: {0} + + + + Cannot access the specified path on the server: {0} + Cannot access the specified path on the server: {0} + + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoQueryModel.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoQueryModel.cs index e012f3b5..8fe4033b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoQueryModel.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoQueryModel.cs @@ -1,16 +1,16 @@ -// This file was generated by a T4 Template. Do not modify directly, instead update the SmoQueryModelDefinition.xml file -// and re-run the T4 template. This can be done in Visual Studio by right-click in and choosing "Run Custom Tool", -// or from the command-line on any platform by running "build.cmd -Target=CodeGen" or "build.sh -Target=CodeGen". - -using System; -using System.Collections.Generic; -using System.Composition; -using System.Linq; -using Microsoft.SqlServer.Management.Smo; -using Microsoft.SqlServer.Management.Smo.Broker; - -namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel -{ +// This file was generated by a T4 Template. Do not modify directly, instead update the SmoQueryModelDefinition.xml file +// and re-run the T4 template. This can be done in Visual Studio by right-click in and choosing "Run Custom Tool", +// or from the command-line on any platform by running "build.cmd -Target=CodeGen" or "build.sh -Target=CodeGen". + +using System; +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.Smo.Broker; + +namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel +{ [Export(typeof(SmoQuerier))] internal partial class SqlDatabaseQuerier: SmoQuerier @@ -1628,5 +1628,5 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel return Enumerable.Empty(); } } -} - +} + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/Scripter.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Scripter.cs index 7f6bfa23..7bc2f1b1 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Scripting/Scripter.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Scripter.cs @@ -1,24 +1,24 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// -// THIS FILE IS GENERATED BY A CODEGEN TOOL. DO NOT EDIT!!!! -// IF YOU NEED TO MAKE CHANGES, EDIT THE .TT FILE!!! - -// To regenerate either (1) save the .TT file from Visual Studio or (2) run the build script codegen task. - -using System; -using System.IO; -using System.Collections.Generic; -using System.Collections.Specialized; -using Microsoft.SqlServer.Management.Smo; -using Microsoft.SqlServer.Management.SqlParser.Intellisense; -using Microsoft.SqlTools.Utility; -namespace Microsoft.SqlTools.ServiceLayer.Scripting -{ - internal partial class Scripter - { - +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// THIS FILE IS GENERATED BY A CODEGEN TOOL. DO NOT EDIT!!!! +// IF YOU NEED TO MAKE CHANGES, EDIT THE .TT FILE!!! + +// To regenerate either (1) save the .TT file from Visual Studio or (2) run the build script codegen task. + +using System; +using System.IO; +using System.Collections.Generic; +using System.Collections.Specialized; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.SqlParser.Intellisense; +using Microsoft.SqlTools.Utility; +namespace Microsoft.SqlTools.ServiceLayer.Scripting +{ + internal partial class Scripter + { + private void Initialize() { AddSupportedType(DeclarationType.Table, GetTableScripts, "Table", "table"); @@ -243,6 +243,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting } } - } -} + } +} \ No newline at end of file diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/BackupServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/BackupServiceTests.cs index b214146b..88498303 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/BackupServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/BackupServiceTests.cs @@ -14,6 +14,8 @@ using Microsoft.SqlTools.ServiceLayer.Admin; using Microsoft.SqlTools.ServiceLayer.Admin.Contracts; using Microsoft.SqlTools.ServiceLayer.DisasterRecovery; using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts; +using Microsoft.SqlTools.ServiceLayer.FileBrowser; +using Microsoft.SqlTools.ServiceLayer.FileBrowser.Contracts; using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; using Microsoft.SqlTools.ServiceLayer.Test.Common; using Moq; @@ -77,7 +79,7 @@ CREATE CERTIFICATE {1} WITH SUBJECT = 'Backup Encryption Certificate'; "; DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); - string backupPath = GetDefaultBackupPath(service, databaseName, helper.DataContainer, sqlConn); + string backupPath = GetDefaultBackupFullPath(service, databaseName, helper.DataContainer, sqlConn); BackupInfo backupInfo = CreateDefaultBackupInfo(databaseName, BackupType.Full, @@ -101,7 +103,7 @@ CREATE CERTIFICATE {1} WITH SUBJECT = 'Backup Encryption Certificate'; "; var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(databaseName); DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); - string backupPath = GetDefaultBackupPath(service, databaseName, helper.DataContainer, sqlConn); + string backupPath = GetDefaultBackupFullPath(service, databaseName, helper.DataContainer, sqlConn); BackupInfo backupInfo = CreateDefaultBackupInfo(databaseName, BackupType.Full, @@ -134,7 +136,7 @@ CREATE CERTIFICATE {1} WITH SUBJECT = 'Backup Encryption Certificate'; "; var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(databaseName); DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); - string backupPath = GetDefaultBackupPath(service, databaseName, helper.DataContainer, sqlConn); + string backupPath = GetDefaultBackupFullPath(service, databaseName, helper.DataContainer, sqlConn); string certificateName = CreateCertificate(testDb); string cleanupCertificateQuery = string.Format(CleanupCertificateQueryFormat, certificateName); @@ -186,7 +188,7 @@ CREATE CERTIFICATE {1} WITH SUBJECT = 'Backup Encryption Certificate'; "; var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(databaseName); DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); - string backupPath = GetDefaultBackupPath(service, databaseName, helper.DataContainer, sqlConn); + string backupPath = GetDefaultBackupFullPath(service, databaseName, helper.DataContainer, sqlConn); string certificateName = CreateCertificate(testDb); string cleanupCertificateQuery = string.Format(CleanupCertificateQueryFormat, certificateName); @@ -228,6 +230,86 @@ CREATE CERTIFICATE {1} WITH SUBJECT = 'Backup Encryption Certificate'; "; testDb.Cleanup(); } + [Fact] + public async void BackupFileBrowserTest() + { + string databaseName = "testfilebrowser_" + new Random().Next(10000000, 99999999); + SqlTestDb testDb = SqlTestDb.CreateNew(TestServerType.OnPrem, false, databaseName); + + // Initialize backup service + var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(databaseName); + DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); + SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); + DisasterRecoveryService disasterRecoveryService = new DisasterRecoveryService(); + BackupConfigInfo backupConfigInfo = disasterRecoveryService.GetBackupConfigInfo(helper.DataContainer, sqlConn, sqlConn.Database); + + // Create backup file + string backupPath = Path.Combine(backupConfigInfo.DefaultBackupFolder, databaseName + ".bak"); + string query = $"BACKUP DATABASE [{databaseName}] TO DISK = N'{backupPath}' WITH NOFORMAT, NOINIT, NAME = N'{databaseName}-Full Database Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10"; + await TestServiceProvider.Instance.RunQueryAsync(TestServerType.OnPrem, "master", query); + + FileBrowserService service = new FileBrowserService(); + + string[] backupFilters = new string[2] { "*.bak", "*.trn" }; + var openParams = new FileBrowserOpenParams + { + OwnerUri = liveConnection.ConnectionInfo.OwnerUri, + ExpandPath = backupConfigInfo.DefaultBackupFolder, + FileFilters = backupFilters + }; + + var serviceHostMock = new Mock(); + service.ServiceHost = serviceHostMock.Object; + + await service.RunFileBrowserOpenTask(openParams); + + // Verify complete notification event was fired and the result + serviceHostMock.Verify(x => x.SendEvent(FileBrowserOpenCompleteNotification.Type, + It.Is(p => p.Succeeded == true + && p.FileTree != null + && p.FileTree.RootNode != null + && p.FileTree.RootNode.Children != null + && p.FileTree.RootNode.Children.Count > 0 + && p.FileTree.SelectedNode.FullPath == backupConfigInfo.DefaultBackupFolder + && p.FileTree.SelectedNode.Children.Count > 0 + && ContainsFileInTheFolder(p.FileTree.SelectedNode, backupPath))), + Times.Once()); + + var expandParams = new FileBrowserExpandParams + { + OwnerUri = liveConnection.ConnectionInfo.OwnerUri, + ExpandPath = backupConfigInfo.DefaultBackupFolder + }; + + // Expand the node in file browser + await service.RunFileBrowserExpandTask(expandParams); + + // Verify result + serviceHostMock.Verify(x => x.SendEvent(FileBrowserExpandCompleteNotification.Type, + It.Is(p => p.Succeeded == true + && p.ExpandedNode.FullPath == backupConfigInfo.DefaultBackupFolder)), + Times.Once()); + + var validateParams = new FileBrowserValidateParams + { + OwnerUri = liveConnection.ConnectionInfo.OwnerUri, + ServiceType = FileValidationServiceConstants.Backup, + SelectedFiles = new string[] { backupPath } + }; + + // Validate selected files in the browser + await service.RunFileBrowserValidateTask(validateParams); + + // Verify complete notification event was fired and the result + serviceHostMock.Verify(x => x.SendEvent(FileBrowserValidateCompleteNotification.Type, It.Is(p => p.Succeeded == true)), Times.Once()); + + // Remove the backup file + if (File.Exists(backupPath)) + { + File.Delete(backupPath); + } + } + #region private methods private string CreateCertificate(SqlTestDb testDb) @@ -258,7 +340,7 @@ CREATE CERTIFICATE {1} WITH SUBJECT = 'Backup Encryption Certificate'; "; return backupInfo; } - private string GetDefaultBackupPath(DisasterRecoveryService service, string databaseName, CDataContainer dataContainer, SqlConnection sqlConn) + private string GetDefaultBackupFullPath(DisasterRecoveryService service, string databaseName, CDataContainer dataContainer, SqlConnection sqlConn) { BackupConfigInfo backupConfigInfo = service.GetBackupConfigInfo(dataContainer, sqlConn, sqlConn.Database); return Path.Combine(backupConfigInfo.DefaultBackupFolder, databaseName + ".bak"); @@ -287,6 +369,17 @@ CREATE CERTIFICATE {1} WITH SUBJECT = 'Backup Encryption Certificate'; "; } } + private bool ContainsFileInTheFolder(FileTreeNode folderNode, string filePath) + { + foreach (FileTreeNode node in folderNode.Children) + { + if (node.FullPath == filePath) + { + return true; + } + } + return false; + } #endregion } } \ No newline at end of file diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/DisasterRecoveryFileValidatorTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/DisasterRecoveryFileValidatorTests.cs new file mode 100644 index 00000000..2b315e26 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/DisasterRecoveryFileValidatorTests.cs @@ -0,0 +1,87 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Data.SqlClient; +using System.IO; +using Microsoft.SqlTools.ServiceLayer.Admin; +using Microsoft.SqlTools.ServiceLayer.DisasterRecovery; +using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts; +using Microsoft.SqlTools.ServiceLayer.FileBrowser; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Xunit; +using System; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DisasterRecovery +{ + /// + /// Integration tests for disaster recovery file validator + /// + public class DisasterRecoveryFileValidatorTests + { + [Fact] + public void ValidateDefaultBackupFullFilePath() + { + var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo("master"); + DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); + SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); + string backupPath = Path.Combine(GetDefaultBackupFolderPath(helper.DataContainer, sqlConn), "master.bak"); + + string message; + bool result = DisasterRecoveryFileValidator.ValidatePaths(new FileBrowserValidateEventArgs + { + ServiceType = FileValidationServiceConstants.Backup, + OwnerUri = liveConnection.ConnectionInfo.OwnerUri, + FilePaths = new string[] { backupPath } + }, out message); + + Assert.True(result); + Assert.Empty(message); + } + + [Fact] + public void ValidateDefaultBackupFolderPath() + { + var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo("master"); + DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); + SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); + string backupPath = GetDefaultBackupFolderPath(helper.DataContainer, sqlConn); + + bool isFolder; + bool result = DisasterRecoveryFileValidator.IsPathExisting(sqlConn, backupPath, out isFolder); + Assert.True(isFolder); + Assert.True(result); + } + + [Fact] + public void ValidatorShouldReturnFalseForInvalidPath() + { + var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo("master"); + DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); + + string message; + bool result = DisasterRecoveryFileValidator.ValidatePaths(new FileBrowserValidateEventArgs + { + ServiceType = FileValidationServiceConstants.Backup, + OwnerUri = liveConnection.ConnectionInfo.OwnerUri, + FilePaths = new string[] { Guid.NewGuid().ToString() } + }, out message); + + Assert.False(result); + Assert.True(!string.IsNullOrEmpty(message)); + } + + #region private methods + + private string GetDefaultBackupFolderPath(CDataContainer dataContainer, SqlConnection sqlConn) + { + DisasterRecoveryService service = new DisasterRecoveryService(); + BackupConfigInfo backupConfigInfo = service.GetBackupConfigInfo(dataContainer, sqlConn, sqlConn.Database); + return backupConfigInfo.DefaultBackupFolder; + } + + #endregion + + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/FileBrowser/FileBrowserServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/FileBrowser/FileBrowserServiceTests.cs new file mode 100644 index 00000000..4763bf77 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/FileBrowser/FileBrowserServiceTests.cs @@ -0,0 +1,184 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading.Tasks; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.FileBrowser; +using Microsoft.SqlTools.ServiceLayer.FileBrowser.Contracts; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.FileBrowser +{ + /// + /// File browser service tests + /// + public class FileBrowserServiceTests + { + #region Request handle tests + + [Fact] + public async void HandleFileBrowserOpenRequestTest() + { + var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(); + FileBrowserService service = new FileBrowserService(); + var openRequestContext = new Mock>(); + openRequestContext.Setup(x => x.SendResult(It.IsAny())) + .Returns(Task.FromResult(new object())); + + var openParams = new FileBrowserOpenParams + { + OwnerUri = liveConnection.ConnectionInfo.OwnerUri, + ExpandPath = "", + FileFilters = new string[1] {"*"} + }; + + await service.HandleFileBrowserOpenRequest(openParams, openRequestContext.Object); + openRequestContext.Verify(x => x.SendResult(It.Is(p => p == true))); + } + + [Fact] + public async void HandleFileBrowserExpandRequestTest() + { + var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(); + FileBrowserService service = new FileBrowserService(); + var requestContext = new Mock>(); + requestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + + var inputParams = new FileBrowserExpandParams + { + OwnerUri = liveConnection.ConnectionInfo.OwnerUri, + ExpandPath = "" + }; + + await service.HandleFileBrowserExpandRequest(inputParams, requestContext.Object); + requestContext.Verify(x => x.SendResult(It.Is(p => p == true))); + } + + [Fact] + public async void HandleFileBrowserValidateRequestTest() + { + var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(); + FileBrowserService service = new FileBrowserService(); + var requestContext = new Mock>(); + requestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + + var inputParams = new FileBrowserValidateParams + { + OwnerUri = liveConnection.ConnectionInfo.OwnerUri, + ServiceType = "" + }; + + await service.HandleFileBrowserValidateRequest(inputParams, requestContext.Object); + requestContext.Verify(x => x.SendResult(It.Is(p => p == true))); + } + + [Fact] + public async void HandleFileBrowserCloseRequestTest() + { + var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(); + FileBrowserService service = new FileBrowserService(); + var requestContext = new Mock>(); + requestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + + var inputParams = new FileBrowserCloseParams + { + OwnerUri = liveConnection.ConnectionInfo.OwnerUri + }; + + await service.HandleFileBrowserCloseRequest(inputParams, requestContext.Object); + + // Result should return false since it's trying to close a filebrowser that was never opened + requestContext.Verify(x => x.SendResult(It.Is(p => p.Succeeded == false))); + } + + #endregion + + [Fact] + public async void OpenFileBrowserTest() + { + var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(); + FileBrowserService service = new FileBrowserService(); + + var openParams = new FileBrowserOpenParams + { + OwnerUri = liveConnection.ConnectionInfo.OwnerUri, + ExpandPath = "", + FileFilters = new string[1] { "*" } + }; + + var serviceHostMock = new Mock(); + service.ServiceHost = serviceHostMock.Object; + await service.RunFileBrowserOpenTask(openParams); + + // Verify complete notification event was fired and the result + serviceHostMock.Verify(x => x.SendEvent(FileBrowserOpenCompleteNotification.Type, + It.Is(p => p.Succeeded == true + && p.FileTree != null + && p.FileTree.RootNode != null + && p.FileTree.RootNode.Children != null + && p.FileTree.RootNode.Children.Count > 0)), + Times.Once()); + } + + [Fact] + public async void ValidateSelectedFilesWithNullValidatorTest() + { + var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(); + FileBrowserService service = new FileBrowserService(); + + var validateParams = new FileBrowserValidateParams + { + // Do not pass any service so that the file validator will be null + ServiceType = "", + OwnerUri = liveConnection.ConnectionInfo.OwnerUri, + SelectedFiles = new string[] { "" } + }; + + var serviceHostMock = new Mock(); + service.ServiceHost = serviceHostMock.Object; + + // Validate files with null file validator + await service.RunFileBrowserValidateTask(validateParams); + + // Verify complete notification event was fired and the result + serviceHostMock.Verify(x => x.SendEvent(FileBrowserValidateCompleteNotification.Type, It.Is(p => p.Succeeded == true)), Times.Once()); + } + + [Fact] + public async void InvalidFileValidationTest() + { + FileBrowserService service = new FileBrowserService(); + service.RegisterValidatePathsCallback("TestService", ValidatePaths); + + var validateParams = new FileBrowserValidateParams + { + // Do not pass any service so that the file validator will be null + ServiceType = "TestService", + SelectedFiles = new string[] { "" } + }; + + var serviceHostMock = new Mock(); + service.ServiceHost = serviceHostMock.Object; + + // Validate files with null file validator + await service.RunFileBrowserValidateTask(validateParams); + + // Verify complete notification event was fired and the result + serviceHostMock.Verify(x => x.SendEvent(FileBrowserValidateCompleteNotification.Type, It.Is(p => p.Succeeded == false)), Times.Once()); + } + + #region private methods + + private bool ValidatePaths(FileBrowserValidateEventArgs eventArgs, out string message) + { + message = string.Empty; + return false; + } + + #endregion + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/DisasterRecovery/BackupTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/DisasterRecovery/BackupTests.cs index af74c789..e0040aee 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/DisasterRecovery/BackupTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/DisasterRecovery/BackupTests.cs @@ -22,7 +22,6 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.DisasterRecovery { using (SqlTaskManager manager = new SqlTaskManager()) { - DisasterRecoveryService service = new DisasterRecoveryService(); var mockBackupOperation = new Mock(); TaskMetadata taskMetaData = this.CreateTaskMetaData(mockBackupOperation.Object); SqlTask sqlTask = manager.CreateTask(taskMetaData); @@ -45,7 +44,6 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.DisasterRecovery { using (SqlTaskManager manager = new SqlTaskManager()) { - DisasterRecoveryService service = new DisasterRecoveryService(); var mockBackupOperation = new Mock(); TaskMetadata taskMetaData = this.CreateTaskMetaData(mockBackupOperation.Object); taskMetaData.TaskExecutionMode = TaskExecutionMode.Script; @@ -69,8 +67,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.DisasterRecovery public async Task VerifyRunningMultipleBackupTasks() { using (SqlTaskManager manager = new SqlTaskManager()) - { - DisasterRecoveryService service = new DisasterRecoveryService(); + { var mockBackupOperation = new Mock(); TaskMetadata taskMetaData = this.CreateTaskMetaData(mockBackupOperation.Object); @@ -103,7 +100,6 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.DisasterRecovery using (SqlTaskManager manager = new SqlTaskManager()) { IBackupOperation backupOperation = new BackupOperationStub(); - DisasterRecoveryService service = new DisasterRecoveryService(); TaskMetadata taskMetaData = this.CreateTaskMetaData(backupOperation); SqlTask sqlTask = manager.CreateTask(taskMetaData); Assert.NotNull(sqlTask); @@ -129,7 +125,6 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.DisasterRecovery { using (SqlTaskManager manager = new SqlTaskManager()) { - DisasterRecoveryService service = new DisasterRecoveryService(); IBackupOperation backupOperation = new BackupOperationStub(); IBackupOperation backupOperation2 = new BackupOperationStub(); TaskMetadata taskMetaData = this.CreateTaskMetaData(backupOperation); @@ -171,7 +166,6 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.DisasterRecovery { using (SqlTaskManager manager = new SqlTaskManager()) { - DisasterRecoveryService service = new DisasterRecoveryService(); IBackupOperation backupOperation = new BackupOperationStub(); TaskMetadata taskMetaData = this.CreateTaskMetaData(backupOperation); SqlTask sqlTask = manager.CreateTask(taskMetaData); diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/DisasterRecovery/DisasterRecoveryFileValidatorUnitTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/DisasterRecovery/DisasterRecoveryFileValidatorUnitTests.cs new file mode 100644 index 00000000..e038006e --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/DisasterRecovery/DisasterRecoveryFileValidatorUnitTests.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.ServiceLayer.DisasterRecovery; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.DisasterRecovery +{ + /// + /// Unit tests for disaster recovery file validator + /// + public class DisasterRecoveryFileValidatorUnitTests + { + [Fact] + public void ValidatorShouldReturnFalseForNullArgument() + { + string message; + bool result = DisasterRecoveryFileValidator.ValidatePaths(null, out message); + Assert.False(result); + } + + [Fact] + public void GetMachineNameForLocalServer() + { + string machineName = DisasterRecoveryFileValidator.GetMachineName(DisasterRecoveryFileValidator.LocalSqlServer); + Assert.True(System.Environment.MachineName == machineName); + } + + [Fact] + public void GetMachineNameForNamedServer() + { + string testMachineName = "testmachine"; + string machineName = DisasterRecoveryFileValidator.GetMachineName(string.Format("{0}\\testserver", testMachineName)); + Assert.True(testMachineName == machineName); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/FileBrowser/FileBrowserTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/FileBrowser/FileBrowserTests.cs new file mode 100644 index 00000000..026645ca --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/FileBrowser/FileBrowserTests.cs @@ -0,0 +1,84 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using Microsoft.SqlTools.ServiceLayer.FileBrowser; +using Microsoft.SqlTools.ServiceLayer.FileBrowser.Contracts; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.FileBrowser +{ + /// + /// File browser unit tests + /// + public class FileBrowserTests + { + [Fact] + public void CreateFileBrowserOperationTest() + { + FileBrowserOperation operation = new FileBrowserOperation(null, "", null); + Assert.True(operation.FileTree != null); + + // It should set "*" filter as default + Assert.True(operation.FileFilters.Length == 1); + } + + [Fact] + public void FilterFilesTest() + { + FileBrowserOperation operation = new FileBrowserOperation(); + string[] supportedFilePaths = new string[] {"te\\s/t1.txt", "te!s.t2.bak" }; + string[] unsupportedFilePaths = new string[] { "te.s*/t3.jpg", "t_est4.trn" }; + string[] filters = new string[] { "*.txt", "*.bak"}; + + foreach (string path in supportedFilePaths) + { + Assert.True(operation.FilterFile(path, filters)); + } + + foreach (string path in unsupportedFilePaths) + { + Assert.False(operation.FilterFile(path, filters)); + } + } + + [Fact] + public void ExpandNodeShouldThrowExceptionForInvalidPath() + { + FileBrowserOperation operation = new FileBrowserOperation(); + Exception exception = null; + + try + { + operation.PathSeparator = '/'; + operation.ExpandSelectedNode("testdrive/filebrowser/test"); + } + catch (FileBrowserException ex) + { + exception = ex; + } + + Assert.NotNull(exception); + Assert.Null(operation.FileTree.SelectedNode); + } + + [Fact] + public void CreateFileTreeTest() + { + FileTree tree = new FileTree(); + Assert.NotNull(tree.RootNode); + Assert.Null(tree.SelectedNode); + } + + [Fact] + public void AddFileTreeNodeChildTest() + { + FileTreeNode node1 = new FileTreeNode(); + FileTreeNode node2 = new FileTreeNode(); + node1.AddChildNode(node2); + Assert.NotNull(node1.Children); + } + } +}