Sources
Delphi Russian Knowledge Base
DRKB - это самая большая и удобная в использовании база знаний по Дельфи в рунете, составленная Виталием Невзоровым

СОМ хранилища: подпольная файловая система

01.01.2007

СОМ хранилища: подпольная файловая система
 

 

Перед многими программистами рано или поздно постает вопрос: "В каком формате хранить данные своей программы". Хорошо, если это тип данных с фиксированной длинной, а если надо сохранить разнородные данные, да еще чтоб в одном файле(чтоб потом не разбираться с десятком другим файлов с данными)... Тут на помощь приходить сама Windows(жуть какую сказал: "Windows... и на помощь") с технологией структурированного хранилища данных.

Определения

Структурированные хранилища данных - это файлы особой "самодокументированной" структуры, в которых могут мирно уживаться разнородные данные (от простого текста, до фильмов, архивов и... программ). Так как эта технология есть неотъемлемой частью Windows, то доступ к ней возможен из любого средства программирования, которое поддерживает технологию COM. Одним из таких приложений является и Delphi, на основе которого будет описана технология доступа к структурированным хранилищам данных.

Структура хранилищ

Как уже было сказано, COM хранилища - файлы особой структуры(рис.1) и напоминают иерархическую файловую систему. Так в них есть корневое хранилище (Root Entry) в котором могут содержаться как отдельные потоки("файлы"), так и хранилища второго уровня("каталоги"), в них в свою очередь хранилища третьего уровня и т.д. Управление каждым хранилищем и потоком осуществляется посредством отдельного экземпляра интерфейса: IStorage - для хранилищ и IStream - для потоков. А теперь рассмотрим конкретнее некоторые операции над ними.

Создание и открытие хранилищ

Создание структурированных хранилищ осуществляется с использованием функции StgCreateDocFile, из модуля ActiveX.pas. Синтаксис этой функции выгладит таким образом:

function StgCreateDocfile(pwcsName: POleStr; grfMode: Longint; reserved: Longint; out stgOpen: IStorage): HResult; stdcall;

где

·pwcsName
название хранилища(т.е. название файла).

·grfMode
флаги доступа(комбинация значений STGM_*).

·reserved
он и в Африке RESERVED.

·StgOpen
ссылка на интерфейс IStorage нашего главного хранилища. Результат функции как всегда транслируем в исключения Delphi посредством OleCheck.

Для открытия хранилища используется функция StgOpenStorage:
function StgOpenStorage(pwcsName: POleStr; stgPriority: IStorage; grfMode: Longint; snbExclude: TSNB; reserved: Longint; out stgOpen: IStorage): HResult; stdcall;

неизвестный параметр - stgPriority указывает на ранее открытый экземпляр главного хранилища (почти всегда nil).

Когда хранилище открыто (запись данных)...

Рассмотрим более подробно методы интерфейса IStorage.
Создание потока - IStorage.CreateStream.

function CreateStream(pwcsName: POleStr; grfMode: Longint; reserved1: Longint;reserved2: Longint; out stm: IStream): HResult; stdcall;

параметры:

·pwcsName
название потока.

·grfMode
Флаги доступа

·reserved1, reserved2
·соответственно.
·stm
указатель на созданный поток.

Открытие потока - IStorage.OpenStream.

function OpenStream(pwcsName: POleStr; reserved1: Pointer; grfMode: Longint;reserved2: Longint; out stm: IStream): HResult; stdcall;

параметры:

·pwcsName
название потока.

·reserved1
nil

·grfMode
флаги доступа

·reserved2
0

·stm
указатель на открытый поток.

Создание подхранилища - IStorage.CreateStorage.
function CreateStorage(pwcsName: POleStr; grfMode: Longint; dwStgFmt: Longint; reserved2: Longint; out stg: IStorage): HResult;stdcall;
Открытие подхранилища - IStorage.OpenStorage.

function OpenStorage(pwcsName: POleStr; const stgPriority: IStorage; grfMode: Longint; snbExclude: TSNB; reserved: Longint;out stg: IStorage): HResult; stdcall;

Теперь мы можем приступить к чтению(записи) данных из(в) потоков посредством интерфейсов IStream. Тут можно заметить до боли знакомые (для Дельфийцев) методы работы с потоками:Read,Write, Seek, SetSize, CopyTo... а если так, то почему бы не перевести их в более простую и понятную(по крайней мере для меня) объектную форму. Для этого воспользуемся наработками Borland собранными в модуле AxCtrls.pas, точнее классом TOleStream, который интерпретирует вызовы методов интерфейса IStream в соответствущие методы класса TStream.

