WWDC Quick Look 💓 By SwiftGGTeam
Sync files to the cloud with FileProvider on macOS

Sync files to the cloud with FileProvider on macOS

元の動画を見る

ハイライト

Apple は、APFS データレス ファイルに基づくクラウド同期機能を macOS Monterey の FileProvider に追加しました。これにより、クラウド ストレージ サービスがユーザー スペース拡張機能を使用して Finder にアクセスし、オンデマンドでファイルをダウンロードし、ローカルおよびリモートの変更を同期し、FUSE または KAUTH に依存するカーネル拡張ソリューションを置き換えることができるようになります。

主要内容

macOS 上でクラウド ディスク アプリが実行する上で最も難しいことの 1 つは、クラウド ファイルをローカル ファイルのように機能させることです。

ユーザーは、Finder サイドバーにクラウド ドライブを表示したいと考えています。ディレクトリは開くことができる必要があります。ファイルは次のことができる必要がありますcat、TextEdit、Photoshop、およびその他の一般的なプロセスを読み取ります。また、システムは、ディスク容量が少ないときにローカル コピーを安全に再利用できる必要もあります。

以前は、多くのクラウド ストレージ サービスがファイル システム呼び出しを傍受していました。一般的なソリューションは FUSE または KAUTH です。これらはカーネルに近く、オンデマンドでダウンロードできますが、複雑なセキュリティ境界、困難なデバッグ、さらに macOS ではカーネル拡張機能も非推奨になりつつあり、コストが高くなります。

このセッションで Apple が提供する代替手段は、macOS 上のファイル プロバイダー フレームワークです。開発者はアプリ拡張機能を実装します。システムは、それを Finder およびファイル システムに接続する責任があります。

この拡張機能は、アップロード、ダウンロード、およびリモート側でどのような変更が行われたかをシステムに通知するという 3 種類の処理のみを処理します。このシステムは、ローカル ファイル変更の検出、Finder ステータス表示、安全なストレージ、ディスク領域のリサイクル、およびファイル調整を担当します。

空のディレクトリから開始します

ユーザーがクラウド ディスク アカウントにログインすると、アプリはドメインを作成します。ドメインは、ユーザーがアクセスできるクラウド ファイル ツリーを表します。システムはそれを Finder サイドバーに配置し、ファイル システムにルート ディレクトリを作成します。

このルート ディレクトリには、最初は実際のファイルの内容はありません。 APFS が提供するデータのないディレクトリです。ユーザーはディレクトリに入ってファイル名を確認できますが、コンテンツはまだマシンにダウンロードされていません。

プロセスがデータのないファイルを読み取るとき、カーネルは読み取りを一時停止します。システムコール拡張fetchContents。拡張子はファイルをダウンロードし、ローカル URL を返します。システムはコンテンツを元のファイルに埋め込み、一時停止されていた読み取りを再開します。

リモートでの変更は内線番号によって事前に通知されます

誰かがクラウド上でファイルを作成、削除、または変更しても、マシンは自動的には認識しません。サーバーは Mac にプッシュ通知を送信できます。アプリが通知を受信すると、システムに通知します。.workingSet変化があります。

次に、システムはワーキング セットの列挙子を呼び出します。拡張機能が渡されましたsyncAnchor(同期アンカー) 前回以降の変更のみを返します。その後、システムはユーザーに表示されるファイルを非同期的に更新します。

ここで重要なのは、APFS の比較と交換です。システムがローカル ファイルを更新するとき、まだアップロードされていないローカルの変更が上書きされることを回避します。また、ファイル調整やアドバイザリー ロックとも連携して、他のアプリとの競合を軽減します。

ローカルの変更はシステムによってマージされ、拡張機能に渡されます。

ユーザーがファイルをローカルに保存すると、システムは変更を検出して呼び出します。modifyItemcreateItemまたはdeleteItem。拡張機能は、基礎となるすべての書き込みイベントを処理する必要はありません。

システムは、低レベルのイベントを同期に適した操作に結合します。たとえば、多くのアプリは安全な保存を使用します。つまり、最初に一時ファイルを書き込んでから、元のファイルを置き換えます。システムはこのパターンを認識し、項目識別子を新しいファイル ID にマップします。

