blob: 894c2f14959b7babc7011be827703f4b661c53d1 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:path/path.dart' as p;
import '../logger.dart';
import 'command.dart';
import 'flags.dart';
/// The `cleanup` command for removing unnecessary on-disk artifacts.
final class CleanupCommand extends CommandBase {
/// Constructs the `cleanup` command.
CleanupCommand({
required super.environment,
super.help = false,
super.usageLineLength,
}) {
argParser.addFlag(
dryRunFlag,
abbr: 'd',
help: 'Write changes to stdout without modifying the file system.',
negatable: false,
);
argParser.addOption(
'untouched-since',
defaultsTo: () {
const thirtyDays = Duration(days: 30);
final dateTime = environment.now().subtract(thirtyDays);
return _toDateString(dateTime);
}(),
help: 'What date to consider artifacts old enough to safely remove.',
valueHelp: 'YYYY-MM-DD',
);
}
@override
String get name => 'cleanup';
@override
String get description => 'Removes stale or unnecessary on-disk artifacts.';
@override
List<String> get aliases => const ['gc'];
static final _dateString = RegExp(r'^(\d{4})-(\d{2})-(\d{2})$');
@override
Future<int> run() async {
final dryRun = argResults!.flag('dry-run');
final since = () {
final yyyyMmDd = argResults!.option('untouched-since')!;
final dateMatch = _dateString.matchAsPrefix(yyyyMmDd);
if (dateMatch == null) {
throw FatalError('Invalid --untouched-since: $yyyyMmDd');
}
return DateTime(
int.parse(dateMatch.group(1)!),
int.parse(dateMatch.group(2)!),
int.parse(dateMatch.group(3)!),
);
}();
// Look at the directories in "out" for ones older than "since".
environment.logger.status('Checking ${environment.engine.outDir.path}...');
final toDelete = [
await for (final entity in environment.engine.outDir.list())
if (entity is Directory &&
_shouldDelete(entity, ifAccessedLaterThan: since))
entity
]..sort((a, b) => a.path.compareTo(b.path));
if (toDelete.isEmpty) {
environment.logger.status(
'No directories were accessed later than ${_toDateString(since)}.',
);
return 0;
}
final totalSize = toDelete.fold(0, (p, n) => p + _getSizeRecursive(n));
if (dryRun) {
environment.logger.status(
'The following directories were accessed later than ${_toDateString(since)}:',
);
for (final e in toDelete) {
environment.logger.status(' ${p.basename(e.path)}');
}
environment.logger.status(
'Run without --dry-run to reclaim '
'${_toReadableBytes(totalSize.toDouble())}.',
);
return 0;
}
final spinner = environment.logger.startSpinner();
for (final e in toDelete) {
try {
await e.delete(recursive: true);
} on FileSystemException catch (_) {
environment.logger.warning('Failed to delete ${p.basename(e.path)}');
}
}
spinner.finish();
environment.logger.status(
'Deleted ${toDelete.length} output directories and reclaimed '
'${_toReadableBytes(totalSize.toDouble())}.',
);
return 0;
}
static bool _shouldDelete(
Directory entity, {
required DateTime ifAccessedLaterThan,
}) {
final accessed = entity.statSync().accessed;
return accessed.isBefore(ifAccessedLaterThan);
}
}
int _getSizeRecursive(Directory dir) {
return dir.listSync().fold(0, (p, n) => p + n.statSync().size);
}
String _toDateString(DateTime dateTime) {
final output = StringBuffer('${dateTime.year}-');
output.write(dateTime.month.toString().padLeft(2, '0'));
output.write('-');
output.write(dateTime.day.toString().padLeft(2, '0'));
return output.toString();
}
String _toReadableBytes(double bytes) {
var type = _FileSize.bytes;
if (bytes >= 1024) {
type = _FileSize.kilobytes;
bytes = bytes / 1024;
}
if (bytes >= 1024) {
type = _FileSize.megabytes;
bytes = bytes / 1024;
}
if (bytes >= 1024) {
type = _FileSize.gigabytes;
bytes = bytes / 1024;
}
return '${bytes.toStringAsFixed(2)}${type.suffix}';
}
enum _FileSize {
bytes('bytes'),
kilobytes('KB'),
megabytes('MB'),
gigabytes('GB');
const _FileSize(this.suffix);
final String suffix;
}