А для того чтоб не быть пустословным - приведу небольшой примерчик:

Implementation
 Uses ActiveX,AxCtrls,ComObj;
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var Stg:IStorage;
    Strm:IStream;
    OS:TOleStream;
    S:String;
begin
 OleCheck(StgCreateDocfile('Testing.stg',STGM_READWRITE or STGM_SHARE_EXCLUSIVE ,0,Stg));
 OleCheck(Stg.CreateStream('Testing',STGM_READWRITE or STGM_SHARE_EXCLUSIVE,0,0,Strm));
 OS:=TOleStream.Create(Strm);
 try
  S:='This is the test';
  OS.WriteBuffer(Pointer(S)^,Length(S));
 finally
  OS.free;
  Strm:=nil;
  Stg:=nil;
 end;
end;
 
end.
 

В этом фрагменте мы создаем новое хранилище с одним потоком в который записываем строку S. Естественно, ничто нам не мешает вместо:

S:='This is the test';

OS.WriteBuffer(Pointer(S)^,Length(S));

Написать например:
Image1.Picture.Bitmap.SaveToStream(OS);

и тем самым записать в поток "Testing" изображение(вот она... "универсальная мусоросвалка").

Теперь ненамного отвлечемся от Delphi и посмотрим на наш файл с точки зрения, скажем, Far (или VC)... Посмотрели? А теперь откройте там же любой текстовый документ созданных в Word'е. Как видим структура та же что и в нашем файле. То же можно сказать и о Excel. Но как проверить на прибегая к помощи notepad какой файл является хранилищем, а какой нет? Для этого все в том же модуле ActiveX.pas предусмотрена функция StgIsStorageFile:
function StgIsStorageFile(pwcsName: POleStr): HResult; stdcall;

которая принимает значение S_OK( 0 ) - если файл является структурированным хранилищем данных и S_FALSE ( 1 ) - если файл не есть им, кроме того эта функция может принимать значения: STG_E_INVALIDFILENAME и STG_E_FILENOTFOUND соответственно если имя файла задано неправильно и если файла с таким именем не существует.

Чтение

Чтение данных из хранилища производится также как и чтение из стандартного потока Delphi. Все, что для этого требуется - это создать объект наследник TOleStream с использованием возвращаемого функцией IStorage.OpenStorage значения stm:

procedure TForm1.Button2Click(Sender: TObject);
var Stg:IStorage;
    Strm:IStream;
    OS:TOleStream;
    S:String;
begin
 OleCheck(StgOpenStorage('Testing.stg',nil,STGM_READWRITE or STGM_SHARE_EXCLUSIVE, nil,0,Stg));
 OleCheck(Stg.OpenStream('Testing',0,STGM_READWRITE or STGM_SHARE_EXCLUSIVE,0,Strm));
 OS:=TOleStream.Create(Strm);
 try
  SetLength(S,OS.Size);
  OS.ReadBuffer(Pointer(S)^,OS.Size);
  Edit1.Text:=S;
 finally
  OS.free;
  Strm:=nil;
  Stg:=nil;
 end;
end;
 

после выполнения этого кода мы в Edit1 увидим ранее записанное нами:"This is the test".

Последовательное перемещение по структурам хранилища

Хорошо... мы создали хранилище, записали в него данные и прочитали их. Но мы это сделали зная имя потока в котором записаны наши данные. Но как быть, если мы не знаем структуры хранилища? Для этого в Интерфейсе IStorage предусмотрен механизм перечисления его элементов, который содержится в интерфейсе IEnumStatStg, указатель на который возвращается функцией IStorage.EnumElements:
function EnumElements(reserved1: Longint; reserved2: Pointer; reserved3: Longint;out enm: IEnumStatStg): HResult; stdcall; употребление этой функции происходит таким образом:
OleCheck(Stg.EnumElements(0,nil,0,Enum));
После этого используем только методы интерфейса IEnumStatStg:
Next(celt:Longint; out elt; pceltFetched: PLongint): HResult; stdcall;- выборка информации следующего елемента хранилища
Skip(celt:longint):HResult;stdcall; - пропуск количества елементов заданных в celt

Reset:HResult;stdcall; - Reset - он и в Африке Reset. Clone(out enm:IEnumStatStg):HResult;stdcall; - Клонирование интерфейса

На данный момент для нас самым важным из этих методов есть метод Next:

Next(celt:Longint; out elt; pceltFetched: PLongint): HResult; stdcall;

