Get top of remote file in Swift - swift

There is a line in multiple files via the Internet. I want to avoid download the entire file. Each file may be long or short. The required line is usually about line 15 - always different, but always within the first 500 bytes.
Is there a way I can get just the top part of a remote file?
I can then use a regex pattern to find the required line.
Although I know how to download a file in a temp. location and copy it to a proper location, I think that process is too much and wasteful.
This is an example:
class func load(url: URL, to localUrl: URL, completion: #escaping () -> ()) {
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = try! URLRequest(url: url, method: .get)
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
// Success
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("Success: \(statusCode)")
}
do {
try FileManager.default.copyItem(at: tempLocalUrl, to: localUrl)
completion()
} catch (let writeError) {
print("error writing file \(localUrl) : \(writeError)")
}
} else {
print("Failure: %#", error?.localizedDescription);
}
}
task.resume()
}
PS. It would also be helpful if you could include a method to find a line beginning with, say "abc=xyz", where I want "xyz".

To only download partial content, you need a server that supports sending ranges. To test this, make a HEAD request and check if the server responds with an Accept-Range: bytes header. If so, you can request partial content by adding a header like Range: bytes=0-499 to your GET requests to only receive the initial 500 bytes.

Related

Combine - how to proceed to decode a local json file if online fetch failed?

I have an up-to-date json file hosted online and a local json file in my Xcode workspace. I would like to proceeed to decode a locally stored file if fetching failed: MyError.fetchError e.g. for no internet connection. This is the pipeline:
func fetchAndDecode<T: Decodable>(url: URL) -> AnyPublisher<T, MyError> {
fetchURL(url: url)
.decode(type: T.self, decoder: JSONDecoder())
.mapError { error in
if let error = error as? DecodingError {
return MyError.parsingError
} else {
return MyError.fetchError //here somehow proceed to parse local json file
}
}
.eraseToAnyPublisher()
}
How to achieve this ?
.mapError is the wrong operator because it considers only the Error branch.
fetchURL returns obviously Data, so before decoding the data you have to replace the fetch error with the local data.
Before the .decode... line insert
.replaceError(with: try! Data(contentsOf: Bundle.main.url(forResource: "local", withExtension: "json")!))
and delete the .mapError operator.
local.json represents the file name of the local file in the bundle.
I can propose an alternate but similar method to download the data and handle the error, using the async functions introduced for iOS 15.
Create a function that reads the data asynchronously and returns the data from the server if the connection worked, otherwise it will return the local JSON if a problem was found:
func getData(fromURL url: URL) async -> Data {
let request = URLRequest(url: url)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print("HTTP response: \(response.debugDescription)")
// Found an issue: return the local JSON
return localJSON
}
// If everything is OK, return the data from the server
return data
}
Decode the data returned:
// Use the code below in an asynchronous environment -
// either an async function or inside a Task { } closure
let data = await getData(fromURL: url)
do {
let decoded = try JSONDecoder().decode(T.self, from: data)
print("Decoded JSON: \(decoded)")
return decoded
} catch {
print("Error decoding JSON: \(error), \(error.localizedDescription)")
}

Swift program never enters CompletionHandler for a dataTask

