C# SQL Server 将远程数据库备份到远程默认备份位置而不直接访问远程位置?

Posted

技术标签:

【中文标题】C# SQL Server 将远程数据库备份到远程默认备份位置而不直接访问远程位置?【英文标题】:C# SQL Server backup of a remote database to the remote default backup location without direct access to the remote location? 【发布时间】:2020-02-04 17:01:46 【问题描述】:

TL;DR - 我希望服务器而不是我的应用程序进行备份,因为服务器已设置为这样做,而我的应用程序将无权访问。

背景

20 年前,我的公司为客户创建了使用 Delphi 7/Pascal 编写的软件。我正在用 C# 重写软件。作为重写的一部分,我创建了新的 Firebird、Oracle 和 SQL Server 数据库。联邦法规要求维护所有现有数据,因此我创建了一个数据库修改/转换工具,以便从旧的数据库结构更改为新的数据库结构。

在开始进行更改之前,我需要备份现有数据库。运行此工具的技术人员无法访问其本地文件结构,也无法手动访问数据库所在的远程服务器。该工具访问本地系统上的类似.ini 的加密文件,以解析出连接字符串的组件并创建连接对象。然后,我使用该连接对象连接到技术人员计算机设置连接的同一数据库。 这部分一切正常

如果我单独保留默认备份路径,它会尝试备份到默认路径但在本地计算机上(技术人员无权创建,我们也不希望技术人员访问 .bak)如果我将默认备份路径修改为取自连接字符串的网络路径,我得到 ​​p>

SmoException:System.Data.SqlClient.SqlError:无法打开备份设备操作系统错误67(找不到网络名称。)。

因为文件路径不是网络共享(也不会是),并且数据库用户凭据无法从 SQL Server 外部访问该路径。

所以问题是:我如何将备份带到远程默认路径,就像我在服务器上一样?

这是产生上述错误的代码(它的远程为空)。

public static void FullSqlBackup (Connection oldProactiveSql)

           String sqlServerLogin = oldProactiveSql.UserName;
           String password = oldProactiveSql.PassWord;
           String instanceName = oldProactiveSql.InstanceName;
           String remoteSvrName = oldProactiveSql.Ip + "," + oldProactiveSql.Port;

           Server srv2;
           Server srv3;
           string device;

           switch (oldProactiveSql.InstanceName)
           
                case null:
                     ServerConnection srvConn2 = new ServerConnection(remoteSvrName);
                     srvConn2.LoginSecure = false;
                     srvConn2.Login = sqlServerLogin;
                     srvConn2.Password = password;
                     srv3 = new Server(srvConn2);
                     srv2 = null;
                     Console.WriteLine(srv3.Information.Version);

                     if (srv3.Settings.DefaultFile is null)
                     
                          device = srv3.Information.RootDirectory + "\\DATA\\";
                          device = device.Substring(2);
                          device = oldProactiveSql.Ip + device;
                     
                     else device = srv3.Settings.DefaultFile;
                     device = device.Substring(2);
                     device = string.Concat("\\\\", oldProactiveSql.Ip, device);
                     break;

                default:
                     ServerConnection srvConn = new ServerConnection();
                     srvConn.ServerInstance = @".\" + instanceName;
                     srvConn.LoginSecure = false;
                     srvConn.Login = sqlServerLogin;
                     srvConn.Password = password;
                     srv2 = new Server(srvConn);
                     srv3 = null;
                     Console.WriteLine(srv2.Information.Version);

                     if (srv2.Settings.DefaultFile is null)
                     
                          device = srv2.Information.RootDirectory + "\\DATA\\";
                     
                     else device = srv2.Settings.DefaultFile;
                     break;
           

           Backup bkpDbFull = new Backup();
           bkpDbFull.Action = BackupActionType.Database;
           bkpDbFull.Database = oldProactiveSql.DbName;
           bkpDbFull.Devices.AddDevice(device, DeviceType.File);
           bkpDbFull.BackupSetName = oldProactiveSql.DbName + " database Backup";
           bkpDbFull.BackupSetDescription = oldProactiveSql.DbName + " database - Full Backup";
           bkpDbFull.Initialize = true;
           bkpDbFull.PercentComplete += CompletionStatusInPercent;
           bkpDbFull.Complete += Backup_Completed;

           switch (oldProactiveSql.InstanceName)
           
                case null:
                     try 
                     
                         bkpDbFull.SqlBackup(srv3); 
                     
                     catch (Exception e)
                     
                          Console.WriteLine (e);
                          Console.WriteLine(e.InnerException.Message);
                          throw;
                     
                     break;

                default:
                     try 
                      
                         bkpDbFull.SqlBackup(srv2); 
                     
                     catch (Exception e)
                     
                          Console.WriteLine(e);
                          Console.WriteLine(e.InnerException.Message);
                          throw;
                     
                     break;
           
      

