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

Материал из DRKB


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


Перед многими программистами рано или поздно постает вопрос: "В каком формате хранить данные своей программы". Хорошо, если это тип данных с фиксированной длинной, а если надо сохранить разнородные данные, да еще чтоб в одном файле(чтоб потом не разбираться с десятком другим файлов с данными)... Тут на помощь приходить сама 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;

// Подтверждение изменетий внесенных в хранилище. Используется совместно с флагом STGM_TRANSACTED при открытии или создании хранилища:
function Commit(grfCommitFlags: Longint): HResult; stdcall;

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

// Удаление элемента из хранилища:
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 вместо внешнего буфера используется незадействованное пространство в самом файле. Более эффективное использование ресурсов компьютера


Author: Михаил Продан
Source: http://tdelphi.spb.ru
ID: 04323