From 45c8881eb289e3b68e890cefa0acd13dbf800147 Mon Sep 17 00:00:00 2001 From: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> Date: Mon, 26 Feb 2024 10:07:22 -0600 Subject: [PATCH] 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 https://github.com/flutter/flutter/issues/144032. --- .../lib/src/base/file_system.dart | 10 +- .../general.shard/base/file_system_test.dart | 100 ++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/base/file_system.dart b/packages/flutter_tools/lib/src/base/file_system.dart index 55802890342..be4cf52a203 100644 --- a/packages/flutter_tools/lib/src/base/file_system.dart +++ b/packages/flutter_tools/lib/src/base/file_system.dart @@ -109,12 +109,19 @@ String getDisplayPath(String fullPath, FileSystem fileSystem) { /// /// Skips files if [shouldCopyFile] returns `false`. /// Does not recurse over directories if [shouldCopyDirectory] returns `false`. +/// +/// If [followLinks] is false, then any symbolic links found are reported as +/// [Link] objects, rather than as directories or files, and are not recursed into. +/// +/// If [followLinks] is true, then working links are reported as directories or +/// files, depending on what they point to. void copyDirectory( Directory srcDir, Directory destDir, { bool Function(File srcFile, File destFile)? shouldCopyFile, bool Function(Directory)? shouldCopyDirectory, void Function(File srcFile, File destFile)? onFileCopied, + bool followLinks = true, }) { if (!srcDir.existsSync()) { throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy'); @@ -124,7 +131,7 @@ void copyDirectory( destDir.createSync(recursive: true); } - for (final FileSystemEntity entity in srcDir.listSync()) { + for (final FileSystemEntity entity in srcDir.listSync(followLinks: followLinks)) { final String newPath = destDir.fileSystem.path.join(destDir.path, entity.basename); if (entity is Link) { final Link newLink = destDir.fileSystem.link(newPath); @@ -145,6 +152,7 @@ void copyDirectory( destDir.fileSystem.directory(newPath), shouldCopyFile: shouldCopyFile, onFileCopied: onFileCopied, + followLinks: followLinks, ); } else { throw Exception('${entity.path} is neither File nor Directory, was ${entity.runtimeType}'); diff --git a/packages/flutter_tools/test/general.shard/base/file_system_test.dart b/packages/flutter_tools/test/general.shard/base/file_system_test.dart index a1b5472ef7d..8218e41b4bc 100644 --- a/packages/flutter_tools/test/general.shard/base/file_system_test.dart +++ b/packages/flutter_tools/test/general.shard/base/file_system_test.dart @@ -88,6 +88,106 @@ void main() { expect(sourceMemoryFs.directory(sourcePath).listSync().length, 3); }); + testWithoutContext('test directory copy with followLinks: true', () async { + final Signals signals = Signals.test(); + final LocalFileSystem fileSystem = LocalFileSystem.test( + signals: signals, + ); + final Directory tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_copy_directory.'); + try { + final String sourcePath = io.Platform.isWindows ? r'some\origin' : 'some/origin'; + final Directory sourceDirectory = tempDir.childDirectory(sourcePath)..createSync(recursive: true); + final File sourceFile1 = sourceDirectory.childFile('some_file.txt')..writeAsStringSync('file 1'); + sourceDirectory.childLink('absolute_linked.txt').createSync(sourceFile1.absolute.path); + final DateTime writeTime = sourceFile1.lastModifiedSync(); + final Directory sourceSubDirectory = sourceDirectory.childDirectory('dir1').childDirectory('dir2')..createSync(recursive: true); + sourceSubDirectory.childFile('another_file.txt').writeAsStringSync('file 2'); + final String subdirectorySourcePath = io.Platform.isWindows ? r'dir1\dir2' : 'dir1/dir2'; + sourceDirectory.childLink('relative_linked_sub_dir').createSync(subdirectorySourcePath); + sourceDirectory.childDirectory('empty_directory').createSync(recursive: true); + + final String targetPath = io.Platform.isWindows ? r'some\non-existent\target' : 'some/non-existent/target'; + final Directory targetDirectory = tempDir.childDirectory(targetPath); + + copyDirectory(sourceDirectory, targetDirectory); + + expect(targetDirectory.existsSync(), true); + expect(targetDirectory.childFile('some_file.txt').existsSync(), true); + expect(targetDirectory.childFile('some_file.txt').readAsStringSync(), 'file 1'); + expect(targetDirectory.childFile('absolute_linked.txt').readAsStringSync(), 'file 1'); + expect(targetDirectory.childLink('absolute_linked.txt').existsSync(), false); + expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').existsSync(), true); + expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').childFile('another_file.txt').existsSync(), true); + expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').childFile('another_file.txt').readAsStringSync(), 'file 2'); + expect(targetDirectory.childDirectory('relative_linked_sub_dir').existsSync(), true); + expect(targetDirectory.childLink('relative_linked_sub_dir').existsSync(), false); + expect(targetDirectory.childDirectory('relative_linked_sub_dir').childFile('another_file.txt').existsSync(), true); + expect(targetDirectory.childDirectory('relative_linked_sub_dir').childFile('another_file.txt').readAsStringSync(), 'file 2'); + expect(targetDirectory.childDirectory('empty_directory').existsSync(), true); + + // Assert that the copy operation hasn't modified the original file in some way. + expect(sourceDirectory.childFile('some_file.txt').lastModifiedSync(), writeTime); + // There's still 5 things in the original directory as there were initially. + expect(sourceDirectory.listSync().length, 5); + } finally { + tryToDelete(tempDir); + } + }); + + testWithoutContext('test directory copy with followLinks: false', () async { + final Signals signals = Signals.test(); + final LocalFileSystem fileSystem = LocalFileSystem.test( + signals: signals, + ); + final Directory tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_copy_directory.'); + try { + final String sourcePath = io.Platform.isWindows ? r'some\origin' : 'some/origin'; + final Directory sourceDirectory = tempDir.childDirectory(sourcePath)..createSync(recursive: true); + final File sourceFile1 = sourceDirectory.childFile('some_file.txt')..writeAsStringSync('file 1'); + sourceDirectory.childLink('absolute_linked.txt').createSync(sourceFile1.absolute.path); + final DateTime writeTime = sourceFile1.lastModifiedSync(); + final Directory sourceSubDirectory = sourceDirectory.childDirectory('dir1').childDirectory('dir2')..createSync(recursive: true); + sourceSubDirectory.childFile('another_file.txt').writeAsStringSync('file 2'); + final String subdirectorySourcePath = io.Platform.isWindows ? r'dir1\dir2' : 'dir1/dir2'; + sourceDirectory.childLink('relative_linked_sub_dir').createSync(subdirectorySourcePath); + sourceDirectory.childDirectory('empty_directory').createSync(recursive: true); + + final String targetPath = io.Platform.isWindows ? r'some\non-existent\target' : 'some/non-existent/target'; + final Directory targetDirectory = tempDir.childDirectory(targetPath); + + copyDirectory(sourceDirectory, targetDirectory, followLinks: false); + + expect(targetDirectory.existsSync(), true); + expect(targetDirectory.childFile('some_file.txt').existsSync(), true); + expect(targetDirectory.childFile('some_file.txt').readAsStringSync(), 'file 1'); + expect(targetDirectory.childFile('absolute_linked.txt').readAsStringSync(), 'file 1'); + expect(targetDirectory.childLink('absolute_linked.txt').existsSync(), true); + expect( + targetDirectory.childLink('absolute_linked.txt').targetSync(), + sourceFile1.absolute.path, + ); + expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').existsSync(), true); + expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').childFile('another_file.txt').existsSync(), true); + expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').childFile('another_file.txt').readAsStringSync(), 'file 2'); + expect(targetDirectory.childDirectory('relative_linked_sub_dir').existsSync(), true); + expect(targetDirectory.childLink('relative_linked_sub_dir').existsSync(), true); + expect( + targetDirectory.childLink('relative_linked_sub_dir').targetSync(), + subdirectorySourcePath, + ); + expect(targetDirectory.childDirectory('relative_linked_sub_dir').childFile('another_file.txt').existsSync(), true); + expect(targetDirectory.childDirectory('relative_linked_sub_dir').childFile('another_file.txt').readAsStringSync(), 'file 2'); + expect(targetDirectory.childDirectory('empty_directory').existsSync(), true); + + // Assert that the copy operation hasn't modified the original file in some way. + expect(sourceDirectory.childFile('some_file.txt').lastModifiedSync(), writeTime); + // There's still 5 things in the original directory as there were initially. + expect(sourceDirectory.listSync().length, 5); + } finally { + tryToDelete(tempDir); + } + }); + testWithoutContext('Skip files if shouldCopyFile returns false', () { final MemoryFileSystem fileSystem = MemoryFileSystem.test(); final Directory origin = fileSystem.directory('/origin');