Skip to content

Commit ec5a025

Browse files
authored
Implement file copying (#257)
1 parent 35e33ea commit ec5a025

8 files changed

+557
-7
lines changed

pkgs/io_file/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ See
1111
| Feature | Android | Linux | iOS | macOS | Windows | Fake POSIX | Fake Windows |
1212
| :--- | :---: | :---: | :---: | :---: | :----: | :--------: | :----------: |
1313
| canonicalize path | | | | | | | |
14-
| copy file | | | | | | | |
14+
| copy file | | | | | | | |
1515
| create directory |||||| | |
1616
| create hard link | | | | | | | |
1717
| create symbolic link | | | | | | | |

pkgs/io_file/lib/src/file_system.dart

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import 'dart:typed_data';
77

88
import 'package:meta/meta.dart' show sealed;
99

10+
import 'exceptions.dart';
11+
1012
// TODO(brianquinlan): When we switch to using exception types outside of
1113
// `dart:io` then change the doc strings to use reference syntax rather than
1214
// code syntax e.g. `PathExistsException` => [PathExistsException].
@@ -141,6 +143,36 @@ class WriteMode {
141143
/// be refered to by the path `r'\\.\NUL'`.
142144
@sealed
143145
abstract class FileSystem {
146+
/// Copy the data from the file at `oldPath` to a new file at `newPath`.
147+
///
148+
/// If `oldPath` is a directory, then `copyFile` throws [IOFileException]. If
149+
/// `oldPath` is a symbolic link to a file, then the contents of the file are
150+
/// copied.
151+
///
152+
/// If `newPath` identifies an existing file system object, then `copyFile`
153+
/// throws [IOFileException].
154+
///
155+
/// The metadata associated with `oldPath` (such as permissions, visibility,
156+
/// and creation time) is not copied to `newPath`.
157+
///
158+
/// This operation is not atomic; if `copyFile` throws then a partial copy of
159+
/// `oldPath` may exist at `newPath`.
160+
// DESIGN NOTES:
161+
//
162+
// Metadata preservation:
163+
// Preserving all metadata from `oldPath` is very difficult. Languages that
164+
// offer metadata preservation on copy (Python, Java) make no guarantees as to
165+
// what metadata is preserved. The most principled approach is to leave
166+
// metadata preservation up to the application.
167+
//
168+
// Existing `newPath`:
169+
// If `newPath` exists then Rust opens the existing file and truncates it.
170+
// This has the effect of preserving the metadata of the **destination file**.
171+
// Python first removes the file at `newPath`. Java fails by default if
172+
// `newPath` exists. The most principled approach is to fail if `newPath`
173+
// exists and let the application deal with it.
174+
void copyFile(String oldPath, String newPath);
175+
144176
/// Create a directory at the given path.
145177
///
146178
/// If the directory already exists, then `PathExistsException` is thrown.
@@ -237,9 +269,9 @@ abstract class FileSystem {
237269
/// different file systems. If that is the case, instead copy the file to the
238270
/// new location and then remove the original.
239271
///
240-
// If `newPath` identifies an existing file or link, that entity is removed
241-
// first. If `newPath` identifies an existing directory, the operation
242-
// fails and raises [PathExistsException].
272+
/// If `newPath` identifies an existing file or link, that entity is removed
273+
/// first. If `newPath` identifies an existing directory, the operation
274+
/// fails and raises [PathExistsException].
243275
void rename(String oldPath, String newPath);
244276

245277
/// Reads the entire file contents as a list of bytes.

pkgs/io_file/lib/src/vm_posix_file_system.dart

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,77 @@ external int write(int fd, Pointer<Uint8> buf, int count);
270270
/// A [FileSystem] implementation for POSIX systems (e.g. Android, iOS, Linux,
271271
/// macOS).
272272
final class PosixFileSystem extends FileSystem {
273+
void _slowCopy(
274+
String oldPath,
275+
String newPath,
276+
int fromFd,
277+
int toFd,
278+
Allocator arena,
279+
) {
280+
final buffer = arena<Uint8>(blockSize);
281+
282+
while (true) {
283+
final r = _tempFailureRetry(() => read(fromFd, buffer, blockSize));
284+
switch (r) {
285+
case -1:
286+
final errno = libc.errno;
287+
throw _getError(errno, systemCall: 'read', path1: oldPath);
288+
case 0:
289+
return;
290+
}
291+
292+
var writeRemaining = r;
293+
var writeBuffer = buffer;
294+
while (writeRemaining > 0) {
295+
final w = _tempFailureRetry(
296+
() => write(toFd, writeBuffer, writeRemaining),
297+
);
298+
if (w == -1) {
299+
final errno = libc.errno;
300+
throw _getError(errno, systemCall: 'write', path1: newPath);
301+
}
302+
writeRemaining -= w;
303+
writeBuffer += w;
304+
}
305+
}
306+
}
307+
308+
@override
309+
void copyFile(String oldPath, String newPath) => ffi.using((arena) {
310+
final oldFd = _tempFailureRetry(
311+
() => libc.open(
312+
oldPath.toNativeUtf8(allocator: arena).cast(),
313+
libc.O_RDONLY | libc.O_CLOEXEC,
314+
0,
315+
),
316+
);
317+
if (oldFd == -1) {
318+
final errno = libc.errno;
319+
throw _getError(errno, systemCall: 'open', path1: oldPath);
320+
}
321+
try {
322+
final newFd = _tempFailureRetry(
323+
() => libc.open(
324+
newPath.toNativeUtf8(allocator: arena).cast(),
325+
libc.O_WRONLY | libc.O_CREAT | libc.O_EXCL | libc.O_CLOEXEC,
326+
_defaultMode,
327+
),
328+
);
329+
if (newFd == -1) {
330+
final errno = libc.errno;
331+
throw _getError(errno, systemCall: 'open', path1: newPath);
332+
}
333+
334+
try {
335+
_slowCopy(oldPath, newPath, oldFd, newFd, arena);
336+
} finally {
337+
libc.close(newFd);
338+
}
339+
} finally {
340+
libc.close(oldFd);
341+
}
342+
});
343+
273344
@override
274345
bool same(String path1, String path2) => ffi.using((arena) {
275346
final stat1 = arena<libc.Stat>();

pkgs/io_file/lib/src/vm_windows_file_system.dart

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,95 @@ final class WindowsMetadata implements Metadata {
391391
/// (e.g. 'COM1'), must be prefixed with `r'\\.\'`. For example, `'NUL'` would
392392
/// be refered to by the path `r'\\.\NUL'`.
393393
final class WindowsFileSystem extends FileSystem {
394+
void _slowCopy(
395+
String oldPath,
396+
String newPath,
397+
int fromHandle,
398+
int toHandle,
399+
Allocator arena,
400+
) {
401+
final buffer = arena<Uint8>(blockSize);
402+
final bytesRead = arena<win32.DWORD>();
403+
404+
while (true) {
405+
if (win32.ReadFile(fromHandle, buffer, blockSize, bytesRead, nullptr) ==
406+
win32.FALSE) {
407+
final errorCode = win32.GetLastError();
408+
// On Windows, reading from a pipe that is closed by the writer results
409+
// in ERROR_BROKEN_PIPE.
410+
if (errorCode == win32.ERROR_BROKEN_PIPE ||
411+
errorCode == win32.ERROR_SUCCESS) {
412+
return;
413+
}
414+
throw _getError(errorCode, systemCall: 'ReadFile', path1: oldPath);
415+
}
416+
417+
if (bytesRead.value == 0) {
418+
return;
419+
}
420+
421+
var writeRemaining = bytesRead.value;
422+
var writeBuffer = buffer;
423+
final bytesWritten = arena<win32.DWORD>();
424+
while (writeRemaining > 0) {
425+
if (win32.WriteFile(
426+
toHandle,
427+
writeBuffer,
428+
writeRemaining,
429+
bytesWritten,
430+
nullptr,
431+
) ==
432+
win32.FALSE) {
433+
final errorCode = win32.GetLastError();
434+
throw _getError(errorCode, systemCall: 'WriteFile', path1: newPath);
435+
}
436+
writeRemaining -= bytesWritten.value;
437+
writeBuffer += bytesWritten.value;
438+
}
439+
}
440+
}
441+
442+
@override
443+
void copyFile(String oldPath, String newPath) => ffi.using((arena) {
444+
_primeGetLastError();
445+
446+
final oldHandle = win32.CreateFile(
447+
_extendedPath(oldPath, arena),
448+
win32.GENERIC_READ | win32.FILE_SHARE_READ,
449+
win32.FILE_SHARE_READ | win32.FILE_SHARE_WRITE,
450+
nullptr,
451+
win32.OPEN_EXISTING,
452+
win32.FILE_ATTRIBUTE_NORMAL,
453+
0,
454+
);
455+
if (oldHandle == win32.INVALID_HANDLE_VALUE) {
456+
final errorCode = win32.GetLastError();
457+
throw _getError(errorCode, systemCall: 'CreateFile', path1: oldPath);
458+
}
459+
try {
460+
final newHandle = win32.CreateFile(
461+
_extendedPath(newPath, arena),
462+
win32.FILE_GENERIC_WRITE,
463+
0,
464+
nullptr,
465+
win32.CREATE_NEW,
466+
win32.FILE_ATTRIBUTE_NORMAL & win32.FILE_FLAG_OPEN_REPARSE_POINT,
467+
0,
468+
);
469+
if (newHandle == win32.INVALID_HANDLE_VALUE) {
470+
final errorCode = win32.GetLastError();
471+
throw _getError(errorCode, systemCall: 'CreateFile', path1: newPath);
472+
}
473+
try {
474+
_slowCopy(oldPath, newPath, oldHandle, newHandle, arena);
475+
} finally {
476+
win32.CloseHandle(newHandle);
477+
}
478+
} finally {
479+
win32.CloseHandle(oldHandle);
480+
}
481+
});
482+
394483
@override
395484
bool same(String path1, String path2) => using((arena) {
396485
// Calling `GetLastError` for the first time causes the `GetLastError`

pkgs/io_file/lib/src/web_posix_file_system.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import 'file_system.dart';
1010
/// A [FileSystem] implementation for POSIX systems (e.g. Android, iOS, Linux,
1111
/// macOS).
1212
final class PosixFileSystem extends FileSystem {
13+
@override
14+
void copyFile(String oldPath, String newPath) {
15+
throw UnimplementedError();
16+
}
17+
1318
@override
1419
void createDirectory(String path) {
1520
throw UnimplementedError();

pkgs/io_file/lib/src/web_windows_file_system.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import 'file_system.dart';
99

1010
/// A [FileSystem] implementation for Windows systems.
1111
base class WindowsFileSystem extends FileSystem {
12+
@override
13+
void copyFile(String oldPath, String newPath) {
14+
throw UnimplementedError();
15+
}
16+
1217
@override
1318
void createDirectory(String path) {
1419
throw UnimplementedError();

0 commit comments

Comments
 (0)