【Dart】File, Directory, Path, PathProvider, XFile 徹底解説

  • 2024年7月31日
  • 2024年7月31日
  • Dart

Table of Contents

対象者

  • Flutterでのファイル操作に関する知識を求めているエンジニア
  • Dartの色々なファイル操作関連のクラス・パッケージを整理して学びたい方
  • ファイル名の取得方法などをど忘れするオレ

はじめに

この記事では、Flutterでのファイル操作やディレクトリ管理について、基本から応用までを丁寧に解説します。
具体的なコード例やエラーハンドリングのベストプラクティスを交えながら、ファイルの読み込み、書き込み、存在確認、ディレクトリの作成、一覧取得、削除、コピーと移動など、様々な操作方法をわかりやすく解説します。また、Dartの標準でないが、Flutterを少しやっていくと必須となるパッケージ周りもカバーしました。
この記事を読むことで、ファイル操作の基礎から応用までをしっかりと身につけ、プロジェクトの成功へと導きましょう。

FileやDirectory関係のクラスやパッケージの概要

Fileとは何か

ファイルとは、データを保存するための単位です。コンピュータ上で様々な種類のデータ(テキスト、画像、音声など)を管理するために使用されます。ファイルはディスク上に保存され、名前やパスで識別されます。

Directoryとは何か

ディレクトリとは、ファイルを整理するためのフォルダのことです。ディレクトリは他のディレクトリを含むことができ、階層構造を作ることができます。これにより、関連するファイルをまとめて管理することが容易になります。

pathパッケージとは何か

pathパッケージとは、Dartでファイルやディレクトリのパスを操作するためのライブラリです。パスの結合、分割、正規化など、さまざまな操作を簡単に行うことができます。
異なるプラットフォーム(Windows、Linux、macOS)でパスの表記が異なるため、pathパッケージを使用することで、プラットフォームに依存しない一貫したパス操作が可能になります。

cross_fileとは何か

cross_fileとは、DartやFlutterでクロスプラットフォームでのファイル操作を簡素化するためのライブラリです。モバイル、ウェブ、デスクトップなど、異なるプラットフォーム間で一貫したファイル操作が可能になります。
これにより、プラットフォームごとに異なるAPIを使用する必要がなくなり、コードの可読性と保守性が向上します。例えば、モバイルアプリとウェブアプリの両方で画像をアップロードする機能を実装する場合、cross_fileを使用することで、同一のコードベースで対応可能です。

path_providerとは何か

path_providerとは、DartやFlutterでアプリケーションのデータ保存用のディレクトリパスを簡単に取得するためのパッケージです。このパッケージを利用することで、アプリケーション固有のディレクトリ(ドキュメントディレクトリ、サポートディレクトリ、一時ディレクトリなど)へのパスを取得でき、ファイルの読み書きや管理が容易になります。

Fileクラスの基本操作

Fileクラスを使用して、ファイルの作成、読み書き、削除、監視などの基本操作を行います。

ファイルを文字列として読み書き

  • File.writeAsString – ファイルに文字列を書き込む
  • File.readAsString – ファイルから文字列を読み込む
final file = File(testFilePath);
await file.writeAsString('Hello, Flutter!');
String contents = await file.readAsString();
expect(contents, 'Hello, Flutter!');

ファイルをバイナリデータとして読み書き

  • File.writeAsBytes – ファイルにバイナリデータを書き込む
  • File.readAsBytes – ファイルからバイナリデータを読み込む
final file = File(testBinaryFilePath);
Uint8List data = Uint8List.fromList([0, 1, 2, 3, 4, 5]);
await file.writeAsBytes(data);
Uint8List contents = await file.readAsBytes();
expect(contents, data);

ファイルの追記方法

  • File.writeAsStringFileMode.appendを設定ことでファイルに文字列を追記する
final file = File(testFilePath);
await file.writeAsString('Hello, Flutter!');
await file.writeAsString('\nAppending data', mode: FileMode.append);
String contents = await file.readAsString();
expect(contents, 'Hello, Flutter!\nAppending data');

ファイルを作成/存在確認/削除する

テストで非同期処理のメソッドをawaitなしで使うと、ファイルを作成する前に存在確認をしてテストに失敗するので、同期処理とawaitのメソッドを使います。
現実的には「ファイルを作っておいて、すぐに使うわけではない」場合は、非同期処理のSyncのないメソッド(create, exists, delete)をawaitなしで使ったら良いかと思います。

  • File.createSync – ファイルを作成する
  • File.existsSync – ファイルの存在確認
  • File.deleteSync – ファイルを削除する
  • File.create – 非同期でファイルを作成する
  • File.exists – 非同期でファイルの存在確認
  • File.delete – 非同期でファイルを削除する
final file = File(testFilePath);
file.createSync();
expect(file.existsSync(), true);
file.deleteSync();
expect(file.existsSync(), false);

await file.create();
expect(await file.exists(), true);
await file.delete();
expect(await file.exists(), false);

// 存在しないファイルを削除しようとすると例外が発生
expect(() => file.deleteSync(), throwsA(isA<PathNotFoundException>()));

ファイルを移動/コピーする

  • File.copy – ファイルをコピーする
  • File.rename – ファイルを移動する

ファイルを作成・コピーして、コピーが成功しており、元ファイルもコピーファイルも存在することを確認してます。
その後、元ファイルを移動して元ファイルが存在しないことと移動先ファイルが存在することを確認します。

final file = File(testFilePath);
await file.writeAsString('Hello, Flutter!');

final copyPath = p.join(testDirectory.path, 'copy_file.txt');
await file.copy(copyPath);
final copyFile = File(copyPath);
expect(await file.exists(), true);
expect(await file.readAsString(), 'Hello, Flutter!');
expect(await copyFile.readAsString(), 'Hello, Flutter!');

final movePath = p.join(testDirectory.path, 'moved_file.txt');
await file.rename(movePath);
expect(await file.exists(), false);

final movedFile = File(movePath);
expect(await movedFile.readAsString(), 'Hello, Flutter!');

ファイルの長さを取得する

  • File.lengthSync – ファイルの長さを取得する
final file = File(testFilePath);
final message = 'Hello, Flutter!';
expect(message.length, 15);
file.writeAsStringSync('Hello, Flutter!');
expect(file.lengthSync(), 15);

ファイルを開いて、一部を取得する

  • File.openSync – ファイルを開く
  • RandomAccessFile.readSync – ファイルからバイトデータを読み込む

ファイルをバイナリモードで開き、一部のデータを読み込みます。

