[dartdev] Update dart install command to handle filesystem some errors

When running on a cloudtop, for example, the tmp space may be part of a
different filesystem than the user space where the package is being
registered. In this case a simple rename doesn't work, we have to copy
the files and delete the old one.

The script was also getting confused when a previous install failed. The
partial setup was leading to errors while trying to uninstall the old
verison.

Change-Id: I67f04547bc5279adc5f0d9f528cc9224c1f54e64
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/452000
Commit-Queue: Nate Biggs <natebiggs@google.com>
Reviewed-by: Daco Harkes <dacoharkes@google.com>
diff --git a/pkg/dartdev/lib/src/commands/install.dart b/pkg/dartdev/lib/src/commands/install.dart
index c78acc0..e37a562 100644
--- a/pkg/dartdev/lib/src/commands/install.dart
+++ b/pkg/dartdev/lib/src/commands/install.dart
@@ -291,6 +291,10 @@
       }
     } on PathAccessException {
       _installException('Deletion failed. The application might be in use.');
+    } on PathNotFoundException {
+      print('Bundle not found when uninstalling. '
+          'Earlier installation may have failed.');
+      // Continue installing
     }
   }
 
@@ -349,12 +353,42 @@
     appBundleDirectory.directory.createSync(recursive: true);
     final bundleDirectory =
         Directory.fromUri(buildDirectory.uri.resolve('bundle/'));
-    await bundleDirectory.rename(
-        appBundleDirectory.directory.uri.resolve('bundle/').toFilePath());
+    await _renameSafe(
+        bundleDirectory, appBundleDirectory.directory.uri.resolve('bundle/'));
     await helperPackageLockFile.copy(appBundleDirectory.pubspecLock.path);
     await sourcePackagePubspecFile.copy(appBundleDirectory.pubspec.path);
   }
 
+  /// This allows us to rename files across different filesystems.
+  ///
+  /// Tries to use the basic [Directory.rename] method but if that fails then
+  /// fall back to copying each entity and then deleting it.
+  Future<void> _renameSafe(Directory from, Uri to) async {
+    try {
+      await from.rename(to.toFilePath());
+    } on FileSystemException {
+      // The rename failed, possibly because `from` and `to` are on different
+      // filesystems. Fall back to copy and delete.
+      await _renameSafeCopyAndDelete(from, to);
+    }
+  }
+
+  Future<void> _renameSafeCopyAndDelete(Directory from, Uri to) async {
+    await Directory.fromUri(to).create(recursive: true);
+    await for (final child in from.list()) {
+      final newChildPath = to.resolve(p.relative(child.path, from: from.path));
+      if (child is File) {
+        await child.copy(newChildPath.toFilePath());
+      } else if (child is Directory) {
+        await _renameSafeCopyAndDelete(child, Uri.parse('$newChildPath/'));
+      } else {
+        await Link.fromUri(newChildPath)
+            .create(await (child as Link).resolveSymbolicLinks());
+      }
+      await child.delete(recursive: false);
+    }
+  }
+
   void _installExecutablesOnPath(
       DartBuildExecutables executables,
       AppBundleDirectory appBundleDirectory,