任何帮助将不胜感激,因为我现在只是在兜圈子。

从下面的 cmets 我会尝试 - 1.在数据库上动态创建存储过程[BackupToDefault]然后运行它。 2. 如果失败,则将数据库链接到自身。 3. 尝试 - 在 [LinkedSelfSynonmym] 处执行 [BackupToDefault]

祝我好运,虽然它看起来很复杂,而且我希望它工作的路很长。

【问题讨论】:

不完全确定你在这里发生了什么。但是运行 sql server 的服务帐户需要访问文件夹位置才能写入备份文件。这不是 SA sql 用户...但是在如此安全的情况下,您为什么要使用 SA 帐户??? 我使用的不是真实的SA账户(SA已被移除)而是原程序安全连接字符串中的账户,该账户已被验证具有执行备份的权限如果我通过服务器上的 ssms 登录到 sql server 数据库。 IE。如果我以服务器上原始连接字符串中的用户身份登录,我可以将备份执行到默认备份位置。如果我尝试从本地计算机执行备份,我会收到无法看到远程网络名称的错误。 从本地机器备份是什么意思?意味着您正在运行一个连接到本地 sql 实例的应用程序?还是完全不同的东西?我无法了解您在这里要做什么。 我正在编写一个将在本地机器上运行的应用程序。此应用程序连接到远程 SQL 服务器。此应用程序需要将连接的数据库备份到连接服务器上的默认备份位置(通过 Microsoft.SqlServer.Management.Smo.Server.Settings.DefaultFile 检索)。如果我在服务器上运行该应用程序,它会按预期运行,但如果我远程运行它则不会。如果我远程运行它,应用程序会返回:SmoException:System.Data.SqlClient.SqlError:无法打开备份设备操作系统错误 67(找不到网络名称。)。 @Sean Lange 客户端不愿意打开他们的服务器以获得远程可用的默认路径,所以我需要一种方法来从远程执行此备份到服务器路径但看起来像来自服务器,因此默认备份位置可用。 【参考方案1】:

为了灵感...备份被分成 3 个文件,每个文件位于不同的目录中(sql 实例备份目录 & sql 实例默认目录 & 数据库主目录)

//// compile with:   
// /r:Microsoft.SqlServer.Smo.dll  
// /r:Microsoft.SqlServer.SmoExtended.dll 
// /r:Microsoft.SqlServer.ConnectionInfo.dll  

using System;
using System.Data;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlServer.Management.Common;