I am in the process of implementing a REST API with Swift. Of course, part of this API is using HTTP requests to retrieve and send data.
Full disclosure, I am inexperienced with Swift and am using this as a learning project to get my feet wet, so to speak. But it's turned into much more of a difficult project than I anticipated.
In implementing the first get method, I have (finally) gotten rid of all the compilation errors. However, when I call the function which utilizes the URLRequest, URLSession, dataTask, etc, it is never entered.
Upon debugging the program, I can watch the program execution reach the CompletionHandler, and skip over it right to "task.resume()."
A similar construction works in a Swift Playground, but does not work in the actual project proper.
So far I have tried a few things, namely making the function access a class instance variable, in hopes that that would force it to execute. But it does not.
I think the issue may be dealing with synchronicity, and perhaps I need to use a Semaphore, but I want to make sure I'm not missing anything obvious first.
import Foundation
/**
A class to wrap all GET and POST requests, to avoid the necessity of repeatedly writing request code in each API method.
*/
class BasicRequest {
private var url: URL
private var header: [String: String]
private var responseType: String
private var jsonResponse: Any?
init(url: URL, header: [String: String], responseType: String) {
self.url = url
self.header = header
self.responseType = responseType
} //END INIT
public func requestJSON() -> Any {
// Create the URLRequest object, and fill the header with the header fields as provided.
var urlRequest = URLRequest(url: self.url)
for (value, key) in self.header {
urlRequest.addValue(value, forHTTPHeaderField: key)
}
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
print("Entered the completion handler")
if error != nil {
return
}
guard let httpResponse = response as? HTTPURLResponse, 200 == httpResponse.statusCode else {
print("HTTP Request unsuccessful")
return
}
guard let mime = response?.mimeType, mime == "application/json" else {
print("Not a JSON response")
return
}
do {
let json = try JSONSerialization.jsonObject(with: data!, options: [])
print(json)
self.jsonResponse = json
} catch {
print("Could not transform to JSON")
return
}
}
task.resume()
return "Function has returned"
} //END REQUESTJSON
}
The expected result would be returning a JSON object, however that does not seem to be the case.
With respect to error messages, I get none. The only log I get in the debugger is the boilerplate "process exited with code 0."
To be truthful, I'm at a loss with what is causing this not to work.
It appears you're writing this in a command-line app. In that case the program is terminating before the URLRequest completes.
I think the issue may be dealing with synchronicity, and perhaps I need to use a Semaphore, but I want to make sure I'm not missing anything obvious first.
Exactly.
The typical tool in Swift is DispatchGroup, which is just a higher-level kind of semaphore. Call dispatchGroup.enter() before starting the request, and all dispatchGroup.leave() at the end of the completion handler. In your calling code, include dispatchGroup.wait() to wait for it. (If that's not clear, I can add code for it, but there are also a lot of SO answers you can find that will demonstrate it.)

Partially downloading data in Swift

I'm trying to develop a download accelerator in Swift. It should get the file's size and divide it to n parts. Then it should download them at once by running multiple threads, and then merge the parts.
I read C# - Creating a Download Accelerator, unfortunately it doesn't help me.
I can do the multiple thread part easily by
DispatchQueue.main.async {
// The new thread
}
but the other part is harder. I usually download a file like this:
try Data(contentsOf: URL(string: assetsUrl!)!)
or I can do the thing that is explained in this answer
class Downloader {
class func load(url: URL, to localUrl: URL, completion: #escaping () -> ()) {
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = try! URLRequest(url: url, method: .get)
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
// Success
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("Success: \(statusCode)")
}
do {
try FileManager.default.copyItem(at: tempLocalUrl, to: localUrl)
completion()
} catch (let writeError) {
print("error writing file \(localUrl) : \(writeError)")
}
} else {
print("Failure: %#", error?.localizedDescription);
}
}
task.resume()
}
}
But this is not C - it's very simplistic and doesn't accept many arguments. How can I make it get "first 200_000 bytes" from the server?
First of all, the server needs to implement HTTP range requests. If it doesn't, and you don't control the server, then you will not be able to do this.
If the server supports HTTP range requests, then you need to specify the range with request headers, as explained here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
The essentials are that you first send a HEAD request to figure out whether the server supports HTTP range requests. This is determined by whether the response includes the Accept-Ranges header, with a non-zero value.
If the server supports HTTP range requests, then you can make a request for the resource, with the Range header set for example to a value of bytes=0-1023 (depends which format the Accept-Ranges header specified, in this case bytes)

REST API calls not working in swift

I'm following this tutorial for making a simple REST API call in swift: https://grokswift.com/simple-rest-with-swift/
The problem I'm running into is that the data task completion handler next gets executed. When I'm debugging it step by step, it just jumps over the completion handler block. Nothing is printed in the console, either.
I've searched for other methods of making REST API calls, but they are all very similar to this one and not working, either.
Here is my code:
let endpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
guard let url = URL(string: endpoint) else {
return
}
let urlRequest = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) { (data, response, error) -> Void in
guard error == nil else {
print("Error calling GET")
return
}
guard let responseData = data else {
print("Error receiving data")
return
}
do {
print ("Parsing response...")
}
}
task.resume()
Your code looks right to me. I tested it in a Playground and I'm getting the Parsing response... message printed to the console which makes me think the issue is elsewhere in your code or environment. I'd be happy to take a look at the whole project if you can post a Github link or something similar.
Here are the steps I would take to debug an issue like this:
1) Confirm my execution environment has an active internet connection. The Safari app can be used to confirm on iOS devices or the Simulator. Playgrounds can be tested by pasting the following lines.
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
print (try? String(contentsOf: url))
Look for a line in the console output similar to:
Optional("{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}")
2) Confirm the url is valid and returns data by pasting it into a web browser url bar and hitting enter. You will either see JSON printed in the browser or not.
3) Confirm my code is actually getting called when the application runs. You can do this with either breakpoints or print() statements. As OOPer2 pointed out asynchronous callback closures like that used in session.dataTask() execute in a different time than the rest of your code which is why "it just jumps over the completion handler block" while stepping through with the debugger. You'll need to put another breakpoint or print() statement inside the completion handler closure. I'd put the breakpoint on the guard error == nil else { line.
4) Make sure the application is still executing when the network request finishes and the completion handler closure executes. If your code is in a ViewController running in an iOS application it's probably fine, but if it's running in a Playground it may not be. Playgrounds by default stop execution once the last line of code has been evaluated which means the completion closure will never execute. You can tell a Playground to continue executing indefinitely by importing the PlaygroundSupport framework and setting needsIndefiniteExecution = true on the current Playground page. Paste the entire code block below into a Playground to see it in action:
import Foundation
import PlaygroundSupport
// Keep executing the program after the last line has evaluated so the
// closure can execute when the asynchronous network request finishes.
PlaygroundPage.current.needsIndefiniteExecution = true
// Generic Result enum useful for returning values OR an error from
// asynchronous functions.
enum Result<T> {
case failure(Error)
case success(T)
}
// Custom Errors to be returned when something goes wrong.
enum NetworkError: Error {
case couldNotCreateURL(for: String)
case didNotReceiveData
}
// Perform network request asynchronous returning the result via a
// completion closure called on the main thread.
//
// In really life the result type will not be a String, it will
// probably be an array of custom structs or similar.
func performNetworkRequest(completion: #escaping (Result<String>)->Void ) {
let endpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
guard let url = URL(string: endpoint) else {
let error = NetworkError.couldNotCreateURL(for: endpoint)
completion(Result.failure(error))
return
}
let urlRequest = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) { (data, response, error) -> Void in
// This closure is still executing on a background thread so
// don't touch anything related to the UI.
//
// Remember to dispatch back to the main thread when calling
// the completion closure.
guard error == nil else {
// Call the completion handler on the main thread.
DispatchQueue.main.async {
completion(Result.failure(error!))
}
return
}
guard let responseData = data else {
// Call the completion handler on the main thread.
DispatchQueue.main.async {
completion(Result.failure(NetworkError.didNotReceiveData))
}
return
}
// Parse response here...
// Call the completion handler on the main thread.
DispatchQueue.main.async {
completion(Result.success("Sucessfully parsed results"))
}
}
task.resume()
}
performNetworkRequest(completion: { result in
// The generic Result type makes handling the success and error
// cases really nice by just using a switch statement.
switch result {
case .failure(let error):
print(error)
case .success(let parsedResponse):
print(parsedResponse)
}
})
Why you dont use this Library Alamofire is an HTTP networking library written in Swift.
Add this line to your Podfile
pod 'Alamofire', '~> 4.4'
Then, run the following command:
pod install
Then in your ViewController file:
import Alamofire
Alamofire.request("https://jsonplaceholder.typicode.com/todos/1").responseJSON { response in
print("Request: \(String(describing: response.request))") // original url request
print("Response: \(String(describing: response.response))") // http url response
print("Result: \(response.result)") // response serialization result
if let json = response.result.value {
print("JSON: \(json)") // serialized json response
}
If let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
print("Data: \(utf8Text)") // original server data as UTF8 string
}
}
And in here are an example of how to parse the result.
https://github.com/CristianCardosoA/JSONParser
For more info about Alamofire:
https://github.com/Alamofire/Alamofire
I hope this help.

