Skip to content

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 42 additions & 14 deletions Foundation.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

754 changes: 754 additions & 0 deletions Foundation/URLSession/NativeProtocol.swift

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Foundation/URLSession/URLSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ open class URLSession : NSObject {
fileprivate static let registerProtocols: () = {
// TODO: We register all the native protocols here.
let _ = URLProtocol.registerClass(_HTTPURLProtocol.self)
let _ = URLProtocol.registerClass(_FTPURLProtocol.self)
}()

/*
Expand Down
2 changes: 1 addition & 1 deletion Foundation/URLSession/URLSessionConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ open class URLSessionConfiguration : NSObject, NSCopying {
self.urlCredentialStorage = nil
self.urlCache = nil
self.shouldUseExtendedBackgroundIdleMode = false
self.protocolClasses = [_HTTPURLProtocol.self]
self.protocolClasses = [_HTTPURLProtocol.self, _FTPURLProtocol.self]
super.init()
}

Expand Down
148 changes: 148 additions & 0 deletions Foundation/URLSession/ftp/FTPURLProtocol.swift
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

// 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 }
Copy link
Contributor

Choose a reason for hiding this comment

The 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.") }
Copy link
Contributor

Choose a reason for hiding this comment

The 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()
Copy link
Contributor

Choose a reason for hiding this comment

The 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.") }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As previous - check files for else { fatalError and indent lines as appropriate

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
Copy link
Contributor

Choose a reason for hiding this comment

The 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() }
Copy link
Contributor

Choose a reason for hiding this comment

The 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.") }
Copy link
Contributor

Choose a reason for hiding this comment

The 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
}
}

}





42 changes: 22 additions & 20 deletions Foundation/URLSession/http/HTTPBodySource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit - cant -> can't

}
self.channel = channel
self.channel.setLimit(highWater: CFURLSessionMaxWriteSize)
}

fileprivate enum _Chunk {
/// Nothing has been read, yet
case empty
Expand All @@ -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.
///
Expand Down Expand Up @@ -182,7 +183,7 @@ fileprivate extension _HTTPBodyFileSource {
}
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The 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:
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -242,3 +243,4 @@ extension _HTTPBodyFileSource : _HTTPBodySource {
}
}
}

Loading