凌云的博客

行胜于言

使用 Volume Shadow Copy Service 进行文件备份

分类:windows| 发布时间:2018-09-11 12:16:00


前言

在上一篇文章中,我们介绍了 VSS 的基本原理。 本文主要介绍如何通过 VSS 进行文件的备份,并给出一个通过 VSS 进行文件备份的完整例子。 本文给出的例子相当于上一篇文章中整个 VSS 架构的 Requestor 的角色。

在阅读本文之前需要注意以下事项:

  • 本文的例子已经在 Windows 2012 以及 Visual Studio 2013 通过测试,如果你使用的是其他版本的系统和开发环境可以需要作相应修改
  • 你不能在 64 位的系统中使用 32 未版本的 VSS 程序,因此如果你的测试系统是 64 位的,请将本例子编译为 64 位程序。
  • 你需要使用管理员权限运行本例子。
  • 为了保持例子简洁,本文并没有进行严谨的错误检查,如果你要在正式环境中使用 VSS 请仔细阅读微软相关文档并进行错误检查和处理。

添加相关头文件

在正式开始前需要先添加相关的头文件,同时我们定义了一个宏用于检查 VSS 相关接口是否成功执行, 如果不是则打印相关错误并结束程序。

#include <Windows.h>
#include <objbase.h>
#include <vss.h>
#include <vswriter.h>
#include <vsbackup.h>
#include <comdef.h>
#include <cstdio>
#include <string>

using std::wstring;

#define CHECK_HR_RETURN(HR, FUNC, RET) \
    do { \
        _com_error err(HR); \
        if (HR != S_OK) { \
            fprintf(stderr, "Failed to call " ## FUNC ## ", %s\n", err.ErrorMessage()); \
            return RET; \
        } \}
    while (0)

检查传入参数,初始化 COM

int wmain(int argc, wchar_t *argv[])
{
    if (argc < 3) {
        fprintf(stderr, "Usage: %ls <src> <dst>\n", argv[0]);
        return -1;
    }

    WCHAR volume[MAX_PATH + 1] = { '\0' };
    if (GetVolumePathNameW(argv[1], volume, MAX_PATH) == FALSE) {
        fprintf(stderr, "Failed to GetVolumePathNameW\n");
        return -1;
    }

    HRESULT rc = CoInitialize(NULL);
    CHECK_HR_RETURN(rc, "CoInitialize", -1);

    // ...
}

我们的程序其实就是一个简化版的 cp 命令。在程序开始我们需要检查传入参数是否正确。 接着就是调用 CoInitialize 在这里不需要过于纠结这个函数的作用,只需要知道调用 VSS 相关接口前,需要先调用此函数进行初始化。

连接到 VSS

IVssBackupComponents *components = NULL;
rc = CreateVssBackupComponents(&components);
CHECK_HR_RETURN(rc, "CreateVssBackupComponents", -1);

rc = components->InitializeForBackup();
CHECK_HR_RETURN(rc, "InitializeForBackup", -1);

IVssAsync *async;
rc = components->GatherWriterMetadata(&async);
CHECK_HR_RETURN(rc, "GatherWriterMetadata", -1);
rc = async->Wait();
CHECK_HR_RETURN(rc, "async->Wait", -1);

rc = components->SetContext(VSS_CTX_BACKUP);
CHECK_HR_RETURN(rc, "SetContext", -1);

VSS_ID snapshot_set_id;
rc = components->StartSnapshotSet(&snapshot_set_id);
CHECK_HR_RETURN(rc, "StartSnapshotSet", -1);

在这里主要就是通过 CreateVssBackupComponents 创建了一个与 VSS 框架通讯的接口类,需要注意的是我们由于在工程文件直接依赖到了 vssapi.lib。 因此这里直接调用 CreateVssBackupComponents 就行了,如果你不希望在编译链接阶段依赖 vssapi.lib, 可以在程序运行的时候再通过 LoadLibrary 和 GetProcAddress 获取 API 的地址。 由于 CreateVssBackupComponents 的实际导出符号为 CreateVssBackupComponentsInternal, 因此你应该查找 CreateVssBackupComponentsInternal 的地址而不是 CreateVssBackupComponents。

接下来就是调用 InitializeForBackup 和 GatherWriterMetadata 准备快照的生成,其实就是想当于上一篇文章的第一步。

添加卷

VSS_ID snapshot_id;
rc = components->AddToSnapshotSet(volume, GUID_NULL, &snapshot_id);
    CHECK_HR_RETURN(rc, "AddToSnapshotSet", -1);

在这里主要通过 AddToSnapshotSet 将需要生成 snapshot 的卷加入到 snapshotset 中, 由于我们在这里只需要备份一个文件因此永远只需要调用这个函数一次,如果你需要备份的数据在 多个卷中,你需要调用这个函数多次,并且需要保存好 volume 和 snapshot_id 之间的关系。

生成快照

rc = components->SetBackupState(true, false, VSS_BT_FULL, false);
CHECK_HR_RETURN(rc, "SetBackupState", -1);

rc = components->PrepareForBackup(&async);
CHECK_HR_RETURN(rc, "PrepareForBackup", -1);
rc = async->Wait();
CHECK_HR_RETURN(rc, "async->Wait", -1);

rc = components->DoSnapshotSet(&async);
CHECK_HR_RETURN(rc, "DoSnapshotSet", -1);
rc = async->Wait();
CHECK_HR_RETURN(rc, "async->Wait", -1);

首先调用 SetBackupState 设置备份的状态,这个函数必须在 PrepareForBackup 之前调用, 然后调用 PrepareForBackup 通知所有的 writers 准备开始备份, 最后调用 DoSnapshotSet 创建快照,这其实就是上一篇文章的第 4 步到最后一步。

获取快照信息

VSS_SNAPSHOT_PROP snapshot_prop;
rc = components->GetSnapshotProperties(snapshot_id, &snapshot_prop);
CHECK_HR_RETURN(rc, "GetSnapshotProperties", -1);

wstring src = snapshot_prop.m_pwszSnapshotDeviceObject;
src += L"\\";
src += (argv[1] + lstrlenW(volume));

VssFreeSnapshotProperties(&snapshot_prop);

在快照生成后,我们就可以使用 GetSnapshotProperties 获取快照的相关属性, 在这里,我们主要用到了 m_pwszSnapshotDeviceObject 这个字段,用这个字段的内容替换掉 原始文件中的卷目录部分,就可以访问快照生成的时候的文件的状态。

备份数据

if (CopyFileW(src.c_str(), argv[2], true) == FALSE) {
    fprintf(stderr, "Failed to copyfile, src=%ls, dst=%ls, last_error=%d\n",
            src.c_str(), argv[2], GetLastError());
    return -1;
}

由于只是演示,这里直接使用 CopyFile 进行数据备份。

释放资源

rc = components->BackupComplete(&async);
CHECK_HR_RETURN(rc, "BackupComplete", -1);
rc = async->Wait();
CHECK_HR_RETURN(rc, "async->Wait", -1);

components->Release();
CoUninitialize();

fprintf(stdout, "Success copy file\n");

最后就是释放资源了,首先调用 BackupComplete 通知 VSS 备份完成,然后调用 Release 释放掉 component 相关资源。 最后就是 CoUninitialize 释放掉 COM 相关资源。

参考