65.9K
CodeProject 正在变化。 阅读更多。
Home

从远程 SQL Server 实例传输备份文件到本地机器,无需网络共享、FTP、HTTP

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (39投票s)

2009年3月8日

BSD

3分钟阅读

viewsIcon

145785

downloadIcon

8241

本文介绍如何在不提升 SQL Server 权限或使用自定义复制工具的情况下传输备份。

谁应该阅读它?

如果你有小型数据库(< 1GB)的客户,他们没有雇佣管理员,他们正在使用 Express 版本的 SQL Server,并且你想在你的备份技术组合中添加一些疯狂的东西,请继续阅读 :-)

引言

几天前,有人问我的程序是否可以备份公司网络上特定计算机上的所有数据(我在我的程序中嵌入了备份功能)。最明显的事情是在该计算机上共享一个文件夹并将备份放在该文件夹中,但诀窍是在不提升 SQL Server 权限的情况下完成。第二件事:制作一个批处理文件,将备份从一台计算机复制到另一台计算机,但是...人们希望尽可能简单。所以...写了这个解决方案 :-)(嗯,不完全是这样,这是我写的基本版本)。

这东西是如何工作的

我尝试在不使用 SQL Server 安装的标准部分的情况下完成此任务;所以,我没有使用 xp_cmdshell。该代码执行几个步骤来完成此任务。

  1. 在服务器上的交换文件中进行备份
  2. 创建一个带有 varbinary 字段的临时表
  3. 将备份插入到表中
  4. 使用 ADO.NET 获取列数据
  5. 将流保存到文件中

整个魔法都发生在 DoLocalBackup 方法中,该方法接受两个参数:我们要创建交换文件的远程服务器上的目录路径,以及我们要存储备份的本地机器上的文件夹路径。

步骤 1

在此步骤中,我们将备份到交换文件中,该文件将在下次调用 DoLocalBackup 时被重用,因此我们不需要使用 xp_cmdshell 来删除它(因为它将被重新创建)。我们将使用 FORMAT, INIT 来创建交换文件,并使用 COPY_ONLY 来确保我们没有弄乱其他备份计划。

_sql = String.Format("BACKUP DATABASE {0} TO DISK " + 
       "= N'{1}\\{0}.bak' WITH FORMAT, COPY_ONLY, INIT, " + 
       "NAME = N'{0} - Full Database Backup', SKIP ", 
       _dbname, AremoteTempPath, _dbname);
_command.CommandText = _sql;
_command.ExecuteNonQuery();

第二步

在此步骤中,我们将创建临时表来存储来自交换文件的备份信息。

_sql = String.Format("IF OBJECT_ID('tempdb..##{0}') IS " + 
                     "NOT NULL DROP TABLE ##{0}", temporaryTableName);
_command.CommandText = _sql;
_command.ExecuteNonQuery();
_sql = String.Format("CREATE TABLE ##{0} (bck VARBINARY(MAX))", 
                     temporaryTableName);
_command.CommandText = _sql;
_command.ExecuteNonQuery();

步骤 3

现在,我们将把备份信息加载到临时表中。我使用了带有 BULKOPENROWSET,它运行得非常好。你可以用 OPENROWSET 做一些有趣的事情,非常值得阅读它。

_sql = String.Format("INSERT INTO ##{0} SELECT bck.* FROM OPENROWSET" + 
       "(BULK '{1}\\{2}.bak',SINGLE_BLOB) bck", 
       temporaryTableName, AremoteTempPath, _dbname);
_command.CommandText = _sql;
_command.ExecuteNonQuery();

步骤 4 和 5

有趣的事情 :-)。我们从临时表中获取一行,并将其作为字节流获取。这有点棘手,因为 MSDN 告诉我们应该将大小设置为 GetUpperBound(0)。如果我错了,请纠正我,但这可能是一个错误,因为当你这样做时,你不会复制最后一个字节,并且稍后文件可能会损坏(特别是如果发生一些 CRC 计算)。所以我只添加了 + 1,并且我的备份完美地恢复了 :-)。

