Create remote file browser service (#448)

* code refactoring

* Add filebrowser service and tests

* change dataset reference

* Address pr comments

* add more tests

* address pr comments

* address pr comments and added more tests

* minor change

* minor fix

* Fix test break and add dataset result check
This commit is contained in:
Kate Shin
2017-09-08 13:57:56 -07:00
committed by GitHub
parent 784f4c5d05
commit 14ec5be961
29 changed files with 1942 additions and 59 deletions

View File

@@ -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
{
/// <summary>
/// Validate selected file paths for backup/restore operations
/// </summary>
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
}
}

View File

@@ -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<DisasterRecoveryService> instance = new Lazy<DisasterRecoveryService>(() => new DisasterRecoveryService());
private static ConnectionService connectionService = null;
private SqlTaskManager sqlTaskManagerInstance = null;
private FileBrowserService fileBrowserService = null;
private RestoreDatabaseHelper restoreDatabaseService = new RestoreDatabaseHelper();
/// <summary>
@@ -78,6 +78,25 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery
}
}
/// <summary>
/// Gets or sets the current filebrowser service instance
/// </summary>
internal FileBrowserService FileBrowserServiceInstance
{
get
{
if (fileBrowserService == null)
{
fileBrowserService = FileBrowserService.Instance;
}
return fileBrowserService;
}
set
{
fileBrowserService = value;
}
}
/// <summary>
/// Initializes the service instance
/// </summary>
@@ -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);
}
/// <summary>

View File

@@ -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
{
/// <summary>
/// Parameters to pass to close file browser
/// </summary>
public class FileBrowserCloseParams
{
/// <summary>
/// Connection uri
/// </summary>
public string OwnerUri;
}
/// <summary>
/// Response for closing the browser
/// </summary>
public class FileBrowserCloseResponse
{
/// <summary>
/// Result of the operation
/// </summary>
public bool Succeeded;
/// <summary>
/// Error message if any
/// </summary>
public string Message;
}
/// <summary>
/// Requst to close the file browser
/// </summary>
class FileBrowserCloseRequest
{
public static readonly
RequestType<FileBrowserCloseParams, FileBrowserCloseResponse> Type =
RequestType<FileBrowserCloseParams, FileBrowserCloseResponse>.Create("filebrowser/close");
}
}

View File

@@ -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
{
/// <summary>
/// Event params for expanding a node
/// </summary>
public class FileBrowserExpandCompleteParams
{
/// <summary>
/// Expanded node
/// </summary>
public FileTreeNode ExpandedNode;
/// <summary>
/// Result of the operation
/// </summary>
public bool Succeeded;
/// <summary>
/// Error message if any
/// </summary>
public string Message;
}
/// <summary>
/// Notification for expand completion
/// </summary>
public class FileBrowserExpandCompleteNotification
{
public static readonly
EventType<FileBrowserExpandCompleteParams> Type =
EventType<FileBrowserExpandCompleteParams>.Create("filebrowser/expandcomplete");
}
}

View File

@@ -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
{
/// <summary>
/// Parameters for expanding a folder node
/// </summary>
public class FileBrowserExpandParams
{
/// <summary>
/// Connection uri
/// </summary>
public string OwnerUri;
/// <summary>
/// The path to expand the nodes for
/// </summary>
public string ExpandPath;
}
/// <summary>
/// Request to expand a node in the file browser
/// </summary>
public class FileBrowserExpandRequest
{
public static readonly
RequestType<FileBrowserExpandParams, bool> Type =
RequestType<FileBrowserExpandParams, bool>.Create("filebrowser/expand");
}
}

View File

