Skip to content

Commit 77d1df2

Browse files
authored
Support creating temporary directories on POSIX (#220)
1 parent 1d0a0eb commit 77d1df2

File tree

5 files changed

+252
-0
lines changed

5 files changed

+252
-0
lines changed

pkgs/io_file/lib/src/file_system.dart

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,37 @@ base class FileSystem {
5454
throw UnsupportedError('createDirectory');
5555
}
5656

57+
/// Creates a temporary directory and returns its path.
58+
///
59+
/// If `parent` is specified, then the temporary directory is created inside
60+
/// that directory. If `parent` is not specified, then the temporary directory
61+
/// will be created inside the directory found in [temporaryDirectory]. If
62+
/// `parent` is the empty string, then the temporary directory will be created
63+
/// in the current working directory. If the parent directory does not exist,
64+
/// then `PathExistsException` is thrown.
65+
///
66+
/// The temporary directory name is constructed by combining the parent
67+
/// directory path, `prefix` (or the empty string if it is not provided), and
68+
/// some random characters to make the temporary directory name unique. Some
69+
/// characters in `prefix` may be removed or replaced. If `prefix` contains
70+
/// any directory navigation characters then they will be used. For example,
71+
/// a `prefix` of `'../foo'` will create a sibling directory to the parent
72+
/// directory.
73+
///
74+
/// TODO(brianquinlan): Write to a file in the created temporary directory
75+
/// when that is supported.
76+
///
77+
/// ```dart
78+
/// import 'package:io_file/io_file.dart';
79+
///
80+
/// void main() {
81+
/// fileSystem.createTemporaryDirectory(prefix: 'myproject');
82+
/// }
83+
/// ```
84+
String createTemporaryDirectory({String? parent, String? prefix}) {
85+
throw UnsupportedError('createTemporaryDirectory');
86+
}
87+
5788
/// Deletes the directory at the given path.
5889
///
5990
/// If `path` is a directory but the directory is not empty, then
@@ -95,6 +126,18 @@ base class FileSystem {
95126
throw UnsupportedError('readAsBytes');
96127
}
97128

129+
/// The directory path used to store temporary files.
130+
///
131+
/// On Android, Linux, macOS and iOS, the path is taken from:
132+
/// 1. the TMPDIR environment variable if set
133+
/// 2. the TMP environment variable if set
134+
/// 3. '/data/local/tmp' on Android, '/tmp' elsewhere
135+
///
136+
/// TODO(brianquinlan): Add the Windows strategy here.
137+
String get temporaryDirectory {
138+
throw UnsupportedError('temporaryDirectory');
139+
}
140+
98141
/// Write the given bytes to a file.
99142
///
100143
/// If `path` is a broken symlink and `mode` is [WriteMode.failExisting]:

pkgs/io_file/lib/src/vm_posix_file_system.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'dart:math';
88
import 'dart:typed_data';
99

1010
import 'package:ffi/ffi.dart' as ffi;
11+
import 'package:path/path.dart' as p;
1112
import 'package:stdlibc/stdlibc.dart' as stdlibc;
1213

1314
import 'file_system.dart';
@@ -85,6 +86,19 @@ base class PosixFileSystem extends FileSystem {
8586
}
8687
}
8788