ファイルの内容が変更された場合、システムは変更されたファイルのクローンを拡張子に与えます。こうすることで、ユーザーが編集を続けた場合でも、拡張機能は一貫したバージョンをアップロードします。

ディスク容量管理はシステムによって処理されます

ローカル ファイルのアップロードが完了すると、システムはファイルを再度ダウンロードできることを認識します。ディスク容量が不足している場合、システムはそのようなファイルを自動的に削除し、データのないファイルに戻すことができます。

この処理では拡張機能は呼び出されません。システムは、ビデオの録画やシステム アップデートのダウンロード用のスペースを確保するために、最も最近使用されていないファイルを選択します。

開発者は引き続き、FileProvider の他の方法を使用してエビクションをアクティブにトリガーしたり阻止したりできますが、緊急ディスクの再利用はシステムによって処理されます。

詳細

1. ドメインを作成し、クラウド ディスクを Finder に配置します

(12:31) FileProviderのエントリはドメインです。講演では、ドメインは通常、クラウドサービス上のログインセッションに相当すると述べた。一意の識別子があります。作成後、マネージャーを通じて追加すると、Finder サイドバーにエントリが表示されます。

import FileProvider

func addFruitBasketDomain() {
    let domain = NSFileProviderDomain(
        identifier: NSFileProviderDomainIdentifier("fruitbasket"),
        displayName: "FruitBasket"
    )

    NSFileProviderManager.add(domain) { error in
        if let error = error {
            NSLog("Failed to add FileProvider domain: \(error)")
            return
        }

        NSLog("FileProvider domain added")
    }
}

キーポイント:

  • import FileProviderFileProvider フレームワークの紹介。
  • NSFileProviderDomain同期可能なファイル ツリーを表します。
  • identifierこれはドメインの一意の識別子であり、通常はユーザーのログイン セッションにバインドされます。
  • displayNameFinder サイドバーでユーザーに表示される名前です。
  • NSFileProviderManager.addドメインをシステムに登録します。
  • 完了ハンドラーは、登録失敗エラーを受信するために使用されます。

ユーザーがアカウントからログアウトすると、ドメインを削除できます。講演では、古い Finder エントリを繰り返し消去することになるため、これは開発およびテスト段階でも役立つと述べられました。

import FileProvider

func removeFruitBasketDomain(_ domain: NSFileProviderDomain) {
    NSFileProviderManager.remove(domain) { error in
        if let error = error {
            NSLog("Failed to remove FileProvider domain: \(error)")
            return
        }

        NSLog("FileProvider domain removed")
    }
}

キーポイント:

  • パラメータdomain削除する FileProvider ドメインです。
  • NSFileProviderManager.removeこのエントリを削除するようにシステムに指示します。
  • ユーザーがログアウトするときに呼び出すと、Finder にクラウド ディスクが表示されなくなります。
  • 開発中に呼び出してテスト ドメインをクリアします。

2. ディレクトリを列挙し、データのないファイルを最初にローカルに表示します。

(04:26) ユーザーがディレクトリを開くと、カーネルはreaddir電話して一時停止します。システムコール拡張の列挙子。この拡張機能はサーバーからメタデータを取得し、いくつかのアイテムをシステムに返します。

この講演では、ディレクトリ列挙がページ分割されることを強調しています。商品の一部を先に返品することも可能です。システムは中断したところからリクエストを続行します。

import FileProvider

final class FruitEnumerator: NSObject, NSFileProviderEnumerator {
    func enumerateItems(
        for observer: NSFileProviderEnumerationObserver,
        startingAt page: NSFileProviderPage
    ) {
        let items: [NSFileProviderItem] = loadMetadataPage(startingAt: page)
        let nextPage: NSFileProviderPage? = nextPageAfter(page)

        observer.didEnumerate(items)
        observer.finishEnumerating(upTo: nextPage)
    }
}

キーポイント:

  • FruitEnumerator成し遂げるNSFileProviderEnumerator
  • enumerateItemsシステムがディレクトリの内容を必要とするときに呼び出されます。
  • observer内線応答システム オブジェクトです。
  • pageこの列挙がどのページから始まるかを示します。
  • loadMetadataPageファイル名、サイズ、バージョンなどのメタデータをサーバーから読み取ることを表します。
  • observer.didEnumerate(items)このページのアイテムをシステムに渡します。
  • observer.finishEnumerating(upTo:)このページを終了し、次のページがあるかどうかをシステムに伝えます。