namespace SMObackup

    class Program
    
        static void Main()
        

            // For remote connection, remote server name / ServerInstance needs to be specified  
            ServerConnection srvConn2 = new ServerConnection("machinename"/* <--default sql instance on machinename*/);  // or (@"machinename\sqlinstance") for named instances
            srvConn2.LoginSecure = false;
            srvConn2.Login = "smologin";
            srvConn2.Password = "SmoL@gin11";
            srvConn2.DatabaseName = "msdb";
            Server srv3 = new Server(srvConn2);

            //server info
            Console.WriteLine("servername:0 ---- version:1", srv3.Name, srv3.Information.Version);

            //server root directory
            string serverRootDir = srv3.Information.RootDirectory;
            //server backup directory
            string serverBackupDir = srv3.Settings.BackupDirectory;
            //database primary directory
            string databasePrimaryFilepath = srv3.Databases[srvConn2.DatabaseName].PrimaryFilePath;

            Console.WriteLine("server_root_dir:0\nserver_backup_dir:1\ndatabase_primary_dir2", serverRootDir, serverBackupDir, databasePrimaryFilepath);

            Backup bkpDbFull = new Backup();
            bkpDbFull.Action = BackupActionType.Database;
            //comment out copyonly ....
            bkpDbFull.CopyOnly = true; //copy only, just for testing....avoid messing up with existing backup processes
            bkpDbFull.Database = srvConn2.DatabaseName;

            //backup file name
            string backupfile = $"\\backuptest_DateTime.Now.ToString("dd/MM/yyyy/hh/mm/ss").bak";

            //add multiple files, in each location
            bkpDbFull.Devices.AddDevice(serverRootDir + backupfile, DeviceType.File);
            bkpDbFull.Devices.AddDevice(serverBackupDir + backupfile, DeviceType.File);
            bkpDbFull.Devices.AddDevice(databasePrimaryFilepath + backupfile, DeviceType.File);
            bkpDbFull.Initialize = true;

            foreach (BackupDeviceItem backupdevice in bkpDbFull.Devices)
            
                Console.WriteLine("deviceitem:0", backupdevice.Name);
            

            //backup is split/divided amongst the 3 devices
            bkpDbFull.SqlBackup(srv3);

            Restore restore = new Restore();
            restore.Devices.AddRange(bkpDbFull.Devices);
            DataTable backupHeader = restore.ReadBackupHeader(srv3);


            //IsCopyOnly=True
            for (int r = 0; r < backupHeader.Rows.Count; r++)
            
                for (int c = 0; c < backupHeader.Columns.Count; c++)
                
                    Console.Write("0=1\n", backupHeader.Columns[c].ColumnName, (string.IsNullOrEmpty(backupHeader.Rows[r].ItemArray[c].ToString())? "**": backupHeader.Rows[r].ItemArray[c].ToString()) );
                
            

            srvConn2.Disconnect(); //redundant

        
    

【讨论】:

【参考方案2】:

谢谢@SeanLange

很确定在这种情况下您需要使用动态 sql。然后路径将相对于执行 sql 的位置。

我更改了代码以添加:

 private static void WriteBackupSp (Server remoteServer, string dbName, out string storedProcedure)
      
           var s = "CREATE PROCEDURE [dbo].[ProactiveDBBackup]\n";
           s += "AS\n";
           s += "BEGIN\n";
           s += "SET NOCOUNT ON\n";
           s += "BACKUP DATABASE " + dbName + " TO DISK = \'" + string.Concat (remoteServer.BackupDirectory,@"\", dbName, ".bak") + "\'\n";
           s += "END\n";
           storedProcedure = s;
      

然后将最后一个开关更改为:

switch (oldProactiveSql.InstanceName)
           
                case null:
                     try
                     
                          WriteBackupSp (srv3, oldProactiveSql.DbName, out var storedProcedure);
                          ConnectionToolsUtility.GenerateSqlConnectionString (oldProactiveSql, out var cs);

                          using (SqlConnection connection = new SqlConnection (cs))
                          
                               using (SqlCommand command = new SqlCommand (storedProcedure, connection))
                               
                                    connection.Open ();
                                    command.ExecuteNonQuery ();
                                    connection.Close ();
                               
                          
                          var execBackup = "EXEC [dbo].[ProactiveDBBackup]\n";
                          using (SqlConnection connection = new SqlConnection (cs))
                          
                               using (SqlCommand command = new SqlCommand (execBackup, connection))
                               
                                    connection.Open ();
                                    command.ExecuteNonQuery ();
                                    connection.Close ();
                               
                          
                     
                     catch (Exception e)
                     
                          Console.WriteLine(e);
                          Console.WriteLine(e.InnerException.Message);
                          throw;
                     
                     break;
                default:
                     try  bkpDbFull.SqlBackup(srv2); 
                     catch (Exception e)
                     
                          Console.WriteLine(e);
                          Console.WriteLine(e.InnerException.Message);
                          throw;
                     
                     break;
           

这使我可以通过连接字符串将数据库备份到默认备份位置,而无需访问网络路径位置的凭据。

【讨论】:

以上是关于C# SQL Server 将远程数据库备份到远程默认备份位置而不直接访问远程位置?的主要内容,如果未能解决你的问题,请参考以下文章

如何远程把sql server中的数据导入到本地sql server中?

如何调用远程 T-SQL 过程将远程 .bak 数据库文件恢复到远程 SQL Server 实例?

Sql Server本地高版本备份数据备份至远程低版本数据库方法

Sql Server远程还原

sqlserver2005远程备份到本地数据库

小企业sql server数据备份shell脚本解决方案