Skip to content

Commit

Permalink
Merge pull request #23 from EvilDrW/master
Browse files Browse the repository at this point in the history
Add Support for File Content HTTP Responses
  • Loading branch information
bykof committed Sep 11, 2018
2 parents 390a035 + 1bee3d0 commit ef48071
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 19 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -106,4 +118,3 @@ Special thanks to:

- https://github.com/NanoHttpd/nanohttpd
- https://github.com/swisspol/GCDWebServer

158 changes: 140 additions & 18 deletions src/android/NanoHTTPDWebserver.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
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;
Expand Down Expand Up @@ -81,6 +85,112 @@ private String getContentType(JSONObject responseObject) throws JSONException {
}
}

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!");
Expand All @@ -106,28 +216,40 @@ public Response serve(IHTTPSession session) {
}

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();
}
return response;
}
else {
try {
response = newFixedLengthResponse(
Response.Status.lookup(responseObject.getInt("status")),
getContentType(responseObject),
responseObject.getString("body")
);

} catch (JSONException e) {
e.printStackTrace();
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;
}
}

0 comments on commit ef48071

Please sign in to comment.