NTFS多流文件、结构化存储和摘要属性集合
Question:首先是一个简单的应用问题。我曾经看过无数的文献。其实看文献无非就是为了归类和总结,如果看了不做总结,便相当于白看。可是做总结是个令人烦恼的过程。写纸上吧,不太好整理;以注释形式写在pdf里一并保存吧,那要查的时候还得一个个pdf打开了去找,多麻烦;用ReferenceManager或者是excel吧,那在复制文件的时候,我还得记得顺带copy一份xls或索引数据文件呢,同时还得在可能使用的每台机器上都装上烦人的索引管理软件,这简直令人如鲠在喉。有啥现成的整理文档的好办法没有?
Answer:用Windows自己提供的NTFS文件属性页来进行文档整理最简单,这是我个人的一点小经验:)不管是何种类型的文件,都可以直接使用"文件属性"中的摘要页来记录注释,填起来也方便快捷,同时也便于查看(用"详细信息"方式查看,然后右键勾选一下显示字段即可);而且,如果使用的是NTFS的话,这些注释是跟着文件跑的,即,在copy/cut文件的同时,这些信息也将被同时复制/剪切,这样便省事多了!
Question:OK,这个方法够简单。它有什么使用限制没有?还有,这些摘要数据究竟是存储在哪里的呢?它们不是保存在文件正文当中吗?或者,它们仅只是些文件的扩展属性而已?
Answer:唯一的限制就是这些添加了注释的文件必须保存在NTFS文件系统中。因为这些摘要信息是以NTFS文件的多流(Alternate Stream)形式存储的,其并不保存在文件正文当中。这里简单介绍一下多流概念,它是NTFS文件系统的一个高级特性;在传统的文件系统中(如FAT32),文件的内容被抽象成为一个单一的数据流及其相应的读写指针,其好处是足够简单,但坏处便是不利于单一文件的读写共享,因为多个组件可能需要同时读写同一份文件,这一问题其实很普遍,打个比方,有个doc文件,里面保存了文字内容和一个visio对象,用word打开它,然后双击visio图并开始编辑,实际上,此时有两个组件在同时访问这个doc文件,一个是word,一个是visio,为了读取doc中的文字内容及visio对象内容,必须考虑其单一文件指针的正确定位,否则就会乱套,因为此时文字数据和visio对象数据是存放在同一个数据流当中的。但在NTFS中,这事便好办了,因M$对NTFS中的文件数据流概念进行了扩充,一份NTFS文件可以包含"多个"数据流,而每个数据流不仅可以单独命名,而且还拥有自己的指针和安全配置,甚至还可以进行独立的压缩保存,这相当于一个NTFS文件里面又可以保存着多个通常意义上的子文件。这下我们保存、共享读取这个doc便方便了,直接将文字内容和visio对象分别存为同一份文件的不同流即可,连读写同步的问题也解决了。
其实,上面这个例子便是COM中一个关于结构化存储(Structured Storage)和复合文档(Compound Documents)的典型示例,而其技术动机正是为了解决单一复杂文档的读写共享问题,而且,这里我连其在NTFS系统中的底层实现也一并解释了:即,采用NTFS所提供的多数据流特性,可以极大简化结构化存储及其复合文档技术的实现;其实在很大程度上,正是因为结构化存储技术提出了文件读写共享的需求,M$才专门在NTFS中引入了多流特性的。
再回到我们的问题上来,便不难找到答案了:在NTFS文件系统中,额外的文件摘要信息均将以"串行化"(这个是通俗的比喻)的方式保存在文件的另外一个数据流中,但并不跟其文件的正文处于同一个数据流。当我们将多流文件copy至FAT32文件系统上时,windows将会提示流数据丢失,因为FAT32只支持单一的文件数据流,它只能保存一份文件正文的copy。说到这里,其实用FAT32(比方说U盘)的筒子们也不必过于担心,因为称手的WinRar已经考虑了多流拷贝丢失的情况:看下面的压缩选项,在勾选状态下,NTFS上被压缩文件的多流信息将会一并压缩到rar文件中。不过,当然,如果想读取多流的话,还是得将文件解压缩到NTFS盘上才行。
以下是一个查看NTFS文件多流信息的示例程序。
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <ole2.h>
#include <assert.h>
WCHAR* StreamType[]=
{
L"Data", L"ExternalData",
L"SecurityData",L"AlternateData",
L"Link", L"PropertyData",
L"ObjectID", L"ReparseData",
L"SparseDock",
};
int _tmain(int argc, _TCHAR* argv[])
{
BOOL ret;
if (argc!=2) return -1;
wprintf(L"NTFS Streams of <<%s>>\n",argv[1]);
wprintf(L"------------------------------------------------------------------------\n");
HANDLE hFile=CreateFile(argv[1],
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
assert(hFile != INVALID_HANDLE_VALUE);
BYTE bBuf[4096];
WIN32_STREAM_ID* pwsi=(WIN32_STREAM_ID*)bBuf;
DWORD dwReaded;
LPVOID lpContext=0; //important!
for (;;)
{
ZeroMemory(bBuf,4096);
ret=BackupRead(hFile,bBuf,20,&dwReaded,FALSE,TRUE,&lpContext);
assert(ret!=0);
if (dwReaded>0)
{
WCHAR wszStreamName[MAX_PATH];
ZeroMemory(wszStreamName,MAX_PATH*2);
if (pwsi->dwStreamNameSize!=0)
BackupRead(hFile,
(LPBYTE) wszStreamName,
pwsi->dwStreamNameSize,
&dwReaded,
FALSE, TRUE, &lpContext);
else
wsprintf(wszStreamName,L"(unnamed)");
wprintf(L"\tName: [%s]\n",wszStreamName);
wprintf(L"\tType: %s\n", StreamType[pwsi->dwStreamId-1]);
wprintf(L"\tLength: %d\n",pwsi->Size);
wprintf(L"------------------------------------------------------------------------\n");
DWORD lo,hi;
BackupSeek(hFile,pwsi->Size.LowPart,pwsi->Size.HighPart,&lo,&hi,&lpContext);
} else
break;
}
ret=CloseHandle(hFile);
assert(ret!=0);
return 0;
}
Question:hmm…明白了...那我是不是直接分析pdf/doc文件多流的二进制格式便能获取和修改所有的"文件摘要"信息了?
Answer:理论上是可以的,不过…嘿嘿…这个二进制格式的分析可没那么容易搞定… 其实,这个编程读取/修改的问题也不一定非要从NTFS多流这一底层的通用机制来着手不可,因为线索我们已经有了:结构化存储。COM提供了专门的结构化存储函数和接口来让我们规范化地访问这些摘要数据。这些信息在COM眼中,只是复合文档的若干Properties而已,而且,这些Properties还被划分成为了若干独立的PropertySet。我们可以通过调用StgOpenStorageEx函数来打开一个文件,并获取一个IPropertySetStorage接口,再通过该接口来对一个固定的属性集来进行读写访问(比方说,像摘要、关键字这些属性便保存在一个名为SummaryInformation的属性集中);同时,还可以对属性集进行自定义扩展,在其中保存任何自己所感兴趣的属性信息。
接下来,便是乏味的COM编程了…COM这个东西其实挺矛盾的,一方面,长久以来,大量厂家在其基础之上逐步实现了无数的类库及现成的、极有价值的功能,而另一方面,其开发、应用起来实在是太烦冗,尤其是在托管环境中的使用,如果显式P/Invoke的话,一大堆声明、定义简直可以淹死人。不过,办法总是有的…这里我偷了个懒,用C++/CLI中的C++ Interop(Implicit Pinvoke)实现了一个简单的控制台示例,哪位筒子有兴趣的话,还可以将其封装一下,当做个托管模块来重复利用呢…
#include <ole2.h>
using namespace System;
using namespace System::Runtime::InteropServices;
static const GUID FMTID_SummaryInformation =
{ 0xF29F85E0, 0x4FF9, 0x1068, { 0xAB, 0x91, 0x08, 0x00,0x2b, 0x27, 0xb3, 0xd9 } };
static const GUID FMTID_DocSummaryInformation =
{ 0xD5CDD502, 0x2E9C, 0x101B, { 0x93, 0x97, 0x08, 0x00,0x2b, 0x2c, 0xf9, 0xae } };
static const GUID IID_IPropertySetStorage =
{ 0x0000013A, 0x0000, 0x0000, { 0xc0, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x46 } };
const CHAR* SummaryPropertyName[]=
{
"", "",
"Title", "Subject",
"Author", "Keywords",
"Comments", "Template",
"Last Saved By", "Revision Number",
"Total Editing Time", "Last Printed",
"Create Time//Date", "Last saved Time//Date",
"Number of Pages", "Number of Words",
"Number of Characters", "Thumbnail",
"Name of Creating Application", "Security"
};
void PrintAllSummaryInformation(IPropertySetStorage *pPropSetStg)
{
HRESULT hr = S_OK;
IPropertyStorage *pPropStg = NULL;
hr=pPropSetStg->Open(FMTID_SummaryInformation, STGM_READ|STGM_SHARE_EXCLUSIVE, &pPropStg );
if (FAILED(hr)) throw gcnew Exception(L"IPropertySetStorage::Open失败");
PROPVARIANT propvarRead[5];
PROPSPEC propspec[5];
for (int i=0;i<5;i++) propspec[i].ulKind = PRSPEC_PROPID;
propspec[0].propid = PIDSI_TITLE;
propspec[1].propid = PIDSI_SUBJECT;
propspec[2].propid = PIDSI_AUTHOR;
propspec[3].propid = PIDSI_KEYWORDS;
propspec[4].propid = PIDSI_COMMENTS;
hr=pPropStg->ReadMultiple(5, propspec, propvarRead);
if (FAILED(hr)) throw gcnew Exception(L"IPropertyStorage::ReadMultiple失败");
if (S_FALSE==hr) throw gcnew Exception(L"所查询的所有属性值均不存在");
for (int i=0;i<5;i++)
if (propvarRead[i].vt==VT_LPSTR)
{
String^ msg=String::Format("{0}: {1}",
Marshal::PtrToStringAnsi(IntPtr((void*)SummaryPropertyName[propspec[i].propid])),
Marshal::PtrToStringAnsi(IntPtr(propvarRead[i].pszVal)));
Console::WriteLine(msg);
}
for (int i=0;i<5;i++)
PropVariantClear(propvarRead+i);
if (pPropStg) pPropStg->Release();
pPropStg = NULL;
}
int main(array<System::String ^> ^args)
{
HRESULT hr = S_OK;
IPropertySetStorage *pPropSetStg = NULL;
if (args->Length!=1) return -1;
IntPtr pFileName=Marshal::StringToBSTR(args[0]);
try {
hr = StgOpenStorageEx(
(const WCHAR *)pFileName.ToPointer(),
STGM_READ|STGM_SHARE_DENY_WRITE,
STGFMT_ANY,
0, NULL, NULL,
IID_IPropertySetStorage,
reinterpret_cast<void**>(&pPropSetStg)
);
if (FAILED(hr)) throw gcnew Exception(L"StgOpenStorageEx失败");
PrintAllSummaryInformation(pPropSetStg);
} catch (Exception^ pError) {
Console::WriteLine(pError->ToString());
}
Marshal::FreeBSTR(pFileName);
if (pPropSetStg) pPropSetStg->Release();
pPropSetStg=NULL;
return 0;
}