final file = File(testFilePath);
file.writeAsStringSync('Hello, Flutter!');
final raf = file.openSync();
raf.setPositionSync(0);
expect(raf.readSync(5), [72, 101, 108, 108, 111]); // "Hello"
raf.closeSync();

この操作は、ファイル内の任意の位置に移動して読み書き操作を行う必要があるときに使用されます。具体的には以下のようなシナリオがあります:

  • バイナリファイルの操作: バイナリファイルを読み書きする際に、ファイルの特定の位置に移動してデータを読み書きする必要がある場合。
  • 部分的なデータの読み込み: 大きなファイルの特定の部分だけを読み込みたい場合に、ファイルの特定の位置に移動してデータを読み込むことができます。

例えば、バイナリ形式のデータファイルのヘッダー部分とデータ部分を分けて読み込みたい場合や、ファイルの最後に追加されたデータを効率的に読み込みたい場合に有用です。

ファイルを文字列として1行ずつ処理する

  • File.openRead – ファイルをストリームとして開く
  • utf8.decoder – バイトを文字列に変換
  • LineSplitter – 文字列を行に分割

ファイルの内容を1行ずつ読み込み、各行を確認します。

final file = File(testFilePath);

// ファイルに複数行を書き込む
final lines = ['Hello, Flutter!', 'Welcome to Dart.', 'Enjoy coding!'];
file.writeAsStringSync(lines.join('\n'));

// ファイルを行ごとに読み込む
Stream<List<int>> streamInt = file.openRead();
Stream<String> streamString = streamInt
    .transform(utf8.decoder) // バイトを文字列に変換
    .transform(const LineSplitter()); // 行に分割

var counter = 0;
await for (String line in streamString) {
  expect(line, lines[counter]);
  counter++;
}
expect(counter, 3);

Streamデータをファイルに出力

  • Stream.pipe – ストリームデータを他のストリームに送る
  • File.openWrite – ファイルを書き込みモードで開く

ストリームデータをファイルに書き込み、その内容を確認します。

final data = [0, 1, 2, 3, 4, 5];
Uint8List uint8list = Uint8List.fromList(data);
Stream<List<int>> streamListInt = Stream.fromIterable([uint8list]);

final file = File(testBinaryFilePath);
final result = await streamListInt.pipe(file.openWrite());
identical(result, streamListInt);

Uint8List contents = await file.readAsBytes();
expect(contents, data);

ファイルの情報を取得する

  • File.statSync – ファイルのステータス情報を取得する

ファイルのステータス情報を取得します。

  • ファイルタイプ: stat.type
    • 一般ファイル: FileSystemEntityType.file
    • ディレクトリ: FileSystemEntityType.directory
    • シンボリックリンク: FileSystemEntityType.link
    • 不明: FileSystemEntityType.notFound
  • ファイルサイズ: stat.size
  • ファイルモード: stat.mode
    • 読み取り専用: 0x81A4
    • 読み書き可能: 0x81B6
    • 実行可能: 0x81FF
  • 最終変更日時: stat.changed
    • Windowsプラットフォームにおける動作: ファイルの作成時刻を指します。
    • ファイルシステムオブジェクト(ファイルやディレクトリ)のデータまたはメタデータに対する最後の変更時刻。ファイルのパーミッションを変更したり、ファイル名を変更したりしたときに更新されます。
  • 最終修正日時: stat.modified
    • ファイルシステムオブジェクトのデータに対する最後の変更時刻。
    • ファイルのメタデータが変更されても、この時刻は更新されません。
  • 最終アクセス日時: stat.accessed
final file = File(testFilePath2);
final createTime = DateTime.now();
final waitTime = 1100 - createTime.millisecond;
await Future.delayed(Duration(milliseconds: waitTime));

expect(file.existsSync(), false);
file.writeAsStringSync('Hello, Flutter!');
final stat = file.statSync();
expect(stat.type, FileSystemEntityType.file);
expect(stat.size, 15);
expect(stat.mode, 0x81B6);

final createdTime = DateTime.now();

//ファイルの変更日時/アクセス日時を取得する
expect(stat.changed.isAfter(createTime), true);
expect(stat.changed.isBefore(createdTime), true);
expect(stat.modified.isAfter(createTime), true);
expect(stat.modified.isBefore(createdTime), true);
expect(stat.accessed.isAfter(createTime), true);
expect(stat.accessed.isBefore(createdTime), true);

final changeTime = DateTime.now();
await Future.delayed(
  Duration(milliseconds: (1100 - changeTime.millisecond)),
);

file.writeAsStringSync('Hello, Flutter!');
final stat2 = file.statSync();
expect(stat2.changed.isAfter(createTime), true);
expect(stat2.changed.isBefore(createdTime), true);

expect(stat2.modified.isAfter(changeTime), true);
expect(stat2.accessed.isAfter(changeTime), true);

final readTime = DateTime.now();
await Future.delayed(
  Duration(milliseconds: (1100 - readTime.millisecond)),
);

file.readAsLinesSync();
final stat3 = file.statSync();
expect(stat3.changed.isAfter(createTime), true);
expect(stat3.changed.isBefore(createdTime), true);

expect(stat3.modified.isAfter(readTime), false);
expect(stat3.accessed.isAfter(readTime), true);

file.deleteSync();

ファイルの変更監視をする

  • File.watch – ファイルの変更を監視する
  • Stream.listen – ストリームのイベントを監視する

ファイルの変更を監視し、変更があるたびにカウンタを増加させます。
Windowsではサポートされていない。

final file = File(testFilePath);
int counter = 0;
final stream = file.watch();
final subscription = stream.listen((event) {
  expect(event.type, FileSystemEvent.modify);
  expect(event.path, testFilePath);
  expect(event.isDirectory, false);
  counter++;
});
file.writeAsStringSync('Hello, again!');
file.writeAsStringSync('Hello, again!');
await Future.delayed(const Duration(milliseconds: 100));
await subscription.cancel();
if (!Platform.isWindows) {
  // WindowsでFileの変更監視はサポートされていない
  expect(counter, 2);
}

Directoryクラスの基本操作

Directoryクラスを使用して、ディレクトリの作成、存在確認、削除、監視などの基本操作を行います。

一つの階層のディレクトリを作成・存在確認・削除

  • Directory.create – ディレクトリを作成する
  • Directory.exists – ディレクトリの存在確認
  • Directory.delete – ディレクトリを削除する
final dir = Directory(singleLevelDirPath);
await dir.create();
expect(await dir.exists(), true);
await dir.delete();
expect(await dir.exists(), false);

