ファイルに拡張属性( Extended Attributes )を付与する

## はじめに

iOS ではデバイス上のファイルに対して、属性のキー [FileAttributeKey] (https://developer.apple.com/documentation/foundation/fileattributekey) に対応した値を取得することができます。これには、ファイルの作成日や更新日、ファイルのサイズといった情報が含まれています。 FileAttributeKey に定義されている値については FileManager に用意されているメソッド [attributesOfItem(atPath:)](https://developer.apple.com/documentation/foundation/filemanager/1410452-attributesofitem) を通して値が取得できますが、これとは別に独自に属性を設定して付加情報を保存/取得することもできます。
利用シーンの例として、S3 上のファイルをローカルにダウンロードした際に、ファイルに更新時間に関する情報 [LastModified](https://docs.aws.amazon.com/sdkfornet1/latest/apidocs/html/P_Amazon_S3_Model_S3Object_LastModified.htm) の値を付与して次回以降のファイルの同期管理に使用する、といったことなどが浮かびます。

## 実装例

簡単の為に以下のコードでは保存する情報は `String` としています。 `String` ではなく `Codable` に準拠した型を対象にするのでもいいと思います。

```Swift
extension URL {
  /// 拡張属性の取得
  func extendedAttribute(for name: String) -> String?  {
    let result = withUnsafeFileSystemRepresentation { fileSystemPath -> String? in
      // name に対応する拡張属性のデータの大きさを取得
      let length = getxattr(fileSystemPath, name, nil, 0, 0, 0)
      guard length >= 0 else {
        return nil
      }
      // データを格納する領域を確保
      var data = Data(count: length)
      let result = data.withUnsafeMutableBytes { [count = data.count] in
        // name に対応する拡張属性のデータを取得
        getxattr(fileSystemPath, name, $0.baseAddress, count, 0, 0)
      }
      guard result >= 0 else {
        return nil
      }
      return String(data: data, encoding: .utf8)
    }
    return result
  }

  /// 拡張属性の保存
  @discardableResult
  func setExtendedAttribute(value: String, for name: String) -> Bool {
    guard let data = value.data(using: .utf8) else {
      return false
    }
    let result = withUnsafeFileSystemRepresentation { fileSystemPath -> Bool in
      let result = data.withUnsafeBytes {
        // name に対応する拡張属性のデータを保存
        setxattr(fileSystemPath, name, $0.baseAddress, data.count, 0, 0)
      }
      return result >= 0
    }
    return result
  }
}
```

##　動作を確認してみる

```Swift
// 設定する拡張属性の名前
let key = "com.example.extended_attribute"
// 拡張属性を設定するファイルの場所
let url = FileManager.default.temporaryDirectory.appendingPathComponent("example_file")

// ファイルがなかったら作成して拡張属性を付与する
if !FileManager.default.fileExists(atPath: url.path) {
  let data = "example_content".data(using: .utf8)!
  try! data.write(to: url)
  // 拡張属性の付与
  url.setExtendedAttribute(value: "example_value", for: key)
}

// 拡張属性が取得できるか確認
print(url.extendedAttribute(for: key))
```

### コンソールの出力

```
Optional("example_value")
```

アプリを再起動しても `setExtendedAttribute` で保存した値が取得できていて、ファイルに紐づいた値が永続化できていることが分かります。


## 参考

- [Stack Overflow - Write extend file attributes swift example
](https://stackoverflow.com/questions/38343186/write-extend-file-attributes-swift-example)
  - 実装はこちらを参考にしました。例として挙げたコードの例では簡単の為にエラーの内容は潰していますが、エラーの把握が必要な場合はリンク先の実装の様にエラーを throw するなどがいいと思います。他にも拡張属性の名前を一覧で取得する、拡張属性を削除するなどのコード例も載っているので、必要な方は参考ください。
- [Apple Developer Documentation - withUnsafeFileSystemRepresentation(_:)](https://developer.apple.com/documentation/foundation/url/1779782-withunsafefilesystemrepresentati)
- [Mac OS X Man Pages - getxattr](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/getxattr.2.html)
  - _On success, the size of the extended attribute data is returned.  On failure, -1 is returned..._
- [Mac OS X Man Pages - setxattr](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/setxattr.2.html)
  - _On success, 0 is returned.  On failure, -1 is returned..._