@@ -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
{
/// <summary>
/// Event params for opening a file browser
/// Returns full directory structure on the server side
/// </summary>
public class FileBrowserOpenCompleteParams
{
/// <summary>
/// Entire file/folder tree
/// </summary>
public FileTree FileTree;
/// <summary>
/// Result of the operation
/// </summary>
public bool Succeeded;
/// <summary>
/// Error message
/// </summary>
public string Message;
}
/// <summary>
/// Notification for completing file browser opening
/// </summary>
public class FileBrowserOpenCompleteNotification
{
public static readonly
EventType<FileBrowserOpenCompleteParams> Type =
EventType<FileBrowserOpenCompleteParams>.Create("filebrowser/opencomplete");
}
}

View File

@@ -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
{
/// <summary>
/// Parameters for opening file browser
/// </summary>
public class FileBrowserOpenParams
{
/// <summary>
/// Connection uri
/// </summary>
public string OwnerUri;
/// <summary>
/// The initial path to expand the nodes for (e.g. Backup will set this path to default backup folder)
/// </summary>
public string ExpandPath;
/// <summary>
/// File extension filter (e.g. *.bak)
/// </summary>
public string[] FileFilters;
}
/// <summary>
/// Request to open a file browser
/// </summary>
public class FileBrowserOpenRequest
{
public static readonly
RequestType<FileBrowserOpenParams, bool> Type =
RequestType<FileBrowserOpenParams, bool>.Create("filebrowser/open");
}
}

View File

@@ -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
{
/// <summary>
/// Event params for validation completion
/// </summary>
public class FileBrowserValidateCompleteParams
{
/// <summary>
/// Result of the operation
/// </summary>
public bool Succeeded;
/// <summary>
/// Error message if any
/// </summary>
public string Message;
}
/// <summary>
/// Notification for validation completion
/// </summary>
public class FileBrowserValidateCompleteNotification
{
public static readonly
EventType<FileBrowserValidateCompleteParams> Type =
EventType<FileBrowserValidateCompleteParams>.Create("filebrowser/validatecomplete");
}
}

View File

@@ -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
{
/// <summary>
/// Parameters for validating selected file paths
/// </summary>
public class FileBrowserValidateParams
{
/// <summary>
/// Connection uri
/// </summary>
public string OwnerUri;
/// <summary>
/// Type of service that uses the file browser
/// </summary>
public string ServiceType;
/// <summary>
/// Selected files
/// </summary>
public string[] SelectedFiles;
}
/// <summary>
/// Requst to validate the selected file paths
/// </summary>
class FileBrowserValidateRequest
{
public static readonly
RequestType<FileBrowserValidateParams, bool> Type =
RequestType<FileBrowserValidateParams, bool>.Create("filebrowser/validate");
}
}

View File

@@ -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
{
/// <summary>
/// Tree to represent file/folder structure
/// </summary>
public class FileTree
{
/// <summary>
/// Root node of the tree
/// </summary>
public FileTreeNode RootNode { get; private set; }
/// <summary>
/// Selected node of the tree
/// </summary>
public FileTreeNode SelectedNode { get; set; }
/// <summary>
/// Constructor
/// </summary>
public FileTree()
{
this.RootNode = new FileTreeNode();
}
}
}

View File

@@ -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
{
/// <summary>
/// Tree node to represent file or folder
/// </summary>
public class FileTreeNode
{
/// <summary>
/// Constructor
/// </summary>
public FileTreeNode()
{
this.Children = new List<FileTreeNode>();
}
/// <summary>
/// Parent node
/// </summary>
public FileTreeNode Parent { get; private set; }
/// <summary>
/// List of children nodes
/// </summary>
public List<FileTreeNode> 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; }
/// <summary>
/// File or folder name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Full path
/// </summary>
public string FullPath { get; set; }
public void AddChildNode(FileTreeNode item)
{
item.Parent = this;
this.Children.Add(item);
}
}
}

View File