一つの階層のディレクトリを作成・存在確認・削除(同期処理)

  • Directory.createSync – ディレクトリを同期的に作成する
  • Directory.existsSync – ディレクトリの存在確認
  • Directory.deleteSync – ディレクトリを同期的に削除する
final dir = Directory(singleLevelDirPath);
dir.createSync();
expect(dir.existsSync(), true);
dir.deleteSync();
expect(dir.existsSync(), false);

複数階層のディレクトリを作成・存在確認・削除

複数階層のディレクトリを作成し、存在確認後に削除します。

final dir = Directory(multiLevelDirPath);
expect(dir.existsSync(), false);

expect(() async => (await dir.create()),
    throwsA(isA<PathNotFoundException>()));
dir.createSync(recursive: true);
expect(dir.existsSync(), true);

// parentディレクトリにdirディレクトリがあるので、deleteが失敗。
final parent = dir.parent;
expect(() => parent.deleteSync(), throwsA(isA<FileSystemException>()));

// recursive:trueにすると、子ディレクトリごと削除
parent.deleteSync(recursive: true);
expect(parent.existsSync(), false);
expect(dir.existsSync(), false);
createメソッドと例外の発生
  • recursive: false(デフォルト)の場合: 親ディレクトリが存在しない場合、例外が発生します。
  • recursive: trueの場合: 例外は発生しません。親ディレクトリが存在しない場合でも、必要なすべての親ディレクトリが再帰的に作成されます。
deleteメソッドと例外の発生
  • recursive: falseの場合: ディレクトリが空でない場合、例外が発生します。
  • recursive: trueの場合: 例外は発生しません。ディレクトリが空でなくても、そのディレクトリとすべてのサブディレクトリおよびファイルが再帰的に削除されます。

ディレクトリ内のファイルとサブディレクトリを一覧取得

  • Directory.listSync
    ディレクトリ内のファイルとサブディレクトリを同期的に一覧取得する
    • recursivefalseの場合

      • ディレクトリ内の直下のファイルとサブディレクトリのみを一覧取得します。
      • サブディレクトリ内のファイルやディレクトリは取得されません。
    • recursivetrueの場合

      • ディレクトリ内のすべてのファイルとサブディレクトリを再帰的に一覧取得します。
      • サブディレクトリ内のファイルやディレクトリもすべて取得されます。
final dir = Directory(multiLevelDirPath);
dir.createSync(recursive: true);

File(p.join(dir.path, 'level2.txt')).writeAsStringSync('level2');
File(p.join(dir.parent.path, 'level1.txt')).writeAsStringSync('level1');

final entities = dir.listSync();
expect(entities.length, 1);
expect(p.basename(entities.first.path), 'level2.txt');

expect(p.basename(dir.parent.listSync().first.path), 'level1.txt');

final recursiveBaseName = dir.parent
    .listSync(recursive: true)
    .map((e) => p.basename(e.path))
    .toSet();
expect(recursiveBaseName, {'level2', 'level1.txt', 'level2.txt'});

dir.parent.deleteSync(recursive: true);

ディレクトリを移動

  • Directory.renameSync – ディレクトリを同期的に移動する

「ディレクトリのコピー」はない。

final srcDir = Directory(singleLevelDirPath);
srcDir.createSync();
File(p.join(singleLevelDirPath, testFilePath))
    .writeAsStringSync('Hello, Flutter!');

final destDirPath = p.join(srcDir.parent.path, 'moved_dir');

expect(srcDir.existsSync(), true);
final destDir = srcDir.renameSync(destDirPath);
expect(srcDir.existsSync(), false);
expect(destDir.existsSync(), true);
expect(File(p.join(destDir.path, testFilePath)).existsSync(), true);

destDir.deleteSync(recursive: true);

ディレクトリのステータス情報を取得

  • Directory.statSync – ディレクトリのステータス情報を同期的に取得する

取得内容については「ファイルの情報を取得する」と同じ(typeの結果が異なる)。

final creatingTime = DateTime.now();
await Future.delayed(
    Duration(milliseconds: 1100 - creatingTime.millisecond));

final dir = Directory(singleLevelDirPath);
await dir.create();
final createdTime = DateTime.now();

// ディレクトリの状態を取得
final statCreated = dir.statSync();
expect(statCreated.type, FileSystemEntityType.directory);
expect(statCreated.size, isZero);
expect(statCreated.changed.isAfter(creatingTime), isTrue);
expect(statCreated.changed.isAfter(createdTime), isFalse);
expect(statCreated.modified.isAfter(creatingTime), isTrue);
expect(statCreated.modified.isAfter(createdTime), isFalse);
expect(statCreated.accessed.isAfter(creatingTime), isTrue);
expect(statCreated.accessed.isAfter(createdTime), isFalse);

// ファイルをディレクトリ内に作成して変更を反映
final file = File('${dir.path}/test.txt');
file.writeAsStringSync('Hello, Dart!');
final changeTime = DateTime.now();
await Future.delayed(
    Duration(milliseconds: (1100 - changeTime.millisecond)));

// ディレクトリの状態を取得(ディレクトリ作成時と変化なし)
final statFileCreated = dir.statSync();
expect(statFileCreated.type, FileSystemEntityType.directory);
expect(statFileCreated.size, isZero);
expect(statFileCreated.changed.isAfter(creatingTime), isTrue);
expect(statFileCreated.changed.isAfter(createdTime), isFalse);
expect(statFileCreated.modified.isAfter(creatingTime), isTrue);
expect(statFileCreated.modified.isAfter(createdTime), isFalse);
expect(statFileCreated.accessed.isAfter(creatingTime), isTrue);
expect(statFileCreated.accessed.isAfter(createdTime), isFalse);

// Clean up
await dir.delete(recursive: true);

ディレクトリの変更を監視する

  • Directory.watch – ディレクトリの変更を監視する
  • Stream.listen – ストリームのイベントを監視する

ディレクトリの変更を監視し、変更結果を確認します。modifyはファイルを開いたときと保存したときの計2回発生していると思うが、実験してないっす。

final dir = Directory(singleLevelDirPath);
dir.createSync();

final controller = StreamController<int>();

expectLater(
  controller.stream,
  emitsInOrder([
    FileSystemEvent.create,
    FileSystemEvent.modify,
    FileSystemEvent.modify,
    FileSystemEvent.delete,
    emitsDone
  ]),
);

final filePath = p.join(singleLevelDirPath, 'test_file.txt');
final file = File(filePath);

final subscription = dir.watch().listen((event) async {
  controller.add(event.type);
});

// ディレクトリ内にファイルを作成、変更、削除
file.createSync();
file.writeAsStringSync('Hello, Flutter!');
file.deleteSync();

