-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Initial implementation of FTP #1122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
// This source file is part of the Swift.org open source project | ||
// | ||
// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors | ||
// Licensed under Apache License v2.0 with Runtime Library Exception | ||
// | ||
// See http://swift.org/LICENSE.txt for license information | ||
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
// | ||
|
||
import CoreFoundation | ||
import Dispatch | ||
|
||
internal class _FTPURLProtocol: _NativeProtocol { | ||
|
||
public required init(task: URLSessionTask, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { | ||
super.init(task: task, cachedResponse: cachedResponse, client: client) | ||
} | ||
|
||
public required init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { | ||
super.init(request: request, cachedResponse: cachedResponse, client: client) | ||
} | ||
|
||
override class func canInit(with request: URLRequest) -> Bool { | ||
guard request.url?.scheme == "ftp" || request.url?.scheme == "ftps" || request.url?.scheme == "sftp" else { return false } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This look like it would be better handled with a switch than a guard. |
||
return true | ||
} | ||
|
||
override class func canonicalRequest(for request: URLRequest) -> URLRequest { | ||
return request | ||
} | ||
|
||
override func startLoading() { | ||
resume() | ||
} | ||
|
||
override func stopLoading() { | ||
if task?.state == .suspended { | ||
suspend() | ||
} else { | ||
self.internalState = .transferFailed | ||
guard let error = self.task?.error else { fatalError() } | ||
completeTask(withError: error) | ||
} | ||
} | ||
|
||
override func didReceive(headerData data: Data, contentLength: Int64) -> _EasyHandle._Action { | ||
guard case .transferInProgress(let ts) = internalState else { fatalError("Received body data, but no transfer in progress.") } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as previous |
||
guard let task = task else { fatalError("Received header data but no task available.") } | ||
task.countOfBytesExpectedToReceive = contentLength > 0 ? contentLength : NSURLSessionTransferSizeUnknown | ||
do { | ||
let newTS = try ts.byAppendingFTP(headerLine: data, expectedContentLength: contentLength) | ||
internalState = .transferInProgress(newTS) | ||
let didCompleteHeader = !ts.isHeaderComplete && newTS.isHeaderComplete | ||
if didCompleteHeader { | ||
// The header is now complete, but wasn't before. | ||
didReceiveResponse() | ||
} | ||
return .proceed | ||
} catch { | ||
return .abort | ||
} | ||
} | ||
|
||
override func seekInputStream(to position: UInt64) throws { | ||
// NSUnimplemented() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why commented out? |
||
} | ||
|
||
override func updateProgressMeter(with propgress: _EasyHandle._Progress) { | ||
//NSUnimplemented() | ||
} | ||
|
||
override func configureEasyHandle(for request: URLRequest) { | ||
easyHandle.set(verboseModeOn: enableLibcurlDebugOutput) | ||
easyHandle.set(debugOutputOn: enableLibcurlDebugOutput, task: task!) | ||
easyHandle.set(skipAllSignalHandling: true) | ||
guard let url = request.url else { fatalError("No URL in request.") } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As previous - check files for |
||
easyHandle.set(url: url) | ||
easyHandle.set(preferredReceiveBufferSize: Int.max) | ||
do { | ||
switch (task?.body, try task?.body.getBodyLength()) { | ||
case (.some(URLSessionTask._Body.none), _): | ||
set(requestBodyLength: .noBody) | ||
case (_, .some(let length)): | ||
set(requestBodyLength: .length(length)) | ||
task!.countOfBytesExpectedToSend = Int64(length) | ||
case (_, .none): | ||
set(requestBodyLength: .unknown) | ||
} | ||
} catch let e { | ||
// Fail the request here. | ||
// TODO: We have multiple options: | ||
// NSURLErrorNoPermissionsToReadFile | ||
// NSURLErrorFileDoesNotExist | ||
self.internalState = .transferFailed | ||
failWith(errorCode: errorCode(fileSystemError: e), request: request) | ||
return | ||
} | ||
let timeoutHandler = DispatchWorkItem { [weak self] in | ||
guard let _ = self?.task else { fatalError("Timeout on a task that doesn't exist") } //this guard must always pass | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as previous |
||
self?.internalState = .transferFailed | ||
let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil)) | ||
self?.completeTask(withError: urlError) | ||
self?.client?.urlProtocol(self!, didFailWithError: urlError) | ||
} | ||
guard let task = self.task else { fatalError() } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as previous |
||
easyHandle.timeoutTimer = _TimeoutSource(queue: task.workQueue, milliseconds: Int(request.timeoutInterval) * 1000, handler: timeoutHandler) | ||
|
||
easyHandle.set(automaticBodyDecompression: true) | ||
} | ||
|
||
} | ||
|
||
|
||
/// Response processing | ||
internal extension _FTPURLProtocol { | ||
/// Whenever we receive a response (i.e. a complete header) from libcurl, | ||
/// this method gets called. | ||
func didReceiveResponse() { | ||
guard let _ = task as? URLSessionDataTask else { return } | ||
guard case .transferInProgress(let ts) = self.internalState else { fatalError("Transfer not in progress.") } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as previous |
||
guard let response = ts.response else { fatalError("Header complete, but not URL response.") } | ||
guard let session = task?.session as? URLSession else { fatalError() } | ||
switch session.behaviour(for: self.task!) { | ||
case .noDelegate: | ||
break | ||
case .taskDelegate(_): | ||
//TODO: There's a problem with libcurl / with how we're using it. | ||
// We're currently unable to pause the transfer / the easy handle: | ||
// https://curl.haxx.se/mail/lib-2016-03/0222.html | ||
// | ||
// For now, we'll notify the delegate, but won't pause the transfer, | ||
// and we'll disregard the completion handler: | ||
|
||
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) | ||
|
||
case .dataCompletionHandler: | ||
break | ||
case .downloadCompletionHandler: | ||
break | ||
} | ||
} | ||
|
||
} | ||
|
||
|
||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,15 +40,15 @@ internal func splitData(dispatchData data: DispatchData, atPosition position: In | |
} | ||
|
||
/// A (non-blocking) source for HTTP body data. | ||
internal protocol _HTTPBodySource: class { | ||
internal protocol _BodySource: class { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As this is no longer specific to HTTP, should it be moved out of the http directory? |
||
/// Get the next chunck of data. | ||
/// | ||
/// - Returns: `.data` until the source is exhausted, at which point it will | ||
/// return `.done`. Since this is non-blocking, it will return `.retryLater` | ||
/// if no data is available at this point, but will be available later. | ||
func getNextChunk(withLength length: Int) -> _HTTPBodySourceDataChunk | ||
func getNextChunk(withLength length: Int) -> _BodySourceDataChunk | ||
} | ||
internal enum _HTTPBodySourceDataChunk { | ||
internal enum _BodySourceDataChunk { | ||
case data(DispatchData) | ||
/// The source is depleted. | ||
case done | ||
|
@@ -58,25 +58,25 @@ internal enum _HTTPBodySourceDataChunk { | |
} | ||
|
||
/// A HTTP body data source backed by `dispatch_data_t`. | ||
internal final class _HTTPBodyDataSource { | ||
var data: DispatchData! | ||
internal final class _BodyDataSource { | ||
var data: DispatchData! | ||
init(data: DispatchData) { | ||
self.data = data | ||
} | ||
} | ||
|
||
extension _HTTPBodyDataSource : _HTTPBodySource { | ||
extension _BodyDataSource : _BodySource { | ||
enum _Error : Error { | ||
case unableToRewindData | ||
} | ||
|
||
func getNextChunk(withLength length: Int) -> _HTTPBodySourceDataChunk { | ||
func getNextChunk(withLength length: Int) -> _BodySourceDataChunk { | ||
let remaining = data.count | ||
if remaining == 0 { | ||
return .done | ||
} else if remaining <= length { | ||
let r: DispatchData! = data | ||
data = DispatchData.empty | ||
data = DispatchData.empty | ||
return .data(r) | ||
} else { | ||
let (chunk, remainder) = splitData(dispatchData: data, atPosition: length) | ||
|
@@ -98,10 +98,10 @@ extension _HTTPBodyDataSource : _HTTPBodySource { | |
/// - Note: Calls to `getNextChunk(withLength:)` and callbacks from libdispatch | ||
/// should all happen on the same (serial) queue, and hence this code doesn't | ||
/// have to be thread safe. | ||
internal final class _HTTPBodyFileSource { | ||
internal final class _BodyFileSource { | ||
fileprivate let fileURL: URL | ||
fileprivate let channel: DispatchIO | ||
fileprivate let workQueue: DispatchQueue | ||
fileprivate let channel: DispatchIO | ||
fileprivate let workQueue: DispatchQueue | ||
fileprivate let dataAvailableHandler: () -> Void | ||
fileprivate var hasActiveReadHandler = false | ||
fileprivate var availableChunk: _Chunk = .empty | ||
|
@@ -127,13 +127,14 @@ internal final class _HTTPBodyFileSource { | |
} | ||
guard let channel = DispatchIO(type: .stream, path: fileSystemRepresentation, | ||
oflag: O_RDONLY, mode: 0, queue: workQueue, | ||
cleanupHandler: {_ in }) else { | ||
fatalError("Cant create DispatchIO channel") | ||
cleanupHandler: {_ in }) | ||
else { | ||
fatalError("Cant create DispatchIO channel") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor nit - |
||
} | ||
self.channel = channel | ||
self.channel.setLimit(highWater: CFURLSessionMaxWriteSize) | ||
} | ||
|
||
fileprivate enum _Chunk { | ||
/// Nothing has been read, yet | ||
case empty | ||
|
@@ -146,7 +147,7 @@ internal final class _HTTPBodyFileSource { | |
} | ||
} | ||
|
||
fileprivate extension _HTTPBodyFileSource { | ||
fileprivate extension _BodyFileSource { | ||
fileprivate var desiredBufferLength: Int { return 3 * CFURLSessionMaxWriteSize } | ||
/// Enqueue a dispatch I/O read to fill the buffer. | ||
/// | ||
|
@@ -182,7 +183,7 @@ fileprivate extension _HTTPBodyFileSource { | |
} | ||
} | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we get rid of these no-change whitespace lines? |
||
fileprivate func append(data: DispatchData, endOfFile: Bool) { | ||
switch availableChunk { | ||
case .empty: | ||
|
@@ -196,7 +197,7 @@ fileprivate extension _HTTPBodyFileSource { | |
fatalError("Trying to append data, but end-of-file was already detected.") | ||
} | ||
} | ||
|
||
fileprivate var availableByteCount: Int { | ||
switch availableChunk { | ||
case .empty: return 0 | ||
|
@@ -208,8 +209,8 @@ fileprivate extension _HTTPBodyFileSource { | |
} | ||
} | ||
|
||
extension _HTTPBodyFileSource : _HTTPBodySource { | ||
func getNextChunk(withLength length: Int) -> _HTTPBodySourceDataChunk { | ||
extension _BodyFileSource : _BodySource { | ||
func getNextChunk(withLength length: Int) -> _BodySourceDataChunk { | ||
switch availableChunk { | ||
case .empty: | ||
readNextChunk() | ||
|
@@ -242,3 +243,4 @@ extension _HTTPBodyFileSource : _HTTPBodySource { | |
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done