Skip to content

Commit ce30034

Browse files
authored
[io] copyPath/copyPathSync: add a deepCopyLinks parameter (#1267)
The original intention was to create new `Link` objects when copying links, but the implementation left the implicit `followLinks: true` argument which meant that in practice links were recursed into and files and directories deeply copied. This behavior is the only behavior that will work when there are links present and the copy is across volumes which do not permit links, but it is not the optimal behavior for most use cases. Add a `deepCopyLinks` argument which defaults to `true` to keep the current behavior by default, but allow the smaller copy with shallow links through a boolean argument. Update the doc and add tests for the behavior around links both with and without the argument.
1 parent 5cdad88 commit ce30034

File tree

4 files changed

+98
-7
lines changed

4 files changed

+98
-7
lines changed

pkgs/io/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.1.0-wip
2+
3+
* Add a `deepCopyLinks` argument to `copyPath` and `copyPathSync`.
4+
15
## 1.0.5
26

37
* Require Dart 3.4.

pkgs/io/lib/src/copy_path.dart

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,23 @@ bool _doNothing(String from, String to) {
1919
/// Copies all of the files in the [from] directory to [to].
2020
///
2121
/// This is similar to `cp -R <from> <to>`:
22-
/// * Symlinks are supported.
2322
/// * Existing files are over-written, if any.
2423
/// * If [to] is within [from], throws [ArgumentError] (an infinite operation).
2524
/// * If [from] and [to] are canonically the same, no operation occurs.
25+
/// * If [deepCopyLinks] is `true` (the default) then links are followed and
26+
/// the content of linked directories and files are copied entirely. If
27+
/// `false` then new [Link] file system entities are created linking to the
28+
/// same target the links under [from].
2629
///
2730
/// Returns a future that completes when complete.
28-
Future<void> copyPath(String from, String to) async {
31+
Future<void> copyPath(String from, String to,
32+
{bool deepCopyLinks = true}) async {
2933
if (_doNothing(from, to)) {
3034
return;
3135
}
3236
await Directory(to).create(recursive: true);
33-
await for (final file in Directory(from).list(recursive: true)) {
37+
await for (final file
38+
in Directory(from).list(recursive: true, followLinks: deepCopyLinks)) {
3439
final copyTo = p.join(to, p.relative(file.path, from: from));
3540
if (file is Directory) {
3641
await Directory(copyTo).create(recursive: true);
@@ -45,18 +50,22 @@ Future<void> copyPath(String from, String to) async {
4550
/// Copies all of the files in the [from] directory to [to].
4651
///
4752
/// This is similar to `cp -R <from> <to>`:
48-
/// * Symlinks are supported.
4953
/// * Existing files are over-written, if any.
5054
/// * If [to] is within [from], throws [ArgumentError] (an infinite operation).
5155
/// * If [from] and [to] are canonically the same, no operation occurs.
56+
/// * If [deepCopyLinks] is `true` (the default) then links are followed and
57+
/// the content of linked directories and files are copied entirely. If
58+
/// `false` then new [Link] file system entities are created linking to the
59+
/// same target the links under [from].
5260
///
5361
/// This action is performed synchronously (blocking I/O).
54-
void copyPathSync(String from, String to) {
62+
void copyPathSync(String from, String to, {bool deepCopyLinks = true}) {
5563
if (_doNothing(from, to)) {
5664
return;
5765
}
5866
Directory(to).createSync(recursive: true);
59-
for (final file in Directory(from).listSync(recursive: true)) {
67+
for (final file in Directory(from)
68+
.listSync(recursive: true, followLinks: deepCopyLinks)) {
6069
final copyTo = p.join(to, p.relative(file.path, from: from));
6170
if (file is Directory) {
6271
Directory(copyTo).createSync(recursive: true);

pkgs/io/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: io
22
description: >-
33
Utilities for the Dart VM Runtime including support for ANSI colors, file
44
copying, and standard exit code values.
5-
version: 1.0.5
5+
version: 1.1.0-wip
66
repository: https://github.com/dart-lang/tools/tree/main/pkgs/io
77

88
environment:

pkgs/io/test/copy_path_test.dart

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
@TestOn('vm')
66
library;
77

8+
import 'dart:io';
9+
810
import 'package:io/io.dart';
911
import 'package:path/path.dart' as p;
1012
import 'package:test/test.dart';
@@ -33,6 +35,82 @@ void main() {
3335
throwsArgumentError,
3436
);
3537
});
38+
39+
group('links', () {
40+
const linkTarget = 'link_target';
41+
const linkSource = 'link_source';
42+
const linkContent = 'link_content.txt';
43+
late String targetPath;
44+
setUp(() async {
45+
await _create();
46+
await d
47+
.dir(linkTarget, [d.file(linkContent, 'original content')]).create();
48+
targetPath = p.join(d.sandbox, linkTarget);
49+
await Link(p.join(d.sandbox, _parentDir, linkSource)).create(targetPath);
50+
});
51+
52+
test('are shallow copied with deepCopyLinks: false in copyPath', () async {
53+
await copyPath(
54+
deepCopyLinks: false,
55+
p.join(d.sandbox, _parentDir),
56+
p.join(d.sandbox, _copyDir));
57+
58+
final expectedLink = Link(p.join(d.sandbox, _copyDir, linkSource));
59+
expect(await expectedLink.exists(), isTrue);
60+
expect(await expectedLink.target(), targetPath);
61+
});
62+
63+
test('are shallow copied with deepCopyLinks: false in copyPathSync',
64+
() async {
65+
copyPathSync(
66+
deepCopyLinks: false,
67+
p.join(d.sandbox, _parentDir),
68+
p.join(d.sandbox, _copyDir));
69+
70+
final expectedLink = Link(p.join(d.sandbox, _copyDir, linkSource));
71+
expect(await expectedLink.exists(), isTrue);
72+
expect(await expectedLink.target(), targetPath);
73+
});
74+
75+
test('are deep copied by default in copyPath', () async {
76+
await copyPath(
77+
p.join(d.sandbox, _parentDir), p.join(d.sandbox, _copyDir));
78+
79+
final expectedDir = Directory(p.join(d.sandbox, _copyDir, linkSource));
80+
final expectedFile =
81+
File(p.join(d.sandbox, _copyDir, linkSource, linkContent));
82+
expect(await expectedDir.exists(), isTrue);
83+
expect(await expectedFile.exists(), isTrue);
84+
85+
expect(await expectedFile.readAsString(), 'original content',
86+
reason: 'The file behind the link was copied with invalid content');
87+
88+
await expectedFile.writeAsString('new content');
89+
final originalFile =
90+
File(p.join(d.sandbox, _parentDir, linkSource, linkContent));
91+
expect(await originalFile.readAsString(), 'original content',
92+
reason: 'The file behind the link should not change');
93+
});
94+
95+
test('are deep copied by default in copyPathSync', () async {
96+
copyPathSync(p.join(d.sandbox, _parentDir), p.join(d.sandbox, _copyDir));
97+
98+
final expectedDir = Directory(p.join(d.sandbox, _copyDir, linkSource));
99+
final expectedFile =
100+
File(p.join(d.sandbox, _copyDir, linkSource, linkContent));
101+
expect(await expectedDir.exists(), isTrue);
102+
expect(await expectedFile.exists(), isTrue);
103+
104+
expect(await expectedFile.readAsString(), 'original content',
105+
reason: 'The file behind the link was copied with invalid content');
106+
107+
await expectedFile.writeAsString('new content');
108+
final originalFile =
109+
File(p.join(d.sandbox, _parentDir, linkSource, linkContent));
110+
expect(await originalFile.readAsString(), 'original content',
111+
reason: 'The file behind the link should not change');
112+
});
113+
});
36114
}
37115

38116
const _parentDir = 'parent';

0 commit comments

Comments
 (0)