|
|
|
@ -18,9 +18,9 @@ import java.io.IOException; |
|
|
|
import java.io.InputStream; |
|
|
|
import java.io.InputStream; |
|
|
|
import java.text.SimpleDateFormat; |
|
|
|
import java.text.SimpleDateFormat; |
|
|
|
import java.util.Date; |
|
|
|
import java.util.Date; |
|
|
|
import java.util.List; |
|
|
|
|
|
|
|
import java.util.Locale; |
|
|
|
import java.util.Locale; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.Map; |
|
|
|
|
|
|
|
import java.util.zip.CRC32; |
|
|
|
|
|
|
|
|
|
|
|
import fi.iki.elonen.NanoHTTPD.IHTTPSession; |
|
|
|
import fi.iki.elonen.NanoHTTPD.IHTTPSession; |
|
|
|
import fi.iki.elonen.NanoHTTPD.Response; |
|
|
|
import fi.iki.elonen.NanoHTTPD.Response; |
|
|
|
@ -44,7 +44,7 @@ public class Local implements Process { |
|
|
|
if (url.startsWith("/file")) return getFile(session.getHeaders(), url); |
|
|
|
if (url.startsWith("/file")) return getFile(session.getHeaders(), url); |
|
|
|
if (url.startsWith("/upload")) return upload(session.getParms(), files); |
|
|
|
if (url.startsWith("/upload")) return upload(session.getParms(), files); |
|
|
|
if (url.startsWith("/newFolder")) return newFolder(session.getParms()); |
|
|
|
if (url.startsWith("/newFolder")) return newFolder(session.getParms()); |
|
|
|
if (url.startsWith("/delFolder") || url.startsWith("/delFile")) return delFolder(session.getParms()); |
|
|
|
if (url.startsWith("/delFolder") || url.startsWith("/delFile")) return delete(session.getParms()); |
|
|
|
return null; |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -77,46 +77,41 @@ public class Local implements Process { |
|
|
|
return Nano.ok(); |
|
|
|
return Nano.ok(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private Response delFolder(Map<String, String> params) { |
|
|
|
private Response delete(Map<String, String> params) { |
|
|
|
String path = params.get("path"); |
|
|
|
String path = params.get("path"); |
|
|
|
Path.clear(Path.root(path)); |
|
|
|
Path.clear(Path.root(path)); |
|
|
|
return Nano.ok(); |
|
|
|
return Nano.ok(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private Response getFolder(File root) { |
|
|
|
private Response getFolder(File dir) { |
|
|
|
List<File> list = Path.list(root); |
|
|
|
File rootDir = Path.root(); |
|
|
|
JsonObject info = new JsonObject(); |
|
|
|
String rootPath = rootDir.getAbsolutePath(); |
|
|
|
info.addProperty("parent", root.equals(Path.root()) ? "." : root.getParent().replace(Path.rootPath(), "")); |
|
|
|
|
|
|
|
if (list.isEmpty()) { |
|
|
|
|
|
|
|
info.add("files", new JsonArray()); |
|
|
|
|
|
|
|
return Nano.ok(info.toString()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
JsonArray files = new JsonArray(); |
|
|
|
JsonArray files = new JsonArray(); |
|
|
|
info.add("files", files); |
|
|
|
for (File file : Path.list(dir)) { |
|
|
|
for (File file : list) { |
|
|
|
|
|
|
|
JsonObject obj = new JsonObject(); |
|
|
|
JsonObject obj = new JsonObject(); |
|
|
|
obj.addProperty("name", file.getName()); |
|
|
|
obj.addProperty("name", file.getName()); |
|
|
|
obj.addProperty("path", file.getAbsolutePath().replace(Path.rootPath(), "")); |
|
|
|
obj.addProperty("path", relativeTo(file, rootPath)); |
|
|
|
obj.addProperty("time", format.format(new Date(file.lastModified()))); |
|
|
|
obj.addProperty("time", format.format(new Date(file.lastModified()))); |
|
|
|
obj.addProperty("dir", file.isDirectory() ? 1 : 0); |
|
|
|
obj.addProperty("dir", file.isDirectory() ? 1 : 0); |
|
|
|
files.add(obj); |
|
|
|
files.add(obj); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
JsonObject info = new JsonObject(); |
|
|
|
|
|
|
|
info.addProperty("parent", parentOf(dir, rootDir, rootPath)); |
|
|
|
|
|
|
|
info.add("files", files); |
|
|
|
return Nano.ok(info.toString()); |
|
|
|
return Nano.ok(info.toString()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private Response getFile(Map<String, String> headers, File file, String mime) throws IOException { |
|
|
|
private Response getFile(Map<String, String> headers, File file, String mime) throws IOException { |
|
|
|
long fileLen = file.length(); |
|
|
|
long fileLen = file.length(); |
|
|
|
|
|
|
|
String etag = etag(file, fileLen); |
|
|
|
String ifNoneMatch = headers.get("if-none-match"); |
|
|
|
String ifNoneMatch = headers.get("if-none-match"); |
|
|
|
String etag = Integer.toHexString((file.getAbsolutePath() + file.lastModified() + fileLen).hashCode()); |
|
|
|
|
|
|
|
if (ifNoneMatch != null && (ifNoneMatch.equals("*") || ifNoneMatch.equals(etag))) { |
|
|
|
if (ifNoneMatch != null && (ifNoneMatch.equals("*") || ifNoneMatch.equals(etag))) { |
|
|
|
return newFixedLengthResponse(Status.NOT_MODIFIED, mime, ""); |
|
|
|
return newFixedLengthResponse(Status.NOT_MODIFIED, mime, ""); |
|
|
|
} |
|
|
|
} |
|
|
|
HttpRange range = HttpRange.from(fileLen, headers, etag); |
|
|
|
HttpRange range = HttpRange.from(fileLen, headers, etag); |
|
|
|
if (!range.valid()) { |
|
|
|
if (!range.valid()) return createRangeNotSatisfiableResponse(fileLen); |
|
|
|
return createRangeNotSatisfiableResponse(fileLen); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
FileInputStream fis = new FileInputStream(file); |
|
|
|
FileInputStream fis = new FileInputStream(file); |
|
|
|
robustSkip(fis, range.start); |
|
|
|
skip(fis, range.start); |
|
|
|
Response res; |
|
|
|
Response res; |
|
|
|
if (range.isPartial(fileLen)) { |
|
|
|
if (range.isPartial(fileLen)) { |
|
|
|
res = newFixedLengthResponse(Status.PARTIAL_CONTENT, mime, fis, range.length); |
|
|
|
res = newFixedLengthResponse(Status.PARTIAL_CONTENT, mime, fis, range.length); |
|
|
|
@ -130,57 +125,67 @@ public class Local implements Process { |
|
|
|
return res; |
|
|
|
return res; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private String etag(File file, long fileLen) { |
|
|
|
|
|
|
|
CRC32 crc = new CRC32(); |
|
|
|
|
|
|
|
crc.update((file.getAbsolutePath() + file.lastModified() + fileLen).getBytes()); |
|
|
|
|
|
|
|
return Long.toHexString(crc.getValue()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private Response createRangeNotSatisfiableResponse(long fileLen) { |
|
|
|
private Response createRangeNotSatisfiableResponse(long fileLen) { |
|
|
|
Response res = newFixedLengthResponse(Status.RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, ""); |
|
|
|
Response res = newFixedLengthResponse(Status.RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, ""); |
|
|
|
res.addHeader("Content-Range", "bytes */" + fileLen); |
|
|
|
res.addHeader("Content-Range", "bytes */" + fileLen); |
|
|
|
return res; |
|
|
|
return res; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private void robustSkip(InputStream fis, long bytesToSkip) throws IOException { |
|
|
|
private void skip(InputStream is, long bytesToSkip) throws IOException { |
|
|
|
if (bytesToSkip <= 0) return; |
|
|
|
if (bytesToSkip <= 0) return; |
|
|
|
long remaining = bytesToSkip; |
|
|
|
long remaining = bytesToSkip; |
|
|
|
while (remaining > 0) { |
|
|
|
while (remaining > 0) { |
|
|
|
long skipped = fis.skip(remaining); |
|
|
|
long skipped = is.skip(remaining); |
|
|
|
if (skipped <= 0) { |
|
|
|
if (skipped <= 0) throw new IOException("Failed to skip desired number of bytes"); |
|
|
|
throw new IOException("Failed to skip desired number of bytes"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
remaining -= skipped; |
|
|
|
remaining -= skipped; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static String relativeTo(File file, String rootPath) { |
|
|
|
|
|
|
|
String path = file.getAbsolutePath(); |
|
|
|
|
|
|
|
return path.startsWith(rootPath) ? path.substring(rootPath.length()) : path; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static String parentOf(File dir, File rootDir, String rootPath) { |
|
|
|
|
|
|
|
if (dir.equals(rootDir)) return "."; |
|
|
|
|
|
|
|
File parent = dir.getParentFile(); |
|
|
|
|
|
|
|
if (parent == null || parent.equals(rootDir)) return ""; |
|
|
|
|
|
|
|
return relativeTo(parent, rootPath); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private record HttpRange(long start, long end, long length, boolean valid) { |
|
|
|
private record HttpRange(long start, long end, long length, boolean valid) { |
|
|
|
|
|
|
|
|
|
|
|
public boolean isPartial(long fileTotalLength) { |
|
|
|
public boolean isPartial(long total) { |
|
|
|
return this.length < fileTotalLength; |
|
|
|
return length < total; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static HttpRange invalid() { |
|
|
|
|
|
|
|
return new HttpRange(0, 0, 0, false); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public static HttpRange from(long fileLen, Map<String, String> headers, String etag) { |
|
|
|
public static HttpRange from(long fileLen, Map<String, String> headers, String etag) { |
|
|
|
long start = 0; |
|
|
|
long start = 0; |
|
|
|
long end = fileLen - 1; |
|
|
|
long end = fileLen - 1; |
|
|
|
String rangeHeader = headers.get("range"); |
|
|
|
String rangeHeader = headers.get("range"); |
|
|
|
String ifRangeHeader = headers.get("if-range"); |
|
|
|
String ifRange = headers.get("if-range"); |
|
|
|
if (ifRangeHeader != null && !ifRangeHeader.equals(etag)) { |
|
|
|
if (ifRange != null && !ifRange.equals(etag)) rangeHeader = null; |
|
|
|
rangeHeader = null; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { |
|
|
|
if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { |
|
|
|
try { |
|
|
|
try { |
|
|
|
String[] parts = rangeHeader.substring(6).split("-", 2); |
|
|
|
String[] parts = rangeHeader.substring(6).split("-", 2); |
|
|
|
if (!parts[0].isEmpty()) { |
|
|
|
if (!parts[0].isEmpty()) start = Long.parseLong(parts[0]); |
|
|
|
start = Long.parseLong(parts[0]); |
|
|
|
if (parts.length > 1 && !parts[1].isEmpty()) end = Long.parseLong(parts[1]); |
|
|
|
} |
|
|
|
if (start >= fileLen || start > end) return invalid(); |
|
|
|
if (parts.length > 1 && !parts[1].isEmpty()) { |
|
|
|
|
|
|
|
end = Long.parseLong(parts[1]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (start >= fileLen || start > end) { |
|
|
|
|
|
|
|
return new HttpRange(0, 0, 0, false); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} catch (NumberFormatException e) { |
|
|
|
} catch (NumberFormatException e) { |
|
|
|
return new HttpRange(0, 0, 0, false); |
|
|
|
return invalid(); |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
if (end >= fileLen) { |
|
|
|
|
|
|
|
end = fileLen - 1; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (end >= fileLen) end = fileLen - 1; |
|
|
|
return new HttpRange(start, end, end - start + 1, true); |
|
|
|
return new HttpRange(start, end, end - start + 1, true); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|