Который принимает следующие параметры:

·Celt
количество елементов структуры, которое будет извлечено при его вызове.

·Elt
Масив приемник елементов типа TStatStg.

·PceltFetched
указатель на переменную куда будет записано действительное количество извлеченных елементов.

Для примера воспользуемся любым doc файлом и перечислим его потоки(и подхранилища если они есть):

procedure TForm1.Button2Click(Sender: TObject);
var Stg:IStorage;
    Enum:IEnumStatStg;
    Data:TStatStg;
begin
 OleCheck(StgOpenStorage('D:\1.doc',nil,STGM_READWRITE or STGM_SHARE_EXCLUSIVE,nil,0,Stg));
 OleCheck(Stg.EnumElements(0,nil,0,Enum));
 try
  While Enum.Next(1,Data,nil)=S_Ok do
   ListBox1.Items.Add(Format('%s(%d)',[Data.pwcsName,Data.cbSize]));
 finally
  Stg:=nil;
  Enum:=nil;
 end;
end;
 

кроме cbSize структура TStatStg содержит такие поля:

·pwcsName: POleStr;
Название потока или хранилища

·dwType: Longint;
Тип елемента (флаги типа STGTY_*)

·cbSize: Largeint;
Размер конкретного елемента

·mtime: TFileTime;
Дата последней модификации

·ctime: TFileTime;
Дата создания

·atime: TFileTime;
Дата последнего обращения

·grfMode: Longint;
Флаг доступа

·grfLocksSupported: Longint;
Не используется в хранилищах

·clsid: TCLSID;
Идентификатор класса хранилища

·grfStateBits: Longint;
Статусные биты

·reserved: Longint;
Зарезервирован

Созданий дополнительных хранилищ

Для создания дополнительных хранилищ главного хранилища используется метод интерфейса IStorage под названием CreateStorage:

function CreateStorage(pwcsName: POleStr; grfMode: Longint; dwStgFmt: Longint; reserved2: Longint; out stg: IStorage): HResult;stdcall;

параметры:

·pwcsName - название подхранилища.
·grfMode - флаги доступа
·dwStgFmt,reserved2 - зарезервированы (принимают значение 0).
·stg - указатель на интерфейс содержащий ссылку на подхранилище.

После вызова этого метода посредством переменной stg вам стают доступны методы по использованию подхранилища:

procedure TForm1.Button2Click(Sender: TObject);
var Stg,Temp:IStorage;
    Str:IStream;
    OS:TOleStream;
    S:String;
begin
 OleCheck(StgOpenStorage('Testing.stg',nil,STGM_READWRITE or STGM_SHARE_EXCLUSIVE,nil,0,Stg));
 OleCheck(Stg.CreateStorage('SubStorage',STGM_READWRITE or STGM_SHARE_EXCLUSIVE,0,0,Temp));
 OleCheck(Temp.CreateStream('SubTesting',STGM_READWRITE or STGM_SHARE_EXCLUSIVE,0,0,Str));
 try
  OS:=TOleStream.Create(Str);
  S:='SubTesting the stream';
  OS.WriteBuffer(Pointer(S)^,Length(S));
 finally
  Temp:=nil;
  Stg:=nil;
 end;
end;
 

после проделанной операции в файле 'Testing.stg' появится новое подхранилище "SubStorage" c одним потоком "SubTesting" в котором записана строка "SubTesting the stream".

Чтение из такого подхранилища может быть реализовано следующим образом:

procedure TForm1.Button2Click(Sender: TObject);
var Stg,Temp:IStorage;
    Str:IStream;
    OS:TOleStream;
    S:String;
begin
 OleCheck(StgOpenStorage('Testing.stg',nil,STGM_READWRITE or STGM_SHARE_EXCLUSIVE,nil,0,Stg));
 OleCheck(Stg.OpenStorage('SubStorage',nil,STGM_READWRITE or STGM_SHARE_EXCLUSIVE,nil,0,Temp));
 OleCheck(Temp.OpenStream('SubTesting',nil,STGM_READWRITE or STGM_SHARE_EXCLUSIVE,0,Str));
 try
  OS:=TOleStream.Create(Str);
  SetLength(S,OS.Size);
  OS.ReadBuffer(Pointer(S)^,OS.Size);
  Edit1.Text:=S;
 finally
  Temp:=nil;
  Stg:=nil;
 end;
end;
 

Дополнительные возможности

К дополнительным возможностям можно отнести наличие таких методов в интерфейсе IStorage:

