Add copyPath and copyPathSync. (#30)

* .

* Dartfmt.

* Oops.

* Clarify.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 69ab12b..c2565ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@
 }
 ```
 
+- Added a `copyPath` and `copyPathSync` function, similar to `cp -R`.
+
+- Added a dependency on `package:path`.
+
 - Added the remaining missing arguments to `ProcessManager.spawnX` which
   forward to `Process.start`. It is now an interchangeable function for running
   a process.
diff --git a/lib/io.dart b/lib/io.dart
index 3ad63fa..39e4450 100644
--- a/lib/io.dart
+++ b/lib/io.dart
@@ -2,6 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+export 'src/copy_path.dart' show copyPath, copyPathSync;
 export 'src/exit_code.dart' show ExitCode;
 export 'src/permissions.dart' show isExecutable;
 export 'src/process_manager.dart' show ProcessManager, Spawn, StartProcess;
diff --git a/lib/src/copy_path.dart b/lib/src/copy_path.dart
new file mode 100644
index 0000000..ed790ed
--- /dev/null
+++ b/lib/src/copy_path.dart
@@ -0,0 +1,70 @@
+// Copyright 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. 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;
+
+bool _doNothing(String from, String to) {
+  if (p.canonicalize(from) == p.canonicalize(to)) {
+    return true;
+  }
+  if (p.isWithin(from, to)) {
+    throw new ArgumentError('Cannot copy from $from to $to');
+  }
+  return false;
+}
+
+/// Copies all of the files in the [from] directory to [to].
+///
+/// This is similar to `cp -R <from> <to>`:
+/// * Symlinks are supported.
+/// * Existing files are over-written, if any.
+/// * If [to] is within [from], throws [ArgumentError] (an infinite operation).
+/// * If [from] and [to] are canonically the same, no operation occurs.
+///
+/// Returns a future that completes when complete.
+Future<Null> copyPath(String from, String to) async {
+  if (_doNothing(from, to)) {
+    return;
+  }
+  await new Directory(to).create(recursive: true);
+  await for (final file in new Directory(from).list(recursive: true)) {
+    final copyTo = p.join(to, p.relative(file.path, from: from));
+    if (file is Directory) {
+      await new Directory(copyTo).create(recursive: true);
+    } else if (file is File) {
+      await new File(file.path).copy(copyTo);
+    } else if (file is Link) {
+      await new Link(copyTo).create(await file.target(), recursive: true);
+    }
+  }
+}
+
+/// Copies all of the files in the [from] directory to [to].
+///
+/// This is similar to `cp -R <from> <to>`:
+/// * Symlinks are supported.
+/// * Existing files are over-written, if any.
+/// * If [to] is within [from], throws [ArgumentError] (an infinite operation).
+/// * If [from] and [to] are canonically the same, no operation occurs.
+///
+/// This action is performed synchronously (blocking I/O).
+void copyPathSync(String from, String to) {
+  if (_doNothing(from, to)) {
+    return;
+  }
+  new Directory(to).createSync(recursive: true);
+  for (final file in new Directory(from).listSync(recursive: true)) {
+    final copyTo = p.join(to, p.relative(file.path, from: from));
+    if (file is Directory) {
+      new Directory(copyTo).createSync(recursive: true);
+    } else if (file is File) {
+      new File(file.path).copySync(copyTo);
+    } else if (file is Link) {
+      new Link(copyTo).createSync(file.targetSync(), recursive: true);
+    }
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 8427910..14b60a6 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -11,9 +11,10 @@
 dependencies:
   charcode: ^1.0.0
   meta: ^1.0.2
+  path: ^1.5.1
   string_scanner: ">=0.1.5 <2.0.0"
 
 dev_dependencies:
   dart_style: ^1.0.7
-  path: ^1.0.0
   test: ^0.12.0
+  test_descriptor: ^1.0.0
diff --git a/test/copy_path_test.dart b/test/copy_path_test.dart
new file mode 100644
index 0000000..ac06bb5
--- /dev/null
+++ b/test/copy_path_test.dart
@@ -0,0 +1,47 @@
+// Copyright 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+import 'dart:async';
+
+import 'package:io/io.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+void main() {
+  test('should copy a directory (async)', () async {
+    await _create();
+    await copyPath(p.join(d.sandbox, 'parent'), p.join(d.sandbox, 'copy'));
+    await _validate();
+  });
+
+  test('should copy a directory (sync)', () async {
+    await _create();
+    copyPathSync(p.join(d.sandbox, 'parent'), p.join(d.sandbox, 'copy'));
+    await _validate();
+  });
+
+  test('should catch an infinite operation', () async {
+    await _create();
+    expect(
+      copyPath(
+        p.join(d.sandbox, 'parent'),
+        p.join(d.sandbox, 'parent', 'child'),
+      ),
+      throwsArgumentError,
+    );
+  });
+}
+
+d.DirectoryDescriptor _struct() {
+  return d.dir('parent', [
+    d.dir('child', [
+      d.file('foo.txt'),
+    ]),
+  ]);
+}
+
+Future _create() => _struct().create();
+Future _validate() => _struct().validate();