// イベントが発生するのを待つ
await Future.delayed(const Duration(milliseconds: 100));
await subscription.cancel();
controller.close();

// Clean up
await dir.delete();

ファイルとディレクトリの存在確認

ファイルやディレクトリの存在確認方法

ファイルやディレクトリが存在するかどうかを確認することは、ファイル・ディレクトリ操作の前提条件として重要です。これにより、存在しない場合のエラーを回避し、安全にファイルを操作できます。

プログラムがファイルやディレクトリにアクセスする前に、存在するかをチェックするようにしましょう。これにより、存在しない場合に適切なエラーメッセージを表示したり、必要な処理を行うことができます。また、存在確認を行うことで、操作時の予期しないエラーを未然に防ぐことができます。

エラーハンドリングのベストプラクティス

エラーハンドリングは、プログラムが予期しない状況に遭遇した際に適切に対処するために重要です。これにより、プログラムのクラッシュを防ぎ、ユーザーに適切なフィードバックを提供することができます。

エラーハンドリングのベストプラクティスとして、以下の点が挙げられます:

  • 早期確認:ファイルやディレクトリの存在を早期に確認し、必要な処理を行うことで、後続の操作で発生するエラーを防ぎます。
  • 明確なエラーメッセージ:エラーが発生した際に、ユーザーに対して明確で理解しやすいエラーメッセージを表示します。
  • 例外処理:try-catch構文を使用して、例外が発生した際に適切に対処します。

実例として、Dartでエラーハンドリングを行う方法は以下の通りです:

import 'dart:io';

void main() async {
  final file = File('example.txt');
  try {
    bool exists = await file.exists();
    if (exists) {
      String contents = await file.readAsString();
      print(contents);
    } else {
      print('File does not exist');
    }
  } catch (e) {
    print('An error occurred: $e');
  }
}

このコードは、example.txtというファイルの存在を確認し、存在する場合は内容を読み込み、存在しない場合はエラーメッセージを表示します。また、エラーが発生した場合には例外をキャッチして適切に対処します。

存在確認とエラーハンドリングを適切に行うことで、プログラムの信頼性とユーザー体験を向上させることができます。

path

pathパッケージを使用して、パスの操作を行います。
パスの書き方の違いで、Windowsとその他のOSで、結果が異なる。

基本的な操作

  • p.join – パスを結合する
  • p.split – パスを分割する
  • p.normalize – パスを正規化する
  • p.canonicalize – パスを正規化する

パスの結合や分割、正規化などの基本的な操作を行います。

if (Platform.isWindows) {
  expect(p.join('path', 'to', 'foo'), 'path\\to\\foo');
  expect(p.joinAll(['path', 'to', 'foo']), 'path\\to\\foo');
} else {
  expect(p.join('path', 'to', 'foo'), 'path/to/foo');
  expect(p.joinAll(['path', 'to', 'foo']), 'path/to/foo');
}

// パスの分割
expect(p.split('path/to/foo'), ['path', 'to', 'foo']);

// 現在の作業ディレクトリの取得
final currentDirPath = p.current;
expect(currentDirPath, isNotNull);

// パスの正規化
if (Platform.isWindows) {
  expect(p.normalize('path/./to/..//foo'), 'path\\foo');
  expect(p.canonicalize('path/./to/../foo'),
      '${currentDirPath.toLowerCase()}\\path\\foo');
} else {
  expect(p.normalize('path/./to/..//foo'), 'path/foo');
  expect(p.canonicalize('path/./to/../foo'),
      '${currentDirPath.toLowerCase()}/path/foo');
}

パスの情報取得

  • p.basename – ファイル名を取得する
  • p.basenameWithoutExtension – 拡張子なしのファイル名を取得する
  • p.dirname – ディレクトリ名を取得する
  • p.extension – ファイル拡張子を取得する
  • p.setExtension – ファイル拡張子を設定する
  • p.withoutExtension – 拡張子なしのパスを取得する
expect(p.basename('path/to/foo.dart'), 'foo.dart');
expect(p.basenameWithoutExtension('path/to/foo.dart'), 'foo');
expect(p.dirname('path/to/foo.dart'), 'path/to');
expect(p.extension('path/to/foo.dart'), '.dart');
expect(p.setExtension('path/to/foo', '.dart'), 'path/to/foo.dart');
expect(p.setExtension('path/to/foo.txt', '.dart'), 'path/to/foo.dart');
expect(p.withoutExtension('path/to/foo.dart'), 'path/to/foo');

パスの変換

  • p.isAbsolute – パスが絶対パスかどうかを確認する
  • p.isRelative – パスが相対パスかどうかを確認する
  • p.relative – 絶対パスを相対パスに変換する
  • p.fromUri – URIからパスを取得する
  • p.toUri – パスをURIに変換する
expect(p.isAbsolute('/path/to/foo'), true);
expect(p.isRelative('path/to/foo'), true);

if (Platform.isWindows) {
  expect(
    p.relative('/root/path/a/b.dart', from: '/root/path'),
    'a\\b.dart',
  );
  expect(p.fromUri('file:///path/to/foo'), '\\path\\to\\foo');
  expect(p.toUri('/path/to/foo').toString(), 'file:///C:/path/to/foo');
} else {
  expect(
    p.relative('/root/path/a/b.dart', from: '/root/path'),
    'a/b.dart',
  );
  expect(p.fromUri('file:///path/to/foo'), '/path/to/foo');
  expect(p.toUri('/path/to/foo').toString(), 'file:///path/to/foo');
}

パスの検証

  • p.equals – パスが一致するか確認する
  • p.isWithin – パスが包含されているか確認する
  • p.rootPrefix – ルートパスを取得する
expect(p.equals('path/to/foo', 'path/to/foo'), true);
expect(p.isWithin('/root/path', '/root/path/a'), true);
expect(p.rootPrefix('/root/path/to/foo'), '/');

パスのスタイルとセパレータ

  • p.style – 現在のパススタイルを取得する
  • p.separator – 現在のセパレータを取得する
expect(p.style, p.Style.platform);
expect(p.separator, p.context.separator);

if (Platform.isWindows) {
    expect(p.style, p.Style.windows);
    expect(p.separator, '\\');
}

高度な操作

  • p.hash – パスをハッシュ化する
  • p.prettyUri – URIを人間可読形式に表示する
expect(p.hash('path/to/foo'), isNotNull);
if (Platform.isWindows) {
  expect(p.prettyUri('file:///path/to/foo'), '\\path\\to\\foo');
} else {
  expect(p.prettyUri('file:///path/to/foo'), 'path/to/foo');
}

