/src/swift-nio/Sources/NIOPosix/NonBlockingFileIO.swift
Line | Count | Source |
1 | | //===----------------------------------------------------------------------===// |
2 | | // |
3 | | // This source file is part of the SwiftNIO open source project |
4 | | // |
5 | | // Copyright (c) 2017-2024 Apple Inc. and the SwiftNIO project authors |
6 | | // Licensed under Apache License v2.0 |
7 | | // |
8 | | // See LICENSE.txt for license information |
9 | | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors |
10 | | // |
11 | | // SPDX-License-Identifier: Apache-2.0 |
12 | | // |
13 | | //===----------------------------------------------------------------------===// |
14 | | |
15 | | #if !os(WASI) |
16 | | |
17 | | import CNIOLinux |
18 | | import CNIOOpenBSD |
19 | | import CNIOWindows |
20 | | import NIOConcurrencyHelpers |
21 | | import NIOCore |
22 | | |
23 | | /// ``NonBlockingFileIO`` is a helper that allows you to read files without blocking the calling thread. |
24 | | /// |
25 | | /// - warning: The `NonBlockingFileIO` API is deprecated, do not use going forward. It's not marked as `deprecated` yet such |
26 | | /// that users don't get the deprecation warnings affecting their APIs everywhere. For file I/O, please use |
27 | | /// the `NIOFileSystem` API. |
28 | | /// |
29 | | /// It is worth noting that `kqueue`, `epoll` or `poll` returning claiming a file is readable does not mean that the |
30 | | /// data is already available in the kernel's memory. In other words, a `read` from a file can still block even if |
31 | | /// reported as readable. This behaviour is also documented behaviour: |
32 | | /// |
33 | | /// - [`poll`](http://pubs.opengroup.org/onlinepubs/009695399/functions/poll.html): "Regular files shall always poll TRUE for reading and writing." |
34 | | /// - [`epoll`](http://man7.org/linux/man-pages/man7/epoll.7.html): "epoll is simply a faster poll(2), and can be used wherever the latter is used since it shares the same semantics." |
35 | | /// - [`kqueue`](https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2): "Returns when the file pointer is not at the end of file." |
36 | | /// |
37 | | /// ``NonBlockingFileIO`` helps to work around this issue by maintaining its own thread pool that is used to read the data |
38 | | /// from the files into memory. It will then hand the (in-memory) data back which makes it available without the possibility |
39 | | /// of blocking. |
40 | | public struct NonBlockingFileIO: Sendable { |
41 | | /// The default and recommended size for ``NonBlockingFileIO``'s thread pool. |
42 | | @inlinable |
43 | 0 | public static var defaultThreadPoolSize: Int { 2 } |
44 | | |
45 | | /// The default and recommended chunk size. |
46 | | @inlinable |
47 | 0 | public static var defaultChunkSize: Int { 128 * 1024 } |
48 | | |
49 | | /// ``NonBlockingFileIO`` errors. |
50 | | public enum Error: Swift.Error { |
51 | | /// ``NonBlockingFileIO`` is meant to be used with file descriptors that are set to the default (blocking) mode. |
52 | | /// It doesn't make sense to use it with a file descriptor where `O_NONBLOCK` is set therefore this error is |
53 | | /// raised when that was requested. |
54 | | case descriptorSetToNonBlocking |
55 | | } |
56 | | |
57 | | private let threadPool: NIOThreadPool |
58 | | |
59 | | /// Initialize a ``NonBlockingFileIO`` which uses the `NIOThreadPool`. |
60 | | /// |
61 | | /// - Parameters: |
62 | | /// - threadPool: The `NIOThreadPool` that will be used for all the IO. |
63 | 0 | public init(threadPool: NIOThreadPool) { |
64 | 0 | self.threadPool = threadPool |
65 | 0 | } |
66 | | |
67 | | /// Read a `FileRegion` in chunks of `chunkSize` bytes on ``NonBlockingFileIO``'s private thread |
68 | | /// pool which is separate from any `EventLoop` thread. |
69 | | /// |
70 | | /// `chunkHandler` will be called on `eventLoop` for every chunk that was read. Assuming `fileRegion.readableBytes` is greater than |
71 | | /// zero and there are enough bytes available `chunkHandler` will be called `1 + |_ fileRegion.readableBytes / chunkSize _|` |
72 | | /// times, delivering `chunkSize` bytes each time. If less than `fileRegion.readableBytes` bytes can be read from the file, |
73 | | /// `chunkHandler` will be called less often with the last invocation possibly being of less than `chunkSize` bytes. |
74 | | /// |
75 | | /// The allocation and reading of a subsequent chunk will only be attempted when `chunkHandler` succeeds. |
76 | | /// |
77 | | /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the |
78 | | /// same `FileRegion` in multiple threads. |
79 | | /// |
80 | | /// - Parameters: |
81 | | /// - fileRegion: The file region to read. |
82 | | /// - chunkSize: The size of the individual chunks to deliver. |
83 | | /// - allocator: A `ByteBufferAllocator` used to allocate space for the chunks. |
84 | | /// - eventLoop: The `EventLoop` to call `chunkHandler` on. |
85 | | /// - chunkHandler: Called for every chunk read. The next chunk will be read upon successful completion of the returned `EventLoopFuture`. If the returned `EventLoopFuture` fails, the overall operation is aborted. |
86 | | /// - Returns: An `EventLoopFuture` which is the result of the overall operation. If either the reading of `fileHandle` or `chunkHandler` fails, the `EventLoopFuture` will fail too. If the reading of `fileHandle` as well as `chunkHandler` always succeeded, the `EventLoopFuture` will succeed too. |
87 | | @preconcurrency |
88 | | public func readChunked( |
89 | | fileRegion: FileRegion, |
90 | | chunkSize: Int = NonBlockingFileIO.defaultChunkSize, |
91 | | allocator: ByteBufferAllocator, |
92 | | eventLoop: EventLoop, |
93 | | chunkHandler: @escaping @Sendable (ByteBuffer) -> EventLoopFuture<Void> |
94 | 0 | ) -> EventLoopFuture<Void> { |
95 | 0 | let readableBytes = fileRegion.readableBytes |
96 | 0 | return self.readChunked( |
97 | 0 | fileHandle: fileRegion.fileHandle, |
98 | 0 | fromOffset: Int64(fileRegion.readerIndex), |
99 | 0 | byteCount: readableBytes, |
100 | 0 | chunkSize: chunkSize, |
101 | 0 | allocator: allocator, |
102 | 0 | eventLoop: eventLoop, |
103 | 0 | chunkHandler: chunkHandler |
104 | 0 | ) |
105 | 0 | } |
106 | | |
107 | | /// Read `byteCount` bytes in chunks of `chunkSize` bytes from `fileHandle` in ``NonBlockingFileIO``'s private thread |
108 | | /// pool which is separate from any `EventLoop` thread. |
109 | | /// |
110 | | /// `chunkHandler` will be called on `eventLoop` for every chunk that was read. Assuming `byteCount` is greater than |
111 | | /// zero and there are enough bytes available `chunkHandler` will be called `1 + |_ byteCount / chunkSize _|` |
112 | | /// times, delivering `chunkSize` bytes each time. If less than `byteCount` bytes can be read from `descriptor`, |
113 | | /// `chunkHandler` will be called less often with the last invocation possibly being of less than `chunkSize` bytes. |
114 | | /// |
115 | | /// The allocation and reading of a subsequent chunk will only be attempted when `chunkHandler` succeeds. |
116 | | /// |
117 | | /// - Note: `readChunked(fileRegion:chunkSize:allocator:eventLoop:chunkHandler:)` should be preferred as it uses |
118 | | /// `FileRegion` object instead of raw `NIOFileHandle`s. In case you do want to use raw `NIOFileHandle`s, |
119 | | /// please consider using `readChunked(fileHandle:fromOffset:chunkSize:allocator:eventLoop:chunkHandler:)` |
120 | | /// because it doesn't use the file descriptor's seek pointer (which may be shared with other file |
121 | | /// descriptors and even across processes.) |
122 | | /// |
123 | | /// - Parameters: |
124 | | /// - fileHandle: The `NIOFileHandle` to read from. |
125 | | /// - byteCount: The number of bytes to read from `fileHandle`. |
126 | | /// - chunkSize: The size of the individual chunks to deliver. |
127 | | /// - allocator: A `ByteBufferAllocator` used to allocate space for the chunks. |
128 | | /// - eventLoop: The `EventLoop` to call `chunkHandler` on. |
129 | | /// - chunkHandler: Called for every chunk read. The next chunk will be read upon successful completion of the returned `EventLoopFuture`. If the returned `EventLoopFuture` fails, the overall operation is aborted. |
130 | | /// - Returns: An `EventLoopFuture` which is the result of the overall operation. If either the reading of `fileHandle` or `chunkHandler` fails, the `EventLoopFuture` will fail too. If the reading of `fileHandle` as well as `chunkHandler` always succeeded, the `EventLoopFuture` will succeed too. |
131 | | @preconcurrency |
132 | | public func readChunked( |
133 | | fileHandle: NIOFileHandle, |
134 | | byteCount: Int, |
135 | | chunkSize: Int = NonBlockingFileIO.defaultChunkSize, |
136 | | allocator: ByteBufferAllocator, |
137 | | eventLoop: EventLoop, |
138 | | chunkHandler: @escaping @Sendable (ByteBuffer) -> EventLoopFuture<Void> |
139 | 0 | ) -> EventLoopFuture<Void> { |
140 | 0 | self.readChunked0( |
141 | 0 | fileHandle: fileHandle, |
142 | 0 | fromOffset: nil, |
143 | 0 | byteCount: byteCount, |
144 | 0 | chunkSize: chunkSize, |
145 | 0 | allocator: allocator, |
146 | 0 | eventLoop: eventLoop, |
147 | 0 | chunkHandler: chunkHandler |
148 | 0 | ) |
149 | 0 | } |
150 | | |
151 | | /// Read `byteCount` bytes from offset `fileOffset` in chunks of `chunkSize` bytes from `fileHandle` in ``NonBlockingFileIO``'s private thread |
152 | | /// pool which is separate from any `EventLoop` thread. |
153 | | /// |
154 | | /// `chunkHandler` will be called on `eventLoop` for every chunk that was read. Assuming `byteCount` is greater than |
155 | | /// zero and there are enough bytes available `chunkHandler` will be called `1 + |_ byteCount / chunkSize _|` |
156 | | /// times, delivering `chunkSize` bytes each time. If less than `byteCount` bytes can be read from `descriptor`, |
157 | | /// `chunkHandler` will be called less often with the last invocation possibly being of less than `chunkSize` bytes. |
158 | | /// |
159 | | /// The allocation and reading of a subsequent chunk will only be attempted when `chunkHandler` succeeds. |
160 | | /// |
161 | | /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the |
162 | | /// same `NIOFileHandle` in multiple threads. |
163 | | /// |
164 | | /// - Note: `readChunked(fileRegion:chunkSize:allocator:eventLoop:chunkHandler:)` should be preferred as it uses |
165 | | /// `FileRegion` object instead of raw `NIOFileHandle`s. |
166 | | /// |
167 | | /// - Parameters: |
168 | | /// - fileHandle: The `NIOFileHandle` to read from. |
169 | | /// - fileOffset: The offset into the file at which the read should begin. |
170 | | /// - byteCount: The number of bytes to read from `fileHandle`. |
171 | | /// - chunkSize: The size of the individual chunks to deliver. |
172 | | /// - allocator: A `ByteBufferAllocator` used to allocate space for the chunks. |
173 | | /// - eventLoop: The `EventLoop` to call `chunkHandler` on. |
174 | | /// - chunkHandler: Called for every chunk read. The next chunk will be read upon successful completion of the returned `EventLoopFuture`. If the returned `EventLoopFuture` fails, the overall operation is aborted. |
175 | | /// - Returns: An `EventLoopFuture` which is the result of the overall operation. If either the reading of `fileHandle` or `chunkHandler` fails, the `EventLoopFuture` will fail too. If the reading of `fileHandle` as well as `chunkHandler` always succeeded, the `EventLoopFuture` will succeed too. |
176 | | @preconcurrency |
177 | | public func readChunked( |
178 | | fileHandle: NIOFileHandle, |
179 | | fromOffset fileOffset: Int64, |
180 | | byteCount: Int, |
181 | | chunkSize: Int = NonBlockingFileIO.defaultChunkSize, |
182 | | allocator: ByteBufferAllocator, |
183 | | eventLoop: EventLoop, |
184 | | chunkHandler: @escaping @Sendable (ByteBuffer) -> EventLoopFuture<Void> |
185 | 0 | ) -> EventLoopFuture<Void> { |
186 | 0 | self.readChunked0( |
187 | 0 | fileHandle: fileHandle, |
188 | 0 | fromOffset: fileOffset, |
189 | 0 | byteCount: byteCount, |
190 | 0 | chunkSize: chunkSize, |
191 | 0 | allocator: allocator, |
192 | 0 | eventLoop: eventLoop, |
193 | 0 | chunkHandler: chunkHandler |
194 | 0 | ) |
195 | 0 | } |
196 | | |
197 | | private typealias ReadChunkHandler = @Sendable (ByteBuffer) -> EventLoopFuture<Void> |
198 | | |
199 | | private func readChunked0( |
200 | | fileHandle: NIOFileHandle, |
201 | | fromOffset: Int64?, |
202 | | byteCount: Int, |
203 | | chunkSize: Int, |
204 | | allocator: ByteBufferAllocator, |
205 | | eventLoop: EventLoop, |
206 | | chunkHandler: @escaping ReadChunkHandler |
207 | 0 | ) -> EventLoopFuture<Void> { |
208 | 0 | precondition(chunkSize > 0, "chunkSize must be > 0 (is \(chunkSize))") |
209 | 0 | let remainingReads = 1 + (byteCount / chunkSize) |
210 | 0 | let lastReadSize = byteCount % chunkSize |
211 | 0 |
|
212 | 0 | let promise = eventLoop.makePromise(of: Void.self) |
213 | 0 |
|
214 | 0 | @Sendable |
215 | 0 | func _read(remainingReads: Int, bytesReadSoFar: Int64) { |
216 | 0 | if remainingReads > 1 || (remainingReads == 1 && lastReadSize > 0) { |
217 | 0 | let readSize = remainingReads > 1 ? chunkSize : lastReadSize |
218 | 0 | assert(readSize > 0) |
219 | 0 | let readFuture = self.read0( |
220 | 0 | fileHandle: fileHandle, |
221 | 0 | fromOffset: fromOffset.map { $0 + bytesReadSoFar }, |
222 | 0 | byteCount: readSize, |
223 | 0 | allocator: allocator, |
224 | 0 | eventLoop: eventLoop |
225 | 0 | ) |
226 | 0 | readFuture.whenComplete { (result) in |
227 | 0 | switch result { |
228 | 0 | case .success(let buffer): |
229 | 0 | guard buffer.readableBytes > 0 else { |
230 | 0 | // EOF, call `chunkHandler` one more time. |
231 | 0 | let handlerFuture = chunkHandler(buffer) |
232 | 0 | handlerFuture.cascade(to: promise) |
233 | 0 | return |
234 | 0 | } |
235 | 0 | let bytesRead = Int64(buffer.readableBytes) |
236 | 0 | chunkHandler(buffer).hop(to: eventLoop).whenComplete { result in |
237 | 0 | switch result { |
238 | 0 | case .success(_): |
239 | 0 | eventLoop.assertInEventLoop() |
240 | 0 | _read( |
241 | 0 | remainingReads: remainingReads - 1, |
242 | 0 | bytesReadSoFar: bytesReadSoFar + bytesRead |
243 | 0 | ) |
244 | 0 | case .failure(let error): |
245 | 0 | promise.fail(error) |
246 | 0 | } |
247 | 0 | } |
248 | 0 | case .failure(let error): |
249 | 0 | promise.fail(error) |
250 | 0 | } |
251 | 0 | } |
252 | 0 | } else { |
253 | 0 | promise.succeed(()) |
254 | 0 | } |
255 | 0 | } |
256 | 0 | _read(remainingReads: remainingReads, bytesReadSoFar: 0) |
257 | 0 |
|
258 | 0 | return promise.futureResult |
259 | 0 | } |
260 | | |
261 | | /// Read a `FileRegion` in ``NonBlockingFileIO``'s private thread pool which is separate from any `EventLoop` thread. |
262 | | /// |
263 | | /// The returned `ByteBuffer` will not have less than `fileRegion.readableBytes` unless we hit end-of-file in which |
264 | | /// case the `ByteBuffer` will contain the bytes available to read. |
265 | | /// |
266 | | /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the |
267 | | /// same `FileRegion` in multiple threads. |
268 | | /// |
269 | | /// - Note: Only use this function for small enough `FileRegion`s as it will need to allocate enough memory to hold `fileRegion.readableBytes` bytes. |
270 | | /// - Note: In most cases you should prefer one of the `readChunked` functions. |
271 | | /// |
272 | | /// - Parameters: |
273 | | /// - fileRegion: The file region to read. |
274 | | /// - allocator: A `ByteBufferAllocator` used to allocate space for the returned `ByteBuffer`. |
275 | | /// - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from. |
276 | | /// - Returns: An `EventLoopFuture` which delivers a `ByteBuffer` if the read was successful or a failure on error. |
277 | | public func read( |
278 | | fileRegion: FileRegion, |
279 | | allocator: ByteBufferAllocator, |
280 | | eventLoop: EventLoop |
281 | 0 | ) -> EventLoopFuture<ByteBuffer> { |
282 | 0 | let readableBytes = fileRegion.readableBytes |
283 | 0 | return self.read( |
284 | 0 | fileHandle: fileRegion.fileHandle, |
285 | 0 | fromOffset: Int64(fileRegion.readerIndex), |
286 | 0 | byteCount: readableBytes, |
287 | 0 | allocator: allocator, |
288 | 0 | eventLoop: eventLoop |
289 | 0 | ) |
290 | 0 | } |
291 | | |
292 | | /// Read `byteCount` bytes from `fileHandle` in ``NonBlockingFileIO``'s private thread pool which is separate from any `EventLoop` thread. |
293 | | /// |
294 | | /// The returned `ByteBuffer` will not have less than `byteCount` bytes unless we hit end-of-file in which |
295 | | /// case the `ByteBuffer` will contain the bytes available to read. |
296 | | /// |
297 | | /// - Note: Only use this function for small enough `byteCount`s as it will need to allocate enough memory to hold `byteCount` bytes. |
298 | | /// - Note: ``read(fileRegion:allocator:eventLoop:)`` should be preferred as it uses `FileRegion` object instead of |
299 | | /// raw `NIOFileHandle`s. In case you do want to use raw `NIOFileHandle`s, |
300 | | /// please consider using ``read(fileHandle:fromOffset:byteCount:allocator:eventLoop:)`` |
301 | | /// because it doesn't use the file descriptor's seek pointer (which may be shared with other file |
302 | | /// descriptors and even across processes.) |
303 | | /// |
304 | | /// - Parameters: |
305 | | /// - fileHandle: The `NIOFileHandle` to read. |
306 | | /// - byteCount: The number of bytes to read from `fileHandle`. |
307 | | /// - allocator: A `ByteBufferAllocator` used to allocate space for the returned `ByteBuffer`. |
308 | | /// - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from. |
309 | | /// - Returns: An `EventLoopFuture` which delivers a `ByteBuffer` if the read was successful or a failure on error. |
310 | | public func read( |
311 | | fileHandle: NIOFileHandle, |
312 | | byteCount: Int, |
313 | | allocator: ByteBufferAllocator, |
314 | | eventLoop: EventLoop |
315 | 0 | ) -> EventLoopFuture<ByteBuffer> { |
316 | 0 | self.read0( |
317 | 0 | fileHandle: fileHandle, |
318 | 0 | fromOffset: nil, |
319 | 0 | byteCount: byteCount, |
320 | 0 | allocator: allocator, |
321 | 0 | eventLoop: eventLoop |
322 | 0 | ) |
323 | 0 | } |
324 | | |
325 | | /// Read `byteCount` bytes starting at `fileOffset` from `fileHandle` in ``NonBlockingFileIO``'s private thread pool |
326 | | /// which is separate from any `EventLoop` thread. |
327 | | /// |
328 | | /// The returned `ByteBuffer` will not have less than `byteCount` bytes unless we hit end-of-file in which |
329 | | /// case the `ByteBuffer` will contain the bytes available to read. |
330 | | /// |
331 | | /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the |
332 | | /// same `fileHandle` in multiple threads. |
333 | | /// |
334 | | /// - Note: Only use this function for small enough `byteCount`s as it will need to allocate enough memory to hold `byteCount` bytes. |
335 | | /// - Note: ``read(fileRegion:allocator:eventLoop:)`` should be preferred as it uses `FileRegion` object instead of raw `NIOFileHandle`s. |
336 | | /// |
337 | | /// - Parameters: |
338 | | /// - fileHandle: The `NIOFileHandle` to read. |
339 | | /// - fileOffset: The offset to read from. |
340 | | /// - byteCount: The number of bytes to read from `fileHandle`. |
341 | | /// - allocator: A `ByteBufferAllocator` used to allocate space for the returned `ByteBuffer`. |
342 | | /// - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from. |
343 | | /// - Returns: An `EventLoopFuture` which delivers a `ByteBuffer` if the read was successful or a failure on error. |
344 | | public func read( |
345 | | fileHandle: NIOFileHandle, |
346 | | fromOffset fileOffset: Int64, |
347 | | byteCount: Int, |
348 | | allocator: ByteBufferAllocator, |
349 | | eventLoop: EventLoop |
350 | 0 | ) -> EventLoopFuture<ByteBuffer> { |
351 | 0 | self.read0( |
352 | 0 | fileHandle: fileHandle, |
353 | 0 | fromOffset: fileOffset, |
354 | 0 | byteCount: byteCount, |
355 | 0 | allocator: allocator, |
356 | 0 | eventLoop: eventLoop |
357 | 0 | ) |
358 | 0 | } |
359 | | |
360 | | private func read0( |
361 | | fileHandle: NIOFileHandle, |
362 | | fromOffset: Int64?, // > 2 GB offset is reasonable on 32-bit systems |
363 | | byteCount rawByteCount: Int, |
364 | | allocator: ByteBufferAllocator, |
365 | | eventLoop: EventLoop |
366 | 0 | ) -> EventLoopFuture<ByteBuffer> { |
367 | 0 | guard rawByteCount > 0 else { |
368 | 0 | return eventLoop.makeSucceededFuture(allocator.buffer(capacity: 0)) |
369 | 0 | } |
370 | 0 | let byteCount = rawByteCount < Int32.max ? rawByteCount : size_t(Int32.max) |
371 | 0 |
|
372 | 0 | return self.threadPool.runIfActive(eventLoop: eventLoop) { () -> ByteBuffer in |
373 | 0 | try self.readSync( |
374 | 0 | fileHandle: fileHandle, |
375 | 0 | fromOffset: fromOffset, |
376 | 0 | byteCount: byteCount, |
377 | 0 | allocator: allocator |
378 | 0 | ) |
379 | 0 | } |
380 | 0 | } |
381 | | |
382 | | private func readSync( |
383 | | fileHandle: NIOFileHandle, |
384 | | fromOffset: Int64?, // > 2 GB offset is reasonable on 32-bit systems |
385 | | byteCount: Int, |
386 | | allocator: ByteBufferAllocator |
387 | 0 | ) throws -> ByteBuffer { |
388 | 0 | var bytesRead = 0 |
389 | 0 | var buf = allocator.buffer(capacity: byteCount) |
390 | 0 |
|
391 | 0 | while bytesRead < byteCount { |
392 | 0 | let n = try buf.writeWithUnsafeMutableBytes(minimumWritableBytes: byteCount - bytesRead) { ptr -> Int in |
393 | 0 | let res = try fileHandle.withUnsafeFileDescriptor { descriptor -> IOResult<ssize_t> in |
394 | 0 | if let offset = fromOffset { |
395 | | #if !os(Windows) |
396 | 0 | return try Posix.pread( |
397 | 0 | descriptor: descriptor, |
398 | 0 | pointer: ptr.baseAddress!, |
399 | 0 | size: byteCount - bytesRead, |
400 | 0 | offset: off_t(offset) + off_t(bytesRead) |
401 | 0 | ) |
402 | | #else |
403 | | return try Windows.pread( |
404 | | descriptor: descriptor, |
405 | | pointer: ptr.baseAddress!, |
406 | | size: byteCount - bytesRead, |
407 | | offset: off_t(offset) + off_t(bytesRead) |
408 | | ) |
409 | | #endif |
410 | 0 | } |
411 | 0 |
|
412 | 0 | return try Posix.read( |
413 | 0 | descriptor: descriptor, |
414 | 0 | pointer: ptr.baseAddress!, |
415 | 0 | size: byteCount - bytesRead |
416 | 0 | ) |
417 | 0 | } |
418 | 0 | switch res { |
419 | 0 | case .processed(let n): |
420 | 0 | assert(n >= 0, "read claims to have read a negative number of bytes \(n)") |
421 | 0 | return numericCast(n) // ssize_t is Int64 on Windows and Int everywhere else. |
422 | 0 | case .wouldBlock: |
423 | 0 | throw Error.descriptorSetToNonBlocking |
424 | 0 | } |
425 | 0 | } |
426 | 0 | if n == 0 { |
427 | 0 | // EOF |
428 | 0 | break |
429 | 0 | } else { |
430 | 0 | bytesRead += n |
431 | 0 | } |
432 | 0 | } |
433 | 0 | return buf |
434 | 0 | } |
435 | | |
436 | | /// Changes the file size of `fileHandle` to `size`. |
437 | | /// |
438 | | /// If `size` is smaller than the current file size, the remaining bytes will be truncated and are lost. If `size` |
439 | | /// is larger than the current file size, the gap will be filled with zero bytes. |
440 | | /// |
441 | | /// - Parameters: |
442 | | /// - fileHandle: The `NIOFileHandle` to write to. |
443 | | /// - size: The new file size in bytes to write. |
444 | | /// - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from. |
445 | | /// - Returns: An `EventLoopFuture` which is fulfilled if the write was successful or fails on error. |
446 | | public func changeFileSize( |
447 | | fileHandle: NIOFileHandle, |
448 | | size: Int64, |
449 | | eventLoop: EventLoop |
450 | 0 | ) -> EventLoopFuture<()> { |
451 | 0 | self.threadPool.runIfActive(eventLoop: eventLoop) { |
452 | 0 | try fileHandle.withUnsafeFileDescriptor { descriptor -> Void in |
453 | 0 | try Posix.ftruncate(descriptor: descriptor, size: off_t(size)) |
454 | 0 | } |
455 | 0 | } |
456 | 0 | } |
457 | | |
458 | | /// Returns the length of the file in bytes associated with `fileHandle`. |
459 | | /// |
460 | | /// - Parameters: |
461 | | /// - fileHandle: The `NIOFileHandle` to read from. |
462 | | /// - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from. |
463 | | /// - Returns: An `EventLoopFuture` which is fulfilled with the length of the file in bytes if the write was successful or fails on error. |
464 | | public func readFileSize( |
465 | | fileHandle: NIOFileHandle, |
466 | | eventLoop: EventLoop |
467 | 0 | ) -> EventLoopFuture<Int64> { |
468 | 0 | self.threadPool.runIfActive(eventLoop: eventLoop) { |
469 | 0 | try fileHandle.withUnsafeFileDescriptor { descriptor in |
470 | 0 | let curr = try Posix.lseek(descriptor: descriptor, offset: 0, whence: SEEK_CUR) |
471 | 0 | let eof = try Posix.lseek(descriptor: descriptor, offset: 0, whence: SEEK_END) |
472 | 0 | try Posix.lseek(descriptor: descriptor, offset: curr, whence: SEEK_SET) |
473 | 0 | return Int64(eof) |
474 | 0 | } |
475 | 0 | } |
476 | 0 | } |
477 | | |
478 | | /// Write `buffer` to `fileHandle` in ``NonBlockingFileIO``'s private thread pool which is separate from any `EventLoop` thread. |
479 | | /// |
480 | | /// - Parameters: |
481 | | /// - fileHandle: The `NIOFileHandle` to write to. |
482 | | /// - buffer: The `ByteBuffer` to write. |
483 | | /// - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from. |
484 | | /// - Returns: An `EventLoopFuture` which is fulfilled if the write was successful or fails on error. |
485 | | public func write( |
486 | | fileHandle: NIOFileHandle, |
487 | | buffer: ByteBuffer, |
488 | | eventLoop: EventLoop |
489 | 0 | ) -> EventLoopFuture<()> { |
490 | 0 | self.write0(fileHandle: fileHandle, toOffset: nil, buffer: buffer, eventLoop: eventLoop) |
491 | 0 | } |
492 | | |
493 | | /// Write `buffer` starting from `toOffset` to `fileHandle` in ``NonBlockingFileIO``'s private thread pool which is separate from any `EventLoop` thread. |
494 | | /// |
495 | | /// - Parameters: |
496 | | /// - fileHandle: The `NIOFileHandle` to write to. |
497 | | /// - toOffset: The file offset to write to. |
498 | | /// - buffer: The `ByteBuffer` to write. |
499 | | /// - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from. |
500 | | /// - Returns: An `EventLoopFuture` which is fulfilled if the write was successful or fails on error. |
501 | | public func write( |
502 | | fileHandle: NIOFileHandle, |
503 | | toOffset: Int64, |
504 | | buffer: ByteBuffer, |
505 | | eventLoop: EventLoop |
506 | 0 | ) -> EventLoopFuture<()> { |
507 | 0 | self.write0(fileHandle: fileHandle, toOffset: toOffset, buffer: buffer, eventLoop: eventLoop) |
508 | 0 | } |
509 | | |
510 | | private func write0( |
511 | | fileHandle: NIOFileHandle, |
512 | | toOffset: Int64?, |
513 | | buffer: ByteBuffer, |
514 | | eventLoop: EventLoop |
515 | 0 | ) -> EventLoopFuture<()> { |
516 | 0 | let byteCount = buffer.readableBytes |
517 | 0 |
|
518 | 0 | guard byteCount > 0 else { |
519 | 0 | return eventLoop.makeSucceededFuture(()) |
520 | 0 | } |
521 | 0 |
|
522 | 0 | return self.threadPool.runIfActive(eventLoop: eventLoop) { |
523 | 0 | try self.writeSync(fileHandle: fileHandle, byteCount: byteCount, toOffset: toOffset, buffer: buffer) |
524 | 0 | } |
525 | 0 | } |
526 | | |
527 | | private func writeSync( |
528 | | fileHandle: NIOFileHandle, |
529 | | byteCount: Int, |
530 | | toOffset: Int64?, |
531 | | buffer: ByteBuffer |
532 | 0 | ) throws { |
533 | 0 | var buf = buffer |
534 | 0 |
|
535 | 0 | var offsetAccumulator: Int = 0 |
536 | 0 | repeat { |
537 | 0 | let n = try buf.readWithUnsafeReadableBytes { ptr in |
538 | 0 | precondition(ptr.count == byteCount - offsetAccumulator) |
539 | 0 | let res: IOResult<ssize_t> = try fileHandle.withUnsafeFileDescriptor { descriptor in |
540 | 0 | if let toOffset = toOffset { |
541 | | #if os(Windows) |
542 | | return try Windows.pwrite( |
543 | | descriptor: descriptor, |
544 | | pointer: ptr.baseAddress!, |
545 | | size: byteCount - offsetAccumulator, |
546 | | offset: off_t(toOffset + Int64(offsetAccumulator)) |
547 | | ) |
548 | | #else |
549 | 0 | return try Posix.pwrite( |
550 | 0 | descriptor: descriptor, |
551 | 0 | pointer: ptr.baseAddress!, |
552 | 0 | size: byteCount - offsetAccumulator, |
553 | 0 | offset: off_t(toOffset + Int64(offsetAccumulator)) |
554 | 0 | ) |
555 | | #endif |
556 | 0 | } else { |
557 | 0 | let result = try Posix.write( |
558 | 0 | descriptor: descriptor, |
559 | 0 | pointer: ptr.baseAddress!, |
560 | 0 | size: byteCount - offsetAccumulator |
561 | 0 | ) |
562 | | #if os(Windows) |
563 | | return result.map { ssize_t($0) } |
564 | | #else |
565 | 0 | return result |
566 | | #endif |
567 | 0 | } |
568 | 0 | } |
569 | 0 | switch res { |
570 | 0 | case .processed(let n): |
571 | 0 | assert(n >= 0, "write claims to have written a negative number of bytes \(n)") |
572 | 0 | return numericCast(n) |
573 | 0 | case .wouldBlock: |
574 | 0 | throw Error.descriptorSetToNonBlocking |
575 | 0 | } |
576 | 0 | } |
577 | 0 | offsetAccumulator += n |
578 | 0 | } while offsetAccumulator < byteCount |
579 | 0 | } |
580 | | |
581 | | /// Open the file at `path` for reading on a private thread pool which is separate from any `EventLoop` thread. |
582 | | /// |
583 | | /// This function will return (a future) of the `NIOFileHandle` associated with the file opened and a `FileRegion` |
584 | | /// comprising of the whole file. The caller must close the returned `NIOFileHandle` when it's no longer needed. |
585 | | /// |
586 | | /// - Note: The reason this returns the `NIOFileHandle` and the `FileRegion` is that both the opening of a file as well as the querying of its size are blocking. |
587 | | /// |
588 | | /// - Parameters: |
589 | | /// - path: The path of the file to be opened for reading. |
590 | | /// - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire. |
591 | | /// - Returns: An `EventLoopFuture` containing the `NIOFileHandle` and the `FileRegion` comprising the whole file. |
592 | | @available( |
593 | | *, |
594 | | deprecated, |
595 | | message: |
596 | | "Avoid using NIOFileHandle. The type is difficult to hold correctly, use NIOFileSystem as a replacement API." |
597 | | ) |
598 | 0 | public func openFile(path: String, eventLoop: EventLoop) -> EventLoopFuture<(NIOFileHandle, FileRegion)> { |
599 | 0 | self.openFile(_deprecatedPath: path, eventLoop: eventLoop) |
600 | 0 | } |
601 | | |
602 | | /// Open the file at `path` for reading on a private thread pool which is separate from any `EventLoop` thread. |
603 | | /// |
604 | | /// This function will return (a future) of the `NIOFileHandle` associated with the file opened and a `FileRegion` |
605 | | /// comprising of the whole file. The caller must close the returned `NIOFileHandle` when it's no longer needed. |
606 | | /// |
607 | | /// - Note: The reason this returns the `NIOFileHandle` and the `FileRegion` is that both the opening of a file as well as the querying of its size are blocking. |
608 | | /// |
609 | | /// - Parameters: |
610 | | /// - path: The path of the file to be opened for reading. |
611 | | /// - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire. |
612 | | /// - Returns: An `EventLoopFuture` containing the `NIOFileHandle` and the `FileRegion` comprising the whole file. |
613 | | public func openFile( |
614 | | _deprecatedPath path: String, |
615 | | eventLoop: EventLoop |
616 | 0 | ) -> EventLoopFuture<(NIOFileHandle, FileRegion)> { |
617 | 0 | self.threadPool.runIfActive(eventLoop: eventLoop) { |
618 | 0 | let fh = try NIOFileHandle(_deprecatedPath: path) |
619 | 0 | do { |
620 | 0 | let fr = try FileRegion(fileHandle: fh) |
621 | 0 | return (fh, fr) |
622 | 0 | } catch { |
623 | 0 | _ = try? fh.close() |
624 | 0 | throw error |
625 | 0 | } |
626 | 0 | } |
627 | 0 | } |
628 | | |
629 | | /// Open the file at `path` with specified access mode and POSIX flags on a private thread pool which is separate from any `EventLoop` thread. |
630 | | /// |
631 | | /// This function will return (a future) of the `NIOFileHandle` associated with the file opened. |
632 | | /// The caller must close the returned `NIOFileHandle` when it's no longer needed. |
633 | | /// |
634 | | /// - Parameters: |
635 | | /// - path: The path of the file to be opened for writing. |
636 | | /// - mode: File access mode. |
637 | | /// - flags: Additional POSIX flags. |
638 | | /// - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire. |
639 | | /// - Returns: An `EventLoopFuture` containing the `NIOFileHandle`. |
640 | | @available( |
641 | | *, |
642 | | deprecated, |
643 | | message: |
644 | | "Avoid using NonBlockingFileIO. The type is difficult to hold correctly, use NIOFileSystem as a replacement API." |
645 | | ) |
646 | | public func openFile( |
647 | | path: String, |
648 | | mode: NIOFileHandle.Mode, |
649 | | flags: NIOFileHandle.Flags = .default, |
650 | | eventLoop: EventLoop |
651 | 0 | ) -> EventLoopFuture<NIOFileHandle> { |
652 | 0 | self.openFile(_deprecatedPath: path, mode: mode, flags: flags, eventLoop: eventLoop) |
653 | 0 | } |
654 | | |
655 | | /// Open the file at `path` with specified access mode and POSIX flags on a private thread pool which is separate from any `EventLoop` thread. |
656 | | /// |
657 | | /// This function will return (a future) of the `NIOFileHandle` associated with the file opened. |
658 | | /// The caller must close the returned `NIOFileHandle` when it's no longer needed. |
659 | | /// |
660 | | /// - Parameters: |
661 | | /// - path: The path of the file to be opened for writing. |
662 | | /// - mode: File access mode. |
663 | | /// - flags: Additional POSIX flags. |
664 | | /// - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire. |
665 | | /// - Returns: An `EventLoopFuture` containing the `NIOFileHandle`. |
666 | | public func openFile( |
667 | | _deprecatedPath path: String, |
668 | | mode: NIOFileHandle.Mode, |
669 | | flags: NIOFileHandle.Flags = .default, |
670 | | eventLoop: EventLoop |
671 | 0 | ) -> EventLoopFuture<NIOFileHandle> { |
672 | 0 | self.threadPool.runIfActive(eventLoop: eventLoop) { |
673 | 0 | try NIOFileHandle(_deprecatedPath: path, mode: mode, flags: flags) |
674 | 0 | } |
675 | 0 | } |
676 | | |
677 | | #if !os(Windows) |
678 | | /// Returns information about a file at `path` on a private thread pool which is separate from any `EventLoop` thread. |
679 | | /// |
680 | | /// - Note: If `path` is a symlink, information about the link, not the file it points to. |
681 | | /// |
682 | | /// - Parameters: |
683 | | /// - path: The path of the file to get information about. |
684 | | /// - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire. |
685 | | /// - Returns: An `EventLoopFuture` containing file information. |
686 | 0 | public func lstat(path: String, eventLoop: EventLoop) -> EventLoopFuture<stat> { |
687 | 0 | self.threadPool.runIfActive(eventLoop: eventLoop) { |
688 | 0 | var s = stat() |
689 | 0 | try Posix.lstat(pathname: path, outStat: &s) |
690 | 0 | return s |
691 | 0 | } |
692 | 0 | } |
693 | | |
694 | | /// Creates a symbolic link to a `destination` file at `path` on a private thread pool which is separate from any `EventLoop` thread. |
695 | | /// |
696 | | /// - Parameters: |
697 | | /// - path: The path of the link. |
698 | | /// - destination: Target path where this link will point to. |
699 | | /// - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire. |
700 | | /// - Returns: An `EventLoopFuture` which is fulfilled if the rename was successful or fails on error. |
701 | 0 | public func symlink(path: String, to destination: String, eventLoop: EventLoop) -> EventLoopFuture<Void> { |
702 | 0 | self.threadPool.runIfActive(eventLoop: eventLoop) { |
703 | 0 | try Posix.symlink(pathname: path, destination: destination) |
704 | 0 | } |
705 | 0 | } |
706 | | |
707 | | /// Returns target of the symbolic link at `path` on a private thread pool which is separate from any `EventLoop` thread. |
708 | | /// |
709 | | /// - Parameters: |
710 | | /// - path: The path of the link to read. |
711 | | /// - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire. |
712 | | /// - Returns: An `EventLoopFuture` containing link target. |
713 | 0 | public func readlink(path: String, eventLoop: EventLoop) -> EventLoopFuture<String> { |
714 | 0 | self.threadPool.runIfActive(eventLoop: eventLoop) { |
715 | 0 | let maxLength = Int(PATH_MAX) |
716 | 0 | let pointer = UnsafeMutableBufferPointer<CChar>.allocate(capacity: maxLength) |
717 | 0 | defer { |
718 | 0 | pointer.deallocate() |
719 | 0 | } |
720 | 0 | let length = try Posix.readlink(pathname: path, outPath: pointer.baseAddress!, outPathSize: maxLength) |
721 | 0 | return String(decoding: UnsafeRawBufferPointer(pointer).prefix(length), as: UTF8.self) |
722 | 0 | } |
723 | 0 | } |
724 | | |
725 | | /// Removes symbolic link at `path` on a private thread pool which is separate from any `EventLoop` thread. |
726 | | /// |
727 | | /// - Parameters: |
728 | | /// - path: The path of the link to remove. |
729 | | /// - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire. |
730 | | /// - Returns: An `EventLoopFuture` which is fulfilled if the rename was successful or fails on error. |
731 | 0 | public func unlink(path: String, eventLoop: EventLoop) -> EventLoopFuture<Void> { |
732 | 0 | self.threadPool.runIfActive(eventLoop: eventLoop) { |
733 | 0 | try Posix.unlink(pathname: path) |
734 | 0 | } |
735 | 0 | } |
736 | | |
737 | 0 | private func createDirectory0(_ path: String, mode: NIOPOSIXFileMode) throws { |
738 | 0 | let pathView = path.utf8 |
739 | 0 | if pathView.isEmpty { |
740 | 0 | return |
741 | 0 | } |
742 | 0 |
|
743 | 0 | // Fail fast if not a directory or file exists |
744 | 0 | do { |
745 | 0 | var s = stat() |
746 | 0 | try Posix.stat(pathname: path, outStat: &s) |
747 | 0 | if (S_IFMT & s.st_mode) == S_IFDIR { |
748 | 0 | return |
749 | 0 | } |
750 | 0 | throw IOError(errnoCode: ENOTDIR, reason: "Not a directory") |
751 | 0 | } catch let error as IOError where error.errnoCode == ENOENT { |
752 | 0 | // if directory does not exist we can proceed with creating it |
753 | 0 | } |
754 | 0 |
|
755 | 0 | // Slow path, check that all intermediate directories exist recursively |
756 | 0 |
|
757 | 0 | // Trim any trailing path separators |
758 | 0 | var index = pathView.index(before: pathView.endIndex) |
759 | 0 | let pathSeparator = UInt8(ascii: "/") |
760 | 0 | while index != pathView.startIndex && pathView[index] == pathSeparator { |
761 | 0 | index = pathView.index(before: index) |
762 | 0 | } |
763 | 0 |
|
764 | 0 | // Find first non-trailing path separator if it exists |
765 | 0 | while index != pathView.startIndex && pathView[index] != pathSeparator { |
766 | 0 | index = pathView.index(before: index) |
767 | 0 | } |
768 | 0 |
|
769 | 0 | // If non-trailing path separator is found, create parent directory |
770 | 0 | if index > pathView.startIndex { |
771 | 0 | try self.createDirectory0(String(Substring(pathView.prefix(upTo: index))), mode: mode) |
772 | 0 | } |
773 | 0 |
|
774 | 0 | do { |
775 | 0 | try Posix.mkdir(pathname: path, mode: mode) |
776 | 0 | } catch { |
777 | 0 | // If user tries to create a path like `/some/path/.` it may fail, as path will be created |
778 | 0 | // by the recursive call. Checks if directory exists and re-throw the error if it does not |
779 | 0 | do { |
780 | 0 | var s = stat() |
781 | 0 | try Posix.lstat(pathname: path, outStat: &s) |
782 | 0 | if (S_IFMT & s.st_mode) == S_IFDIR { |
783 | 0 | return |
784 | 0 | } |
785 | 0 | } catch { |
786 | 0 | // fallthrough |
787 | 0 | } |
788 | 0 | throw error |
789 | 0 | } |
790 | 0 | } |
791 | | |
792 | | /// Creates directory at `path` on a private thread pool which is separate from any `EventLoop` thread. |
793 | | /// |
794 | | /// - Parameters: |
795 | | /// - path: The path of the directory to be created. |
796 | | /// - createIntermediates: Whether intermediate directories should be created. |
797 | | /// - mode: POSIX file mode. |
798 | | /// - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire. |
799 | | /// - Returns: An `EventLoopFuture` which is fulfilled if the rename was successful or fails on error. |
800 | | public func createDirectory( |
801 | | path: String, |
802 | | withIntermediateDirectories createIntermediates: Bool = false, |
803 | | mode: NIOPOSIXFileMode, |
804 | | eventLoop: EventLoop |
805 | 0 | ) -> EventLoopFuture<Void> { |
806 | 0 | self.threadPool.runIfActive(eventLoop: eventLoop) { |
807 | 0 | if createIntermediates { |
808 | | #if canImport(Darwin) |
809 | | try Posix.mkpath_np(pathname: path, mode: mode) |
810 | | #else |
811 | 0 | try self.createDirectory0(path, mode: mode) |
812 | | #endif |
813 | 0 | } else { |
814 | 0 | try Posix.mkdir(pathname: path, mode: mode) |
815 | 0 | } |
816 | 0 | } |
817 | 0 | } |
818 | | |
819 | | /// List contents of the directory at `path` on a private thread pool which is separate from any `EventLoop` thread. |
820 | | /// |
821 | | /// - Parameters: |
822 | | /// - path: The path of the directory to list the content of. |
823 | | /// - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire. |
824 | | /// - Returns: An `EventLoopFuture` containing the directory entries. |
825 | 0 | public func listDirectory(path: String, eventLoop: EventLoop) -> EventLoopFuture<[NIODirectoryEntry]> { |
826 | 0 | self.threadPool.runIfActive(eventLoop: eventLoop) { |
827 | 0 | let dir = try Posix.opendir(pathname: path) |
828 | 0 | var entries: [NIODirectoryEntry] = [] |
829 | 0 | do { |
830 | 0 | while let entry = try Posix.readdir(dir: dir) { |
831 | 0 | let name = withUnsafeBytes(of: entry.pointee.d_name) { pointer -> String in |
832 | 0 | let ptr = pointer.baseAddress!.assumingMemoryBound(to: CChar.self) |
833 | 0 | return String(cString: ptr) |
834 | 0 | } |
835 | | #if os(OpenBSD) |
836 | | let ino = entry.pointee.d_fileno |
837 | | #else |
838 | 0 | let ino = entry.pointee.d_ino |
839 | | #endif |
840 | 0 | entries.append( |
841 | 0 | NIODirectoryEntry(ino: UInt64(ino), type: entry.pointee.d_type, name: name) |
842 | 0 | ) |
843 | 0 | } |
844 | 0 | try? Posix.closedir(dir: dir) |
845 | 0 | } catch { |
846 | 0 | try? Posix.closedir(dir: dir) |
847 | 0 | throw error |
848 | 0 | } |
849 | 0 | return entries |
850 | 0 | } |
851 | 0 | } |
852 | | |
853 | | /// Renames the file at `path` to `newName` on a private thread pool which is separate from any `EventLoop` thread. |
854 | | /// |
855 | | /// - Parameters: |
856 | | /// - path: The path of the file to be renamed. |
857 | | /// - newName: New file name. |
858 | | /// - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire. |
859 | | /// - Returns: An `EventLoopFuture` which is fulfilled if the rename was successful or fails on error. |
860 | 0 | public func rename(path: String, newName: String, eventLoop: EventLoop) -> EventLoopFuture<Void> { |
861 | 0 | self.threadPool.runIfActive(eventLoop: eventLoop) { |
862 | 0 | try Posix.rename(pathname: path, newName: newName) |
863 | 0 | } |
864 | 0 | } |
865 | | |
866 | | /// Removes the file at `path` on a private thread pool which is separate from any `EventLoop` thread. |
867 | | /// |
868 | | /// - Parameters: |
869 | | /// - path: The path of the file to be removed. |
870 | | /// - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire. |
871 | | /// - Returns: An `EventLoopFuture` which is fulfilled if the remove was successful or fails on error. |
872 | 0 | public func remove(path: String, eventLoop: EventLoop) -> EventLoopFuture<Void> { |
873 | 0 | self.threadPool.runIfActive(eventLoop: eventLoop) { |
874 | 0 | try Posix.remove(pathname: path) |
875 | 0 | } |
876 | 0 | } |
877 | | #endif |
878 | | } |
879 | | |
880 | | #if !os(Windows) |
881 | | /// A `NIODirectoryEntry` represents a single directory entry. |
882 | | public struct NIODirectoryEntry: Hashable, Sendable { |
883 | | // File number of entry |
884 | | public var ino: UInt64 |
885 | | // File type |
886 | | public var type: UInt8 |
887 | | // File name |
888 | | public var name: String |
889 | | |
890 | 0 | public init(ino: UInt64, type: UInt8, name: String) { |
891 | 0 | self.ino = ino |
892 | 0 | self.type = type |
893 | 0 | self.name = name |
894 | 0 | } |
895 | | } |
896 | | #endif |
897 | | |
898 | | extension NonBlockingFileIO { |
899 | | /// Read a `FileRegion` in ``NonBlockingFileIO``'s private thread pool. |
900 | | /// |
901 | | /// The returned `ByteBuffer` will not have less than the minimum of `fileRegion.readableBytes` and `UInt32.max` unless we hit |
902 | | /// end-of-file in which case the `ByteBuffer` will contain the bytes available to read. |
903 | | /// |
904 | | /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the |
905 | | /// same `FileRegion` in multiple threads. |
906 | | /// |
907 | | /// - Note: Only use this function for small enough `FileRegion`s as it will need to allocate enough memory to hold `fileRegion.readableBytes` bytes. |
908 | | /// - Note: In most cases you should prefer one of the `readChunked` functions. |
909 | | /// |
910 | | /// - Parameters: |
911 | | /// - fileRegion: The file region to read. |
912 | | /// - allocator: A `ByteBufferAllocator` used to allocate space for the returned `ByteBuffer`. |
913 | | /// - Returns: ByteBuffer. |
914 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
915 | 0 | public func read(fileRegion: FileRegion, allocator: ByteBufferAllocator) async throws -> ByteBuffer { |
916 | 0 | let readableBytes = fileRegion.readableBytes |
917 | 0 | return try await self.read( |
918 | 0 | fileHandle: fileRegion.fileHandle, |
919 | 0 | fromOffset: Int64(fileRegion.readerIndex), |
920 | 0 | byteCount: readableBytes, |
921 | 0 | allocator: allocator |
922 | 0 | ) |
923 | 0 | } |
924 | | |
925 | | /// Read `byteCount` bytes from `fileHandle` in ``NonBlockingFileIO``'s private thread pool. |
926 | | /// |
927 | | /// The returned `ByteBuffer` will not have less than `byteCount` bytes unless we hit end-of-file in which |
928 | | /// case the `ByteBuffer` will contain the bytes available to read. |
929 | | /// |
930 | | /// - Note: Only use this function for small enough `byteCount`s as it will need to allocate enough memory to hold `byteCount` bytes. |
931 | | /// - Note: ``read(fileRegion:allocator:eventLoop:)`` should be preferred as it uses `FileRegion` object instead of |
932 | | /// raw `NIOFileHandle`s. In case you do want to use raw `NIOFileHandle`s, |
933 | | /// please consider using ``read(fileHandle:fromOffset:byteCount:allocator:eventLoop:)`` |
934 | | /// because it doesn't use the file descriptor's seek pointer (which may be shared with other file |
935 | | /// descriptors and even across processes.) |
936 | | /// |
937 | | /// - Parameters: |
938 | | /// - fileHandle: The `NIOFileHandle` to read. |
939 | | /// - byteCount: The number of bytes to read from `fileHandle`. |
940 | | /// - allocator: A `ByteBufferAllocator` used to allocate space for the returned `ByteBuffer`. |
941 | | /// - Returns: ByteBuffer. |
942 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
943 | | public func read( |
944 | | fileHandle: NIOFileHandle, |
945 | | byteCount: Int, |
946 | | allocator: ByteBufferAllocator |
947 | 0 | ) async throws -> ByteBuffer { |
948 | 0 | try await self.read0( |
949 | 0 | fileHandle: fileHandle, |
950 | 0 | fromOffset: nil, |
951 | 0 | byteCount: byteCount, |
952 | 0 | allocator: allocator |
953 | 0 | ) |
954 | 0 | } |
955 | | |
956 | | /// Read `byteCount` bytes starting at `fileOffset` from `fileHandle` in ``NonBlockingFileIO``'s private thread pool |
957 | | ///. |
958 | | /// |
959 | | /// The returned `ByteBuffer` will not have less than `byteCount` bytes unless we hit end-of-file in which |
960 | | /// case the `ByteBuffer` will contain the bytes available to read. |
961 | | /// |
962 | | /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the |
963 | | /// same `fileHandle` in multiple threads. |
964 | | /// |
965 | | /// - Note: Only use this function for small enough `byteCount`s as it will need to allocate enough memory to hold `byteCount` bytes. |
966 | | /// - Note: ``read(fileRegion:allocator:eventLoop:)`` should be preferred as it uses `FileRegion` object instead of raw `NIOFileHandle`s. |
967 | | /// |
968 | | /// - Parameters: |
969 | | /// - fileHandle: The `NIOFileHandle` to read. |
970 | | /// - fileOffset: The offset to read from. |
971 | | /// - byteCount: The number of bytes to read from `fileHandle`. |
972 | | /// - allocator: A `ByteBufferAllocator` used to allocate space for the returned `ByteBuffer`. |
973 | | /// - Returns: ByteBuffer. |
974 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
975 | | public func read( |
976 | | fileHandle: NIOFileHandle, |
977 | | fromOffset fileOffset: Int64, |
978 | | byteCount: Int, |
979 | | allocator: ByteBufferAllocator |
980 | 0 | ) async throws -> ByteBuffer { |
981 | 0 | try await self.read0( |
982 | 0 | fileHandle: fileHandle, |
983 | 0 | fromOffset: fileOffset, |
984 | 0 | byteCount: byteCount, |
985 | 0 | allocator: allocator |
986 | 0 | ) |
987 | 0 | } |
988 | | |
989 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
990 | | private func read0( |
991 | | fileHandle: NIOFileHandle, |
992 | | fromOffset: Int64?, // > 2 GB offset is reasonable on 32-bit systems |
993 | | byteCount rawByteCount: Int, |
994 | | allocator: ByteBufferAllocator |
995 | 0 | ) async throws -> ByteBuffer { |
996 | 0 | guard rawByteCount > 0 else { |
997 | 0 | return allocator.buffer(capacity: 0) |
998 | 0 | } |
999 | 0 | let byteCount = rawByteCount < Int32.max ? rawByteCount : size_t(Int32.max) |
1000 | 0 |
|
1001 | 0 | return try await self.threadPool.runIfActive { () -> ByteBuffer in |
1002 | 0 | try self.readSync( |
1003 | 0 | fileHandle: fileHandle, |
1004 | 0 | fromOffset: fromOffset, |
1005 | 0 | byteCount: byteCount, |
1006 | 0 | allocator: allocator |
1007 | 0 | ) |
1008 | 0 | } |
1009 | 0 | } |
1010 | | |
1011 | | /// Changes the file size of `fileHandle` to `size`. |
1012 | | /// |
1013 | | /// If `size` is smaller than the current file size, the remaining bytes will be truncated and are lost. If `size` |
1014 | | /// is larger than the current file size, the gap will be filled with zero bytes. |
1015 | | /// |
1016 | | /// - Parameters: |
1017 | | /// - fileHandle: The `NIOFileHandle` to write to. |
1018 | | /// - size: The new file size in bytes to write. |
1019 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1020 | | public func changeFileSize( |
1021 | | fileHandle: NIOFileHandle, |
1022 | | size: Int64 |
1023 | 0 | ) async throws { |
1024 | 0 | try await self.threadPool.runIfActive { |
1025 | 0 | try fileHandle.withUnsafeFileDescriptor { descriptor -> Void in |
1026 | 0 | try Posix.ftruncate(descriptor: descriptor, size: off_t(size)) |
1027 | 0 | } |
1028 | 0 | } |
1029 | 0 | } |
1030 | | |
1031 | | /// Returns the length of the file associated with `fileHandle`. |
1032 | | /// |
1033 | | /// - Parameters: |
1034 | | /// - fileHandle: The `NIOFileHandle` to read from. |
1035 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1036 | 0 | public func readFileSize(fileHandle: NIOFileHandle) async throws -> Int64 { |
1037 | 0 | try await self.threadPool.runIfActive { |
1038 | 0 | try fileHandle.withUnsafeFileDescriptor { descriptor in |
1039 | 0 | let curr = try Posix.lseek(descriptor: descriptor, offset: 0, whence: SEEK_CUR) |
1040 | 0 | let eof = try Posix.lseek(descriptor: descriptor, offset: 0, whence: SEEK_END) |
1041 | 0 | try Posix.lseek(descriptor: descriptor, offset: curr, whence: SEEK_SET) |
1042 | 0 | return Int64(eof) |
1043 | 0 | } |
1044 | 0 | } |
1045 | 0 | } |
1046 | | |
1047 | | /// Write `buffer` to `fileHandle` in ``NonBlockingFileIO``'s private thread pool. |
1048 | | /// |
1049 | | /// - Parameters: |
1050 | | /// - fileHandle: The `NIOFileHandle` to write to. |
1051 | | /// - buffer: The `ByteBuffer` to write. |
1052 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1053 | | public func write( |
1054 | | fileHandle: NIOFileHandle, |
1055 | | buffer: ByteBuffer |
1056 | 0 | ) async throws { |
1057 | 0 | try await self.write0(fileHandle: fileHandle, toOffset: nil, buffer: buffer) |
1058 | 0 | } |
1059 | | |
1060 | | /// Write `buffer` starting from `toOffset` to `fileHandle` in ``NonBlockingFileIO``'s private thread pool. |
1061 | | /// |
1062 | | /// - Parameters: |
1063 | | /// - fileHandle: The `NIOFileHandle` to write to. |
1064 | | /// - toOffset: The file offset to write to. |
1065 | | /// - buffer: The `ByteBuffer` to write. |
1066 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1067 | | public func write( |
1068 | | fileHandle: NIOFileHandle, |
1069 | | toOffset: Int64, |
1070 | | buffer: ByteBuffer |
1071 | 0 | ) async throws { |
1072 | 0 | try await self.write0(fileHandle: fileHandle, toOffset: toOffset, buffer: buffer) |
1073 | 0 | } |
1074 | | |
1075 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1076 | | private func write0( |
1077 | | fileHandle: NIOFileHandle, |
1078 | | toOffset: Int64?, |
1079 | | buffer: ByteBuffer |
1080 | 0 | ) async throws { |
1081 | 0 | let byteCount = buffer.readableBytes |
1082 | 0 |
|
1083 | 0 | guard byteCount > 0 else { |
1084 | 0 | return |
1085 | 0 | } |
1086 | 0 |
|
1087 | 0 | return try await self.threadPool.runIfActive { |
1088 | 0 | try self.writeSync(fileHandle: fileHandle, byteCount: byteCount, toOffset: toOffset, buffer: buffer) |
1089 | 0 | } |
1090 | 0 | } |
1091 | | |
1092 | | /// Open file at `path` and query its size on a private thread pool, run an operation given |
1093 | | /// the resulting file region and then close the file handle. |
1094 | | /// |
1095 | | /// The will return the result of the operation. |
1096 | | /// |
1097 | | /// - Note: This function opens a file and queries it size which are both blocking operations |
1098 | | /// |
1099 | | /// - Parameters: |
1100 | | /// - path: The path of the file to be opened for reading. |
1101 | | /// - body: operation to run with file handle and region |
1102 | | /// - Returns: return value of operation |
1103 | | @available( |
1104 | | *, |
1105 | | deprecated, |
1106 | | message: |
1107 | | "Avoid using NonBlockingFileIO. The API is difficult to hold correctly, use NIOFileSystem as a replacement API." |
1108 | | ) |
1109 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1110 | | public func withFileRegion<Result>( |
1111 | | path: String, |
1112 | | _ body: (_ fileRegion: FileRegion) async throws -> Result |
1113 | 0 | ) async throws -> Result { |
1114 | 0 | try await self.withFileRegion(_deprecatedPath: path, body) |
1115 | 0 | } |
1116 | | |
1117 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1118 | | public func withFileRegion<Result>( |
1119 | | _deprecatedPath path: String, |
1120 | | _ body: (_ fileRegion: FileRegion) async throws -> Result |
1121 | 0 | ) async throws -> Result { |
1122 | 0 | let fileRegion = try await self.threadPool.runIfActive { |
1123 | 0 | let fh = try NIOFileHandle(_deprecatedPath: path) |
1124 | 0 | do { |
1125 | 0 | return try FileRegion(fileHandle: fh) |
1126 | 0 | } catch { |
1127 | 0 | _ = try? fh.close() |
1128 | 0 | throw error |
1129 | 0 | } |
1130 | 0 | } |
1131 | 0 | let result: Result |
1132 | 0 | do { |
1133 | 0 | result = try await body(fileRegion) |
1134 | 0 | } catch { |
1135 | 0 | try fileRegion.fileHandle.close() |
1136 | 0 | throw error |
1137 | 0 | } |
1138 | 0 | try fileRegion.fileHandle.close() |
1139 | 0 | return result |
1140 | 0 | } |
1141 | | |
1142 | | /// Open file at `path` on a private thread pool, run an operation given the file handle and then close the file handle. |
1143 | | /// |
1144 | | /// This function will return the result of the operation. |
1145 | | /// |
1146 | | /// - Parameters: |
1147 | | /// - path: The path of the file to be opened for writing. |
1148 | | /// - mode: File access mode. |
1149 | | /// - flags: Additional POSIX flags. |
1150 | | /// - body: operation to run with the file handle |
1151 | | /// - Returns: return value of operation |
1152 | | @available( |
1153 | | *, |
1154 | | deprecated, |
1155 | | message: |
1156 | | "Avoid using NonBlockingFileIO. The API is difficult to hold correctly, use NIOFileSystem as a replacement API." |
1157 | | ) |
1158 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1159 | | public func withFileHandle<Result>( |
1160 | | path: String, |
1161 | | mode: NIOFileHandle.Mode, |
1162 | | flags: NIOFileHandle.Flags = .default, |
1163 | | _ body: (NIOFileHandle) async throws -> Result |
1164 | 0 | ) async throws -> Result { |
1165 | 0 | try await self.withFileHandle(_deprecatedPath: path, mode: mode, flags: flags, body) |
1166 | 0 | } |
1167 | | |
1168 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1169 | | public func withFileHandle<Result>( |
1170 | | _deprecatedPath path: String, |
1171 | | mode: NIOFileHandle.Mode, |
1172 | | flags: NIOFileHandle.Flags = .default, |
1173 | | _ body: (NIOFileHandle) async throws -> Result |
1174 | 0 | ) async throws -> Result { |
1175 | 0 | let fileHandle = try await self.threadPool.runIfActive { |
1176 | 0 | try NIOFileHandle(_deprecatedPath: path, mode: mode, flags: flags) |
1177 | 0 | } |
1178 | 0 | let result: Result |
1179 | 0 | do { |
1180 | 0 | result = try await body(fileHandle) |
1181 | 0 | } catch { |
1182 | 0 | try fileHandle.close() |
1183 | 0 | throw error |
1184 | 0 | } |
1185 | 0 | try fileHandle.close() |
1186 | 0 | return result |
1187 | 0 | } |
1188 | | |
1189 | | #if !os(Windows) |
1190 | | |
1191 | | /// Returns information about a file at `path` on a private thread pool. |
1192 | | /// |
1193 | | /// - Note: If `path` is a symlink, information about the link, not the file it points to. |
1194 | | /// |
1195 | | /// - Parameters: |
1196 | | /// - path: The path of the file to get information about. |
1197 | | /// - Returns: file information. |
1198 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1199 | 0 | public func lstat(path: String) async throws -> stat { |
1200 | 0 | try await self.threadPool.runIfActive { |
1201 | 0 | var s = stat() |
1202 | 0 | try Posix.lstat(pathname: path, outStat: &s) |
1203 | 0 | return s |
1204 | 0 | } |
1205 | 0 | } |
1206 | | |
1207 | | /// Creates a symbolic link to a `destination` file at `path` on a private thread pool. |
1208 | | /// |
1209 | | /// - Parameters: |
1210 | | /// - path: The path of the link. |
1211 | | /// - destination: Target path where this link will point to. |
1212 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1213 | 0 | public func symlink(path: String, to destination: String) async throws { |
1214 | 0 | try await self.threadPool.runIfActive { |
1215 | 0 | try Posix.symlink(pathname: path, destination: destination) |
1216 | 0 | } |
1217 | 0 | } |
1218 | | |
1219 | | /// Returns target of the symbolic link at `path` on a private thread pool. |
1220 | | /// |
1221 | | /// - Parameters: |
1222 | | /// - path: The path of the link to read. |
1223 | | /// - Returns: link target. |
1224 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1225 | 0 | public func readlink(path: String) async throws -> String { |
1226 | 0 | try await self.threadPool.runIfActive { |
1227 | 0 | let maxLength = Int(PATH_MAX) |
1228 | 0 | let pointer = UnsafeMutableBufferPointer<CChar>.allocate(capacity: maxLength) |
1229 | 0 | defer { |
1230 | 0 | pointer.deallocate() |
1231 | 0 | } |
1232 | 0 | let length = try Posix.readlink(pathname: path, outPath: pointer.baseAddress!, outPathSize: maxLength) |
1233 | 0 | return String(decoding: UnsafeRawBufferPointer(pointer).prefix(length), as: UTF8.self) |
1234 | 0 | } |
1235 | 0 | } |
1236 | | |
1237 | | /// Removes symbolic link at `path` on a private thread pool which is separate from any `EventLoop` thread. |
1238 | | /// |
1239 | | /// - Parameters: |
1240 | | /// - path: The path of the link to remove. |
1241 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1242 | 0 | public func unlink(path: String) async throws { |
1243 | 0 | try await self.threadPool.runIfActive { |
1244 | 0 | try Posix.unlink(pathname: path) |
1245 | 0 | } |
1246 | 0 | } |
1247 | | |
1248 | | /// Creates directory at `path` on a private thread pool. |
1249 | | /// |
1250 | | /// - Parameters: |
1251 | | /// - path: The path of the directory to be created. |
1252 | | /// - createIntermediates: Whether intermediate directories should be created. |
1253 | | /// - mode: POSIX file mode. |
1254 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1255 | | public func createDirectory( |
1256 | | path: String, |
1257 | | withIntermediateDirectories createIntermediates: Bool = false, |
1258 | | mode: NIOPOSIXFileMode |
1259 | 0 | ) async throws { |
1260 | 0 | try await self.threadPool.runIfActive { |
1261 | 0 | if createIntermediates { |
1262 | | #if canImport(Darwin) |
1263 | | try Posix.mkpath_np(pathname: path, mode: mode) |
1264 | | #else |
1265 | 0 | try self.createDirectory0(path, mode: mode) |
1266 | | #endif |
1267 | 0 | } else { |
1268 | 0 | try Posix.mkdir(pathname: path, mode: mode) |
1269 | 0 | } |
1270 | 0 | } |
1271 | 0 | } |
1272 | | |
1273 | | /// List contents of the directory at `path` on a private thread pool. |
1274 | | /// |
1275 | | /// - Parameters: |
1276 | | /// - path: The path of the directory to list the content of. |
1277 | | /// - Returns: The directory entries. |
1278 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1279 | 0 | public func listDirectory(path: String) async throws -> [NIODirectoryEntry] { |
1280 | 0 | try await self.threadPool.runIfActive { |
1281 | 0 | let dir = try Posix.opendir(pathname: path) |
1282 | 0 | var entries: [NIODirectoryEntry] = [] |
1283 | 0 | do { |
1284 | 0 | while let entry = try Posix.readdir(dir: dir) { |
1285 | 0 | let name = withUnsafeBytes(of: entry.pointee.d_name) { pointer -> String in |
1286 | 0 | let ptr = pointer.baseAddress!.assumingMemoryBound(to: CChar.self) |
1287 | 0 | return String(cString: ptr) |
1288 | 0 | } |
1289 | | #if os(OpenBSD) |
1290 | | let ino = entry.pointee.d_fileno |
1291 | | #else |
1292 | 0 | let ino = entry.pointee.d_ino |
1293 | | #endif |
1294 | 0 | entries.append( |
1295 | 0 | NIODirectoryEntry(ino: UInt64(ino), type: entry.pointee.d_type, name: name) |
1296 | 0 | ) |
1297 | 0 | } |
1298 | 0 | try? Posix.closedir(dir: dir) |
1299 | 0 | } catch { |
1300 | 0 | try? Posix.closedir(dir: dir) |
1301 | 0 | throw error |
1302 | 0 | } |
1303 | 0 | return entries |
1304 | 0 | } |
1305 | 0 | } |
1306 | | |
1307 | | /// Renames the file at `path` to `newName` on a private thread pool. |
1308 | | /// |
1309 | | /// - Parameters: |
1310 | | /// - path: The path of the file to be renamed. |
1311 | | /// - newName: New file name. |
1312 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1313 | 0 | public func rename(path: String, newName: String) async throws { |
1314 | 0 | try await self.threadPool.runIfActive { |
1315 | 0 | try Posix.rename(pathname: path, newName: newName) |
1316 | 0 | } |
1317 | 0 | } |
1318 | | |
1319 | | /// Removes the file at `path` on a private thread pool. |
1320 | | /// |
1321 | | /// - Parameters: |
1322 | | /// - path: The path of the file to be removed. |
1323 | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
1324 | 0 | public func remove(path: String) async throws { |
1325 | 0 | try await self.threadPool.runIfActive { |
1326 | 0 | try Posix.remove(pathname: path) |
1327 | 0 | } |
1328 | 0 | } |
1329 | | #endif |
1330 | | } |
1331 | | #endif // !os(WASI) |