mv method

Future<void> mv(
  1. String src,
  2. String dst
)

Moves a file or directory from src to dst.

Matches Kubo's mfs.Mv: a dst ending in / or naming an existing directory moves the source inside it under its basename; an existing file at dst is silently replaced; a name collision inside a target directory is an error.

Implementation

Future<void> mv(String src, String dst) async {
  final denylist = _denylistService;
  if (denylist != null && _pathLooksBlocked(src)) {
    throw DenylistBlockedException(src);
  }

  final srcParts = _splitPath(src);
  if (srcParts.isEmpty) {
    throw Exception('Cannot move MFS root: $src');
  }
  final srcCid = await _resolvePath(_rootCid!, srcParts);
  if (srcCid == null) {
    throw Exception('Source path not found: $src');
  }

  final srcName = srcParts.last;
  var dstPath = dst;
  var redirectedIntoDir = dst.endsWith('/');
  if (!redirectedIntoDir) {
    final dstCid = await _resolvePath(_rootCid!, _splitPath(dst));
    if (dstCid != null) {
      final dstType = await _unixfsType(dstCid);
      if (dstType == Data_DataType.Directory ||
          dstType == Data_DataType.HAMTShard) {
        dstPath = '${_normalizePath(dst)}/$srcName';
        redirectedIntoDir = true;
      }
    }
  } else {
    dstPath = '$dst$srcName';
  }

  final normalizedDst = _normalizePath(dstPath);
  if (_normalizePath(src) == normalizedDst) {
    return; // Moving a node onto itself is a no-op.
  }

  final dstParts = _splitPath(normalizedDst);
  final existing = await _resolvePath(_rootCid!, dstParts);
  if (existing != null) {
    if (redirectedIntoDir) {
      // Inside a target directory Kubo's AddChild fails on any name
      // collision, file or directory.
      throw Exception('file already exists: $normalizedDst');
    }
    final existingType = await _unixfsType(existing);
    if (existingType == Data_DataType.Directory ||
        existingType == Data_DataType.HAMTShard) {
      // Unreachable: when the destination resolves to a directory the path
      // is redirected inside it above, so `existing` here is never a dir.
      // coverage:ignore-start
      throw Exception('file already exists: $normalizedDst');
      // coverage:ignore-end
    }
    // An existing file at the destination is silently replaced (Kubo
    // unlinks it before re-adding the source node).
  }

  await cp(src, normalizedDst, force: true);
  await rm(src, recursive: true);
}