これらの項目は、Finder に表示されるディレクトリ項目になります。ファイルの内容はまだダウンロードされていないため、データのないファイルのままです。

3. データのないファイルを読み取るときにコンテンツをダウンロードする

(03:36) プロセスがデータのないファイルを読み取ると、システムはfetchContents。この拡張機能は、ファイルの内容をディスク上の場所にダウンロードし、完了ハンドラーを使用して URL をシステムに渡します。

import FileProvider

final class FruitProvider: NSObject, NSFileProviderReplicatedExtension {
    func fetchContents(
        for itemIdentifier: NSFileProviderItemIdentifier,
        version requestedVersion: NSFileProviderItemVersion?,
        request: NSFileProviderRequest,
        completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void
    ) -> Progress {
        let progress = Progress(totalUnitCount: 100)

        downloadFile(for: itemIdentifier, version: requestedVersion) { localURL, item, error in
            if let error = error {
                completionHandler(nil, nil, error)
                return
            }

            completionHandler(localURL, item, nil)
        }

        return progress
    }
}

キーポイント:

  • FruitProvider成し遂げるNSFileProviderReplicatedExtension
  • itemIdentifierこれは、システムがダウンロードする必要があるファイルです。
  • requestedVersionのためにnil、ドキュメントには最新バージョンをダウンロードすることが記載されています。
  • request今回はシステムが要求した情報を運びます。
  • completionHandler最初のパラメータは、ダウンロードされたローカル ファイルの URL です。
  • completionHandler2 番目のパラメータは、ファイルの最終的な項目ステータスです。
  • 戻るProgress、システムは Finder にダウンロードの進行状況を表示できます。

デモでは、講演者はターミナルを使用して実行しましたcatデータのないファイルを読み取ります。 Xcode ブレークポイントは次で停止しますfetchContents時間、catこちらもブロック。 Finder の雲のアイコンが進行状況インジケーターに変わります。完了ハンドラーが呼び出された後、システムはダウンロードされたコンテンツをユーザーに表示されるファイルに置き換えます。cat実行を続行します。

(11:58) 同じファイルが 2 回目に実行されますcat、ブレークポイントはトリガーされなくなります。ファイルはすでに通常のローカル ファイルであるため、読み取り時に拡張子は呼び出されません。

4. ワーキング セットを使用してリモートの変更を同期する

(05:27) リモート変更への入り口は次のとおりです。.workingSet。サーバー側に変更があった場合、アプリはシステムに通知します。次に、システムはワーキング セット列挙子を要求します。syncAnchorその後の変化。

import FileProvider

func notifyRemoteChanges() {
    NSFileProviderManager.default.signalEnumerator(for: .workingSet) { error in
        if let error = error {
            NSLog("Failed to signal working set: \(error)")
            return
        }

        NSLog("Working set signaled")
    }
}

キーポイント:

  • .workingSetはスピーチで言及された特別な列挙者です。
  • signalEnumerator(for:)リマインダー システム: 指定された場所でコンテンツの変更があります。
  • 完了ハンドラーは、シグナル操作が完了したことを示すだけであり、システムが変更の取り込みを完了したことを示すものではありません。
  • このメソッドは、リモート プッシュ通知が Mac に到着した後に呼び出すことができます。

ワーキング セット列挙子は、現在の同期アンカー ポイントと、特定のアンカー ポイントから始まる変更の列挙を実装する必要があります。

import FileProvider

final class WorkingSetEnumerator: NSObject, NSFileProviderEnumerator {
    func currentSyncAnchor(
        completionHandler: @escaping (NSFileProviderSyncAnchor?) -> Void
    ) {
        completionHandler(loadCurrentServerChangeToken())
    }

    func enumerateChanges(
        for observer: NSFileProviderChangeObserver,
        from syncAnchor: NSFileProviderSyncAnchor
    ) {
        let changes = loadServerChanges(after: syncAnchor)

        observer.didUpdate(changes.updatedItems)
        observer.didDeleteItems(withIdentifiers: changes.deletedItemIdentifiers)
        observer.finishEnumeratingChanges(upTo: changes.newSyncAnchor, moreComing: false)
    }
}

