倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

これからの「Java I/O」の話をしようwww (13) : 続々・ファイル、ディレクトリの属性

Java nio の API を見ていくシリーズ(目次)。 今回はファイル、ディレクトリの属性の続きの続き。 どんどんマニアックになっていく気がする(笑) 未来の自分のためのメモ書きみたいなものダス。

この記事の最後では、ファイルの作成と同時に属性を指定する方法を見ます。 特に読み書きの権限などはファイルを作成してから設定してるとセキュリティ的な空白の時間ができるので、ファイル作成時に属性を設定できるようにしておくべきでしょう。

この記事の内容

ユーザー定義属性

使用している OS (というかファイルシステム)が UserDefinedFileAttributeView をサポートしていれば、任意のユーザー定義属性が使用できます。 ユーザー定義属性が使用できるかどうかは FileSystem オブジェクトもしくは FileStore オブジェクトから以下のようにして確認できます(Groovy コード):

// FileSystem から確認
def fileSystem = FileSystems.getDefault()
assert fileSystem.supportedFileAttributeViews().contains("user")

// FileStore から確認
def fileStore = Files.getFileStore(Paths.get("."))
assert fileStore.supportsFileAttributeView("user")
// もしくは
assert fileStore.supportsFileAttributeView(UserDefinedFileAttributeView.class)

さて、ではユーザー定義の属性をちょっと使ってみましょう。 ユーザー定義の属性には FileAttributes に対応するものがないので、属性の読み書きは UserDefinedFileAttributeView を用います。 属性値には java.nio.ByteBuffer オブジェクトを使わないといけないのがちょっと面倒なところ。 具体的な使い方はコード参照。

サンプルコードでは一時ファイルに次の2つの属性

を設定します。 encoding 属性の値自体はシステムデフォルトのエンコーディングを使います。 当然ですが encoding 属性や mimetype 属性を設定しても、読み取り側がそれらを無視すれば無意味です。

import java.nio.ByteBuffer
import java.nio.charset.Charset
import java.nio.file.Files
import java.nio.file.attribute.UserDefinedFileAttributeView

def path = Files.createTempFile(null, null)

//***** 属性の書き込み *****
def view1 = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class)
def enc = Charset.forName("ISO-2022-JP")
view1.write("encoding", ByteBuffer.wrap(enc.name().getBytes()))
view1.write("mimetype", ByteBuffer.wrap("text/html".getBytes(enc)))

//***** 属性の確認 *****
println Files.readAttributes(path, "user:*").collectEntries{
    String key, byte[] value -> [key, new String(value)]
        // ここでは面倒なのでエンコーディングは無視
}

//***** 属性の読み込み *****
// encoding 属性
def view2 = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class)
def encoding = ByteBuffer.allocate(view2.size("encoding"))  // サイズを読み取ってバッファ割り当て
view2.read("encoding", encoding)  // 属性値を読み取る

def charset = Charset.forName(new String(encoding.array()))
assert charset.name() == "ISO-2022-JP"

