Merge branch 'master' of https://github.com/bykof/cordova-plugin-webserver
This commit is contained in:
commit
a93a136535
13
README.md
13
README.md
@ -73,6 +73,18 @@ The params have to look like this (there are not default values for the params!)
|
||||
}
|
||||
```
|
||||
|
||||
### sendFileResponse(responseObject, callbackSuccess, callbackError) (currently Android-only support)
|
||||
|
||||
This method allows sending file content in response to an http request. It is intended that this would be used in conjunction with cordova-plugin-file to locate the path of the file data to be sent.
|
||||
|
||||
The response object should look like this. Here, the provided path should be accessible by your cordova app and the type should be the mime type of the file. Note that the MIME type of the file can be found from the [.type property of the File object](https://developer.mozilla.org/en-US/docs/Web/API/File).
|
||||
```
|
||||
{
|
||||
path: '/sdcard0/Downloads/whatever.txt',
|
||||
type: 'text/plain'
|
||||
}
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```javascript
|
||||
@ -106,4 +118,3 @@ Special thanks to:
|
||||
|
||||
- https://github.com/NanoHttpd/nanohttpd
|
||||
- https://github.com/swisspol/GCDWebServer
|
||||
|
||||
|
@ -28,6 +28,7 @@
|
||||
|
||||
<platform name="ios">
|
||||
<source-file src="src/ios/Webserver.swift" />
|
||||
<source-file src="src/ios/SynchronizedDictionary.swift" />
|
||||
<dependency id="cordova-plugin-add-swift-support" version="1.6.1"/>
|
||||
|
||||
<framework src="libz.tbd" />
|
||||
|
@ -21,6 +21,10 @@ import java.util.UUID;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
public class NanoHTTPDWebserver extends NanoHTTPD {
|
||||
|
||||
Webserver webserver;
|
||||
@ -81,6 +85,112 @@ public class NanoHTTPDWebserver extends NanoHTTPD {
|
||||
}
|
||||
}
|
||||
|
||||
private Response newFixedFileResponse(File file, String mime) throws FileNotFoundException {
|
||||
Response res;
|
||||
res = newFixedLengthResponse(Response.Status.OK, mime, new FileInputStream(file), (int) file.length());
|
||||
res.addHeader("Accept-Ranges", "bytes");
|
||||
return res;
|
||||
}
|
||||
|
||||
Response serveFile(Map<String, String> header, File file, String mime) {
|
||||
Response res;
|
||||
try {
|
||||
// Calculate etag
|
||||
String etag = Integer.toHexString((file.getAbsolutePath() + file.lastModified() + "" + file.length()).hashCode());
|
||||
|
||||
// Support (simple) skipping:
|
||||
long startFrom = 0;
|
||||
long endAt = -1;
|
||||
String range = header.get("range");
|
||||
if (range != null) {
|
||||
if (range.startsWith("bytes=")) {
|
||||
range = range.substring("bytes=".length());
|
||||
int minus = range.indexOf('-');
|
||||
try {
|
||||
if (minus > 0) {
|
||||
startFrom = Long.parseLong(range.substring(0, minus));
|
||||
endAt = Long.parseLong(range.substring(minus + 1));
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get if-range header. If present, it must match etag or else we
|
||||
// should ignore the range request
|
||||
String ifRange = header.get("if-range");
|
||||
boolean headerIfRangeMissingOrMatching = (ifRange == null || etag.equals(ifRange));
|
||||
|
||||
String ifNoneMatch = header.get("if-none-match");
|
||||
boolean headerIfNoneMatchPresentAndMatching = ifNoneMatch != null && ("*".equals(ifNoneMatch) || ifNoneMatch.equals(etag));
|
||||
|
||||
// Change return code and add Content-Range header when skipping is
|
||||
// requested
|
||||
long fileLen = file.length();
|
||||
|
||||
if (headerIfRangeMissingOrMatching && range != null && startFrom >= 0 && startFrom < fileLen) {
|
||||
// range request that matches current etag
|
||||
// and the startFrom of the range is satisfiable
|
||||
if (headerIfNoneMatchPresentAndMatching) {
|
||||
// range request that matches current etag
|
||||
// and the startFrom of the range is satisfiable
|
||||
// would return range from file
|
||||
// respond with not-modified
|
||||
res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
|
||||
res.addHeader("ETag", etag);
|
||||
} else {
|
||||
if (endAt < 0) {
|
||||
endAt = fileLen - 1;
|
||||
}
|
||||
long newLen = endAt - startFrom + 1;
|
||||
if (newLen < 0) {
|
||||
newLen = 0;
|
||||
}
|
||||
|
||||
FileInputStream fis = new FileInputStream(file);
|
||||
fis.skip(startFrom);
|
||||
|
||||
res = newFixedLengthResponse(Response.Status.PARTIAL_CONTENT, mime, fis, newLen);
|
||||
res.addHeader("Accept-Ranges", "bytes");
|
||||
res.addHeader("Content-Length", "" + newLen);
|
||||
res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen);
|
||||
res.addHeader("ETag", etag);
|
||||
}
|
||||
} else {
|
||||
|
||||
if (headerIfRangeMissingOrMatching && range != null && startFrom >= fileLen) {
|
||||
// return the size of the file
|
||||
// 4xx responses are not trumped by if-none-match
|
||||
res = newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.MIME_PLAINTEXT, "");
|
||||
res.addHeader("Content-Range", "bytes */" + fileLen);
|
||||
res.addHeader("ETag", etag);
|
||||
} else if (range == null && headerIfNoneMatchPresentAndMatching) {
|
||||
// full-file-fetch request
|
||||
// would return entire file
|
||||
// respond with not-modified
|
||||
res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
|
||||
res.addHeader("ETag", etag);
|
||||
} else if (!headerIfRangeMissingOrMatching && headerIfNoneMatchPresentAndMatching) {
|
||||
// range request that doesn't match current etag
|
||||
// would return entire (different) file
|
||||
// respond with not-modified
|
||||
|
||||
res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
|
||||
res.addHeader("ETag", etag);
|
||||
} else {
|
||||
// supply the file
|
||||
res = newFixedFileResponse(file, mime);
|
||||
res.addHeader("Content-Length", "" + fileLen);
|
||||
res.addHeader("ETag", etag);
|
||||
}
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
res = newFixedLengthResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: Reading file failed.");
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response serve(IHTTPSession session) {
|
||||
Log.d(this.getClass().getName(), "New request is incoming!");
|
||||
@ -106,28 +216,40 @@ public class NanoHTTPDWebserver extends NanoHTTPD {
|
||||
}
|
||||
|
||||
JSONObject responseObject = (JSONObject) this.webserver.responses.get(requestUUID);
|
||||
Log.d(this.getClass().getName(), "responseObject: " + responseObject.toString());
|
||||
Response response = null;
|
||||
Log.d(this.getClass().getName(), "responseObject: " + responseObject.toString());
|
||||
|
||||
try {
|
||||
response = newFixedLengthResponse(
|
||||
Response.Status.lookup(responseObject.getInt("status")),
|
||||
getContentType(responseObject),
|
||||
responseObject.getString("body")
|
||||
);
|
||||
|
||||
Iterator<?> keys = responseObject.getJSONObject("headers").keys();
|
||||
while (keys.hasNext()) {
|
||||
String key = (String) keys.next();
|
||||
response.addHeader(
|
||||
key,
|
||||
responseObject.getJSONObject("headers").getString(key)
|
||||
);
|
||||
if (responseObject.has("path")) {
|
||||
// TODO should specify a more correct mime-type
|
||||
try {
|
||||
return serveFile(session.getHeaders(), new File(responseObject.getString("path")), responseObject.getString("type"));
|
||||
}
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return response;
|
||||
}
|
||||
else {
|
||||
try {
|
||||
response = newFixedLengthResponse(
|
||||
Response.Status.lookup(responseObject.getInt("status")),
|
||||
getContentType(responseObject),
|
||||
responseObject.getString("body")
|
||||
);
|
||||
|
||||
Iterator<?> keys = responseObject.getJSONObject("headers").keys();
|
||||
while (keys.hasNext()) {
|
||||
String key = (String) keys.next();
|
||||
response.addHeader(
|
||||
key,
|
||||
responseObject.getJSONObject("headers").getString(key)
|
||||
);
|
||||
}
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return response;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
25
src/ios/SynchronizedDictionary.swift
Normal file
25
src/ios/SynchronizedDictionary.swift
Normal file
@ -0,0 +1,25 @@
|
||||
public class SynchronizedDictionary<KeyType:Hashable, ValueType> {
|
||||
private var dictionary: [KeyType:ValueType] = [:]
|
||||
private let accessQueue = DispatchQueue(label: "SynchronizedDictionaryAccess", attributes: .concurrent)
|
||||
|
||||
public func removeValue(forKey: KeyType) {
|
||||
self.accessQueue.async(flags:.barrier) {
|
||||
self.dictionary.removeValue(forKey: forKey)
|
||||
}
|
||||
}
|
||||
|
||||
public subscript(key: KeyType) -> ValueType? {
|
||||
set {
|
||||
self.accessQueue.async(flags:.barrier) {
|
||||
self.dictionary[key] = newValue
|
||||
}
|
||||
}
|
||||
get {
|
||||
var element: ValueType?
|
||||
self.accessQueue.sync {
|
||||
element = self.dictionary[key]
|
||||
}
|
||||
return element
|
||||
}
|
||||
}
|
||||
}
|
@ -3,13 +3,13 @@
|
||||
let TIMEOUT: Int = 60 * 3 * 1000000
|
||||
|
||||
var webServer: GCDWebServer = GCDWebServer()
|
||||
var responses: Dictionary<String, Any> = [:]
|
||||
var responses = SynchronizedDictionary<AnyHashable,Any?>()
|
||||
var onRequestCommand: CDVInvokedUrlCommand? = nil
|
||||
|
||||
override func pluginInitialize() {
|
||||
self.webServer = GCDWebServer()
|
||||
self.onRequestCommand = nil
|
||||
self.responses = [:]
|
||||
self.responses = SynchronizedDictionary<AnyHashable,Any?>()
|
||||
self.initHTTPRequestHandlers()
|
||||
}
|
||||
|
||||
@ -63,6 +63,9 @@
|
||||
response?.setValue(value, forAdditionalHeader: key)
|
||||
}
|
||||
|
||||
// Remove the handled response
|
||||
self.responses.removeValue(forKey: requestUUID)
|
||||
|
||||
// Complete the async response
|
||||
completionBlock(response!)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user