キーポイント:

  • currentSyncAnchor現在のサーバー側変更カーソルを返します。
  • syncAnchor拡張によって定義され、システムは最後に列挙された場所を保存することのみを担当します。
  • enumerateChanges指定されたアンカーポイント以降の変更を返します。
  • observer.didUpdate新しく追加または変更されたアイテムをシステムに返します。
  • observer.didDeleteItems削除されたアイテム識別子をシステムに返します。
  • finishEnumeratingChangesシステムが次回使用できるように、新しいアンカー ポイントを返します。

その後、システムはユーザーに表示されるファイルを非同期に更新し、APFS 比較交換機能を使用してローカルの変更が失われないようにするとプレゼンテーションで述べられています。

5. アップロードのローカル作成、変更、削除

(06:50) ローカルな変更は逆の方向に進みます。システムはローカル項目の変更を検出し、拡張機能のmodifyItem。パラメータには、変更されたフィールドがそのまま含まれています。

(14:37) このスピーチでは、最終的に同期を実装し、作成フローを示すことが示唆されました。システムは、新しい項目、フォーカス フィールド、およびコンテンツ URL を拡張機能に渡します。拡張機能はサーバーにアップロードされ、完了ハンドラーを使用してリモート エンドから確認済みのアイテムが返されます。

import FileProvider

final class UploadingProvider: NSObject, NSFileProviderReplicatedExtension {
    func createItem(
        basedOn itemTemplate: NSFileProviderItem,
        fields: NSFileProviderItemFields,
        contents url: URL?,
        options: NSFileProviderCreateItemOptions,
        request: NSFileProviderRequest,
        completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void
    ) -> Progress {
        let progress = Progress(totalUnitCount: 100)

        uploadNewItem(itemTemplate, changedFields: fields, contents: url) { remoteItem, remainingFields, error in
            if let error = error {
                completionHandler(nil, fields, false, error)
                return
            }

            completionHandler(remoteItem, remainingFields, false, nil)
        }

        return progress
    }
}

キーポイント:

  • itemTemplateシステム上で作成が必要なアイテムです。
  • fieldsコンテンツや拡張属性など、どのフィールドの処理が必要かを説明します。
  • urlファイルコンテンツの場所です。フォルダーとシンボリック リンクにはコンテンツがない場合があります。
  • uploadNewItemローカルの新しいアイテムをサーバーにアップロードすることを表します。
  • remoteItemサーバーに受け付けられた後の最終的なアイテムのステータスです。
  • 完了ハンドラーは、システムがこの変更を拡張機能に渡したことを確認します。

ローカルな変更についても同様です。システムコールmodifyItem、コンテンツが変更されたときにファイルのクローンを提供し、アップロード プロセスで安定したバージョンを取得できるようにします。

import FileProvider

final class ModifyingProvider: NSObject, NSFileProviderReplicatedExtension {
    func modifyItem(
        _ item: NSFileProviderItem,
        baseVersion version: NSFileProviderItemVersion,
        changedFields: NSFileProviderItemFields,
        contents newContents: URL?,
        options: NSFileProviderModifyItemOptions,
        request: NSFileProviderRequest,
        completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void
    ) -> Progress {
        let progress = Progress(totalUnitCount: 100)

        uploadModifiedItem(item, baseVersion: version, changedFields: changedFields, contents: newContents) { finalItem, remainingFields, error in
            if let error = error {
                completionHandler(nil, changedFields, false, error)
                return
            }

            completionHandler(finalItem, remainingFields, false, nil)
        }

        return progress
    }
}

キーポイント:

  • itemローカルで変更されたアイテムです。
  • versionこれは、この変更のベースとなっているバージョンです。
  • changedFieldsどのフィールドが変更されたかを拡張機能に伝えます。
  • newContentsシステムによって提供される安定したコンテンツ URL です。
  • finalItemサーバー側の処理が完了した後の最終状態です。
  • サーバーが競合を検出した場合、ローカル ファイルは最終的なアイテムのステータスを通じてクラウドの真実を反映できます。

6. Finder の統合: アイコンの装飾、メニュー、プリフライト リマインダー

(15:59) FileProvider は、オプションのシステム統合ポイントも提供します。

アイコンの装飾により、Finder アイテムにバッジを追加したり、フォルダーをエンボスしたり、共有ステータスを表示したりできます。開発者は、アプリで宣言された UTType を通じてカスタム アートワークを提供します。