@@ -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;
}
/// <summary>
/// Base class for file browser
/// </summary>
public abstract class FileBrowserBase
{
private Enumerator enumerator = null;
protected SqlConnection sqlConnection = null;
protected Enumerator Enumerator
{
get
{
return this.enumerator = (this.enumerator ?? new Enumerator());
}
}
/// <summary>
/// Separator string for components of the file path. Defaults to \ for Windows and / for Linux
/// </summary>
internal char PathSeparator { get; set; }
/// <summary>
/// Returns the PathSeparator values of the Server.
/// </summary>
/// <returns>PathSeparator</returns>
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];
}
/// <summary>
/// Enumerates the FileInfo objects associated with drives
/// </summary>
/// <param name="enumerator"></param>
/// <param name="connection"></param>
/// <returns></returns>
internal static IEnumerable<FileInfo> 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;
}
}
}
}
/// <summary>
/// Enumerates files and folders that are immediate children of the given path on the server
/// </summary>
/// <param name="enumerator"></param>
/// <param name="connection"></param>
/// <param name="path"></param>
/// <returns></returns>
internal static IEnumerable<FileInfo> 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) ;
}
}
}

View File

@@ -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
{
/// <summary>
/// Exception raised from file browser operation
/// </summary>
internal sealed class FileBrowserException : Exception
{
internal FileBrowserException(string m) : base(m)
{
}
}
}

View File

@@ -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
{
/// <summary>
/// Implementation for file browser operation
/// </summary>
internal class FileBrowserOperation : FileBrowserBase
{
private FileTree fileTree;
private string expandPath;
private string[] fileFilters;
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="FileBrowser"/> class.
/// </summary>
public FileBrowserOperation()
{
this.fileTree = new FileTree();
}
/// <summary>
/// Initializes a new instance of the <see cref="FileBrowser"/> class.
/// </summary>
/// <param name="connectionInfo">The connection info</param>
/// <param name="fileFilters">The file extension filters</param>
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);
}
/// <summary>
/// Expand nodes for the selected path.
/// </summary>
public void ExpandSelectedNode(string expandPath)
{
this.expandPath = expandPath;
if (!string.IsNullOrEmpty(this.expandPath))
{
var dirs = this.expandPath.TrimEnd(this.PathSeparator).Split(this.PathSeparator);
List<FileTreeNode> 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);
}
}
}
/// <summary>
/// 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
/// </summary>
internal bool FilterFile(string fileName, string[] masks)
{
for (int index = 0; index < masks.Length; index++)
{
if (MatchFileToSubMask(fileName, masks[index]))
{
return true;
}
}
return false;
}
/// <summary>
/// Compares a file name to the user specified mask using a regular expression
/// </summary>
/// <param name="fileName"></param>
/// <param name="mask"></param>
/// <returns></returns>
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;
}
}
}
}

View File