_sql = String.Format("SELECT bck FROM ##{0}", temporaryTableName);
SqlDataAdapter da = new SqlDataAdapter(_sql, _conn);
DataSet ds = new DataSet();
da.Fill(ds);
DataRow dr = ds.Tables[0].Rows[0];
byte[] backupFromServer = new byte[0];
backupFromServer = (byte[])dr["bck"];
int aSize = new int();
aSize = backupFromServer.GetUpperBound(0) + 1;

FileStream fs = new FileStream(String.Format("{0}\\{1}", 
                AlocalPath, fileName), FileMode.OpenOrCreate, FileAccess.Write);
fs.Write(backupFromServer, 0, aSize);
fs.Close();

完整的备份方法

public void DoLocalBackup(string AremoteTempPath, string AlocalPath)
{
    try
    {
        if (_conn == null)
            return;
        SqlCommand _command = new SqlCommand();
        _command.Connection = _conn;
        // nice filename on local side, so we know when backup was done
        string fileName = _dbname + DateTime.Now.Year.ToString() + "-" +
            DateTime.Now.Month.ToString() + "-" +
            DateTime.Now.Day.ToString() + "-" + 
                DateTime.Now.Millisecond.ToString() + ".bak";
        // we invoke this method to ensure we didnt mess up with other programs
        string temporaryTableName = findUniqueTemporaryTableName();
         
        string _sql;

        _sql = String.Format("BACKUP DATABASE {0} TO DISK = N'{1}\\{0}.bak' " + 
               "WITH FORMAT, COPY_ONLY, INIT, NAME = N'{0} - Full Database " + 
               "Backup', SKIP ", _dbname, AremoteTempPath, _dbname);
        _command.CommandText = _sql;
        _command.ExecuteNonQuery();
        _sql = String.Format("IF OBJECT_ID('tempdb..##{0}') IS " + 
               "NOT NULL DROP TABLE ##{0}", temporaryTableName);
        _command.CommandText = _sql;
        _command.ExecuteNonQuery();
        _sql = String.Format("CREATE TABLE ##{0} (bck VARBINARY(MAX))", 
                             temporaryTableName);
        _command.CommandText = _sql;
        _command.ExecuteNonQuery();
        _sql = String.Format("INSERT INTO ##{0} SELECT bck.* FROM " + 
               "OPENROWSET(BULK '{1}\\{2}.bak',SINGLE_BLOB) bck", 
               temporaryTableName, AremoteTempPath, _dbname);
        _command.CommandText = _sql;
        _command.ExecuteNonQuery();
        _sql = String.Format("SELECT bck FROM ##{0}", temporaryTableName);
        SqlDataAdapter da = new SqlDataAdapter(_sql, _conn);
        DataSet ds = new DataSet();
        da.Fill(ds);
        DataRow dr = ds.Tables[0].Rows[0];
        byte[] backupFromServer = new byte[0];
        backupFromServer = (byte[])dr["bck"];
        int aSize = new int();
        aSize = backupFromServer.GetUpperBound(0) + 1;

        FileStream fs = new FileStream(String.Format("{0}\\{1}", 
                        AlocalPath, fileName), FileMode.OpenOrCreate, 
                        FileAccess.Write);
        fs.Write(backupFromServer, 0, aSize);
        fs.Close();

        _sql = String.Format("DROP TABLE ##{0}", temporaryTableName);
        _command.CommandText = _sql;
        _command.ExecuteNonQuery();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
        throw;
    }
}

我使用的一些其他代码

本文使用了来自我的 另一篇文章 的组件。它基本上为你在表单上放置的每个控件添加一个标签。在我看来,如果你不喜欢每次使用控件都放置标签,这是一个很棒的工具。它会自动执行一些无聊的任务 :-) 所以如果你有空闲时间,请阅读这篇文章,投票,留下评论。非常感谢反馈。

关注点

嗯,我肯定学到了一件事:不要盲目相信文档 :-)。我花了一些时间试图修复那个 1 字节的错误 :) (我认为 SQL Server 做了某些 CRC 计算)。此外,我喜欢这个解决方案的原因是它节省了我的时间。没有备份和复制,只需按一下按钮。

历史

  • 2009 年 3 月 7 日 - 第一个版本。
© . All rights reserved.