Skip to content

Commit 26c42cf

Browse files
authored
Add support for removing directories (#218)
1 parent 45279b7 commit 26c42cf

File tree

5 files changed

+166
-2
lines changed

5 files changed

+166
-2
lines changed

pkgs/io_file/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ See
1717
| create symbolic link | | | | | | | |
1818
| create tmp directory | | | | | | | |
1919
| create tmp file | | | | | | | |
20-
| delete directory | | | | | | | |
20+
| delete directory | | | | | | | |
2121
| delete file | | | | | | | |
2222
| delete tree | | | | | | | |
2323
| enum dir contents | | | | | | | |

pkgs/io_file/lib/src/file_system.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,22 @@ base class FileSystem {
4343
throw UnsupportedError('createDirectory');
4444
}
4545

46+
/// Deletes the directory at the given path.
47+
///
48+
/// If `path` is a directory but the directory is not empty, then
49+
/// `FileSystemException` is thrown.
50+
///
51+
/// TODO(bquinlan): Explain how to delete non-empty directories.
52+
///
53+
/// If `path` is not directory:
54+
///
55+
/// - On Windows, if `path` is a symbolic link to a directory then the
56+
/// symbolic link is deleted. Otherwise, a `FileSystemException` is thrown.
57+
/// - On POSIX, a `FileSystemException` is thrown.
58+
void removeDirectory(String path) {
59+
throw UnsupportedError('removeDirectory');
60+
}
61+
4662
/// Renames, and possibly moves a file system object from one path to another.
4763
///
4864
/// If `newPath` is a relative path, it is resolved against the current

pkgs/io_file/lib/src/vm_posix_file_system.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ base class PosixFileSystem extends FileSystem {
6868
}
6969
}
7070

71+
@override
72+
void removeDirectory(String path) {
73+
if (stdlibc.unlinkat(stdlibc.AT_FDCWD, path, stdlibc.AT_REMOVEDIR) == -1) {
74+
final errno = stdlibc.errno;
75+
throw _getError(errno, 'remove directory failed', path);
76+
}
77+
}
78+
7179
@override
7280
void rename(String oldPath, String newPath) {
7381
// See https://pubs.opengroup.org/onlinepubs/000095399/functions/rename.html

pkgs/io_file/lib/src/vm_windows_file_system.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,24 @@ base class WindowsFileSystem extends FileSystem {
7979
void createDirectory(String path) => using((arena) {
8080
_primeGetLastError();
8181

82-
if (win32.CreateDirectory(path.toNativeUtf16(), nullptr) == win32.FALSE) {
82+
if (win32.CreateDirectory(path.toNativeUtf16(allocator: arena), nullptr) ==
83+
win32.FALSE) {
8384
final errorCode = win32.GetLastError();
8485
throw _getError(errorCode, 'create directory failed', path);
8586
}
8687
});
8788

89+
@override
90+
void removeDirectory(String path) => using((arena) {
91+
_primeGetLastError();
92+
93+
if (win32.RemoveDirectory(path.toNativeUtf16(allocator: arena)) ==
94+
win32.FALSE) {
95+
final errorCode = win32.GetLastError();
96+
throw _getError(errorCode, 'remove directory failed', path);
97+
}
98+
});
99+
88100
@override
89101
void rename(String oldPath, String newPath) => using((arena) {
90102
_primeGetLastError();
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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';
9+
10+
import 'package:io_file/io_file.dart';
11+
import 'package:stdlibc/stdlibc.dart' as stdlibc;
12+
import 'package:test/test.dart';
13+
import 'package:win32/win32.dart' as win32;
14+
15+
import 'test_utils.dart';
16+
17+
void main() {
18+
group('removeDirectory', () {
19+
late String tmp;
20+
21+
setUp(() => tmp = createTemp('createDirectory'));
22+
23+
tearDown(() => deleteTemp(tmp));
24+
25+
//TODO(brianquinlan): test with a very long path.
26+
27+
test('success', () {
28+
final path = '$tmp/dir';
29+
Directory(path).createSync();
30+
31+
fileSystem.removeDirectory(path);
32+
33+
expect(FileSystemEntity.typeSync(path), FileSystemEntityType.notFound);
34+
});
35+
36+
test('non-empty directory', () {
37+
final path = '$tmp/dir';
38+
Directory(path).createSync();
39+
File('$tmp/dir/file').writeAsStringSync('Hello World!');
40+
41+
expect(
42+
() => fileSystem.removeDirectory(path),
43+
throwsA(
44+
isA<FileSystemException>()
45+
.having((e) => e.message, 'message', 'remove directory failed')
46+
.having(
47+
(e) => e.osError?.errorCode,
48+
'errorCode',
49+
Platform.isWindows
50+
? win32.ERROR_DIR_NOT_EMPTY
51+
: stdlibc.ENOTEMPTY,
52+
),
53+
),
54+
);
55+
});
56+
57+
test('non-existent directory', () {
58+
final path = '$tmp/foo/dir';
59+
60+
expect(
61+
() => fileSystem.removeDirectory(path),
62+
throwsA(
63+
isA<PathNotFoundException>()
64+
.having((e) => e.message, 'message', 'remove directory failed')
65+
.having(
66+
(e) => e.osError?.errorCode,
67+
'errorCode',
68+
Platform.isWindows
69+
? win32.ERROR_PATH_NOT_FOUND
70+
: stdlibc.ENOENT,
71+
),
72+
),
73+
);
74+
});
75+
76+
test('file', () {
77+
final path = '$tmp/file';
78+
File(path).writeAsStringSync('Hello World!');
79+
80+
expect(
81+
() => fileSystem.removeDirectory(path),
82+
throwsA(
83+
isA<FileSystemException>()
84+
.having((e) => e.message, 'message', 'remove directory failed')
85+
.having(
86+
(e) => e.osError?.errorCode,
87+
'errorCode',
88+
Platform.isWindows ? win32.ERROR_DIRECTORY : stdlibc.ENOTDIR,
89+
),
90+
),
91+
);
92+
});
93+
94+
test('link', () {
95+
final dirPath = '$tmp/dir';
96+
final linkPath = '$tmp/link';
97+
Directory(dirPath).createSync();
98+
Link(linkPath).createSync(dirPath);
99+
100+
if (Platform.isWindows) {
101+
fileSystem.removeDirectory(linkPath);
102+
expect(
103+
FileSystemEntity.typeSync(dirPath),
104+
FileSystemEntityType.directory,
105+
);
106+
expect(
107+
FileSystemEntity.typeSync(linkPath),
108+
FileSystemEntityType.notFound,
109+
);
110+
} else {
111+
expect(
112+
() => fileSystem.removeDirectory(linkPath),
113+
throwsA(
114+
isA<FileSystemException>()
115+
.having((e) => e.message, 'message', 'remove directory failed')
116+
.having(
117+
(e) => e.osError?.errorCode,
118+
'errorCode',
119+
Platform.isWindows
120+
? win32.ERROR_PATH_NOT_FOUND
121+
: stdlibc.ENOTDIR,
122+
),
123+
),
124+
);
125+
}
126+
});
127+
});
128+
}

0 commit comments

Comments
 (0)