cross_file

XFileクラスを使用して、クロスプラットフォームでファイルを扱います。

基本操作

  • XFile.fromData – バイトデータからXFileを作成する
  • XFile.length – ファイルの長さを取得する
  • XFile.readAsBytes – ファイルをバイトデータとして読み込む
  • XFile.readAsString – ファイルを文字列として読み込む
  • XFile.openRead – ファイルをストリームとして読み込む
  • XFile.saveTo – ファイルを指定したパスに保存する
  • XFile.lastModified – ファイルの最終更新日時を取得する
const String testPath = 'test.txt';
const List<int> testBytes = [72, 101, 108, 108, 111]; // 'Hello'

final xFileFromData = XFile.fromData(Uint8List.fromList(testBytes));
expect(xFileFromData.path.isEmpty, true);
expect(await xFileFromData.length(), equals(testBytes.length));
expect(await xFileFromData.readAsBytes(), equals(testBytes));
expect(await xFileFromData.readAsString(), 'Hello');
expect(() => xFileFromData.lastModified(),
    throwsA(isA<PathNotFoundException>()));

final actualStream = xFileFromData.openRead();
expect(await actualStream.first, Uint8List.fromList(testBytes));

final actualStreamPart = xFileFromData.openRead(1, 3);
expect(await actualStreamPart.first, Uint8List.fromList([101, 108]));

expect(XFile(testPath), isNotNull);
expect(
  () => XFile(testPath).length(),
  throwsA(isA<PathNotFoundException>()),
);

await xFileFromData.saveTo(testPath);
expect(xFileFromData.path.isEmpty, true);

final xFileFromPath = XFile(testPath);
expect(await xFileFromPath.length(), 5);

final lastModifiedTime = await xFileFromPath.lastModified();
final now = DateTime.now();
expect(
    lastModifiedTime,
    now.subtract(Duration(
        microseconds: now.microsecond, milliseconds: now.millisecond)));

final file = File(xFileFromPath.path);
expect(file.lengthSync(), 5);

final xFileFromFile = XFile(file.path);
expect(await xFileFromFile.length(), 5);

file.delete();

今回、以前カメラで画像を扱った時に使用したXFileを改めて細かく使ってみました。
XFileには2つのモードがあると感じました。一つはメモリー上にバイナリデータとして展開するケース、もう一つは実際のファイルを取得するケースです。
例えば、カメラで撮影した画像は一度メモリー上に保存され、写真の画像は実際のファイルを取得する扱いになります。その後、クラスド上にアップロードしたり、File経由で保存したりと同様の処理が行えます。
今回のテストを通して、このようなXFileの使い方と考え方が分かったのは良かったです。

通常のFileとの互換性

XFileとFileの違い

  • XFileはクロスプラットフォームで動作するファイル抽象化
  • FileはDart標準のファイルクラス

XFileからFileへの変換

XFileをFileに変換する方法を示します。

final xFile = XFile('test_path');
final file = File(xFile.path);
expect(file, isNotNull);

FileからXFileへの変換

FileをXFileに変換する方法を示します。

final file = File('test_path');
final xFile = XFile(file.path);
expect(xFile, isNotNull);

path_providerについて

path_providerは、Flutterでプラットフォーム固有のファイルシステムディレクトリのパスを取得するためのパッケージです。アプリケーションのデータ保存先を簡単に特定することができ、複数のプラットフォームで一貫したディレクトリ管理を実現します。

ドキュメントディレクトリの取得

アプリケーションのドキュメントファイルを保存するためのディレクトリパスを取得できます。

Directory docDir = await getApplicationDocumentsDirectory();

一時ディレクトリの取得

一時的なファイルを保存するためのディレクトリパスを取得できます。このディレクトリの内容は、システムによっていつでも削除される可能性があります。

Directory tempDir = await getTemporaryDirectory();

サポートディレクトリの取得

アプリケーションのサポートファイルを保存するためのディレクトリパスを取得できます。このディレクトリは、ユーザーが直接アクセスすることを意図していないデータに使用します。

Directory appSupportDir = await getApplicationSupportDirectory();

ライブラリディレクトリの取得

アプリケーションのライブラリファイルを保存するためのディレクトリパスを取得できます(iOSとmacOSのみ対応)。

Directory appLibraryDir = await getLibraryDirectory();

キャッシュディレクトリの取得

アプリケーションのキャッシュファイルを保存するためのディレクトリパスを取得できます。このディレクトリは、システムによっていつでも削除される可能性があります。

Directory appCacheDir = await getApplicationCacheDirectory();

外部ストレージディレクトリの取得

外部ストレージにファイルを保存するためのディレクトリパスを取得できます(Androidのみ対応)。

Directory extStorageDir = await getExternalStorageDirectory();

外部キャッシュディレクトリの取得

外部ストレージのキャッシュファイルを保存するためのディレクトリパスを取得できます(Androidのみ対応)。

List<Directory> extCacheDirs = await getExternalCacheDirectories();

外部ストレージディレクトリ(複数)の取得

外部ストレージに複数のディレクトリパスを取得できます(Androidのみ対応)。

List<Directory> extStorageDirs = await getExternalStorageDirectories();

ダウンロードディレクトリの取得

ダウンロードファイルを保存するためのディレクトリパスを取得できます。

Directory downloadsDir = await getDownloadsDirectory();

各メソッドを使用することで、アプリケーションのデータ保存場所を適切に管理し、ユーザー体験を向上させることができます

Q&A

Q1: ファイルの存在確認はどのように行いますか?

A1: ファイルの存在確認は、ファイル操作の前提条件として重要です。Dartでは、Fileクラスのexistsメソッドを使って確認できます。以下の例では、example.txtというファイルが存在するかを確認し、結果をコンソールに表示します。

import 'dart:io';

void main() async {
  final file = File('example.txt');
  bool exists = await file.exists();
  print(exists ? 'File exists' : 'File does not exist');
}

Q2: ディレクトリ内のファイル一覧を取得するにはどうすればいいですか?

A2: ディレクトリ内のファイル一覧を取得するには、DirectoryクラスのlistSyncメソッドを使用します。以下のコード例では、example_directory内の全てのファイルとサブディレクトリのパスを取得し、コンソールに表示します。

import 'dart:io';

void main() async {
  final directory = Directory('example_directory');
  if (await directory.exists()) {
    var contents = directory.listSync();
    for (var entity in contents) {
      print(entity.path);
    }
  } else {
    print('Directory does not exist');
  }
}

Q3: cross_fileライブラリはどのように利用しますか?