// mimetype 属性
def mimetype = ByteBuffer.allocate(view2.size("mimetype"))
view2.read("mimetype", mimetype)
assert new String(mimetype.array(), charset) == "text/html"
  • 属性値を書き出すときには、String --(getBytes)--> byte[] --(ByteBuffer#wrap)--> ByteBuffer の手順で String オブジェクトを ByteBuffer オブジェクトに変換しています
  • 属性の確認の箇所で readAttributes() メソッドに渡している "user:*" は、ユーザー定義の属性全てを表す文字列です。 『続・ファイル、ディレクトリの属性』参照。
  • 属性値を読み出すときには、まずバッファサイズ(文字列の長さみたいなの)を UserDefinedFileAttributeView#size() メソッドで取得してから、バッファを割り当て (allocate)、そこに属性値を読み込んでいます。 さらに(必要ならエンコーディングを指定して) String オブジェクトを生成しています。 うーん、面倒だ。

ACL 属性

ACL 属性は、Unix 系の OS でファイルの読み書き実行の権限を指定する POSIX ファイル・パーミッション("rwx------" みたいな文字列)を一般化したような権限指定属性です。 どのように一般化されているかというと、ファイル内容やファイル属性、ACL 属性などを個別に読み書き設定できるところです(他にもあるかも知れませんが)。 ファイルの実行権限がなくても ACL 属性を書き込めれば実質的に実行できるようにできてしまいますからね。

ACL 属性は最近の Windows ではサポートされているので、WindowsPOSIX ファイル・パーミッションの指定のようなことをしたい場合には ACL 属性を使うと良いかと思います。

AclFileAttributeView インターフェース
nio で ACL 属性を扱うためのファイル属性ビューは AclFileAttributeView インターフェースです。 このインターフェースを介すと ACL 属性の読み書きができます。 読み込み専用の FileAttributes はないようです(『Files クラスのメソッド 〜続・ファイル、ディレクトリの属性』参照)。

AclFileAttributeView は FileOwnerAttributeView を拡張していて、ファイル所有者の owner プロパティを持っています。 加えて ACL 属性のリストの acl プロパティも持っています:

package java.nio.file.attribute;

public interface AclFileAttributeView extends FileOwnerAttributeView{

    String name()

    // ower プロパティ(FileOwnerAttributeView から)
    UserPrincipal getOwner()
    void setOwner(UserPrincipal owner)

    // acl プロパティ
    List<AclEntry> getAcl()
    void setAcl(List<AclEntry> acl)
}

AclEntry クラスは後で見ますが、acl 属性の値は AclEntry オブジェクトのリストである点に注意。 ACL エントリを複数指定できるようにするためですが、Files#readAttributes() メソッドで型付けされてないコードを書いてるとコンパイラをすり抜けて実行時例外が投げられたりします。 ちなみに、ACL 権限はこのリストの先頭から探して最初にマッチしたユーザ(グループ)の AclEntry の権限が適用されます。

この AclFileAttributeView クラスを使って ACL 属性を設定するためには以下のようにします(AclEntry オブジェクトの作成方法は後で):

def file = ...

// AclFileAttributeView オブジェクトを取得
def view = Files.getFileAttributeView(file, AclFileAttributeView.class)

// AclEntry の作成(後を参照)
AclEntry entry = ...

// ACL 権限の設定(元々設定されてる権限に作成した権限を追加)
List<AclEntry> acl = view.getAcl()
acl.add(0, entry)    // 作成した権限を最優先させるために先頭へ挿入
view.setAcl(acl)

ファイル属性ビューのオブジェクトを取得してプロパティを設定する、というのは通常のファイル属性設定と同じです。 このビューから AclEntry のリストを取得して、作成した AclEntry オブジェクトを追加し、変更したリストをビューの acl 属性に設定し直しています。 属性ビューへの変更は元のファイルの属性へ反映されますが、acl 属性のリストへの変更は設定し直さないと変更が反映されないので注意。 また、作成した AclEntry を優先させるために、リストの先頭へ挿入しています。

ちなみに、自分でリストを作成して属性ビューに設定することもできますが、通常、デフォルトで設定されている AclEntry があるので上記のような設定方法の方が無難かと思います。

AclEntry, AclEntry.Builder クラス
ACL 権限を表す AclEntry は次の4つのプロパティから構成されています:

  • principal: 権限を指定する対象のユーザもしくはグループ
  • permissions: 設定する権限(読み書き実行など)
  • type: 権限のタイプ(許可する、拒否する、警告を出すなど)
  • flags: 権限継承・伝搬の指定フラグ(指定したディレクトリ内に作成されたファイルに ACL 権限を継承させるなど)

AclEntry クラス自体はイミュータブルで、一度インスタンスを生成すると上記のプロパティを変更できません。 AclEntry.Builder は AclEntry クラスのインスタンスを簡単に作成できるようにする、よくあるビルダークラスです。 使用方法は以下の手順

  1. static メソッド AclEntry#newBuilder() で AclEntry.Builderオブジェクトを作成
  2. 各 setter メソッドでプロパティを設定
  3. build() メソッドで AclEntry オブジェクトを作成

です。 具体的にやってみると

// ユーザの準備
def upls = FileSystem.getDefault().getUserPrincipalLookupService()
def guest = upls.lookupPrincipalByName("Guest")

// ACL エントリの作成
def aclEntry = AclEntry.newBuilder()    // 1. AclEntry.Builder オブジェクトの作成
         .setPrincipal(guest)    // 2. プロパティの設定
         .setPermissions(AclEntryPermission.READ_DATA, AclEntryPermission.READ_ATTRIBUTES)
         .setType(AclEntryType.ALLOW)
         .build()    // 3. AclEntry オブジェクトの作成

のようになります。 Builder のプロパティはどんな順序で設定してもかまいません。 また、flag は設定しなくても OK です(それ以外は必須)。 まぁ、よくあるビルダーの使い方ですね。

ユーザ、グループを表す UserPrincipal オブジェクトは UserPrincipalLookUpService から取得する必要がありますが、それ以外の permission, type, flag は java.nio.file.attribute パッケージに定義されている定数 (Enum) で指定します。

ACL 権限の設定方法はこんな感じで OK ですが、後は設定できる項目を知る必要がありますね。 以下でどんな定数が定義されているのかをちょっと見てみましょう。 各定数の意味は各 JavaDoc を参照のこと。

AclEntryPermission 定数

package java.nio.file.attribute;

public enum AclEntryPermission extends Enum<AclEntryPermission>{
    WRITE_OWNER,

    READ_ACL,
    WRITE_ACL,

    READ_ATTRIBUTES,
    WRITE_ATTRIBUTES,

    READ_NAMED_ATTRS,
    WRITE_NAMED_ATTRS,

    READ_DATA,
    WRITE_DATA,
    APPEND_DATA,

    SYNCHRONIZE,

    EXECUTE,
    DELETE,
    DELETE_CHILD
}

ファイル内容だけでなく、ファイル属性や ACL 属性も別個に指定できます。 NAMED_ATTRS というのがイマイチ何なのか分からないんだけど。 JavaDoc では仕様書へのリンクが張られてるんだけど・・・

AclEntryType 定数

package java.nio.file.attribute;

public enum AclEntryType extends Enum<AclEntryType>{
    ALLOW,    // 許可する
    AUDIT,    // ログを残す
    ALARM,    // 警告を出す
    DENY      // 拒否する
}

ログの残し方や警告の仕方はファイルシステムに依るそうです。

AclEntryFlag 定数

package java.nio.file.attribute;

public enum AclEntryFlag extends Enum<AclEntryFlag>{
    DIRECTORY_INHERIT,
    FILE_INHERIT,
    INHERIT_ONLY,
    NO_PROPAGATE_INHERIT
}

ディレクトリに指定した ACL 属性をそのディレクトリ内のファイルやディレクトリに継承するかどうかのフラグみたいです。 JavaDoc を見ると 「この (Java) バージョンでは AclEntryType.AUDIT と AclEntryType.ALARM に関するフラグは定義していない」と書かれてます。 ホントはもっとあるのね・・・。 でも Enum として定数を列挙されてるとどんなものが設定できるのかが簡単に分かっていいですね。

属性とファイル・ディレクトリ作成

ACL 属性や POSIX パーミッション権限などはファイルを作成してから設定するとセキュリティ的な空白ができるので、ファイル生成と同時にそれらが設定されるようにしておくべきです。 これを行うには、ファイル・ディレクトリなどの生成メソッドに FileAttribute オブジェクトを直接渡します。 ただし、FileAttribute インターフェースを実装しているクラスは標準 API で提供されていないので自分で作る必要があるようです。 以下のサンプルでは無名クラスで作成しています。

import java.nio.file.*
import java.nio.file.attribute.*

import static java.util.Arrays.asList
import static java.nio.file.attribute.AclEntryPermission.*

//***** ACL エントリの準備 *****
def lookup = FileSystems.getDefault().getUserPrincipalLookupService()

// 1つ目
def waman = lookup.lookupPrincipalByName("《ユーザー名》")
def wamanACL = AclEntry.newBuilder()
                       .setPrincipal(waman)
                       .setPermissions(READ_DATA, READ_ATTRIBUTES, DELETE)
                       .setType(AclEntryType.ALLOW)
                       .build()

// 2つ目
def system = lookup.lookupPrincipalByGroupName("SYSTEM")
def systemACL = AclEntry.newBuilder()
                       .setPrincipal(system)
                       .setPermissions(AclEntryPermission.values())
                       .setType(AclEntryType.ALLOW)
                       .build()

// 3つ目
def admin = lookup.lookupPrincipalByName("Administrators")
def adminACL = AclEntry.newBuilder()
                       .setPrincipal(admin)
                       .setPermissions(AclEntryPermission.values())
                       .setType(AclEntryType.ALLOW)
                       .build()

//***** ファイルの作成と同時に属性の設定 *****
def path = Files.createTempFile(null, null, new FileAttribute<List<AclEntry>>(){
    
    @Override String name(){
        return "acl:acl"    // ACL 属性の属性名
    }
    
    @Override List<AclEntry> value(){
        return asList(wamanACL, systemACL, adminACL) 
    }
})

printFileAttributes(path)

// ACL 属性を見やすく整形して出力。 本筋とは関係ないです。
def printFileAttributes(Path path){
    Files.readAttributes(path, "acl:*").each{ name, value ->
        if(name != "acl") {
            println "$name=$value"
        }else{
            println "acl="
            value.each{ AclEntry entry ->
                println "\tprincipal   : ${entry.principal()}"
                println "\tpermissions : ${entry.permissions().join("\n\t              ")}"
                println "\ttype        : ${entry.type()}"
                println "\tflags       : ${entry.flags()}"
                println()
            }
        }
    }

    println()
}
  • ここでは3つの ACL エントリを定義しています。 Windows では、「SYSTEM」と「Administrators」は FileAttribute を渡さずに作成したファイルにデフォルトで付与される ACL エントリで、全権限が与えられています(加えて現ユーザーにも全権限が与えられます)。 当然のことながら手動でファイル属性を設定するとこれらは付与されないので、ここではこれらも手動で設定してみてます(別にしなくても構いませんが)。
  • ビルダーの setPermissions() に渡されている「AclEntryPermission.values()」は全定数(全権限)を指定しています。

このコードを実行すると

owner=《コンピュータ名》\《ユーザー名》 (User)
acl=
	principal   : 《コンピュータ名》\《ユーザー名》 (User)
	permissions : DELETE
	              READ_DATA
	              READ_ATTRIBUTES
	type        : ALLOW
	flags       : []

	principal   : NT AUTHORITY\SYSTEM (Well-known group)
	permissions : DELETE_CHILD
	              WRITE_ATTRIBUTES
	              WRITE_ACL
	              READ_DATA
	              APPEND_DATA
	              WRITE_OWNER
	              DELETE
	              READ_ACL
	              READ_NAMED_ATTRS
	              WRITE_NAMED_ATTRS
	              WRITE_DATA
	              READ_ATTRIBUTES
	              EXECUTE
	              SYNCHRONIZE
	type        : ALLOW
	flags       : []

	principal   : BUILTIN\Administrators (Alias)
	permissions : DELETE_CHILD
	              WRITE_ATTRIBUTES
	              WRITE_ACL
	              READ_DATA
	              APPEND_DATA
	              WRITE_OWNER
	              DELETE
	              READ_ACL
	              READ_NAMED_ATTRS
	              WRITE_NAMED_ATTRS
	              WRITE_DATA
	              READ_ATTRIBUTES
	              EXECUTE
	              SYNCHRONIZE
	type        : ALLOW
	flags       : []

と表示されます。 デバッグなどで文字列として出力するときにはいくらか整形してからでないと読むに堪えない文字列が出力されます。 上記の出力を念頭におけば読めなくはないですが。

ちなみに、全てのファイル属性がファイル作成時に設定できるわけではないようです。 というか ACL 属性や POSIX パーミッションのような権限設定しかできないような感じ(特にどの属性が設定できるのかという仕様は見当たりませんでしたが)。 また、FileAttribute は OpenOption や CopyOption のような Enum 定数ではないので、同じ属性名で値が異なるものを複数設定することが可能ですが、この場合最後の設定が採用されます。


さて、これで nio で扱える属性関連の API は大体見てこられたと思います。 ほとんど自己満足な内容ですが(笑) 次は nio の目玉機能 WatchService をやる予定。

ZFS 仮想化されたファイルシステムの徹底活用

ZFS 仮想化されたファイルシステムの徹底活用

Windows NT ファイルシステム詳説―A Developer’s Guide

Windows NT ファイルシステム詳説―A Developer’s Guide