<key>NSExtensionFileProviderDecorations</key>
<array>
    <dict>
        <key>Identifier</key>
        <string>com.example.fruitbasket.shared</string>
        <key>BadgeImageType</key>
        <string>com.example.fruitbasket.shared-badge</string>
        <key>Category</key>
        <string>Badge</string>
    </dict>
</array>

キーポイント:

  • NSExtensionFileProviderDecorations延長でInfo.plistステートメント内の装飾。
  • Identifier装飾的なロゴです。
  • BadgeImageTypeアプリ内で宣言された UTType アートワークを指します。
  • Categoryこのタイプの装飾がバッジに使用されることを示します。

コンテキスト メニュー アクションを使用すると、ユーザーは Finder の右クリック メニューでカスタム アクションを実行できます。講演では、アクションには UI ありと UI なしの 2 種類があると述べられました。該当するファイルは以下から提供されます。Info.plistNSPredicate決める。

<key>NSExtensionFileProviderActions</key>
<array>
    <dict>
        <key>Identifier</key>
        <string>com.example.fruitbasket.share</string>
        <key>ActivationRule</key>
        <string>TRUEPREDICATE</string>
    </dict>
</array>

キーポイント:

  • NSExtensionFileProviderActionsFinder コンテキスト メニュー アクションを宣言します。
  • Identifierさまざまなアクションを区別します。
  • ActivationRule述語を使用して、このアクションを表示するファイルを制御します。
  • 「リンクを共有」「クラウド版を表示」「Webページで開く」などの操作に適しています。

プリフライト アラートは、ユーザーが副作用を引き起こす可能性のあるアクションを実行する前に警告をポップアップ表示するために使用されます。講義ノート、リマインダー UI、トリガー条件も渡されますInfo.plist構成。

重要ポイント

  1. 作業内容: チーム クラウド ディスクのネイティブ Finder エントリを作成します。 実行する価値がある理由: FileProvider ドメインは各ログイン アカウントを Finder サイドバー エントリにマップできるため、ユーザーはファイルを見つけるためにアプリに入る必要がありません。 開始方法: まず作成しますNSFileProviderDomain、使用NSFileProviderManager.add登録する;次に、ルート ディレクトリがサーバー フォルダーを一覧表示できるようにディレクトリ列挙子を実装します。

  2. やるべきこと: 「開いたときのみダウンロード」マテリアル ライブラリを作成します。 実行する価値がある理由: APFS データレス ファイルを使用すると、Finder は最初に完全なディレクトリ構造を表示し、コンテンツが読み取られたときにそれをトリガーできます。fetchContents、ローカルディスクを保存できます。 開始方法: 列挙子にリモート マテリアルのメタデータを返させます。でfetchContents(for:version:request:completionHandler:)元の画像またはビデオをローカル URL にダウンロードします。

  3. 手順: Finder 共有ステータス バッジをデザイン ファイルに追加します。 実行する価値がある理由: アイコン装飾を使用すると、ユーザーが Web ページを開いてアクセス許可を表示する必要がなく、ファイル共有ステータスを Finder に直接表示できます。 開始方法:Info.plistFileProvider デコレーションを宣言し、アイテムのメタデータを使用して、どのファイルがバッジを共有するかを決定します。

  4. やるべきこと: Finder を実装して、[共有リンクのコピー] を右クリックします。 実行する価値がある理由: コンテキスト メニューのアクションにより、クラウド サービスの高頻度操作をファイルの隣に配置できるため、ユーザーが Finder と Web ページを切り替える必要性が減ります。 開始方法:Info.plistFileProvider アクションを宣言し、述語を使用して通常のファイルまたはフォルダーを制限し、アクション ハンドラーでリンクを生成するようにサーバーに要求します。

  5. 機能: 大きなファイルのアップロードのための安全なローカル編集同期。 実行する価値がある理由: システムはコンテンツが変更されたときにファイルのクローンを提供し、拡張機能はユーザーによる後続の編集によって中断されることなく安定したバージョンをアップロードできます。 開始方法: 実装modifyItem(_:baseVersion:changedFields:contents:options:request:completionHandler:)、でのみchangedFields内容が変わったときに読むcontentsURLを入力してアップロードします。

関連セッション

コメント

GitHub Issues · utterances