keep track of already downloaded files with `alamofire`

how can i keep a track or record of already downloaded files in alamofire swift 2.1 so that i don t have to download the same file again ? do we have any native method for that provided by alamofire or we have to do a check before downloading any file on our directory if we already have file with that name there ???? i'm confused on how to accomplish this with a proper approach
if anybody would clear my confusion about this then it'll be so helpful for me thanks
UPDATE:
let documentsURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
let fileUrl = documentsURL.URLByAppendingPathComponent(suggestedFileName)
print(fileUrl)
if !(NSFileManager.defaultManager().fileExistsAtPath(fileUrl.path!)){
self.suggestedFileName = (self.request?.response?.suggestedFilename)! // here how can i get the suggested download name before starting the download preocess ???
print("\(destination)")
request = Alamofire.download(.GET, "http://contentserver.adobe.com/store/books/GeographyofBliss_oneChapter.epub", destination: destination)
.progress { bytesRead, totalBytesRead, totalBytesExpectedToRead in
print(totalBytesRead)
// This closure is NOT called on the main queue for performance
// reasons. To update your ui, dispatch to the main queue.
dispatch_async(dispatch_get_main_queue()) {
print("Total bytes read on main queue: \(totalBytesRead)")
self.progressView.setProgress(Float(totalBytesRead) / Float(totalBytesExpectedToRead), animated: true)
}
}
.response { _, _, _, error in
if let error = error {
print("Failed with error: \(error)")
} else {
print("Downloaded file successfully")
}
}
}else {
print("file already exists")
}
in the above update am trying to get the suggestedFileName which is generated by alamofire but there's one problem when am trying to get sugestedFileName like this : suggestedFileName = (request?.response?.suggestedFilename)! in viewdidload am getting a null exception off course because there's no suggestedFileName because download not yet started so my question is that how can i get the suggestedFileName from response before starting the download ??
According to the docs https://github.com/Alamofire/Alamofire#downloading, you can download to a file. If your file destinations names are predictable, you could simply check to see if the contents of the file exists. For example if your are downloading data:
if let data = NSData(contentsOfURL: yourDestinationURL) {
//Do your stuff here
}
else {
//Download it
}
If you want consistency between names I suggest you avoid the Alamofire suggested destination and do this instead:
let path = NSFileManager.defaultManager().URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)[0] as NSURL
let newPath = path.URLByAppendingPathComponent(fileName)
Alamofire.download(.GET, "https://httpbin.org/stream/100", destination: { _ in
newPath //You have to give the destination in this closure. We could say 'return newPath' instead, they're the same thing.
})
.progress({ _ in
//progress stuff
})
.response { _, _, data, _ in
//Handle response once it's all over
}