tomXdgTrash function

bool tomXdgTrash(
  1. String absolute, {
  2. Map<String, String>? environment,
  3. DateTime? now,
})

Moves absolute into the home trash: $XDG_DATA_HOME/Trash, or ~/.local/share/Trash. Answers false, with nothing changed, when there is no home to put a trash in or the item is on another filesystem than the trash — the specification gives such an item its mount's own trash, which only the desktop's tools know how to find.

environment and now are for tests; the defaults are the process's.

Implementation

bool tomXdgTrash(
  String absolute, {
  Map<String, String>? environment,
  DateTime? now,
}) {
  final env = environment ?? Platform.environment;
  final dataHome =
      env['XDG_DATA_HOME'] ??
      (env['HOME'] == null ? null : p.join(env['HOME']!, '.local', 'share'));
  if (dataHome == null || dataHome.isEmpty) return false;
  final trash = p.join(dataHome, 'Trash');
  final files = Directory(p.join(trash, 'files'));
  final info = Directory(p.join(trash, 'info'));
  try {
    files.createSync(recursive: true);
    info.createSync(recursive: true);
  } on FileSystemException {
    return false;
  }

  // The name in the trash is the item's own, made unique; the record is
  // created exclusively, so two deletes of the same name cannot claim it.
  final base = p.basename(absolute);
  File? record;
  var name = base;
  for (var n = 1; record == null; n++) {
    final candidate = File(p.join(info.path, '$name.trashinfo'));
    if (FileSystemEntity.typeSync(p.join(files.path, name)) ==
        FileSystemEntityType.notFound) {
      try {
        candidate.createSync(exclusive: true);
        record = candidate;
        break;
      } on FileSystemException {
        // Taken by now; try the next name.
      }
    }
    name = '$base.$n';
    if (n > 10000) return false;
  }
  final at = (now ?? DateTime.now()).toLocal();
  String two(int v) => v.toString().padLeft(2, '0');
  final date =
      '${at.year.toString().padLeft(4, '0')}-${two(at.month)}-'
      '${two(at.day)}T${two(at.hour)}:${two(at.minute)}:${two(at.second)}';
  try {
    record.writeAsStringSync(
      '[Trash Info]\n'
      'Path=${Uri(path: absolute).toString()}\n'
      'DeletionDate=$date\n',
      flush: true,
    );
    final target = p.join(files.path, name);
    if (FileSystemEntity.isDirectorySync(absolute)) {
      Directory(absolute).renameSync(target);
    } else {
      File(absolute).renameSync(target);
    }
    return true;
  } on FileSystemException {
    // Another filesystem, or the item could not be moved: take the record
    // back, since a record with no item is a phantom in every file manager.
    try {
      record.deleteSync();
    } on FileSystemException {
      // Nothing more to do.
    }
    return false;
  }
}