Skip to content

Commit 45c8881

Browse files
authored
Update copyDirectory to allow links to not be followed (#144040)
In other words, copy links within a directory as links rather than copying them as files/directories. Fixes flutter/flutter#144032.
1 parent d05eaf4 commit 45c8881

File tree

2 files changed

+109
-1
lines changed

2 files changed

+109
-1
lines changed

packages/flutter_tools/lib/src/base/file_system.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,19 @@ String getDisplayPath(String fullPath, FileSystem fileSystem) {
109109
///
110110
/// Skips files if [shouldCopyFile] returns `false`.
111111
/// Does not recurse over directories if [shouldCopyDirectory] returns `false`.
112+
///
113+
/// If [followLinks] is false, then any symbolic links found are reported as
114+
/// [Link] objects, rather than as directories or files, and are not recursed into.
115+
///
116+
/// If [followLinks] is true, then working links are reported as directories or
117+
/// files, depending on what they point to.
112118
void copyDirectory(
113119
Directory srcDir,
114120
Directory destDir, {
115121
bool Function(File srcFile, File destFile)? shouldCopyFile,
116122
bool Function(Directory)? shouldCopyDirectory,
117123
void Function(File srcFile, File destFile)? onFileCopied,
124+
bool followLinks = true,
118125
}) {
119126
if (!srcDir.existsSync()) {
120127
throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy');
@@ -124,7 +131,7 @@ void copyDirectory(
124131
destDir.createSync(recursive: true);
125132
}
126133

127-
for (final FileSystemEntity entity in srcDir.listSync()) {
134+
for (final FileSystemEntity entity in srcDir.listSync(followLinks: followLinks)) {
128135
final String newPath = destDir.fileSystem.path.join(destDir.path, entity.basename);
129136
if (entity is Link) {
130137
final Link newLink = destDir.fileSystem.link(newPath);
@@ -145,6 +152,7 @@ void copyDirectory(
145152
destDir.fileSystem.directory(newPath),
146153
shouldCopyFile: shouldCopyFile,
147154
onFileCopied: onFileCopied,
155+
followLinks: followLinks,
148156
);
149157
} else {
150158
throw Exception('${entity.path} is neither File nor Directory, was ${entity.runtimeType}');

packages/flutter_tools/test/general.shard/base/file_system_test.dart

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,106 @@ void main() {
8888
expect(sourceMemoryFs.directory(sourcePath).listSync().length, 3);
8989
});
9090

91+
testWithoutContext('test directory copy with followLinks: true', () async {
92+
final Signals signals = Signals.test();
93+
final LocalFileSystem fileSystem = LocalFileSystem.test(
94+
signals: signals,
95+
);
96+
final Directory tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_copy_directory.');
97+
try {
98+
final String sourcePath = io.Platform.isWindows ? r'some\origin' : 'some/origin';
99+
final Directory sourceDirectory = tempDir.childDirectory(sourcePath)..createSync(recursive: true);
100+
final File sourceFile1 = sourceDirectory.childFile('some_file.txt')..writeAsStringSync('file 1');
101+
sourceDirectory.childLink('absolute_linked.txt').createSync(sourceFile1.absolute.path);
102+
final DateTime writeTime = sourceFile1.lastModifiedSync();
103+
final Directory sourceSubDirectory = sourceDirectory.childDirectory('dir1').childDirectory('dir2')..createSync(recursive: true);
104+
sourceSubDirectory.childFile('another_file.txt').writeAsStringSync('file 2');
105+
final String subdirectorySourcePath = io.Platform.isWindows ? r'dir1\dir2' : 'dir1/dir2';
106+
sourceDirectory.childLink('relative_linked_sub_dir').createSync(subdirectorySourcePath);
107+
sourceDirectory.childDirectory('empty_directory').createSync(recursive: true);
108+
109+
final String targetPath = io.Platform.isWindows ? r'some\non-existent\target' : 'some/non-existent/target';
110+
final Directory targetDirectory = tempDir.childDirectory(targetPath);
111+
112+
copyDirectory(sourceDirectory, targetDirectory);
113+
114+
expect(targetDirectory.existsSync(), true);
115+
expect(targetDirectory.childFile('some_file.txt').existsSync(), true);
116+
expect(targetDirectory.childFile('some_file.txt').readAsStringSync(), 'file 1');
117+
expect(targetDirectory.childFile('absolute_linked.txt').readAsStringSync(), 'file 1');
118+
expect(targetDirectory.childLink('absolute_linked.txt').existsSync(), false);
119+
expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').existsSync(), true);
120+
expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').childFile('another_file.txt').existsSync(), true);
121+
expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').childFile('another_file.txt').readAsStringSync(), 'file 2');
122+
expect(targetDirectory.childDirectory('relative_linked_sub_dir').existsSync(), true);
123+
expect(targetDirectory.childLink('relative_linked_sub_dir').existsSync(), false);
124+
expect(targetDirectory.childDirectory('relative_linked_sub_dir').childFile('another_file.txt').existsSync(), true);
125+
expect(targetDirectory.childDirectory('relative_linked_sub_dir').childFile('another_file.txt').readAsStringSync(), 'file 2');
126+
expect(targetDirectory.childDirectory('empty_directory').existsSync(), true);
127+
128+
// Assert that the copy operation hasn't modified the original file in some way.
129+
expect(sourceDirectory.childFile('some_file.txt').lastModifiedSync(), writeTime);
130+
// There's still 5 things in the original directory as there were initially.
131+
expect(sourceDirectory.listSync().length, 5);
132+
} finally {
133+
tryToDelete(tempDir);
134+
}
135+
});
136+
137+
testWithoutContext('test directory copy with followLinks: false', () async {
138+
final Signals signals = Signals.test();
139+
final LocalFileSystem fileSystem = LocalFileSystem.test(
140+
signals: signals,
141+
);
142+
final Directory tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_copy_directory.');
143+
try {
144+
final String sourcePath = io.Platform.isWindows ? r'some\origin' : 'some/origin';
145+
final Directory sourceDirectory = tempDir.childDirectory(sourcePath)..createSync(recursive: true);
146+
final File sourceFile1 = sourceDirectory.childFile('some_file.txt')..writeAsStringSync('file 1');
147+
sourceDirectory.childLink('absolute_linked.txt').createSync(sourceFile1.absolute.path);
148+
final DateTime writeTime = sourceFile1.lastModifiedSync();
149+
final Directory sourceSubDirectory = sourceDirectory.childDirectory('dir1').childDirectory('dir2')..createSync(recursive: true);
150+
sourceSubDirectory.childFile('another_file.txt').writeAsStringSync('file 2');
151+
final String subdirectorySourcePath = io.Platform.isWindows ? r'dir1\dir2' : 'dir1/dir2';
152+
sourceDirectory.childLink('relative_linked_sub_dir').createSync(subdirectorySourcePath);
153+
sourceDirectory.childDirectory('empty_directory').createSync(recursive: true);
154+
155+
final String targetPath = io.Platform.isWindows ? r'some\non-existent\target' : 'some/non-existent/target';
156+
final Directory targetDirectory = tempDir.childDirectory(targetPath);
157+
158+
copyDirectory(sourceDirectory, targetDirectory, followLinks: false);
159+
160+
expect(targetDirectory.existsSync(), true);
161+
expect(targetDirectory.childFile('some_file.txt').existsSync(), true);
162+
expect(targetDirectory.childFile('some_file.txt').readAsStringSync(), 'file 1');
163+
expect(targetDirectory.childFile('absolute_linked.txt').readAsStringSync(), 'file 1');
164+
expect(targetDirectory.childLink('absolute_linked.txt').existsSync(), true);
165+
expect(
166+
targetDirectory.childLink('absolute_linked.txt').targetSync(),
167+
sourceFile1.absolute.path,
168+
);
169+
expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').existsSync(), true);
170+
expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').childFile('another_file.txt').existsSync(), true);
171+
expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').childFile('another_file.txt').readAsStringSync(), 'file 2');
172+
expect(targetDirectory.childDirectory('relative_linked_sub_dir').existsSync(), true);
173+
expect(targetDirectory.childLink('relative_linked_sub_dir').existsSync(), true);
174+
expect(
175+
targetDirectory.childLink('relative_linked_sub_dir').targetSync(),
176+
subdirectorySourcePath,
177+
);
178+
expect(targetDirectory.childDirectory('relative_linked_sub_dir').childFile('another_file.txt').existsSync(), true);
179+
expect(targetDirectory.childDirectory('relative_linked_sub_dir').childFile('another_file.txt').readAsStringSync(), 'file 2');
180+
expect(targetDirectory.childDirectory('empty_directory').existsSync(), true);
181+
182+
// Assert that the copy operation hasn't modified the original file in some way.
183+
expect(sourceDirectory.childFile('some_file.txt').lastModifiedSync(), writeTime);
184+
// There's still 5 things in the original directory as there were initially.
185+
expect(sourceDirectory.listSync().length, 5);
186+
} finally {
187+
tryToDelete(tempDir);
188+
}
189+
});
190+
91191
testWithoutContext('Skip files if shouldCopyFile returns false', () {
92192
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
93193
final Directory origin = fileSystem.directory('/origin');

0 commit comments

Comments
 (0)