私は、15年以上もの間、複合ファイルを扱ってきました。 常に、複合ファイルの長所と短所に関する十分な情報を蓄積してきました。
一方で、それらは実際に非常に便利な情報のストレージであり、その場でデータを変更できます。他方では、この利便性はデータへのアクセス速度によって部分的に平準化されます。
一般的に、通常は複合ファイルは何に使用されますか?
特定のコンテナ(NoSQLサブセット)に保存する必要があるすべてのもの。
たとえば、97〜2003の古いバージョンのMicrosoft Officeのファイル(実際には数十個のファイルで構成されています)は、複合ファイルに保存されていました。 これで、これらも保存され、ZIPのみがコンテナとして使用されます。
MSIインストールパッケージも複合ファイルであり、Thumbs.dbフォルダーのサムネイルキャッシュファイルもこの形式を使用します。
確かに、同じWordには、破損したドキュメントを回復する、または少なくとも回復しようとするユーティリティ(Wordの回復、Word回復ツールボックス、Munsoft Easy Word回復)があります。 独自の結論を引き出すことができます。
ただし、複合ファイルを適切に処理すれば、それらの損傷の問題を解決できます(そして、その方法を示します)。
そして、もちろん、この形式の間違いない利点は、ファイルとフォルダーを備えた本格的なファイルシステムがストレージ内でエミュレートされることです。
ところで、ニュアンス。 記事を始める前に、いくつかのフォーラムで調査を実施しましたが、ほとんどの開発者は複合ファイルを使用せず、単純な理由で、それが何であるかを聞きませんでした。
今、このギャップを埋めます。
1.複合ファイルとその作成に関する一般情報
複合ファイルの構造と内部形式についてはすぐに説明しませんが、これは不要です。
最初に、あなたはそれを「感じる」必要があります-彼が何についてなのか。
それでは、StgCreateDocfileを呼び出して新しい複合ファイルを作成することから始めましょう。
用途では、このActiveXとAxCtrlsのカップルを接続します(便利になります)。
そして今、私たちは書きます:
procedure CheckHResult(Code: HRESULT); begin if not Succeeded(Code) then RaiseLastOSError; end; var TestFilePath: string; WideBuff: WideString; Root: IStorage; begin TestFilePath := ExtractFilePath(ParamStr(0)) + '..\data\simple.bin'; ForceDirectories(ExtractFilePath(TestFilePath)); WideBuff := TestFilePath; CheckHResult(StgCreateDocfile(@WideBuff[1], STGM_CREATE or STGM_WRITE or STGM_SHARE_EXCLUSIVE, 0, Root));
まず、旗に注意を払います。
STGM_CREATEおよびSTGM_WRITE-これら2つのフラグは新しい複合ファイルを作成するために使用され、この場合のSTGM_WRITEフラグの存在は必須です(そうでない場合、フォーカスは機能しません)。
重要:しかし、3番目のフラグSTGM_SHARE_EXCLUSIVEを使用すると、すべてが非常に複雑になります。 2番目の章で説明されているように、読み取り専用モードでファイルを開く場合を除き、その存在は常に必要です。
IDA Pro Freewareで自分で確認できます。
StgCreateDocfileは、VerifyPermsが呼び出されるDfOpenDocfile関数を呼び出します。この関数では、次のようなチェックが行われます。
アドレス72554E62では、このフラグの存在がチェックされ、突然検出されない場合は、エラーが開かれます。 したがって、複数回記録するために複合ファイルを同時に開くことは禁止されています。
3番目のリングでこのようなチェックを見るのは少し驚きで、(実験的に)すくい上げた後、同時に2回録音するためにファイルを開くことができました。 しかし-もちろん、両方のファイルに正しく書き込むことはできませんでした。 :)
実際、これはデータストレージ形式そのもののため、かなり有能な決定ですが、記事の終わり近くで、少し後で説明します。
すべてのチェックが成功し、戻りコードStgCreateDocfileがS_OKである場合、この関数の4番目のパラメーターで、IStorageインターフェイスは、複合ファイルのルート要素を指し示して戻ります。
次に何ができますか?
たとえば、ルートに新しいファイルを作成し(まだファイルシステムがあります)、データブロックを書き込みます。
この関数を書きます:
procedure WriteFile(Storage: IStorage; AName: WideString; Data: AnsiString); var Stream: IStream; OS: TOleStream; begin CheckHResult(Storage.CreateStream(@AName[1], STGM_WRITE or STGM_SHARE_EXCLUSIVE, 0, 0, Stream)); OS := TOleStream.Create(Stream); try OS.WriteBuffer(Data[1], Length(Data)); finally OS.Free; end; end;
最初に、Storage.CreateStream関数を呼び出して新しい「ファイル」を作成します。 これは、以前に検討したStgCreateDocfileとほぼ同じですが、結果として、ファイルのコンテンツが
処理される
IStreamインターフェイスを返し
ます 。
フラグに注意してください:STGM_SHARE_EXCLUSIVEを指定する必要があり、2番目(作成する場合)はSTGM_WRITEまたはSTGM_READWRITEのいずれかでなければなりませんが、 複合ファイルはSTGM_WRITEフラグを使用して作成されました-使用されています。
便宜上、IStreamの処理は、データを記録するTOleStreamレイヤークラスを介して実行されます。
もちろん、これは原理的な問題ではなく、ISequentialStreamインターフェイスのWrite関数の呼び出しを使用できます。ISequentialStreamインターフェイスの子孫はIStreamですが、TOleStreamクラスの操作は簡単です。
前に実装した関数を呼び出します:
WriteFile(Root, 'RootFile', 'First file data');
その結果、RootFileという名前と「最初のファイルデータ」の内容を持つファイルがルートに表示されます。
重要:ここで1つの注意点があります:複合ファイル内のファイルとフォルダーの名前は、31 Unicode文字の長さを超えることはできません(実際、32文字以下ですが、ゼロの終了を忘れてはなりません)。
はい、そのように、フォルダまたはファイルを「123」と呼ぶことができますが、「私の長いファイル名とそれ以上の数字」を呼び出すことはできません。 さらに、仕様上、名前に使用できない文字のセットがあります(0〜0x1F)。
おそらくあなたは言うだろう-なぜそのような制限があるのか、そしてネストの巨大な深さを持つ巨大な分岐ファイルシステムを展開したい場合はどうするのか?
したがって、標準のファイル制限とは異なり、MAX_PATH定数は影響しません。
「マイビッグネーム」という名前の500個のサブフォルダー
簡単ですが、仮想ファイルシステムを使用して作業します-必要なことを行います。 :)
ラムに戻りましょう。ルートにフォルダーを作成します。
CheckHResult(Root.CreateStorage('SubFolder', STGM_WRITE or STGM_SHARE_EXCLUSIVE, 0, 0, Folder));
コードはStorage.CreateStreamの呼び出しとほとんど同じですが、今回は新しく作成されたフォルダーを指す別のIStorageインターフェイスを取得します。
すぐに新しいファイルを作成できます:
WriteFile(Folder, 'SubFolderFile', 'Second file data');
これを行うために、最初のパラメーターはルートではなく、ルートを参照しますが、作成されたばかりのForderです。
重要:そして今、ニュアンスは、今すぐアプリケーションを閉じると、データが保存されない場合があります。
ここでは、実際、すべてがそれほど単純ではありません。たとえば、自宅のマシンではこの動作が再現されることが保証されていますが、動作しているマシンではまったく逆です。
データの保存を保証するには、次のコードを実行する必要があります。
CheckHResult(Root.Commit(STGC_DEFAULT));
このコードを実行すると、すべてのデータがディスク上のファイルに保存されることが保証されます。 さて、突然 "突然"気が変わった場合は、次のコードを呼び出すことで、前回のコミット以降に発生したすべての変更を取り消すことができます。
CheckHResult(Root.Revert);
ところで、ファイルを閉じることについて。
これは、ルートの通常のブロック解除によって行われます。その後、@ IntfClearがルート変数のインターフェイスに対して呼び出されると、他のすべてのインターフェイスが階層順に破棄されます。
他に何が残っていますか?
ええ、もっとCopyTo / MoveElementTo / EnumElementsメソッドなど...
それらについては少し後で説明しますが、今のところは、記事に添付されたアーカイブを開き
、ファイル ".. \ simple \ StorageCreateDemo.dpr"で上記のコードの実装を確認
できます。今、私たちはこのすべての問題を読み込もうとしています。
2.複合ファイルの読み取り
新しいプロジェクトを作成し、ActiveXとAxCtrlsを再度接続して、開始コードを記述しましょう。
var TestFilePath: string; WideBuff: WideString; Root: IStorage; begin TestFilePath := ExtractFilePath(ParamStr(0)) + '..\data\simple.bin'; WideBuff := TestFilePath; CheckHResult(StgOpenStorage(@WideBuff[1], nil, STGM_READ or STGM_SHARE_DENY_WRITE, nil, 0, Root));
書き込みアクセスが必要ないため、STGM_READフラグを使用します。ここでは、STGM_SHARE_DENY_WRITEを使用するか、STGM_SHARE_EXCLUSIVEのままにするかを選択できます(2つのフラグのいずれかが必要です)。
コード実行の結果は、ルートを指すIStorageクラスのルート変数です。
ディスク上の指定されたフォルダー内のファイルをどのように検索しますか?
当然、FindFirstFileを使用した再帰的なディレクトリトラバーサル。
この場合、似たようなものがあります。これはIStorageインターフェイスのEnumElementsメソッドで、呼び出しは次のようになります。
var Enum: IEnumStatStg; begin CheckHResult(Storage.EnumElements(0, nil, 0, Enum));
大まかに言うと、これはFindFirstFile呼び出しに類似していますが、ここではさらに処理するためのハンドルではなく、IEnumStatStgインターフェイスを取得します。
注目する価値がある興味深い点が1つあります。
このインターフェイス(使用する場合)はTStatStg構造を返します。そのフィールドの1つは、タイプが
POleStrであるpwcsNameパラメーターになります。
シムスの状況を理解していますか?
もちろん、OLEはネイティブメモリマネージャーの存在を認識せず、IMallocインターフェイスを介して独自の手段でこの行を格納するためのブロックを割り当てるため、これは潜在的なメモリです。
この状況を処理しない場合、アプリケーションメモリはビクトリアフォールズのように流れますが、メモリ消費カウンタを見るのは楽しいでしょう。 :)
したがって、最初にこのインターフェイスのインスタンスへのリンクを取得する必要があります:
if (CoGetMalloc(1, ShellMalloc) <> S_OK) or (ShellMalloc = nil) then raise Exception.Create('CoGetMalloc failed.');
割り当てられたメモリを解放するために必要ではありません。
このようなもの:
ShellMalloc.Free(TmpElement.pwcsName);
さらに、もう1つのニュアンス:
返されるTStatStgのデータ型は、次の値を取ることができます。
- STGTY_STORAGEはフォルダーです
- STGTY_STREAMはファイルです
他のオプションはすべて純粋にサービスであり、興味はありません。
これがどのように起こるかを見てみましょう。
procedure Enumerate(const Root: string; Storage: IStorage); var Enum: IEnumStatStg; TmpElement: TStatStg; ShellMalloc: IMalloc; Fetched: Int64; Folder: IStorage; AFile: IStream; begin
そして、最初の章で作成されたファイルを読んだときに何が起こるか見てみましょう:
実際、これはまさに最初の章で記録したデータです。
この例のコードは記事のアーカイブにあり
、パス「.. \ simple \ StorageReadDemo.dpr」に
従っています。次に、これをもう少し便利に使用する方法を見てみましょう。
3.ラッパークラス
かつて、私は小さなモジュール(コメント付きの1000行)を開発しました。このモジュールは、複合ファイルを操作する際のすべてのニュアンスを考慮し、より便利な作業メカニズムを提供するいくつかのクラスを実装します。
アーカイブの
「.. \ StorageReader \ FWStorage.pas」フォルダにあります 。
いくつかの欠陥があります。 実際、私はその開発をかなり前に放棄したため、Delphiのユニコードバージョンでは、文字列の操作に関連する不満を発行します。
[dcc32警告] FWStorage.pas(860):W1057「AnsiString」から「string」への暗黙的な文字列キャスト
[dcc32警告] uStgReader.pas(102):W1057「ShortString」から「string」への暗黙的な文字列キャスト
しかし同時に、それは非常に機能的であり、これらの変動はパフォーマンスに影響を与えません。 (正直に言うと、それらをとかすには面倒です)。
このモジュールは、次の予約で自由裁量で使用できます。
クラスコードを突然変更し(ものを追加し、見つかった場合はエラーを修正して)、それをインターネットにアップロードすると、モジュールの作成者の名前がヘッダーに保存されます。
私はもうこのモジュールに同行していません(私にとっては時代遅れです)ので、すぐに完了するためのリクエストを拒否します。
したがって、このモジュールでは、複合ファイルを操作するTFWStorageクラスと、IStorageのラッパーであるTFWStorageCursorクラスに興味があります。
最初に、これらのクラスのメソッドをリストし、次にそれらを操作する例を示します。
したがって、TFWStorageクラスは、ファイルの操作のみを目的としており、いくつかの実用的なメソッドを提供します。
- OpenFile、OpenFileReadOnly-まあ、すべてが明確で、単に複合ファイルを開きます。 どちらのメソッドも、ファイルのルートディレクトリを指すTFWStorageCursorクラスを作成して返します。
- CloseFile-したがって、以前に開いたファイルを閉じます。
- 再接続-以前に開いたファイルを再検出します。 TFWStorageCursorも返します。
- 圧縮-指定したファイルを圧縮し、断片化したブロックを削除します。 圧縮ファイルは開かないでください。
- IsStgValidBinaryFmt-指定したファイルですべてが正常であり、その構造が破壊されていないかどうかを確認します。 指定されたファイルは開かないでください。
- ForceStorage-指定されたパスにある複合ファイル内にフォルダーを作成または開きます。 パスはルートから指定する必要があり、「\」がセパレータとして使用されます。 例:「ファイル\ Subfolder1 \ subfolder2 \ subsubfolderへのパス」。 TFWStorageCursorを返します。
つまり 原則として、その主なタスクは、TFWStorageCursorクラスのインスタンスを提供することです。これにより、複合ファイルの主な作業が行われます。
彼の方法は次のとおりです。
- CreateStorage-現在のフォルダー内に新しいフォルダーを作成し、新しく作成されたフォルダーを指すTFWStorageCursorを返します。
- OpenStorage-現在のフォルダー内のフォルダーを開きます。 開いているフォルダーを指すTFWStorageCursorを返します。
- DeleteStorage-現在のフォルダー内の指定されたフォルダーを削除します。
- コピー-指定したファイルまたはフォルダーを現在のフォルダーから別のフォルダーにコピーします。 コピー先のフォルダーは、TFWStorageCursorクラスの形式で転送されます。
- MoveTo-Copyメソッドと同様に、コピーされるアイテムのみが現在のフォルダーから削除されます。
- CreateStream-現在のフォルダーに空のファイルを作成します。
- ReadStream-指定されたファイルの内容を読み取ります。
- WriteStream-新しいデータをファイルに書き込みます。 この名前のファイルが存在しない場合は、作成します。
- DeleteStream-現在のフォルダー内のファイルを削除します。
- FlushBuffer-変更を保存します。
- 名前の変更-現在のフォルダー内の指定されたファイルまたはフォルダーの名前を変更します。
- 列挙-現在のフォルダーの内容をリストし、TFWStorageEnumの配列として結果を返します
- 後方-親フォルダーへのリンクを返し、それ自体が破棄されます(ルートでない限り)。
- リリース-現在のクラスを破棄します。
- IsRoot-現在のクラスが複合ファイルのルートフォルダーを指しているかどうかを示します。
- GetName-現在のフォルダーの名前を返します。
- パス-現在のフォルダーへのパスを返します。
- ストレージ-すべての子サブフォルダーのリスト。
ご覧のとおり、IStreamにはラッパーはありません。このインターフェイスでの作業は、CreateStream、ReadStream、WriteStreamメソッドに委任されます。
Enumerateメソッドが返すTFWStorageEnum配列では、pacsNameに割り当てられたメモリを解放する必要はありません。これは既に完了しており、ネイティブメモリマネージャによって割り当てられたメモリに格納されているデータのコピーを使用しています。
Backwardメソッドで発生する可能性がある唯一の質問は、それがどのように発生するかということです。
そして今、私はそれが本当に便利であることを示します。
ここで、たとえば、そのようなパスを開く必要がある場合:「ファイル\ Subfolder1 \ subfolder2 \ subsubfolderへのパス」。これは、第2章の通常のインターフェイスを使用して実行する必要があります。
ファイル自体を開き、ルートを指すIStorageインターフェースを取得してから、最初のフォルダーのIStorageを取得し、次に2番目と3番目のフォルダー(「サブサブフォルダー」)のIStorageを取得します。
これらはどこかに格納する必要がある最大4つの要素です。
TFWStorageを使用すると、物事がずっと簡単になります。
procedure TForm1.Button1Click(Sender: TObject); var Path: string; Storage: TFWStorage; Root, Folder: TFWStorageCursor; Data: TStringStream; begin Storage := TFWStorage.Create; try
プログラミングの観点から見ると、非常に便利でした。
それでは、もっと深刻なもの、つまり複合ファイルの内容のエディターを作成しましょう。
新しいプロジェクトを開き、その中に次のようなものを作成します。
プライベートでは、3つの変数を追加します。 private FCurrentFileName: string; FStorage: TFWStorage; FRoot: TFWStorageCursor;
フォームコンストラクターで、次のコードを記述します。 procedure TForm1.FormCreate(Sender: TObject); begin
次に、ファイル自体を開く手順を記述します。これは簡単です。
procedure TForm1.OpenFile(CreateNew: Boolean); begin
これまでのところとても簡単ですよね? 原則として、残りのコードは単純です。
次に、画面にフォルダの内容を表示する手順を記述します。
procedure TForm1.ShowStorageData(AStorage: TFWStorageCursor); procedure AddItem(const ACaption: string; AIndex: Integer); begin with ListView1.Items.Add do begin Caption := ACaption; case AIndex of -1: ImageIndex := -1; 1: begin ImageIndex := 0; SubItems.Add('Folder'); end else ImageIndex := 1; SubItems.Add('File'); end;
すべてが正常に完了したら、プロジェクトを開始すると、最初の章で作成したファイル「.. \ data \ simple.bin」が開き、すべてが次のようになります。
それでは、リポジトリをナビゲートしましょう。
そのロジックは簡単です:
- フォルダをダブルクリックします-フォルダを開いて、その内容を表示します。
- ファイルをダブルクリックします-ファイル内容エディターを開きます。
- 要素「..」をダブルクリックします-次のレベルに進みます。
これを行うには、ListViewのOnDblClickイベントハンドラーで次のコードを記述します。
procedure TForm1.ListView1DblClick(Sender: TObject); begin
これで、ダブルクリックでストアを歩き回ることができます。 :)
次のようにファイルを編集します。 新しいフォームをプロジェクトに接続し、保存ボタンとキャンセルボタンを追加します。さらに、ファイルの内容が表示されるTMemoを追加し、その後、次のコードを記述します。
procedure TForm1.EditFile; var Buff: TMemoryStream; Data: AnsiString; begin Buff := TMemoryStream.Create; try
さて、ここにはほぼ本格的なエディターがありますが、フォームの上部にあるボタンの機能を追加することは残っています。
新しい複合ファイルを作成し、既存のファイルを開くためのハンドラーは次のようになります。 procedure TForm1.btnCreateDFaseClick(Sender: TObject); begin if SaveDialog1.Execute then begin FCurrentFileName := SaveDialog1.FileName; OpenFile(True); end; end; procedure TForm1.btnOpenDBaseClick(Sender: TObject); begin if OpenDialog1.Execute then begin FCurrentFileName := OpenDialog1.FileName; OpenFile(False); end; end;
これは、新しいフォルダーを作成し、既存のフォルダーを削除するためのボタンコードになります。 procedure TForm1.btnAddFolderClick(Sender: TObject); var NewFolderName: string; Tmp: TFWStorageCursor; begin if InputQuery('New folder', 'Enter folder name', NewFolderName) then begin FRoot.CreateStorage(AnsiString(NewFolderName), Tmp); FRoot.FlushBuffer; end; ShowStorageData(FRoot); end; procedure TForm1.btnDelFolderClick(Sender: TObject); begin if Application.MessageBox( PChar(Format('Delete folder: "%s"?', [ListView1.Selected.Caption])), 'Delete folder', MB_ICONQUESTION or MB_YESNO) = ID_YES then begin FRoot.DeleteStorage(AnsiString(ListView1.Selected.Caption)); FRoot.FlushBuffer; ShowStorageData(FRoot); end; end;
同じこと、ファイルを開いて削除するボタンのみ procedure TForm1.btnAddFileClick(Sender: TObject); var NewFileName: string; begin if InputQuery('New file', 'Enter file name', NewFileName) then begin FRoot.CreateStream(AnsiString(NewFileName)); FRoot.FlushBuffer; end; ShowStorageData(FRoot); end; procedure TForm1.btnDelFileClick(Sender: TObject); begin if Application.MessageBox( PChar(Format('Delete file: "%s"?', [ListView1.Selected.Caption])), 'Delete file', MB_ICONQUESTION or MB_YESNO) = ID_YES then begin FRoot.DeleteStream(AnsiString(ListView1.Selected.Caption)); FRoot.FlushBuffer; ShowStorageData(FRoot); end; end;
それだけです、私の意見では簡単です:) これで見ることができますが、DOCファイル内に保存されているものは何ですか? :)
おそらくこれで停止し、複合ファイルが引き起こすさまざまなトラブルの説明に移るでしょう。そして、アーカイブ内のこのサンプルのソースコードをパスに従って取得できます: ".. \ StorageReader \"4.複合ファイルの短所
, , , .
— .
.
— .
, . :))
, , .
, , :
- → →
- → → →
- 建設グループ→建設→キュー→オブジェクト→見積
実際、これらのすべての建設プロジェクト、オブジェクトなど(推定を除く)は階層オブジェクトにすぎません-基本的にフォルダーですが、これらのフォルダーは厳密に定義された順序で移動する必要があります。ファイルシステムツールを使用してこの階層を作成し、フォルダーの種類(階層のレベルが「ビルド/オブジェクト/キュー」が属する)を実装することを想像すると、たとえば、ルート(la thumbs.dbにある) 、それから指揮者の中でこの構造全体を台無しにすることができる過度に遊び心のあるペンを持っているユーザーで何をすべきでしょうか?これらの考慮事項から、何年も前に、データウェアハウスとして複合ファイルを選択したのは、ユーザーがそこに侵入してすべてを壊すことができないからです。このストレージ形式を使用して、フォルダ作成の目的の階層を制御し、ユーザーが足で撮影するのを防ぐことができます。しかし、ニュアンスが出てきました。当社のソフトウェアで新しい見積もりを作成するとき、何らかの理由で、ユーザーは、見積もりの名前で観察したことに関する完全な情報を表示しようとします。例:「旧素材のトラックのオーバーホール。 Selegvozh-Chimセクション、片道、142 pc1-163 pc10、長さ22.0 km”。私たちは思い出します-ファイル名の長さの制限はわずか31文字であり、これは極端な場合です:「古いパスへのオーバーホール」。いいえ、「ウェイの修理」と名付けます。そしてもちろん、パスに沿ってファイル名を虐殺すると、ユーザーはひどく気分を害するので、それについて何かしなければなりませんでした。:
, ( — ), . «Properties», ( , , ) ( 1024 — , , ).
— — «Data», .
:
, ?
GUID «» 31 . :)
31 , - .
, 5 ?
-, , 5 , . , , — . :)
「デザイン研究所」と呼ばれるそのようなクライアントがあります-ロシア中にはそれらの多くがあり、そこでは膨大な数の見積り者が働いています。彼らはすべてプロであり、したがって、単に「巨大な」一連の推定値で常に動作します-これが仕事です。そしてある日、次のようなバグレポートが寄せられました。「皆さん、ソフトウェアの起動を待つのにうんざりしていますが、毎日何をしているのですか?」また、これらのオフィスの一部は、サイドに提供できないデータを処理しているため(一部の政府機関は観察しています)、まったく理解できませんでした-野生のブレーキはどこから来たのですか?!!!しかし、幸運なことに、データは機密ではなく、私たちに提供されました。そして今、私のフォルダにはほぼ2ギガバイトのファイルがあります約20万件の見積もりを船内で(部門)もちろん、私はそのようなボリュームに夢中でしたが...しかし、本当に-必要かどうかにかかわらず、このサイズのファイルは5分以上開かれませんでした(次の章でその理由を理解します)。彼らは速度のテストを開始し、実験的に確立されました:すでに50メガバイトのサイズの複合ファイルのボリュームで作業するのは気に入らないでしょう。テストはもちろんテストですが、何かする必要があります。できるだけ早く、複合ファイルではなくデータベースに保存されたデータを処理するネットワークサービスが実装されました。さらに、彼らはすぐに、通常のFirebird / Interbaseとより深刻なデータベースの両方(MS SQL / Olracle、およびADOがヒープにねじ込まれた)のサポートを追加しました。複合ファイルからデータベースにデータを変換する小さなユーティリティも作成されました。私たちはテストします-それは飛ぶ、私の母は心配しませんが、ニュアンスがあります。2ギガファイルからすべてのデータを取得してデータベースに転送することはできません。ある時点で、OpenStorageを介して次のフォルダーを開くと、開くエラーが発生する可能性があります。この場合、これ以上心配する必要はありません。呼び出しはエラーになります。これはまさにその目的のためであり、TFWStorageに2つのメソッドが追加されました。ReConnect-複合ファイルを再検出できる方法と、エラーが発生したフォルダーをすぐに開くForceStorageメソッドです。ただし、その時点では、まだ十分なコーンが得られず、別の製品で複合ファイルを使用していました。このプロジェクトは、情報照会システム(ISS)であり、推定者に彼に必要なドキュメント(MSDNの偏狭な類似物)へのアクセスを提供します。そして今がきて、ベースを埋める責任のある技術者がやって来て、「ベースが開かないので、次のアップデートをリリースできません」と言います。私はチェックし始めています。, , StgOpenStorage.
…
— - 4 , .
, . .
, — , ( ) — MSDN , . , .
, :
たとえば、ファイル(ストリーム)に何かを書き込み、EnumElementsを呼び出して、ファイルが存在するかどうかを確認しようとします。また、この呼び出しによって返されるIEnumStatStgは、そのようなストリームを認識しません。次に、CreateStreamを呼び出して作成しますが、エラーが発生します。この場合、このポイントを回避するのは非常に簡単です。指定されたストリームに対してDestroyElementを呼び出し、CreateStreamを呼び出して再作成するだけで十分ですが、これは非常に素晴らしい呼び出しです。「複合ファイルの問題は本当に問題です」。そして、彼にとっては本当に悪いので、私たちは彼からまだ利用可能なデータを取得する方法を学ぶ必要があります。5. RAWモードでのデータの読み取り
あなたは今、この意見を持っていると思う:まあ、あなた自身、nifiga、複合ファイルを使用するとき、いくつの問題があるでしょうか?なぜ著者はそれらについて話すのでしょうか?これは正しい意見ではありません。現在使用されているさまざまなテクノロジーに大量のエラーを投げることができますが、これは、これが最初に失敗したことを意味するものではありません。たとえば、対応するAPIを呼び出すために誤ったパラメーターを使用するだけで、3番目のリングから直接、NTFSファイルシステムに基本的に削除できないフォルダーを直接作成できることがわかっている場合、ファイルシステムの使用を拒否しませんか? :)
複合ファイルは本当に優れていますが、正しく調理する方法を学ぶ必要があります。一般的には、MS は技術の説明を公開形式で公開しており、この形式で作業を開始したとき、Java POIFSからの形式の説明、Wikiからの短い抜粋、およびPOIFSの構造の詳細な説明を含む別のファイルしか取得できませんでした(特に省庁によると))、しかし私は今それを見つけることができません(何年も経ちました)。だから、私が必要なときにフォーマットを開かなかったのです。 :)
したがって、私は自分ですべてを選択する必要がありました。複合ファイルを構成するもの、つまりヘッダーを調べます。 TPoifsFileHeader = packed record
さて、ここではすべてが明確で、すべてのkamentyが添付されていると思います-最初にこの構造を検討する必要があります。_uSectorShiftフィールドと_uMiniSectorShiftフィールドの唯一のものは2の累乗であるため、それらを通常に戻す必要があります。 procedure TPoifsFile.InitHeader; begin FStream.ReadBuffer(FHeader, SizeOf(TPoifsFileHeader)); FHeader._uSectorShift := Round(IntPower(2, FHeader._uSectorShift)); FHeader._uMiniSectorShift := Round(IntPower(2, FHeader._uMiniSectorShift)); end;
次に、FATを読み取る必要があります。FATは、ヘッダーの_ulMiniSectorCutoffフィールドの値以上のサイズのファイルに関するデータを格納します。 procedure TPoifsFile.ComposeFAT; var I, J, X, FatLength: Integer; FatBlock: TPoifsFatBlock; CurrentFat, Offset: Integer; XFat: array of Integer; begin
ここではTPoifsFatBlock構造が使用されます。これは、128個の整数の配列です。GetBlockOffsetおよびGetBlock関数と同様に。それらは本質的に単純です。 function TPoifsFile.GetBlockOffset(BlockIndex: Integer): Int64; begin Result := HEADER_SIZE + FHeader._uSectorShift * BlockIndex; end; function TPoifsFile.GetBlock(Adress: Integer): TPoifsBlock; begin FStream.Position := GetBlockOffset(Adress); FStream.ReadBuffer(Result, SizeOf(TPoifsBlock)); end;
次の手順では、サイズが_ulMiniSectorCutoffフィールドの値より小さいファイルに関するデータを保存するミニファットを検討します。 procedure TPoifsFile.ComposeMiniFat; var I, CurrChain: Integer; TmpPosition: int64; begin
最後の手順は、すべてのファイルとフォルダーのプロパティを読み取ることです。これらは、これらの構造の配列に保存されます。 TPoifsProperty = packed record
そして、次のコードでそれらを読みます。 function TPoifsFile.ReadPropsArray: Boolean; var I, J, Len: Integer; PropsBlock: TPoifsPropsBlock; begin Result := True;
その後、手元にあります:- FAT値の配列。各値にはデータ付きのセクション番号が含まれます。
- ファイル内のデータへのオフセットの配列
- MiniFAT値の配列
- すべてのファイルのプロパティの配列
FATおよびMiniFATとは何ですか?大まかに言うと、複合ファイルはヘッダーであり、他のすべてが存在するFHeader._uSectorShiftのサイズのデータセクターの配列です。FATには、これらのセクターにデータを保存する手順が含まれています(ファイルの内容と、ユーザーがアクセスできない厳密なサービスブロックの両方)。たとえば、サイズが1メガバイトのファイルがあり、正確に2048セクターが割り当てられます。各セクターのサイズは512バイトです(デフォルト)。断片化により、このファイルのデータは常に連続して送信されるとは限らず、最初の10セクターにファイルの終わりが含まれ、残りがその始まりになる可能性があります。, ( ) FAT StartBlock TPoifsProperty, , , ( FAT).
, , .
, .
. , TPoifsProperty, PreviousProp, NextProp ChildProp, NodeColor. Red-Black-Tree.
, .
, .
, :
: ( TreeView), .
, Extract:
begin FileStream := TFileStream.Create(edSrc.Text, fmOpenReadWrite); try AFile := TPoifsFile.Create(FileStream); try
最初の段階(ファイルからのデータの読み取り)は既に実装されています。2番目と3番目に進みましょう。考えすぎなかったので、ツリーの構造を復元するために、ソリューションの基礎としてグラフを使用しました。考え方は簡単です。まず、グラフにN個のノードを追加します。各ノードはTPoifsProperty配列の要素の1つを担当します(これは実際にコードで表示される「ノードを埋める」ブロックです)。そして次のステップは、ノード、誰が、何を参照するかの間のクロスリンクを構築することです。一般に、ツリー自体は非常に単純に構築されます。主なことは、いくつかの単純なルールに従うことです。- TPoifsProperty.ChildProp-常にフォルダー内の最初の子を指します(フォルダーのみで満たされます)
- TPoifsProperty.PreviousProp-現在のフォルダー内の前のアイテムを示します。
- TPoifsProperty.NextProp-現在のフォルダー内の次のアイテムを示します。
より明確にするために、ここにあなたのための写真があります:下矢印はChildProp、右はNextProp、左はPreviousPropです。その結果、複合ファイルのルート内に2つのファイルと1つのフォルダーがあり、その中にさらに3つのファイルがあることがすぐに明らかになります。ただし、第3段階がどのように見えるか、つまり、グラフノード間のリンクの構築を見てみましょう。 var ATree: TStorageTree; ... procedure FillAllChilds(RootIndex, CurrentIndex: Integer); var SubChildIndex: Integer; RootNode, CurrNode, ChildNode: TStorageElement; begin if CurrentIndex < 0 then Exit;
TStorageTreeクラスで表されるグラフクラスの実装は、記事のトピックとは関係がないため、考慮しません。このクラスのコードはsourceに表示されます。現時点では、インデックスによってグラフノードを返すGetNodeメソッド(Dataプロパティを介して制御されるTPoifsProperty配列要素への参照を含む)と、グラフの2つのノード間のリンクを作成するAddVectorメソッドを知っているだけで十分です。次に、第4段階-グラフに基づいて、フォルダーとファイルのツリーを作成します。 procedure FillTree(Node: TTreeNode; RootNodeIndex: Integer); var W: WideString; TreeNode: TTreeNode; I: Integer; RootStorageNode, ChildStorageNode: TStorageElement; begin
一般的な再帰。ルートから開始してグラフのノードを大まかに実行し、従属ノードへのリンク(グラフのエッジGetVector(I)。SlaveNodeから取得)が現在のフォルダーに格納されます。NodeColorフィールドを考慮して、「Red-Black-Tree」に構造ツリーを構築しなかった理由を尋ねますか?しかし、道化師は彼を知っています。私はこのアルゴリズムを十数年前に書きましたが、「うまくいきます-何も触れないでください」という黄金のルールがあります。 :)
次に、5番目の段階に進みます-データを外部フォルダーに抽出します。ただし、このためには、ファイルに関連付けられたデータセット全体を取得する方法を正確に把握する必要があります。FATセクションについて述べたことを思い出してください-データは常に連続して送信されるとは限らず、FATで記述されたセクションのインデックスに焦点を当てて取得する必要があります。「大きなファイル」(ヘッダーの_ulMiniSectorCutoffフィールドに保存されている値以上のサイズ)のデータを取得する方法を参照してください。 procedure TPoifsFile.GetDataFromStream(ChainStart: ULONG; NeedLength: DWORD; const Stream: TStream); begin Stream.Size := 0; while (Integer(ChainStart) >= 0) and (Stream.Size < NeedLength) do begin
セクター内のデータはブロック単位で保存され、実際のファイルサイズは常にそのサイズの倍数ではないため、最終編集が必要です。同意します。最終改訂版を削除するには、少し書き直す価値がありますが、必要ですか? :)
ただし、パラメーターの点では、秘密が何もないChainStartに興味があります。これは、TPoifsProperty構造体のStartBlockフィールドの値です。そして、_ulMiniSectorCutoffよりも小さなファイルのデータを取得します。 procedure TPoifsFile.GetDataFromMiniStream(ChainStart: ULONG; NeedLength: DWORD; const Stream: TStream); var MiniStreamOffset: DWORD; RealMiniStreamSector, TmpChain: Integer; begin Stream.Size := 0; while (Integer(ChainStart) >= 0) and (Stream.Size < NeedLength) do begin
少しトリッキーですよね?確かに、よく見ると、ここでの変更は、TmpChainと同じFATを介したオフセットの計算のみです。セクターが許容値(8)を超えてノックアウトされた場合、ミニファットへのリンクがルートにあるため、TmpChainが許容値より小さくなるまで、ルートからFATチェーンに沿って進みます。これで、指定したファイルからデータを抽出する手順を作成できます。 procedure GetStorageData(ANode: TStorageElement; const Stream: TStream); begin if ANode.Data.Size < Integer(AFile.Header._ulMiniSectorCutoff) then AFile.GetDataFromMiniStream(ANode.Data.StartBlock, ANode.Data.Size, Stream) else AFile.GetDataFromStream(ANode.Data.StartBlock, ANode.Data.Size, Stream); end;
この手順では、複合ファイルからフォルダーに抽出されたグラフのノードを転送し、そのサイズに基づいて、上記で実装したメソッドの1つを呼び出します。ほとんどすべてのように見えますが、最後の5番目のステージは残ります-複合ファイルの内容全体をフォルダーに解凍します。 procedure Extract(Path: string; RootNodeIndex: Integer); var W: WideString; I: Integer; RootStorageNode, ChildStorageNode: TStorageElement; F: TFileStream; begin RootStorageNode := ATree.GetNode(RootNodeIndex); W := RootStorageNode.Data.Caption; case RootStorageNode.Data.PropertyType of STGTY_STORAGE: Path := Path + W + '\'; STGTY_STREAM: begin try ForceDirectories(Path); F := TFileStream.Create(Path + W, fmCreate); try GetStorageData(RootStorageNode, F); finally F.Free; end; except DebugLog.Add(Path + W); end; end; end; for I := 0 to RootStorageNode.VectorCount - 1 do begin ChildStorageNode := TStorageElement(RootStorageNode.GetVector(I).SlaveNode); if ChildStorageNode = nil then Continue; if ChildStorageNode.ID <> RootNodeIndex then Extract(Path, ChildStorageNode.ID); end; end;
さて、ここでコメントなしですでに。私たちはすべて以前に見ました-通常のアルゴリズム。作成したプロジェクトを起動して、Wordドキュメントに設定すると、次のようになります。そして、私たち全員がそれを手に入れたパパには、これがあります:すべてのファイルではありませんか?とても簡単です。ツリーの名前を見ると、「|」の形でダッシュが表示されます。 「CompObj」の前のファイル名または理解できないスペースの前。これらは、最初の章で言及したOLE用に予約されている文字です(0〜0x1F)。名前にそのような文字を含むファイルを作成できないため、スキップされますが、それらのデータは「cannotread.log」というログに書き込まれます。もちろん、これは簡単に処理できますが、デモでも同様です。この例のコードは、フォルダー「.. \ RawStorageReader \」 のアーカイブにあります。しかし、なぜこれをすべて書いたのですか?前の章で書いたアプリケーションを使用して、このファイルを開きましょう: ".. \破損した\ corrupted_storage.bin"次のようなものになります。それでは、APIから直接、第2章の読者を読んでみましょう。悲しみ、その後、どのようなトラブルが発生したかを見て、このファイルを既にRAWモードで開きます。ええ、エラーを読んで、スタックを見てください:ファイルプロパティの読み取り段階でエラーが発生しました。GetBlock関数でReadBufferを実行できませんでした。決定します。6.データのエラーを修正し、利用可能なものすべてを読み込もうとします。
記事の冒頭で、Wordのドキュメントのさまざまな「復元者」のコホート全体についてお話しました。今、私たちはそれらのようなものを書きます。 :)
これらのユーティリティはすべて2つのモードで動作します。- 複合ファイルの形式について知っています。
- Wordがストリームに保存するデータ形式について知っています。
もちろん、2番目の段落に関する情報はありませんし、必要ありませんが、最初の段落については、5番目の章を読んだ後、ご存じのとおりです。 :)
:
最初のものは処理されません。いいえ、まあ、多分、FATを構築する最初のセクターの値を計算しようとするファンがいるでしょうが、私はそのようなものを見たことはありません。2番目は実質的に未処理です。FATにセクター番号が含まれていることがわかっているため、次の条件によって破壊を判断できます。次の値は、ENDOFCHAIN定数(-2)より小さいか、FAT配列のサイズより大きくなります。失敗したブロックの値をENDOFCHAIN定数に変更することで修正できますが、原則として、そのような介入の後でも、ミニファットセクターは部分的に考慮され、ファイルプロパティの配列は最小限にアクセスできます(幸運な場合でも)。3番目のオプションは処理中です。大まかに、セクター読み取りエラーが発生したFATセルのアドレスを調べて、ENDOFCHAINに設定します。もちろん、これはデータの一部を切り取ります(99%のケースが殺されました)が、実際に利用できるものを読みます。4番目のオプションは処理されません。このデータは複合ファイルに属さないため、単に保存するだけで、制御はしません(サイズだけがなくなります)。分析を開始します:良いことには、3番目の問題にしか対処できません。つまり、失敗したFATインデックスの数を特定し、修正します。RAWモードでデータを操作する方法を知っていれば、それは難しくありません。最初に、データブロックの読み取り手順を変更します。 function TPoifsFile.GetBlock(Adress: Integer): TPoifsBlock; var BlockOffset: Integer; begin BlockOffset := GetBlockOffset(Adress); if BlockOffset < FStream.Size then begin FStream.Position := BlockOffset; FStream.ReadBuffer(Result, SizeOf(TPoifsBlock)); end else raise Exception.Create('Wrong block offset at addres: ' + IntToStr(Adress)); end;
突然何かが一緒に成長しなかった場合、彼女はオフセットをチェックし、例外を発生させましょう。ReadPropsArrayプロシージャで2番目の変更を行い、FAT配列の状態をより厳密に制御します。 function TPoifsFile.ReadPropsArray: Boolean; var I, J, Len, LastGood: Integer; PropsBlock: TPoifsPropsBlock; begin Result := True;
それでは、FixFatEntryプロシージャを記述する必要があります。 procedure TPoifsFile.FixFatEntry(FatIndex, NewValue: Integer); var J, Offset: Integer; begin
元のファイルのFATチェーンに変更を加えるのは、彼女の助けを借りてです。では、何が起こったのか見てみましょう。… :)
, , , .
, , FixFatEntry FStream.WriteBuffer.
, , , , , , .
:)
: "..\RawStorageReader\PoifsWithRepair.pas" .
, .
7.
— , .
, , — , , ,
( , ) .
— , .
, , ?
, , — , , :)
, , :
, , , , .
() .
, , , .
:
8 (, RAW ) 473 , .
, — 150 , .
24 , 12 .
カウント:1日150,000回の打ち上げ* 24日* 12か月* 8年= 345、数百万回の打ち上げ(平均)。実際、私の手元にはちょうど473個の「半ば焼き」ファイルがあります(月に1〜2回発生することもありますが、数か月にわたって落ち着くこともあります)。これらのうち(この点を考慮する必要があります)、約100人がbeatられたFATを使用していました(そして、私が言ったように、beatられたベール、それは非常に悪いです)。そのため、FATレベルで破られた数百のファイルのほとんどすべてが複合ファイルを削除し、UnEraseなどのユーティリティで復元され、尋ねられました。そして、それらとは何の関係もありません-削除しませんでした。復元することはできません。したがって、この100個を返して、見てみましょう:複合ファイルを壊す可能性は何ですか?はい、100万分の1-記録速度の速いCDはこの数よりも頻繁に失敗します:)信じられない?
( , ) ?
?
— : .
«» — :)
, , — , ( , ).
, :
- , — , .
- AddVectoredExceptionHandler. — SEH. , , — . ( , False Alarm :)
,
.
, "
" .
: , ( 1024 ). , , NTFS? :)
頑張ってね:)
— (Rouse_)
, 2015