Skip to content

Commit d29c6fb

Browse files
committed
Implement file copying on POSIX
1 parent 383a038 commit d29c6fb

File tree

6 files changed

+275
-0
lines changed

6 files changed

+275
-0
lines changed

pkgs/io_file/lib/src/file_system.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ class WriteMode {
141141
/// be refered to by the path `r'\\.\NUL'`.
142142
@sealed
143143
abstract class FileSystem {
144+
void copyFile(String oldPath, String newPath);
145+
144146
/// Create a directory at the given path.
145147
///
146148
/// If the directory already exists, then `PathExistsException` is thrown.

pkgs/io_file/lib/src/vm_posix_file_system.dart

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,83 @@ 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 stat = arena<libc.Stat>();
323+
if (libc.fstat(oldFd, stat) == -1) {
324+
final errno = libc.errno;
325+
throw _getError(errno, systemCall: 'fstat', path1: oldPath);
326+
}
327+
328+
final newFd = _tempFailureRetry(
329+
() => libc.open(
330+
newPath.toNativeUtf8(allocator: arena).cast(),
331+
libc.O_WRONLY | libc.O_TRUNC | libc.O_CREAT | libc.O_CLOEXEC,
332+
stat.ref.st_mode,
333+
),
334+
);
335+
if (newFd == -1) {
336+
final errno = libc.errno;
337+
throw _getError(errno, systemCall: 'open', path1: newPath);
338+
}
339+
340+
try {
341+
_slowCopy(oldPath, newPath, oldFd, newFd, arena);
342+
} finally {
343+
libc.close(newFd);
344+
}
345+
} finally {
346+
libc.close(oldFd);
347+
}
348+
});
349+
273350
@override
274351
bool same(String path1, String path2) => ffi.using((arena) {
275352
final stat1 = arena<libc.Stat>();

pkgs/io_file/lib/src/vm_windows_file_system.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,9 @@ 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+
@override
395+
void copyFile(String oldPath, String newPath) {}
396+
394397
@override
395398
bool same(String path1, String path2) => using((arena) {
396399
// 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();

pkgs/io_file/test/copy_file_test.dart

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
@TestOn('posix')
6+
library;
7+
8+
import 'dart:io' as io;
9+
import 'dart:typed_data';
10+
11+
import 'package:io_file/io_file.dart';
12+
import 'package:io_file/src/internal_constants.dart' show blockSize;
13+
import 'package:path/path.dart' as p;
14+
import 'package:test/test.dart';
15+
import 'package:win32/win32.dart' as win32;
16+
17+
import 'errors.dart' as errors;
18+
import 'fifo.dart';
19+
import 'test_utils.dart';
20+
21+
void main() {
22+
/// XXX check metadata
23+
24+
group('copyFile', () {
25+
late String tmp;
26+
late String cwd;
27+
28+
setUp(() {
29+
tmp = createTemp('copyFile');
30+
cwd = fileSystem.currentDirectory;
31+
fileSystem.currentDirectory = tmp;
32+
});
33+
34+
tearDown(() {
35+
fileSystem.currentDirectory = cwd;
36+
deleteTemp(tmp);
37+
});
38+
39+
test('copy file absolute path', () {
40+
final data = randomUint8List(1024);
41+
final oldPath = '$tmp/file1';
42+
final newPath = '$tmp/file2';
43+
io.File(oldPath).writeAsBytesSync(data);
44+
45+
fileSystem.copyFile(oldPath, newPath);
46+
47+
expect(io.File(newPath).readAsBytesSync(), data);
48+
});
49+
50+
test('copy between absolute paths, long file names', () {
51+
final data = randomUint8List(1024);
52+
final oldPath = p.join(tmp, '1' * 255);
53+
final newPath = p.join(tmp, '2' * 255);
54+
io.File(oldPath).writeAsBytesSync(data);
55+
56+
fileSystem.copyFile(oldPath, newPath);
57+
58+
expect(io.File(newPath).readAsBytesSync(), data);
59+
});
60+
61+
test('copy between relative path, long file names', () {
62+
final data = randomUint8List(1024);
63+
final oldPath = '1' * 255;
64+
final newPath = '2' * 255;
65+
io.File(oldPath).writeAsBytesSync(data);
66+
67+
fileSystem.copyFile(oldPath, newPath);
68+
69+
expect(io.File(newPath).readAsBytesSync(), data);
70+
});
71+
72+
test('copy file to existing', () {
73+
final data = randomUint8List(1024);
74+
final oldPath = '1' * 255;
75+
final newPath = '2' * 255;
76+
io.File(oldPath).writeAsBytesSync(data);
77+
io.File(newPath).writeAsStringSync('Hello World!');
78+
79+
fileSystem.copyFile(oldPath, newPath);
80+
81+
expect(io.File(newPath).readAsBytesSync(), data);
82+
});
83+
84+
test('copy non-existent', () {
85+
final oldPath = '$tmp/file1';
86+
final newPath = '$tmp/file2';
87+
88+
expect(
89+
() => fileSystem.copyFile(oldPath, newPath),
90+
throwsA(
91+
isA<PathNotFoundException>()
92+
.having((e) => e.path1, 'path1', oldPath)
93+
.having(
94+
(e) => e.errorCode,
95+
'errorCode',
96+
io.Platform.isWindows
97+
? win32.ERROR_FILE_NOT_FOUND
98+
: errors.enoent,
99+
),
100+
),
101+
);
102+
});
103+
104+
group('fifo', () {
105+
for (var i = 0; i <= 1024; ++i) {
106+
test('Read small file: $i bytes', () async {
107+
final data = randomUint8List(i);
108+
final oldPath = '$tmp/file1';
109+
final newPath = '$tmp/file2';
110+
final fifo =
111+
(await Fifo.create(oldPath))
112+
..write(data)
113+
..close();
114+
115+
fileSystem.copyFile(fifo.path, newPath);
116+
117+
expect(io.File(newPath).readAsBytesSync(), data);
118+
});
119+
}
120+
121+
test('many single byte reads', () async {
122+
final data = randomUint8List(20);
123+
final oldPath = '$tmp/file1';
124+
final newPath = '$tmp/file2';
125+
final fifo = await Fifo.create(oldPath);
126+
for (var byte in data) {
127+
fifo
128+
..write(Uint8List(1)..[0] = byte)
129+
..delay(const Duration(milliseconds: 10));
130+
}
131+
fifo.close();
132+
133+
fileSystem.copyFile(fifo.path, newPath);
134+
135+
expect(io.File(newPath).readAsBytesSync(), data);
136+
});
137+
138+
for (var i = blockSize - 2; i <= blockSize + 2; ++i) {
139+
test('Read close to `blockSize`: $i bytes', () async {
140+
final data = randomUint8List(i);
141+
final oldPath = '$tmp/file1';
142+
final newPath = '$tmp/file2';
143+
final fifo =
144+
(await Fifo.create(oldPath))
145+
..write(data)
146+
..close();
147+
148+
fileSystem.copyFile(fifo.path, newPath);
149+
150+
expect(io.File(newPath).readAsBytesSync(), data);
151+
});
152+
}
153+
});
154+
155+
group('regular files', () {
156+
for (var i = 0; i <= 1024; ++i) {
157+
test('copyFile small file: $i bytes', () {
158+
final data = randomUint8List(i);
159+
final oldPath = '$tmp/file1';
160+
final newPath = '$tmp/file2';
161+
io.File(oldPath).writeAsBytesSync(data);
162+
163+
fileSystem.copyFile(oldPath, newPath);
164+
165+
expect(io.File(newPath).readAsBytesSync(), data);
166+
});
167+
}
168+
169+
for (var i = blockSize - 2; i <= blockSize + 2; ++i) {
170+
test('copyFile close to `blockSize`: $i bytes', () {
171+
final data = randomUint8List(i);
172+
final oldPath = '$tmp/file1';
173+
final newPath = '$tmp/file2';
174+
io.File(oldPath).writeAsBytesSync(data);
175+
176+
fileSystem.copyFile(oldPath, newPath);
177+
178+
expect(io.File(newPath).readAsBytesSync(), data);
179+
});
180+
}
181+
});
182+
});
183+
}

0 commit comments

Comments
 (0)