function CopyTo(ciidExclude: Longint; rgiidExclude: PIID; snbExclude: TSNB; const stgDest: IStorage): HResult; stdcall;

Копирование содержимого хранилища в другое хранилище:

function MoveElementTo(pwcsName: POleStr; const stgDest: IStorage;pwcsNewName: POleStr; grfFlags: Longint): HResult; stdcall;

Перемещение хранилища в другое хранилище:

function Commit(grfCommitFlags: Longint): HResult; stdcall;

Подтверждение изменетий внесенных в хранилище. Используется совместно с флагом STGM_TRANSACTED при открытии или создании хранилища:

function Revert: HResult; stdcall;

Отмена изменений вносимых в хранилище. Используется совместно с флагом STGM_TRANSACTED при открытии или создании хранилища:

function DestroyElement(pwcsName: POleStr): HResult; stdcall;

Удаление элемента из хранилища:

function RenameElement(pwcsOldName: POleStr; pwcsNewName: POleStr): HResult; stdcall;

Переименование элемента хранилища:

function SetElementTimes(pwcsName: POleStr; const ctime: TFileTime; const atime: TFileTime; const mtime: TFileTime): HResult; stdcall;

Обновление информации о дате создания, модификации и последнего обращения к элементу хранилища.

Сжатие хранилищ

Как и файловая система, ее аналог - структурированные хранилища данных подвержены фрагментации. Они неспособности "экономично" заполнять освободившееся пространство от удаленных элементов. А самое главное, что в них не предусмотрен механизм автоматического сжатия данных и освобождения незанятых ресурсов диска. Но на каждый фрагмент есть свой "дефрагмент". Так сжать хранилище можно воспользовавшись методом интерфейса IStorage под названием CopyTo:
function CopyTo(ciidExclude: Longint; rgiidExclude: PIID; snbExclude: TSNB; const stgDest: IStorage): HResult; stdcall;

при этом все нужные данные переписываются в новое хранилище, а ненужные (т.е. уже удаленные) исчезают навеки. Примером для такого сжатия может служить код:

procedure TForm1.Button2Click(Sender: TObject);
var Stg,Temp:IStorage;
    Enum:IEnumStatStg;
    Data:TStatStg;
begin
 OleCheck(StgOpenStorage('D:\1.doc',nil,STGM_READ or STGM_SHARE_EXCLUSIVE,nil,0,Stg));
 OleCheck(StgCreateDocFile('D:\1c.doc',STGM_READWRITE or STGM_SHARE_EXCLUSIVE,0,Temp));
 try
 Stg.CopyTo(0,nil,nil,Temp);
 finally
  Temp:=nil;
  Stg:=nil;
 end;
end;
 

конечно такой метод эффективен если данный обрабатываются непосредственно в хранилище, а не действует принцип: "Все прочитал... удалил... изменил... написал наново".

Флаги доступа к хранилищам и потокам


STGM_DIRECT

Каждое изменение содержания сразу же записывается в файл

STGM_TRANSACTED

Изменения записываются в буфер, а потом по команде Commit в файл. Команда Revert отменяет изменения

STGM_SIMPLE

Упрощенный вариант хранения данных:

·Нет поддержки подхранилищ
·Нельзя повторно открыть поток для записи
·Все потоки имеют длину не меньше 4096
·Поддерживается ограниченное количество методов интерфейсов IStorage и IStream.

STGM_READ

Открытие только для чтения

STGM_WRITE

То же для записи

STGM_READWRITE

Чтение и запись

STGM_SHARE_DENY_READ

Запрет параллельного чтения

STGM_SHARE_DENY_WRITE

Запрет параллельной записи

STGM_SHARE_EXCLUSIVE

Полный запрет на параллельное использование файла

STGM_PRIORITY

Блокирует возможность параллельного внесения изменений в файл по команде Commit

STGM_DELETEONRELEASE

Файл автоматически стирается при освобождении интерфейса. Используется для временных файлов

STGM_CREATE

Стирает существующий файл с тем же именем

STGM_CONVERT

Создает новый файл в поток CONTENTS которого заносит данные из существующего файла с тем же именем , если такой существует

STGM_FAILSAFE

Если существует файл с таким же именем - возвращает значение STG_E_FILEALREADYEXISTS

STGM_NOSCRATCH

При установленном флаге STGM_TRANSACTED вместо внешнего буфера используется незадействованное пространство в самом файле. Более эффективное использование ресурсов компьютера

 

 
Михаил Продан
 
https://tdelphi.spb.ru