Skip to content

Implement file copying #257

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkgs/io_file/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ See
| Feature | Android | Linux | iOS | macOS | Windows | Fake POSIX | Fake Windows |
| :--- | :---: | :---: | :---: | :---: | :----: | :--------: | :----------: |
| canonicalize path | | | | | | | |
| copy file | | | | | | | |
| copy file | | | | | | | |
| create directory | ✓ | ✓ | ✓ | ✓ | ✓ | | |
| create hard link | | | | | | | |
| create symbolic link | | | | | | | |
Expand Down
38 changes: 35 additions & 3 deletions pkgs/io_file/lib/src/file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import 'dart:typed_data';

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

import 'exceptions.dart';

// TODO(brianquinlan): When we switch to using exception types outside of
// `dart:io` then change the doc strings to use reference syntax rather than
// code syntax e.g. `PathExistsException` => [PathExistsException].
Expand Down Expand Up @@ -141,6 +143,36 @@ class WriteMode {
/// be refered to by the path `r'\\.\NUL'`.
@sealed
abstract class FileSystem {
/// Copy the data from the file at `oldPath` to a new file at `newPath`.
///
/// If `oldPath` is a directory, then `copyFile` throws [IOFileException]. If
/// `oldPath` is a symbolic link to a file, then the contents of the file are
/// copied.
///
/// If `newPath` identifies an existing file system object, then `copyFile`
/// throws [IOFileException].
///
/// The metadata associated with `oldPath` (such as permissions, visibility,
/// and creation time) is not copied to `newPath`.
///
/// This operation is not atomic; if `copyFile` throws then a partial copy of
/// `oldPath` may exist at `newPath`.
// DESIGN NOTES:
//
// Metadata preservation:
// Preserving all metadata from `oldPath` is very difficult. Languages that
// offer metadata preservation on copy (Python, Java) make no guarantees as to
// what metadata is preserved. The most principled approach is to leave
// metadata preservation up to the application.
//
// Existing `newPath`:
// If `newPath` exists then Rust opens the existing file and truncates it.
// This has the effect of preserving the metadata of the **destination file**.
// Python first removes the file at `newPath`. Java fails by default if
// `newPath` exists. The most principled approach is to fail if `newPath`
// exists and let the application deal with it.
void copyFile(String oldPath, String newPath);

/// Create a directory at the given path.
///
/// If the directory already exists, then `PathExistsException` is thrown.
Expand Down Expand Up @@ -237,9 +269,9 @@ abstract class FileSystem {
/// different file systems. If that is the case, instead copy the file to the
/// new location and then remove the original.
///
// If `newPath` identifies an existing file or link, that entity is removed
// first. If `newPath` identifies an existing directory, the operation
// fails and raises [PathExistsException].
/// If `newPath` identifies an existing file or link, that entity is removed
/// first. If `newPath` identifies an existing directory, the operation
/// fails and raises [PathExistsException].
void rename(String oldPath, String newPath);

/// Reads the entire file contents as a list of bytes.
Expand Down
71 changes: 71 additions & 0 deletions pkgs/io_file/lib/src/vm_posix_file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,77 @@ external int write(int fd, Pointer<Uint8> buf, int count);
/// A [FileSystem] implementation for POSIX systems (e.g. Android, iOS, Linux,
/// macOS).
final class PosixFileSystem extends FileSystem {
void _slowCopy(
String oldPath,
String newPath,
int fromFd,
int toFd,
Allocator arena,
) {
final buffer = arena<Uint8>(blockSize);

while (true) {
final r = _tempFailureRetry(() => read(fromFd, buffer, blockSize));
switch (r) {
case -1:
final errno = libc.errno;
throw _getError(errno, systemCall: 'read', path1: oldPath);
case 0:
return;
}

var writeRemaining = r;
var writeBuffer = buffer;
while (writeRemaining > 0) {
final w = _tempFailureRetry(
() => write(toFd, writeBuffer, writeRemaining),
);
if (w == -1) {
final errno = libc.errno;
throw _getError(errno, systemCall: 'write', path1: newPath);
}
writeRemaining -= w;
writeBuffer += w;
}
}
}

@override
void copyFile(String oldPath, String newPath) => ffi.using((arena) {
final oldFd = _tempFailureRetry(
() => libc.open(
oldPath.toNativeUtf8(allocator: arena).cast(),
libc.O_RDONLY | libc.O_CLOEXEC,
0,
),
);
if (oldFd == -1) {
final errno = libc.errno;
throw _getError(errno, systemCall: 'open', path1: oldPath);
}
try {
final newFd = _tempFailureRetry(
() => libc.open(
newPath.toNativeUtf8(allocator: arena).cast(),
libc.O_WRONLY | libc.O_CREAT | libc.O_EXCL | libc.O_CLOEXEC,
_defaultMode,
),
);
if (newFd == -1) {
final errno = libc.errno;
throw _getError(errno, systemCall: 'open', path1: newPath);
}

try {
_slowCopy(oldPath, newPath, oldFd, newFd, arena);
} finally {
libc.close(newFd);
}
} finally {
libc.close(oldFd);
}
});

@override
bool same(String path1, String path2) => ffi.using((arena) {
final stat1 = arena<libc.Stat>();
Expand Down
89 changes: 89 additions & 0 deletions pkgs/io_file/lib/src/vm_windows_file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,95 @@ final class WindowsMetadata implements Metadata {
/// (e.g. 'COM1'), must be prefixed with `r'\\.\'`. For example, `'NUL'` would
/// be refered to by the path `r'\\.\NUL'`.
final class WindowsFileSystem extends FileSystem {
void _slowCopy(
String oldPath,
String newPath,
int fromHandle,
int toHandle,
Allocator arena,
) {
final buffer = arena<Uint8>(blockSize);
final bytesRead = arena<win32.DWORD>();

while (true) {
if (win32.ReadFile(fromHandle, buffer, blockSize, bytesRead, nullptr) ==
win32.FALSE) {
final errorCode = win32.GetLastError();
// On Windows, reading from a pipe that is closed by the writer results
// in ERROR_BROKEN_PIPE.
if (errorCode == win32.ERROR_BROKEN_PIPE ||
errorCode == win32.ERROR_SUCCESS) {
return;
}
throw _getError(errorCode, systemCall: 'ReadFile', path1: oldPath);
}

if (bytesRead.value == 0) {
return;
}

var writeRemaining = bytesRead.value;
var writeBuffer = buffer;
final bytesWritten = arena<win32.DWORD>();
while (writeRemaining > 0) {
if (win32.WriteFile(
toHandle,
writeBuffer,
writeRemaining,
bytesWritten,
nullptr,
) ==
win32.FALSE) {
final errorCode = win32.GetLastError();
throw _getError(errorCode, systemCall: 'WriteFile', path1: newPath);
}
writeRemaining -= bytesWritten.value;
writeBuffer += bytesWritten.value;
}
}
}

@override
void copyFile(String oldPath, String newPath) => ffi.using((arena) {
_primeGetLastError();

final oldHandle = win32.CreateFile(
_extendedPath(oldPath, arena),
win32.GENERIC_READ | win32.FILE_SHARE_READ,
win32.FILE_SHARE_READ | win32.FILE_SHARE_WRITE,
nullptr,
win32.OPEN_EXISTING,
win32.FILE_ATTRIBUTE_NORMAL,
0,
);
if (oldHandle == win32.INVALID_HANDLE_VALUE) {
final errorCode = win32.GetLastError();
throw _getError(errorCode, systemCall: 'CreateFile', path1: oldPath);
}
try {
final newHandle = win32.CreateFile(
_extendedPath(newPath, arena),
win32.FILE_GENERIC_WRITE,
0,
nullptr,
win32.CREATE_NEW,
win32.FILE_ATTRIBUTE_NORMAL & win32.FILE_FLAG_OPEN_REPARSE_POINT,
0,
);
if (newHandle == win32.INVALID_HANDLE_VALUE) {
final errorCode = win32.GetLastError();
throw _getError(errorCode, systemCall: 'CreateFile', path1: newPath);
}
try {
_slowCopy(oldPath, newPath, oldHandle, newHandle, arena);
} finally {
win32.CloseHandle(newHandle);
}
} finally {
win32.CloseHandle(oldHandle);
}
});

@override
bool same(String path1, String path2) => using((arena) {
// Calling `GetLastError` for the first time causes the `GetLastError`
Expand Down
5 changes: 5 additions & 0 deletions pkgs/io_file/lib/src/web_posix_file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import 'file_system.dart';
/// A [FileSystem] implementation for POSIX systems (e.g. Android, iOS, Linux,
/// macOS).
final class PosixFileSystem extends FileSystem {
@override
void copyFile(String oldPath, String newPath) {
throw UnimplementedError();
}

@override
void createDirectory(String path) {
throw UnimplementedError();
Expand Down
5 changes: 5 additions & 0 deletions pkgs/io_file/lib/src/web_windows_file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import 'file_system.dart';

/// A [FileSystem] implementation for Windows systems.
base class WindowsFileSystem extends FileSystem {
@override
void copyFile(String oldPath, String newPath) {
throw UnimplementedError();
}

@override
void createDirectory(String path) {
throw UnimplementedError();
Expand Down
Loading
Loading