@@ -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
{
/// <summary>
/// Main class for file browser service
/// </summary>
public sealed class FileBrowserService
{
private static readonly Lazy<FileBrowserService> LazyInstance = new Lazy<FileBrowserService>(() => new FileBrowserService());
public static FileBrowserService Instance => LazyInstance.Value;
// Cache file browser operations for expanding node request
private Dictionary<string, FileBrowserOperation> ownerToFileBrowserMap = new Dictionary<string, FileBrowserOperation>();
private Dictionary<string, ValidatePathsCallback> validatePathsCallbackMap = new Dictionary<string, ValidatePathsCallback>();
private ConnectionService connectionService = null;
/// <summary>
/// Signature for callback method that validates the selected file paths
/// </summary>
/// <param name="eventArgs"></param>
public delegate bool ValidatePathsCallback(FileBrowserValidateEventArgs eventArgs, out string errorMessage);
internal ConnectionService ConnectionServiceInstance
{
get
{
if (connectionService == null)
{
connectionService = ConnectionService.Instance;
}
return connectionService;
}
set
{
connectionService = value;
}
}
/// <summary>
/// Service host object for sending/receiving requests/events.
/// Internal for testing purposes.
/// </summary>
internal IProtocolEndpoint ServiceHost
{
get;
set;
}
/// <summary>
/// Constructor
/// </summary>
public FileBrowserService()
{
}
/// <summary>
/// Register validate path callback
/// </summary>
/// <param name="service"></param>
/// <param name="callback"></param>
public void RegisterValidatePathsCallback(string service, ValidatePathsCallback callback)
{
if (this.validatePathsCallbackMap.ContainsKey(service))
{
this.validatePathsCallbackMap.Remove(service);
}
this.validatePathsCallbackMap.Add(service, callback);
}
/// <summary>
/// Initializes the service instance
/// </summary>
/// <param name="serviceHost"></param>
/// <param name="context"></param>
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<bool> requestContext)
{
try
{
Task.Run(() => RunFileBrowserOpenTask(fileBrowserParams));
await requestContext.SendResult(true);
}
catch
{
await requestContext.SendResult(false);
}
}
internal async Task HandleFileBrowserExpandRequest(
FileBrowserExpandParams fileBrowserParams,
RequestContext<bool> requestContext)
{
try
{
Task.Run(() => RunFileBrowserExpandTask(fileBrowserParams));
await requestContext.SendResult(true);
}
catch
{
await requestContext.SendResult(false);
}
}
internal async Task HandleFileBrowserValidateRequest(
FileBrowserValidateParams fileBrowserParams,
RequestContext<bool> requestContext)
{
try
{
Task.Run(() => RunFileBrowserValidateTask(fileBrowserParams));
await requestContext.SendResult(true);
}
catch
{
await requestContext.SendResult(false);
}
}
internal async Task HandleFileBrowserCloseRequest(
FileBrowserCloseParams fileBrowserParams,
RequestContext<FileBrowserCloseResponse> 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);
}
}
}

View File

@@ -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
{
/// <summary>
/// Event arguments for validating selected files in file browser
/// </summary>
public sealed class FileBrowserValidateEventArgs : EventArgs
{
/// <summary>
/// Connection uri
/// </summary>
public string OwnerUri { get; set; }
/// <summary>
/// Service which provide validation callback
/// </summary>
public string ServiceType { get; set; }
/// <summary>
/// Selected files
/// </summary>
public string[] FilePaths { get; set; }
}
}

View File

@@ -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
{
/// <summary>
/// List of services that provide file validation callback to file browser service
/// </summary>
public static class FileValidationServiceConstants
{
/// <summary>
/// Backup
/// </summary>
public const string Backup = "Backup";
/// <summary>
/// Restore
/// </summary>
public const string Restore = "Restore";
}
}

View File

@@ -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()
{ }

View File

@@ -1815,6 +1815,14 @@
<value>Backup Database</value>
<comment></comment>
</data>
<data name="BackupPathIsFolderError" xml:space="preserve">
<value>The provided path specifies a directory but a file path is required: {0}</value>
<comment></comment>
</data>
<data name="InvalidBackupPathError" xml:space="preserve">
<value> Cannot verify the existence of the backup file location: {0}</value>
<comment></comment>
</data>
<data name="TaskInProgress" xml:space="preserve">
<value>In progress</value>
<comment></comment>
@@ -1919,4 +1927,8 @@
<value>scripting</value>
<comment></comment>
</data>
<data name="InvalidPathError" xml:space="preserve">
<value>Cannot access the specified path on the server: {0}</value>
<comment></comment>
</data>
</root>

View File

@@ -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
ScriptTaskName = scripting
############################################################################
# File Browser Validation Errors
InvalidPathError = Cannot access the specified path on the server: {0}

View File

