2019年12月20日

WindowsのNTFSでハードリンクを扱う

このアーティクルはDelphi Advent Calendar 2019の20日目の記事です(1日ぶり6回目)。

Windows上でファイル/ディレクトリに対してリンク(複数のエントリを用意する)する方法にはシンボリックリンクジャンクション、ハードリンクがありますが、ここではDelphiからハードリンクを扱います。

ハードリンクはNTFS上のファイル本体に対するディレクトリエントリを複数用意する(あるいはファイル本体に複数のパス名をつける)、というもので、一般のユーザ権限で作れ、NTFS以外にSMB3.0でもサポート(ReFSは不可)されていますが、ディレクトリを扱うことができず、同一ボリューム上にしかリンクを作ることができません。また最大のリンク数が1023に制限されています。

Windowsのシンボリックリンクとジャンクションとハードリンクの違い:Tech TIPS - @IT

普通に(CreateFile関数で)作成したファイルはリンク数が1になっており、ハードリンクを作成する毎にリンク数が増え、逆にハードリンクをDeleteFile関数で削除する毎にリンク数は減っていき、リンク数が0になるとそのファイルの実体も削除されます。

ここではハードリンクの作成と、指定されたファイルのハードリンクの数と一覧の取得をDelphiから行います。以下のコードは(System.)IOUtilsユニットのTPathレコード型や無名メソッドを使っているためにDelphi 2010以降の対応になっていますが、それ以前のバージョンであっても適当に修正すれば動くはずです。

interface

{$IF RTLVersion < 21.0}
{$MESSAGE ERROR 'Delphi 2010 or later is required.'}
{$IFEND}

uses
{$IF RTLVersion < 23.0}
  Windows, SysUtils, Classes, IOUtils;
{$ELSE}
  Winapi.Windows,
  System.SysUtils, System.Classes, System.IOUtils;
{$IFEND}

type
  { THardLink }
  THardLink = record
  private
    class procedure DoGetFileList(const Filename: String; EnumProc: TProc); static;
  public
    class procedure Create(const LinkFile: String; const SourceFile: String); static;
    class function GetFileList(const Filename: String): TArray; overload; static;
    class procedure GetFileList(const Filename: String; Strings: TStrings); overload; static;
    class function GetNumberOfLinks(const Filename: String): Integer; static;
  end;

implementation

{ HANDLE FindFirstFileNameW(LPCWSTR lpFileName, DWORD dwFlags, LPDWORD StringLength, PWSTR LinkName); }
function FindFirstFileNameW(const lpFileName: PWideChar; dwFlags: DWORD; var StringLength: DWORD; LinkName: PWideChar): THandle; stdcall; external kernel32;
{$EXTERNALSYM FindFirstFileNameW}

{ BOOL FindNextFileNameW(HANDLE hFindStream, LPDWORD StringLength, PWSTR LinkName); }
function FindNextFileNameW(hFindStream: THandle; var StringLength: DWORD; LinkName: PWideChar): BOOL; stdcall; external kernel32;
{$EXTERNALSYM FindNextFileNameW}

class procedure THardLink.Create(const LinkFile: String; const SourceFile: String);
begin
  if CreateHardLink(PChar(LinkFile),PChar(SourceFile),nil) = False then
  begin
    RaiseLastOSError;
  end;
end;

class function THardLink.GetFileList(const Filename: String): TArray;
var
  Files: TArray;
begin
  SetLength(Files,0);

  DoGetFileList(Filename,
    procedure (Filename: String)
    begin
{$IF RTLVersion >= 28.0}
      Files := Files + [Filename];
{$ELSE}
      SetLength(Files,Length(Files) + 1);
      Files[Length(Files) - 1] := Filename;
{$IFEND}
    end);

  Result := Files;
end;

class procedure THardLink.GetFileList(const Filename: String; Strings: TStrings);
begin
  Strings.Clear;

  DoGetFileList(Filename,
    procedure (Filename: String)
    begin
      Strings.Add(Filename);
    end);
end;

class function THardLink.GetNumberOfLinks(const Filename: String): Integer;
var
  hFile: THandle;
  FileInformation: TByHandleFileInformation;
begin
  hFile := CreateFile(PChar(Filename),GENERIC_READ,FILE_SHARE_READ,nil,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
  if hFile = INVALID_HANDLE_VALUE then
  begin
    RaiseLastOSError;
  end;

  try
    if GetFileInformationByHandle(hFile,FileInformation) = False then
    begin
      RaiseLastOSError;
    end;
    Result := FileInformation.nNumberOfLinks;

  finally
    CloseHandle(hFile);
  end;
end;

class procedure THardLink.DoGetFileList(const Filename: String; EnumProc: TProc);
var
  hFindStream: THandle;
  Len: DWORD;
  Buffer: String;
  Root: String;
begin
  Root := ExcludeTrailingPathDelimiter(TPath.GetPathRoot(Filename));

  { Retrieve buffer size }
  Len := 0;
  FindFirstFilenameW(PWideChar(Filename),0,Len,nil);
  if GetLastError <> ERROR_MORE_DATA then
  begin
    RaiseLastOSError;
  end;
  SetLength(Buffer,Len);

  { Get first filename without drive letter }
  hFindStream := FindFirstFilenameW(PWideChar(Filename),0,Len,PWideChar(Buffer));
  if hFindStream = INVALID_HANDLE_VALUE then
  begin
    RaiseLastOSError;
  end;

  try
    while True do
    begin
      { Adjust buffer size }
      SetLength(Buffer,Len - 1);

      { Callback }
      EnumProc(Root + Buffer);

      { Retrieve buffer size }
      Len := 0;
      FindNextFileNameW(hFindStream,Len,nil);
      case GetLastError of
        ERROR_HANDLE_EOF:
        begin
          Break;
        end;

        ERROR_MORE_DATA:
        begin
        end;

        else
        begin
          RaiseLastOSError;
        end;
      end;

      { Get next filename without drive letter }
      SetLength(Buffer,Len);
      if FindNextFileNameW(hFindStream,Len,PWideChar(Buffer)) = False then
      begin
        RaiseLastOSError;
      end;
    end;

  finally
    { Close }
    {$IF RTLVersion >= 23.0}Winapi.{$IFEND}Windows.FindClose(hFindStream);
  end;
end;

ハードリンクはCreateHardLink関数で作成し、リンク数はGetFileInformationByHandle関数で取得したBY_HANDLE_FILE_INFORMATION構造体のnNumberOfLinksで知ることができます。またハードリンクの一覧はFindFirstFileName関数/FindNextFileName関数/FindClose関数で取得できます。このときFindFirstFilename関数/FindNextFileName関数を一旦LinkName=nilで呼び出して必要なサイズを取得し、ファイル名の格納に必要な領域を確保してからもう一度FindFirstFilename関数/FindNextFileName関数を呼び出しています。

WindowsのNTFSでハードリンクを扱う(Gist)

0 件のコメント: