対象者
- Dart/Flutterでファイル操作や圧縮・解凍を行いたい方
archive
パッケージの使い方を知りたい方- ZIPファイルの操作方法を学びたい方
はじめに
ファイルの圧縮や解凍は、データの保存や転送において重要な機能です。本記事では、Dart/Flutterを使用してファイルの書き込みからZIP圧縮、ZIPファイルの内容確認、そして解凍までの一連の流れを解説します。これにより、アプリケーションでのデータ管理が効率化し、ユーザー体験の向上につながります。
インストール
fltuter pub add archive
個人的にarchive
を使用しているので、こちらで解説します。
「Flutter で Zip を扱うパッケージを3つ試した」によると、200MB程度で失敗するようになるらしいですが、そんなデカいファイル使わんし。(大きなファイルを扱う場合は、動作検証が必要かも)
Tar、GZIP、BIzip2などでも圧縮、解凍可能みたいです。
ZIPファイルで圧縮
圧縮対象のファイルを作成
まず、圧縮するファイルを作成します。
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:path/path.dart' as path;
void main() async {
const fileSize = 1000;
// ファイル作成
final file1 = File('a.txt');
final directory = Directory('b');
await directory.create();
final file2 = File(path.join(directory.path, 'c.txt'));
final stream1 = file1.openWrite();
final stream2 = file2.openWrite();
for (int i = 0; i < fileSize; i++) {
stream1.write('a');
stream2.write('A');
}
await stream1.close();
await stream2.close();
final archive = Archive();
// 圧縮
for (final file in [file1, file2]) {
archive.addFile(ArchiveFile(
file.path, await file.length(), await file.readAsBytes()));
}
final encodedArchive = ZipEncoder().encode(archive);
final zipFile = await File('test.zip').writeAsBytes(encodedArchive!);
}
ここでは、a.txt
とb/c.txt
の2つのファイルを作成し、それぞれに文字を1000回書き込みます。
ZIPで圧縮
次に、これらのファイルをZIPアーカイブに追加します。
final archive = Archive();
// 圧縮
for (final file in [file1, file2]) {
archive.addFile(ArchiveFile(
file.path, await file.length(), await file.readAsBytes()));
}
final encodedArchive = ZipEncoder().encode(archive);
final zipFile = await File('test.zip').writeAsBytes(encodedArchive!);
Archive
オブジェクトにファイルを追加し、ZipEncoder
でエンコードしてtest.zip
というZIPファイルを作成します。
ZIPファイルの中身を見る
作成したZIPファイルの内容を確認します。
final input = InputFileStream(zipFile.path);
final zipArchive = ZipDecoder().decodeBuffer(input);
expect(zipArchive.files.length, equals(2));
ZipDecoder
を使用してZIPファイルを読み込み、含まれるファイルの数を確認します。
特定のファイルの情報を取得することも可能です。
final file1InZip = zipArchive.files.firstWhere((e) => e.name == 'a.txt');
expect(file1InZip.name, equals('a.txt'));
expect(file1InZip.size, equals(fileSize));
ファイルの内容を取得して確認することもできます。
final file2InZip = zipArchive.files.firstWhere((e) => e.name == 'b/c.txt');
final content = utf8.decode(file2InZip.content!);
expect(content, equals('A' * fileSize));
ファイルをclose
すると読めなくなります。content
がnullになります。必要な場合は、Archive: ZipDecoder().decodeBuffer
を作成し直しましょう。(3年くらい前に初めて使ったときも、ここでつまずいたorz)
expect(file1InZip.content, isNotNull);
expect(file1InZip.content, isNotNull); // 2回目も読み込める
await file1InZip.close();
// 一度閉じると後でアクセスしても、データが読み込めない
expect(file1InZip.size, equals(fileSize));
expect(file1InZip.content, isNull);
final file1InZip2 = zipArchive.files.firstWhere((e) => e.name == 'a.txt');
expect(file1InZip2.content, isNull);
await file1InZip2.close();
ZIPファイルを解凍する
ZIPファイルからファイルを解凍する方法です。
特定のファイルを解凍する場合:
for (final fileInZip in zipArchive.files) {
final outputStream = OutputFileStream(fileInZip.name);
fileInZip.writeContent(outputStream);
await outputStream.close();
}
全てのファイルを解凍する場合:
extractFileToDisk(zipFile.path, '.');
extractFileToDisk
関数を使用すると、ZIPファイル内の全てのファイルを指定したディレクトリに解凍できます。
Q&A
Q1: length
はあるのに、content
がnull
になるのはなぜですか?
A: 一度ファイルをclose()
すると、そのファイルのcontent
はnull
になります。具体的な例を以下に示します。
final file1InZip = zipArchive.files.firstWhere((e) => e.name == 'a.txt');
expect(file1InZip.content, isNotNull);
expect(file1InZip.content, isNotNull);
await file1InZip.close();
// 一度閉じると後でアクセスしても、データが読み込めない
expect(file1InZip.size, equals(fileSize));
expect(file1InZip.content, isNull);
この例では、file1InZip
のcontent
にアクセスする前はnull
ではありませんが、close()
を呼び出した後はcontent
がnull
になります。再度アクセスする必要がある場合は、close()
しないようにしましょう。
Q2: 圧縮時に特定のファイルのみを除外するにはどうすれば良いですか?
A: ファイルを追加する際に、条件を設けて特定のファイルを除外することができます。例えば、拡張子が.tmp
のファイルを除外する場合は以下のようにします。
for (final file in [file1, file2, temporaryFile]) {
if (!file.path.endsWith('.tmp')) {
archive.addFile(ArchiveFile(
file.path, await file.length(), await file.readAsBytes()));
}
}
これにより、.tmp
拡張子のファイルはZIPアーカイブに含まれなくなります。
Q3: 解凍先のディレクトリが存在しない場合、どう対処すれば良いですか?
A: 解凍先のディレクトリが存在しない場合は、割と適切に再帰的にディレクトリを作ってくれます。
まとめ
Dart/Flutterでのファイル操作からZIP圧縮、内容確認、解凍までを解説しました。archive
パッケージを活用することで、これらの機能を簡単に実装できます。ファイル管理が必要なアプリケーション開発において、ぜひ参考にしてみてください。
参考
ソース(main.dartにコピペして動作確認用)
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:archive/archive_io.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as path;
main() {
test('ファイル書き込み', () async {
final file = File('a.txt');
final stream = file.openWrite();
stream.write('test');
await stream.close();
expect(file.readAsStringSync(), 'test');
file.delete();
});
test('圧縮・解凍', () async {
const fileSize = 1000;
// ファイル作成
final file1 = File('a.txt');
final directory = Directory('b');
await directory.create();
final file2 = File(path.join(directory.path, 'c.txt'));
final files = [file1, file2, directory];
final stream1 = file1.openWrite();
final stream2 = file2.openWrite();
for (int i = 0; i < fileSize; i++) {
stream1.write('a');
stream2.write('A');
}
await stream1.close();
await stream2.close();
expect(await file1.length(), fileSize);
expect(await file2.length(), fileSize);
final archive = Archive();
// 圧縮
for (final file in [file1, file2]) {
archive.addFile(ArchiveFile(
file.path, await file.length(), await file.readAsBytes()));
}
final encodedArchive = ZipEncoder().encode(archive);
final zipFile = await File('test.zip').writeAsBytes(encodedArchive!);
expect(zipFile.existsSync(), true);
// 表示
final input = InputFileStream(zipFile.path);
final zipArchive = ZipDecoder().decodeBuffer(input);
expect(zipArchive.files.length, equals(2));
final file1InZip = zipArchive.files.firstWhere((e) => e.name == 'a.txt');
expect(file1InZip.isFile, isTrue);
expect(file1InZip.name, equals('a.txt'));
expect(file1InZip.compress, isTrue);
expect(file1InZip.size, equals(fileSize));
expect(file1InZip.content, isNotNull);
expect(file1InZip.content, isNotNull); // 2回目も読み込める
await file1InZip.close();
// 一度閉じると後でアクセスしても、データが読み込めない
expect(file1InZip.size, equals(fileSize));
expect(file1InZip.content, isNull);
final file1InZip2 = zipArchive.files.firstWhere((e) => e.name == 'a.txt');
expect(file1InZip2.content, isNull);
await file1InZip2.close();
final file2InZip =
zipArchive.files.firstWhere((e) => e.name.endsWith('c.txt'));
expect(file2InZip.isFile, isTrue);
expect(file2InZip.name, equals('b/c.txt'));
expect(file2InZip.compress, isTrue);
expect(file2InZip.size, equals(fileSize));
expect(file2InZip.content, isNotNull);
expect(file2InZip.content, isA<List<int>>());
// zipファイルの中のファイルをメモリに展開して確認: 0x41=>A
expect(file2InZip.content, [for (int i = 0; i < fileSize; i++) 0x41]);
// zipファイルの中のファイルをメモリに展開して、文字列に変換
final content = utf8.decode(file2InZip.content);
expect(RegExp(r'^A{1000}$').hasMatch(content), isTrue);
expect(content, equals('A' * fileSize));
await file2InZip.close();
expect(file2InZip.content, isNull);
for (var file in files) {
file.deleteSync();
}
expect(files.any((e) => e.existsSync()), isFalse);
// zip内の特定のファイルを解凍
final zipArchive2 =
ZipDecoder().decodeBuffer(InputFileStream(zipFile.path));
for (final fileInZip in zipArchive2.files) {
expect(fileInZip.size, equals(fileSize));
final outputStream = OutputFileStream(fileInZip.name);
fileInZip.writeContent(outputStream);
await outputStream.close();
}
expect(files.every((e) => e.existsSync()), isTrue);
expect(await file1.length(), equals(fileSize));
expect(await file2.length(), equals(fileSize));
for (var file in files) {
file.deleteSync();
}
// zipを全部解凍
expect(files.every((e) => e.existsSync()), isFalse);
extractFileToDisk(zipFile.path, '.');
expect(files.every((e) => e.existsSync()), isTrue);
// 後処理
for (var file in files) {
file.deleteSync();
}
zipFile.delete();
});
}