@@ -2250,6 +2250,21 @@
<target state="new">scripting</target>
<note></note>
</trans-unit>
<trans-unit id="BackupPathIsFolderError">
<source>The provided path specifies a directory but a file path is required: {0}</source>
<target state="new">The file name specified is also a directory name: {0}</target>
<note></note>
</trans-unit>
<trans-unit id="InvalidBackupPathError">
<source> Cannot verify the existence of the backup file location: {0}</source>
<target state="new"> Cannot verify the existence of the backup file location: {0}</target>
<note></note>
</trans-unit>
<trans-unit id="InvalidPathError">
<source>Cannot access the specified path on the server: {0}</source>
<target state="new">Cannot access the specified path on the server: {0}</target>
<note></note>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -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<SqlSmoObject>();
}
}
}
}

View File

@@ -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
}
}
}
}
}
}

View File

@@ -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<IProtocolEndpoint>();
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<FileBrowserOpenCompleteParams>(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<FileBrowserExpandCompleteParams>(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<FileBrowserValidateCompleteParams>(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
}
}

View File

@@ -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
{
/// <summary>
/// Integration tests for disaster recovery file validator
/// </summary>
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
}
}

View File

@@ -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
{
/// <summary>
/// File browser service tests
/// </summary>
public class FileBrowserServiceTests
{
#region Request handle tests
[Fact]
public async void HandleFileBrowserOpenRequestTest()
{
var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo();
FileBrowserService service = new FileBrowserService();
var openRequestContext = new Mock<RequestContext<bool>>();
openRequestContext.Setup(x => x.SendResult(It.IsAny<bool>()))
.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<bool>(p => p == true)));
}
[Fact]
public async void HandleFileBrowserExpandRequestTest()
{
var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo();
FileBrowserService service = new FileBrowserService();
var requestContext = new Mock<RequestContext<bool>>();
requestContext.Setup(x => x.SendResult(It.IsAny<bool>())).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<bool>(p => p == true)));
}
[Fact]
public async void HandleFileBrowserValidateRequestTest()
{
var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo();
FileBrowserService service = new FileBrowserService();
var requestContext = new Mock<RequestContext<bool>>();
requestContext.Setup(x => x.SendResult(It.IsAny<bool>())).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<bool>(p => p == true)));
}
[Fact]
public async void HandleFileBrowserCloseRequestTest()
{
var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo();
FileBrowserService service = new FileBrowserService();
var requestContext = new Mock<RequestContext<FileBrowserCloseResponse>>();
requestContext.Setup(x => x.SendResult(It.IsAny<FileBrowserCloseResponse>())).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<FileBrowserCloseResponse>(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<IProtocolEndpoint>();
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<FileBrowserOpenCompleteParams>(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<IProtocolEndpoint>();
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<FileBrowserValidateCompleteParams>(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<IProtocolEndpoint>();
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<FileBrowserValidateCompleteParams>(p => p.Succeeded == false)), Times.Once());
}
#region private methods
private bool ValidatePaths(FileBrowserValidateEventArgs eventArgs, out string message)
{
message = string.Empty;
return false;
}
#endregion
}
}

View File

@@ -22,7 +22,6 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.DisasterRecovery
{
using (SqlTaskManager manager = new SqlTaskManager())
{
DisasterRecoveryService service = new DisasterRecoveryService();
var mockBackupOperation = new Mock<IBackupOperation>();
TaskMetadata taskMetaData = this.CreateTaskMetaData(mockBackupOperation.Object);
SqlTask sqlTask = manager.CreateTask<SqlTask>(taskMetaData);
@@ -45,7 +44,6 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.DisasterRecovery
{
using (SqlTaskManager manager = new SqlTaskManager())
{
DisasterRecoveryService service = new DisasterRecoveryService();
var mockBackupOperation = new Mock<IBackupOperation>();
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<IBackupOperation>();
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<SqlTask>(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<SqlTask>(taskMetaData);

View File

@@ -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
{
/// <summary>
/// Unit tests for disaster recovery file validator
/// </summary>
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);
}
}
}

View File

@@ -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
{
/// <summary>
/// File browser unit tests
/// </summary>
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);
}
}
}