Skip to content

Commit 77464e7

Browse files
authored
Add removeFile (#268)
1 parent 488cabf commit 77464e7

10 files changed

+219
-1
lines changed

pkgs/io_file/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ See
1818
| create tmp directory |||||| | |
1919
| create tmp file | | | | | | | |
2020
| delete directory |||||| | |
21-
| delete file | | | | | | |
21+
| delete file | | | | | | | |
2222
| delete tree |||||| | |
2323
| enum dir contents | | | | | | | |
2424
| exists | | | | | | |

pkgs/io_file/lib/src/file_system.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,17 @@ abstract class FileSystem {
256256
/// those links are deleted but their targets are not.
257257
void removeDirectoryTree(String path);
258258

259+
/// Deletes the file at the given path.
260+
///
261+
/// If `path` represents a directory, then [IOFileException] is thrown.
262+
///
263+
/// If `path` represents a symbolic link to a file, then the symbolic link is
264+
/// deleted.
265+
///
266+
/// If `path` represents a symbolic link to a directory then, on POSIX, the
267+
/// symbolic link is deleted. On Windows, a [IOFileException] is thrown.
268+
void removeFile(String path);
269+
259270
/// Reads the entire file contents as a list of bytes.
260271
Uint8List readAsBytes(String path);
261272

pkgs/io_file/lib/src/vm_posix_file_system.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,15 @@ final class PosixFileSystem extends FileSystem {
517517
);
518518
});
519519