89+
@override
90+
String createTemporaryDirectory({String? parent, String? prefix}) {
91+
final directory = parent ?? temporaryDirectory;
92+
final template = p.join(directory, '${prefix ?? ''}XXXXXX');
93+
94+
final path = stdlibc.mkdtemp(template);
95+
if (path == null) {
96+
final errno = stdlibc.errno;
97+
throw _getError(errno, 'mkdtemp failed', template);
98+
}
99+
return path;
100+
}
101+
88102
@override
89103
void removeDirectory(String path) {
90104
if (stdlibc.unlinkat(stdlibc.AT_FDCWD, path, stdlibc.AT_REMOVEDIR) == -1) {
@@ -182,6 +196,13 @@ base class PosixFileSystem extends FileSystem {
182196
}
183197
}
184198

199+
@override
200+
String get temporaryDirectory => p.canonicalize(
201+
stdlibc.getenv('TMPDIR') ??
202+
stdlibc.getenv('TMP') ??
203+
(io.Platform.isAndroid ? '/data/local/tmp' : '/tmp'),
204+
);
205+
185206
@override
186207
void writeAsBytes(
187208
String path,

pkgs/io_file/pubspec.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ environment:
99

1010
dependencies:
1111
ffi: ^2.1.4
12+
path: ^1.9.1
1213
stdlibc:
1314
git:
1415
# Change this to a released version.
@@ -19,5 +20,6 @@ dev_dependencies:
1920
args: ^2.7.0
2021
benchmark_harness: ^2.3.1
2122
dart_flutter_team_lints: ^3.4.0
23+
dartdoc_test: ^0.1.0
2224
test: ^1.24.0
2325
uuid: ^4.5.1
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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';
9+
10+
import 'package:io_file/io_file.dart';
11+
import 'package:path/path.dart' as p;
12+
import 'package:stdlibc/stdlibc.dart' as stdlibc;
13+
import 'package:test/test.dart';
14+
15+
import 'test_utils.dart';
16+
17+
void main() {
18+
group('createTemporaryDirectory', () {
19+
late String tmp;
20+
21+
setUp(() => tmp = createTemp('createTemporaryDirectory'));
22+
23+
tearDown(() => deleteTemp(tmp));
24+
25+
//TODO(brianquinlan): test with a very long path.
26+
27+
test('no arguments', () {
28+
final tmp1 = fileSystem.createTemporaryDirectory();
29+
addTearDown(() => Directory(tmp1).deleteSync());
30+
final tmp2 = fileSystem.createTemporaryDirectory();
31+
addTearDown(() => Directory(tmp2).deleteSync());
32+
33+
expect(fileSystem.same(tmp1, tmp2), isFalse);
34+
expect(Directory(tmp1).existsSync(), isTrue);
35+
expect(Directory(tmp2).existsSync(), isTrue);
36+
});
37+
38+
test('prefix', () {
39+
final tmp1 = fileSystem.createTemporaryDirectory(prefix: 'myprefix');
40+
addTearDown(() => Directory(tmp1).deleteSync());
41+
final tmp2 = fileSystem.createTemporaryDirectory(prefix: 'myprefix');
42+
addTearDown(() => Directory(tmp2).deleteSync());
43+
44+
expect(tmp1, contains('myprefix'));
45+
expect(tmp2, contains('myprefix'));
46+
expect(fileSystem.same(tmp1, tmp2), isFalse);
47+
expect(Directory(tmp1).existsSync(), isTrue);
48+
expect(Directory(tmp2).existsSync(), isTrue);
49+
});
50+
51+
test('prefix is empty string', () {
52+
final tmp1 = fileSystem.createTemporaryDirectory(prefix: '');
53+
addTearDown(() => Directory(tmp1).deleteSync());
54+
final tmp2 = fileSystem.createTemporaryDirectory(prefix: '');
55+
addTearDown(() => Directory(tmp2).deleteSync());
56+
57+
expect(fileSystem.same(tmp1, tmp2), isFalse);
58+
expect(Directory(tmp1).existsSync(), isTrue);
59+
expect(Directory(tmp2).existsSync(), isTrue);
60+
});
61+
62+
test('prefix contains XXXXXX', () {
63+
final tmp1 = fileSystem.createTemporaryDirectory(
64+
prefix: 'myprefix-XXXXXX',
65+
);
66+
addTearDown(() => Directory(tmp1).deleteSync());
67+
final tmp2 = fileSystem.createTemporaryDirectory(
68+
prefix: 'myprefix-XXXXXX',
69+
);
70+
addTearDown(() => Directory(tmp2).deleteSync());
71+
72+
expect(tmp1, contains('myprefix-'));
73+
expect(tmp2, contains('myprefix-'));
74+
expect(fileSystem.same(tmp1, tmp2), isFalse);
75+
expect(Directory(tmp1).existsSync(), isTrue);
76+
expect(Directory(tmp2).existsSync(), isTrue);
77+
});
78+
79+
test('parent', () {
80+
final tmp1 = fileSystem.createTemporaryDirectory(parent: tmp);
81+
final tmp2 = fileSystem.createTemporaryDirectory(parent: tmp);
82+
83+
expect(tmp1, startsWith(tmp));
84+
expect(tmp2, startsWith(tmp));
85+
expect(fileSystem.same(tmp1, tmp2), isFalse);
86+
expect(Directory(tmp1).existsSync(), isTrue);
87+
expect(Directory(tmp2).existsSync(), isTrue);
88+
});
89+
90+
test('parent is empty string', () {
91+
final tmp1 = fileSystem.createTemporaryDirectory(parent: '');
92+
addTearDown(() => Directory(tmp1).deleteSync());
93+
final tmp2 = fileSystem.createTemporaryDirectory(parent: '');
94+
addTearDown(() => Directory(tmp2).deleteSync());
95+
96+
expect(p.isRelative(tmp1), isTrue);
97+
expect(p.isRelative(tmp2), isTrue);
98+
expect(fileSystem.same(tmp1, tmp2), isFalse);
99+
expect(Directory(tmp1).existsSync(), isTrue);
100+
expect(Directory(tmp2).existsSync(), isTrue);
101+
});
102+
103+
test('parent does not exist', () {
104+
expect(
105+
() => fileSystem.createTemporaryDirectory(parent: '/foo/bar/baz'),
106+
throwsA(
107+
isA<PathNotFoundException>().having(
108+
(e) => e.osError?.errorCode,
109+
'errorCode',
110+
stdlibc.ENOENT,
111+
),
112+
),
113+
);
114+
});
115+
116+
test('prefix is absolute path inside of parent', () {
117+
final subdir1 = '$tmp/dir1';
118+
Directory(subdir1).createSync();
119+
120+
final tmp1 = fileSystem.createTemporaryDirectory(
121+
parent: subdir1,
122+
prefix: '$subdir1/file',
123+
);
124+
125+
expect(tmp1, startsWith(subdir1));
126+
});
127+
128+
test('prefix is absolute path outside of parent', () {
129+
final subdir1 = '$tmp/dir1';
130+
final subdir2 = '$tmp/dir2';
131+
Directory(subdir1).createSync();
132+
Directory(subdir2).createSync();
133+
134+
final tmp1 = fileSystem.createTemporaryDirectory(
135+
parent: subdir1,
136+
prefix: '$subdir2/file',
137+
);
138+
139+
expect(tmp1, startsWith(subdir2));
140+
});
141+
142+
test('prefix is non-existant path inside temp directory', () {
143+
expect(
144+
() => fileSystem.createTemporaryDirectory(prefix: 'subdir/file'),
145+
throwsA(
146+
isA<PathNotFoundException>().having(
147+
(e) => e.osError?.errorCode,
148+
'errorCode',
149+
stdlibc.ENOENT,
150+
),
151+
),
152+
);
153+
});
154+
155+
test('prefix is existant path inside temp directory', () {
156+
final subdir1 = '$tmp/dir1';
157+
Directory(subdir1).createSync();
158+
159+
final tmp1 = fileSystem.createTemporaryDirectory(
160+
parent: tmp,
161+
prefix: 'dir1/file',
162+
);
163+
expect(tmp1, startsWith(subdir1));
164+
expect(Directory(tmp1).existsSync(), isTrue);
165+
});
166+
});
167+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 'package:io_file/io_file.dart';
9+
import 'package:path/path.dart' as p;
10+
import 'package:test/test.dart';
11+
12+
void main() {
13+
group('temporaryDirectory', () {
14+
test('success', () {
15+
final tmp = fileSystem.temporaryDirectory;
16+
expect(p.isAbsolute(tmp), isTrue);
17+
});
18+
});
19+
}

0 commit comments

Comments
 (0)