A3: cross_fileライブラリは、DartおよびFlutterでクロスプラットフォームなファイル操作を簡素化します。例えば、ファイルの読み込みにはXFileクラスを使用します。以下のコード例では、example.txtファイルの内容を読み込み、コンソールに表示します。

import 'package:cross_file/cross_file.dart';

void main() async {
  final file = XFile('example.txt');
  String contents = await file.readAsString();
  print(contents);
}

まとめ

この記事を通じて、ファイルとディレクトリの基本操作や周辺のパッケージについて学びました。ファイルの存在確認やディレクトリの存在確認の重要性を理解し、Dartを使ってそれらを確認する具体的な方法も勉強しました。また、エラーハンドリングのベストプラクティスについても理解を深め、プログラムの信頼性を向上させるための実践的なテクニックを習得しました。これらの知識を活用することで、ファイルシステムの操作がより効率的かつ安全に行えるようになりました。

参考

ソース(main.dartにコピペして動作確認用)

一応WindowsとMacでは(無理矢理)テストが通過した。他のOSは分かりません、、

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:cross_file/cross_file.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;

void main() {
  final testDirectory = Directory.systemTemp.createTempSync('flutter_test');
  final testFilePath = p.join(testDirectory.path, 'test_file.txt');
  final testFilePath2 = p.join(testDirectory.path, 'test_file2.txt');
  final testBinaryFilePath = p.join(testDirectory.path, 'test_binary_file.bin');

  final singleLevelDirPath = p.join(testDirectory.path, 'level1');
  final multiLevelDirPath = p.join(testDirectory.path, 'level1', 'level2');

  group('Fileクラスの基本操作', () {
    test('ファイルを文字列として読み書き', () async {
      final file = File(testFilePath);
      await file.writeAsString('Hello, Flutter!');
      String contents = await file.readAsString();
      expect(contents, 'Hello, Flutter!');
    });

    test('ファイルをバイナリデータとして読み書き', () async {
      final file = File(testBinaryFilePath);
      Uint8List data = Uint8List.fromList([0, 1, 2, 3, 4, 5]);
      await file.writeAsBytes(data);
      Uint8List contents = await file.readAsBytes();
      expect(contents, data);
    });

    test('ファイルの追記方法', () async {
      final file = File(testFilePath);
      await file.writeAsString('Hello, Flutter!');
      await file.writeAsString('\nAppending data', mode: FileMode.append);
      String contents = await file.readAsString();
      expect(contents, 'Hello, Flutter!\nAppending data');
    });

    test('ファイルを作成/存在確認/削除する', () async {
      final file = File(testFilePath);
      file.createSync();
      expect(file.existsSync(), true);
      file.deleteSync();
      expect(file.existsSync(), false);
      await file.create();
      expect(await file.exists(), true);
      await file.delete();
      expect(await file.exists(), false);

      // 存在しないファイルを削除しようとすると例外が発生
      expect(() => file.deleteSync(), throwsA(isA<PathNotFoundException>()));
    });

    test('ファイルを移動/コピーする', () async {
      final file = File(testFilePath);
      await file.writeAsString('Hello, Flutter!');

      final copyPath = p.join(testDirectory.path, 'copy_file.txt');
      await file.copy(copyPath);
      final copyFile = File(copyPath);
      expect(await file.exists(), true);
      expect(await file.readAsString(), 'Hello, Flutter!');
      expect(await copyFile.readAsString(), 'Hello, Flutter!');

      final movePath = p.join(testDirectory.path, 'moved_file.txt');
      await file.rename(movePath);
      expect(await file.exists(), false);

      final movedFile = File(movePath);
      expect(await movedFile.readAsString(), 'Hello, Flutter!');
    });

    test('ファイルの長さを取得する', () {
      final file = File(testFilePath);
      final message = 'Hello, Flutter!';
      expect(message.length, 15);
      file.writeAsStringSync('Hello, Flutter!');
      expect(file.lengthSync(), 15);
    });

    test('ファイルを開いて、一部を取得する', () {
      final file = File(testFilePath);
      file.writeAsStringSync('Hello, Flutter!');
      final raf = file.openSync();
      raf.setPositionSync(0);
      expect(raf.readSync(5), [72, 101, 108, 108, 111]); // "Hello"
      raf.closeSync();
    });

    test('ファイルを文字列として1行ずつ処理する', () async {
      final file = File(testFilePath);

      // ファイルに複数行を書き込む
      final lines = ['Hello, Flutter!', 'Welcome to Dart.', 'Enjoy coding!'];
      file.writeAsStringSync(lines.join('\n'));

      // ファイルを行ごとに読み込む
      Stream<List<int>> streamInt = file.openRead();
      Stream<String> streamString = streamInt
          .transform(utf8.decoder) // バイトを文字列に変換
          .transform(const LineSplitter()); // 行に分割

      var counter = 0;
      await for (String line in streamString) {
        expect(line, lines[counter]);
        counter++;
      }
      expect(counter, 3);
    });

    test('Streamデータをファイルに出力', () async {
      final data = [0, 1, 2, 3, 4, 5];
      Uint8List uint8list = Uint8List.fromList(data);
      Stream<List<int>> streamListInt = Stream.fromIterable([uint8list]);

      final file = File(testBinaryFilePath);
      final result = await streamListInt.pipe(file.openWrite());
      identical(result, streamListInt);

      Uint8List contents = await file.readAsBytes();
      expect(contents, data);
    });

    test('ファイルの情報を取得する', () async {
      final file = File(testFilePath2);
      final createTime = DateTime.now();
      final waitTime = 1100 - createTime.millisecond;
      await Future.delayed(Duration(milliseconds: waitTime));

      expect(file.existsSync(), false);
      file.writeAsStringSync('Hello, Flutter!');
      final stat = file.statSync();
      expect(stat.type, FileSystemEntityType.file);
      expect(stat.size, 15);
      if (Platform.isWindows) {
        expect(stat.mode, 0x81B6);
      } else {
        expect(stat.mode, 0x81A4);
      }

      final createdTime = DateTime.now();

      //ファイルの変更日時/アクセス日時を取得する
      expect(stat.changed.isAfter(createTime), true);
      expect(stat.changed.isBefore(createdTime), true);
      expect(stat.modified.isAfter(createTime), true);
      expect(stat.modified.isBefore(createdTime), true);
      expect(stat.accessed.isAfter(createTime), true);
      expect(stat.accessed.isBefore(createdTime), true);

      final changeTime = DateTime.now();
      await Future.delayed(
        Duration(milliseconds: (1100 - changeTime.millisecond)),
      );

      file.writeAsStringSync('Hello, Flutter!');
      final stat2 = file.statSync();
      expect(stat2.changed.isAfter(createTime), true);
//      expect(stat2.changed.isBefore(createdTime),  true);

      expect(stat2.modified.isAfter(changeTime), true);
      //expect(stat2.accessed.isAfter(changeTime), true);

      final readTime = DateTime.now();
      await Future.delayed(
        Duration(milliseconds: (1100 - readTime.millisecond)),
      );

      file.readAsLinesSync();
      final stat3 = file.statSync();
      expect(stat3.changed.isAfter(createTime), true);
      // expect(stat3.changed.isBefore(createdTime), true);

      expect(stat3.modified.isAfter(readTime), false);
      expect(stat3.accessed.isAfter(readTime), true);

      file.deleteSync();
    });

    test('ファイルの変更監視をする', () async {
      final file = File(testFilePath);
      int counter = 0;
      final stream = file.watch();
      final subscription = stream.listen((event) {
        expect(event.type, FileSystemEvent.modify);
        expect(event.path, testFilePath);
        expect(event.isDirectory, false);
        counter++;
      });
      file.writeAsStringSync('Hello, again!');
      file.writeAsStringSync('Hello, again!');
      await Future.delayed(const Duration(milliseconds: 100));
      await subscription.cancel();

      if (Platform.isWindows || Platform.isMacOS) {
        // WindowsでFileの変更監視はサポートされていない.Macでのサポートされてなかった
        expect(counter, 0);
      } else {
        expect(counter, 2);
      }
    });
  });

  group('Directoryクラスの基本操作', () {
    test('一つの階層のディレクトリを作成・存在確認・削除', () async {
      final dir = Directory(singleLevelDirPath);
      await dir.create();
      expect(await dir.exists(), true);
      await dir.delete();
      expect(await dir.exists(), false);
    });

    test('一つの階層のディレクトリを作成・存在確認・削除(同期処理)', () {
      final dir = Directory(singleLevelDirPath);
      dir.createSync();
      expect(dir.existsSync(), true);
      dir.deleteSync();
      expect(dir.existsSync(), false);
    });

    test('複数階層のディレクトリを作成・存在確認・削除', () async {
      final dir = Directory(multiLevelDirPath);
      expect(dir.existsSync(), false);

      expect(() async => (await dir.create()),
          throwsA(isA<PathNotFoundException>()));
      dir.createSync(recursive: true);
      expect(dir.existsSync(), true);

      // parentディレクトリにdirディレクトリがあるので、deleteが失敗。
      final parent = dir.parent;
      expect(() => parent.deleteSync(), throwsA(isA<FileSystemException>()));

      // recursive:trueにすると、子ディレクトリごと削除
      parent.deleteSync(recursive: true);
      expect(parent.existsSync(), false);
      expect(dir.existsSync(), false);
    });

    test('ディレクトリ内のファイルとサブディレクトリを一覧取得', () async {
      final dir = Directory(multiLevelDirPath);
      dir.createSync(recursive: true);

      File(p.join(dir.path, 'level2.txt')).writeAsStringSync('level2');
      File(p.join(dir.parent.path, 'level1.txt')).writeAsStringSync('level1');

      final entities = dir.listSync();
      expect(entities.length, 1);
      expect(p.basename(entities.first.path), 'level2.txt');

      expect(p.basename(dir.parent.listSync().first.path), 'level1.txt');

      final recursiveBaseName = dir.parent
          .listSync(recursive: true)
          .map((e) => p.basename(e.path))
          .toSet();
      expect(recursiveBaseName, {'level2', 'level1.txt', 'level2.txt'});

      dir.parent.deleteSync(recursive: true);
    });

    test('ディレクトリを移動', () async {
      final srcDir = Directory(singleLevelDirPath);
      srcDir.createSync();
      File(p.join(singleLevelDirPath, testFilePath))
          .writeAsStringSync('Hello, Flutter!');

      final destDirPath = p.join(srcDir.parent.path, 'moved_dir');

      expect(srcDir.existsSync(), true);
      final destDir = srcDir.renameSync(destDirPath);
      expect(srcDir.existsSync(), false);
      expect(destDir.existsSync(), true);
      expect(File(p.join(destDir.path, testFilePath)).existsSync(), true);

      destDir.deleteSync(recursive: true);
    });

    test('ディレクトリのステータス情報を取得', () async {
      // テスト用のディレクトリを作成

      final creatingTime = DateTime.now();
      await Future.delayed(
          Duration(milliseconds: 1100 - creatingTime.millisecond));

      final dir = Directory(singleLevelDirPath);
      await dir.create();
      final createdTime = DateTime.now();

      // ディレクトリの状態を取得
      final statCreated = dir.statSync();
      expect(statCreated.type, FileSystemEntityType.directory);
      if (Platform.isWindows) {
        expect(statCreated.size, isZero);
      } else {
        expect(statCreated.size, 64);
      }
      expect(statCreated.changed.isAfter(creatingTime), isTrue);
      expect(statCreated.changed.isAfter(createdTime), isFalse);
      expect(statCreated.modified.isAfter(creatingTime), isTrue);
      expect(statCreated.modified.isAfter(createdTime), isFalse);
      expect(statCreated.accessed.isAfter(creatingTime), isTrue);
      expect(statCreated.accessed.isAfter(createdTime), isFalse);

      // ファイルをディレクトリ内に作成して変更を反映
      final file = File('${dir.path}/test.txt');
      file.writeAsStringSync('Hello, Dart!');
      final changeTime = DateTime.now();
      await Future.delayed(
          Duration(milliseconds: (1100 - changeTime.millisecond)));

      // ディレクトリの状態を取得(ディレクトリ作成時と変化なし)
      final statFileCreated = dir.statSync();
      expect(statFileCreated.type, FileSystemEntityType.directory);
      if (Platform.isWindows) {
        expect(statFileCreated.size, isZero);
      } else {
        expect(statFileCreated.size, 96);
      }

      final isAfterCreatedTime = Platform.isWindows ? isFalse : isTrue;
      expect(statFileCreated.changed.isAfter(creatingTime), isTrue);
      expect(statFileCreated.changed.isAfter(createdTime), isAfterCreatedTime);
      expect(statFileCreated.modified.isAfter(creatingTime), isTrue);
      expect(statFileCreated.modified.isAfter(createdTime), isAfterCreatedTime);
      expect(statFileCreated.accessed.isAfter(creatingTime), isTrue);
      expect(statFileCreated.accessed.isAfter(createdTime), isAfterCreatedTime);

      // Clean up
      await dir.delete(recursive: true);
    });

    test('ディレクトリの変更を監視する', () async {
      final dir = Directory(singleLevelDirPath);
      dir.createSync();

      final controller = StreamController<int>();

      expectLater(
        controller.stream,
        emitsInOrder(Platform.isMacOS
            ? [emitsDone] // Macではサポートされてない?
            : [
                FileSystemEvent.create,
                FileSystemEvent.modify,
                FileSystemEvent.modify,
                FileSystemEvent.delete,
                emitsDone
              ]),
      );

      final filePath = p.join(singleLevelDirPath, 'test_file.txt');
      final file = File(filePath);

      final subscription = dir.watch().listen((event) async {
        controller.add(event.type);
      });

      // ディレクトリ内にファイルを作成、変更、削除
      file.createSync();
      file.writeAsStringSync('Hello, Flutter!');
      file.deleteSync(recursive: true);

      // イベントが発生するのを待つ
      await Future.delayed(const Duration(milliseconds: 100));
      await subscription.cancel();
      controller.close();

      // Clean up
      await dir.delete(recursive: true);
    });
  });

  group('path', () {
    test('基本的な操作', () {
      // パスの結合
      if (Platform.isWindows) {
        expect(p.join('path', 'to', 'foo'), 'path\\to\\foo');
        expect(p.joinAll(['path', 'to', 'foo']), 'path\\to\\foo');
      } else {
        expect(p.join('path', 'to', 'foo'), 'path/to/foo');
        expect(p.joinAll(['path', 'to', 'foo']), 'path/to/foo');
      }

      // パスの分割
      expect(p.split('path/to/foo'), ['path', 'to', 'foo']);

      // 現在の作業ディレクトリの取得
      final currentDirPath = p.current;
      expect(currentDirPath, isNotNull);

      // パスの正規化
      if (Platform.isWindows) {
        expect(p.normalize('path/./to/..//foo'), 'path\\foo');
        expect(p.canonicalize('path/./to/../foo'),
            '${currentDirPath.toLowerCase()}\\path\\foo');
      } else {
        expect(p.normalize('path/./to/..//foo'), 'path/foo');
        expect(
            p.canonicalize('path/./to/../foo'), '${currentDirPath}/path/foo');
      }
    });

    test('パスの情報取得', () {
      // ファイル名の取得
      expect(p.basename('path/to/foo.dart'), 'foo.dart');
      expect(p.basenameWithoutExtension('path/to/foo.dart'), 'foo');

      // ディレクトリ名の取得
      expect(p.dirname('path/to/foo.dart'), 'path/to');

      // ファイル拡張子の取得と設定
      expect(p.extension('path/to/foo.dart'), '.dart');
      expect(p.setExtension('path/to/foo', '.dart'), 'path/to/foo.dart');
      expect(p.setExtension('path/to/foo.txt', '.dart'), 'path/to/foo.dart');
      expect(p.withoutExtension('path/to/foo.dart'), 'path/to/foo');
    });

    test('パスの変換', () {
      // 絶対パスと相対パスの変換
      expect(p.isAbsolute('/path/to/foo'), true);
      expect(p.isRelative('path/to/foo'), true);

      if (Platform.isWindows) {
        expect(
          p.relative('/root/path/a/b.dart', from: '/root/path'),
          'a\\b.dart',
        );
        expect(p.fromUri('file:///path/to/foo'), '\\path\\to\\foo');
        expect(p.toUri('/path/to/foo').toString(), 'file:///C:/path/to/foo');
      } else {
        expect(
          p.relative('/root/path/a/b.dart', from: '/root/path'),
          'a/b.dart',
        );
        expect(p.fromUri('file:///path/to/foo'), '/path/to/foo');
        expect(p.toUri('/path/to/foo').toString(), 'file:///path/to/foo');
      }
      // URIとの相互変換
    });

    test('パスの検証', () {
      // パスの一致確認
      expect(p.equals('path/to/foo', 'path/to/foo'), true);

      // パスの包含確認
      expect(p.isWithin('/root/path', '/root/path/a'), true);

      // ルートパスの取得
      expect(p.rootPrefix('/root/path/to/foo'), '/');
    });

    test('パスのスタイルとセパレータ', () {
      // パススタイルの取得
      expect(p.style, p.Style.platform);

      // パスセパレータの取得
      expect(p.separator, p.context.separator);

      if (Platform.isWindows) {
        expect(p.style, p.Style.windows);
        expect(p.separator, '\\');
      }
    });

    test('高度な操作', () {
      // パスのハッシュ化
      expect(p.hash('path/to/foo'), isNotNull);

      // 人間可読形式のURI表示
      if (Platform.isWindows) {
        expect(p.prettyUri('file:///path/to/foo'), '\\path\\to\\foo');
      } else {
        expect(p.prettyUri('file:///path/to/foo'), '/path/to/foo');
      }
    });
  });

  group('XFile', () {
    test('基本操作', () async {
      const String testPath = 'test.txt';
      const List<int> testBytes = [72, 101, 108, 108, 111]; // 'Hello'

      final xFileFromData = XFile.fromData(Uint8List.fromList(testBytes));
      expect(xFileFromData.path.isEmpty, true);
      expect(await xFileFromData.length(), equals(testBytes.length));
      expect(await xFileFromData.readAsBytes(), equals(testBytes));
      expect(await xFileFromData.readAsString(), 'Hello');
      expect(() => xFileFromData.lastModified(),
          throwsA(isA<PathNotFoundException>()));

      final actualStream = xFileFromData.openRead();
      expect(await actualStream.first, Uint8List.fromList(testBytes));

      final actualStreamPart = xFileFromData.openRead(1, 3);
      expect(await actualStreamPart.first, Uint8List.fromList([101, 108]));

      expect(XFile(testPath), isNotNull);
      expect(
        () => XFile(testPath).length(),
        throwsA(isA<PathNotFoundException>()),
      );

      await xFileFromData.saveTo(testPath);
      expect(xFileFromData.path.isEmpty, true);

      final xFileFromPath = XFile(testPath);
      expect(await xFileFromPath.length(), 5);

      final lastModifiedTime = await xFileFromPath.lastModified();
      final now = DateTime.now();
      expect(
          lastModifiedTime,
          now.subtract(Duration(
              microseconds: now.microsecond, milliseconds: now.millisecond)));

      final file = File(xFileFromPath.path);
      expect(file.lengthSync(), 5);

      final xFileFromFile = XFile(file.path);
      expect(await xFileFromFile.length(), 5);

      file.delete();
    });
  });
}