520+
@override
521+
void removeFile(String path) => ffi.using((arena) {
522+
final nativePath = path.toNativeUtf8(allocator: arena);
523+
if (libc.unlinkat(libc.AT_FDCWD, nativePath.cast(), 0) == -1) {
524+
final errno = libc.errno;
525+
throw _getError(errno, systemCall: 'unlinkat', path1: path);
526+
}
527+
});
528+
520529
@override
521530
Uint8List readAsBytes(String path) => ffi.using((arena) {
522531
final fd = _tempFailureRetry(

pkgs/io_file/lib/src/vm_windows_file_system.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,16 @@ final class WindowsFileSystem extends FileSystem {
678678
}
679679
});
680680

681+
@override
682+
void removeFile(String path) => using((arena) {
683+
_primeGetLastError();
684+
685+
if (win32.DeleteFile(_extendedPath(path, arena)) == win32.FALSE) {
686+
final errorCode = win32.GetLastError();
687+
throw _getError(errorCode, systemCall: 'DeleteFile', path1: path);
688+
}
689+
});
690+
681691
@override
682692
Uint8List readAsBytes(String path) => using((arena) {
683693
_primeGetLastError();

pkgs/io_file/lib/src/web_posix_file_system.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ final class PosixFileSystem extends FileSystem {
5555
throw UnimplementedError();
5656
}
5757

58+
@override
59+
void removeFile(String path) {
60+
throw UnimplementedError();
61+
}
62+
5863
@override
5964
void rename(String oldPath, String newPath) {
6065
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
@@ -54,6 +54,11 @@ base class WindowsFileSystem extends FileSystem {
5454
throw UnimplementedError();
5555
}
5656

57+
@override
58+
void removeFile(String path) {
59+
throw UnimplementedError();
60+
}
61+
5762
@override
5863
void rename(String oldPath, String newPath) {
5964
throw UnimplementedError();

pkgs/io_file/test/dart_io_file_utils.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ class DartIOFileUtils implements FileUtils {
1919
void deleteDirectoryTree(String path) =>
2020
Directory(path).deleteSync(recursive: true);
2121

22+
@override
23+
bool exists(String path) =>
24+
FileSystemEntity.typeSync(path) != FileSystemEntityType.notFound;
25+
2226
@override
2327
bool isDirectory(String path) => FileSystemEntity.isDirectorySync(path);
2428

pkgs/io_file/test/file_system_file_utils.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ class FileSystemFileUtils implements FileUtils {
4040
fs.removeDirectoryTree(path);
4141
}
4242

43+
@override
44+
bool exists(String path) {
45+
// TODO(brianquinlan): Switch to `FileSystem.exists` when such a method
46+
// exists.
47+
try {
48+
fs.metadata(path);
49+
} on PathNotFoundException {
50+
return false;
51+
}
52+
return true;
53+
}
54+
4355
@override
4456
bool isDirectory(String path) => fs.metadata(path).isDirectory;
4557

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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('vm')
6+
library;
7+
8+
import 'dart:io' as io;
9+
10+
import 'package:io_file/io_file.dart';
11+
import 'package:io_file/src/vm_windows_file_system.dart';
12+
import 'package:path/path.dart' as p;
13+
import 'package:test/test.dart';
14+
import 'package:win32/win32.dart' as win32;
15+
16+
import 'errors.dart' as errors;
17+
import 'file_system_file_utils.dart' hide fileUtils;
18+
import 'test_utils.dart';
19+
20+
void tests(FileUtils utils, FileSystem fs) {
21+
late String tmp;
22+
late String cwd;
23+
24+
setUp(() {
25+
tmp = utils.createTestDirectory('removeFile');
26+
cwd = fs.currentDirectory;
27+
fs.currentDirectory = tmp;
28+
});
29+
30+
tearDown(() {
31+
fs.currentDirectory = cwd;
32+
utils.deleteDirectoryTree(tmp);
33+
});
34+
35+
test('success', () {
36+
final path = '$tmp/file';
37+
utils.createTextFile(path, 'Hello World!');
38+
39+
fileSystem.removeFile(path);
40+
41+
expect(utils.exists(path), isFalse, reason: '$path exists');
42+
});
43+
44+
test('absolute path, long directory name', () {
45+
// On Windows:
46+
// When using an API to create a directory, the specified path cannot be
47+
// so long that you cannot append an 8.3 file name (that is, the directory
48+
// name cannot exceed MAX_PATH minus 12).
49+
final dirname = 'd' * (io.Platform.isWindows ? win32.MAX_PATH - 12 : 255);
50+
final path = p.join(tmp, dirname);
51+
utils.createTextFile(path, 'Hello World!');
52+
53+
fileSystem.removeFile(path);
54+
55+
expect(utils.exists(path), isFalse, reason: '$path exists');
56+
});
57+
58+
test('relative path, long directory name', () {
59+
// On Windows:
60+
// When using an API to create a directory, the specified path cannot be
61+
// so long that you cannot append an 8.3 file name (that is, the directory
62+
// name cannot exceed MAX_PATH minus 12).
63+
final path = 'd' * (io.Platform.isWindows ? win32.MAX_PATH - 12 : 255);
64+
utils.createTextFile(path, 'Hello World!');
65+
66+
fileSystem.removeFile(path);
67+
68+
expect(utils.exists(path), isFalse, reason: '$path exists');
69+
});
70+
71+
test('directory', () {
72+
final path = p.join(tmp, 'dir');
73+
utils.createDirectory(path);
74+
75+
expect(
76+
() => fileSystem.removeFile(path),
77+
throwsA(
78+
isA<IOFileException>().having(
79+
(e) => e.errorCode,
80+
'errorCode',
81+
fileSystem is WindowsFileSystem
82+
? win32.ERROR_ACCESS_DENIED
83+
: anyOf(
84+
errors.eperm, // POSIX
85+
errors.eisdir, // Linux
86+
),
87+
),
88+
),
89+
);
90+
});
91+
92+
test('link to directory', () {
93+
final dirPath = p.join(tmp, 'dir');
94+
final linkPath = p.join(tmp, 'link');
95+
utils.createDirectory(dirPath);
96+
io.Link(linkPath).createSync(dirPath);
97+
98+
if (fs is WindowsFileSystem) {
99+
expect(
100+
() => fileSystem.removeFile(linkPath),
101+
throwsA(
102+
isA<PathAccessException>().having(
103+
(e) => e.errorCode,
104+
'errorCode',
105+
win32.ERROR_ACCESS_DENIED,
106+
),
107+
),
108+
);
109+
expect(utils.exists(dirPath), isTrue, reason: '$dirPath does not exist');
110+
expect(
111+
utils.exists(linkPath),
112+
isTrue,
113+
reason: '$linkPath does not exist',
114+
);
115+
} else {
116+
fileSystem.removeFile(linkPath);
117+
118+
expect(utils.exists(dirPath), isTrue, reason: '$dirPath does not exist');
119+
expect(utils.exists(linkPath), isFalse, reason: '$linkPath exists');
120+
}
121+
});
122+
123+
test('link to file', () {
124+
final filePath = p.join(tmp, 'file');
125+
final linkPath = p.join(tmp, 'link');
126+
utils.createTextFile(filePath, 'Hello World!');
127+
io.Link(linkPath).createSync(filePath);
128+
129+
fileSystem.removeFile(linkPath);
130+
131+
expect(utils.exists(filePath), isTrue, reason: '$filePath does not exist');
132+
expect(utils.exists(linkPath), isFalse, reason: '$linkPath exists');
133+
});
134+
135+
test('path does not exist', () {
136+
final path = '$tmp/file';
137+
138+
expect(
139+
() => fileSystem.removeFile(path),
140+
throwsA(
141+
isA<PathNotFoundException>().having(
142+
(e) => e.errorCode,
143+
'errorCode',
144+
fileSystem is WindowsFileSystem
145+
? win32.ERROR_FILE_NOT_FOUND
146+
: errors.enoent,
147+
),
148+
),
149+
);
150+
});
151+
}
152+
153+
void main() {
154+
group('removeFile', () {
155+
group('dart:io verification', () => tests(fileUtils(), fileSystem));
156+
group(
157+
'self verification',
158+
() => tests(FileSystemFileUtils(fileSystem), fileSystem),
159+
);
160+
});
161+
}

pkgs/io_file/test/test_utils.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ abstract interface class FileUtils {
2727
String createTestDirectory(String testName);
2828
void deleteDirectoryTree(String path);
2929

30+
bool exists(String path);
3031
bool isDirectory(String path);
3132

3233
void createDirectory(String path);

0 commit comments

Comments
 (0)