diff --git a/app/build.gradle b/app/build.gradle index a1812b25a..b75acb596 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -80,6 +80,7 @@ dependencies { implementation 'me.jessyan:autosize:1.2.1' implementation 'org.greenrobot:eventbus:3.3.1' implementation 'org.nanohttpd:nanohttpd:2.3.1' + implementation 'org.apache.commons:commons-compress:1.18' implementation('org.simpleframework:simple-xml:2.7.1') { exclude group: 'stax', module: 'stax-api' exclude group: 'xpp3', module: 'xpp3' } leanbackImplementation 'androidx.leanback:leanback:1.2.0-alpha02' annotationProcessor 'androidx.room:room-compiler:2.4.3' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 94afe26bc..e8799fbb9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,8 +8,9 @@ - + + processes; + private final SimpleDateFormat format; private Listener listener; public Nano(int port) { super(port); addRequestProcess(); + format = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()); } private void addRequestProcess() { processes = new ArrayList<>(); processes.add(new InputRequestProcess(this)); - processes.add(new RawRequestProcess("/", R.raw.index, NanoHTTPD.MIME_HTML)); - processes.add(new RawRequestProcess("/index.html", R.raw.index, NanoHTTPD.MIME_HTML)); + processes.add(new RawRequestProcess("/", R.raw.index, MIME_HTML)); + processes.add(new RawRequestProcess("/index.html", R.raw.index, MIME_HTML)); processes.add(new RawRequestProcess("/ui.css", R.raw.ui, "text/css")); processes.add(new RawRequestProcess("/style.css", R.raw.style, "text/css")); processes.add(new RawRequestProcess("/script.js", R.raw.script, "application/x-javascript")); @@ -51,55 +68,159 @@ public class Nano extends NanoHTTPD { @Override public Response serve(IHTTPSession session) { String url = session.getUri().trim(); + Map files = new HashMap<>(); if (url.contains("?")) url = url.substring(0, url.indexOf('?')); - if (session.getMethod() == Method.POST) parseBody(session); + if (session.getMethod() == Method.POST) parseBody(session, files); for (RequestProcess process : processes) { if (process.isRequest(session, url)) { return process.doResponse(session, url); } } - if (session.getMethod() == Method.GET) { - if (url.equals("/proxy")) { - Map params = session.getParms(); - if (params.containsKey("do")) { - Object[] rs = ApiConfig.get().proxyLocal(params); - try { - int code = (int) rs[0]; - String mime = (String) rs[1]; - InputStream stream = rs[2] != null ? (InputStream) rs[2] : null; - return NanoHTTPD.newChunkedResponse(Response.Status.lookup(code), mime, stream); - } catch (Exception e) { - return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "500"); - } - } - } else if (url.startsWith("/file")) { - try { - return NanoHTTPD.newChunkedResponse(Response.Status.OK, "application/octet-stream", new FileInputStream(FileUtil.getLocal(url.substring(1)))); - } catch (FileNotFoundException e) { - return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, e.getMessage()); - } - } + switch (session.getMethod()) { + case GET: + if (url.startsWith("/file")) return doFile(url); + else if (url.startsWith("/proxy")) return doProxy(session.getParms()); + break; + case POST: + if (url.startsWith("/upload")) return doUpload(session.getParms(), files); + else if (url.startsWith("/newFolder")) return doNewFolder(session.getParms()); + else if (url.startsWith("/delFolder") || url.startsWith("/delFile")) return doDelFolder(session.getParms()); + break; } return processes.get(0).doResponse(session, ""); } - private void parseBody(IHTTPSession session) { - Map files = new HashMap<>(); + private void parseBody(IHTTPSession session, Map files) { + String ct = session.getHeaders().get("content-type"); + if (ct != null && ct.toLowerCase().contains("multipart/form-data") && !ct.toLowerCase().contains("charset=")) { + Matcher matcher = Pattern.compile("[ |\t]*(boundary[ |\t]*=[ |\t]*['|\"]?[^\"^'^;^,]*['|\"]?)", Pattern.CASE_INSENSITIVE).matcher(ct); + String boundary = matcher.find() ? matcher.group(1) : null; + if (boundary != null) session.getHeaders().put("content-type", "multipart/form-data; charset=utf-8; " + boundary); + } try { - String hd = session.getHeaders().get("content-type"); - if (hd == null) return; - if (hd.toLowerCase().contains("multipart/form-data") && !hd.toLowerCase().contains("charset=")) { - Matcher matcher = Pattern.compile("[ |\t]*(boundary[ |\t]*=[ |\t]*['|\"]?[^\"^'^;^,]*['|\"]?)", Pattern.CASE_INSENSITIVE).matcher(hd); - String boundary = matcher.find() ? matcher.group(1) : null; - if (boundary != null) session.getHeaders().put("content-type", "multipart/form-data; charset=utf-8; " + boundary); - } session.parseBody(files); } catch (Exception ignored) { } } + private Response doFile(String url) { + try { + String path = url.substring(6); + File file = FileUtil.getRootFile(path); + if (file.isFile()) return newChunkedResponse(Response.Status.OK, "application/octet-stream", new FileInputStream(file)); + else return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, listFiles(file)); + } catch (FileNotFoundException e) { + return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, e.getMessage()); + } + } + + private Response doProxy(Map params) { + try { + Object[] rs = ApiConfig.get().proxyLocal(params); + return newChunkedResponse(Response.Status.lookup((Integer) rs[0]), (String) rs[1], (InputStream) rs[2]); + } catch (Exception e) { + return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "500"); + } + } + + private Response doUpload(Map params, Map files) { + String path = params.get("path"); + for (String k : files.keySet()) { + if (k.startsWith("files-")) { + String fn = params.get(k); + String tmpFile = files.get(k); + File tmp = new File(tmpFile); + String root = Environment.getExternalStorageDirectory().getAbsolutePath(); + File file = new File(root + "/" + path + "/" + fn); + if (file.exists()) + file.delete(); + if (tmp.exists()) { + if (fn.toLowerCase().endsWith(".zip")) { + try { + unzip(tmp, root + "/" + path); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + FileUtil.copy(tmp, file); + } + } + if (tmp.exists()) + tmp.delete(); + } + } + return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, "OK"); + } + + private Response doNewFolder(Map params) { + String path = params.get("path"); + String name = params.get("name"); + FileUtil.getRootFile(path + File.separator + name).mkdirs(); + return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, "OK"); + } + + private Response doDelFolder(Map params) { + String path = params.get("path"); + FileUtil.clearDir(FileUtil.getRootFile(path)); + return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, "OK"); + } + + private String getParent(File root) { + if (root.getAbsolutePath().equals(FileUtil.getRootPath())) return "."; + return root.getParentFile().getAbsolutePath().replace(FileUtil.getRootPath() + File.separator, "").replace(FileUtil.getRootPath(), ""); + } + + private String listFiles(File root) { + File[] list = root.listFiles(); + String parent = getParent(root); + JsonObject info = new JsonObject(); + info.addProperty("parent", parent); + if (list == null || list.length == 0) { + info.add("files", new JsonArray()); + return info.toString(); + } + Arrays.sort(list, (o1, o2) -> { + if (o1.isDirectory() && o2.isFile()) return -1; + return o1.isFile() && o2.isDirectory() ? 1 : o1.getName().compareTo(o2.getName()); + }); + JsonArray files = new JsonArray(); + info.add("files", files); + for (File file : list) { + JsonObject obj = new JsonObject(); + obj.addProperty("name", file.getName()); + obj.addProperty("path", file.getAbsolutePath().replace(FileUtil.getRootPath() + File.separator, "")); + obj.addProperty("time", format.format(new Date(file.lastModified()))); + obj.addProperty("dir", file.isDirectory() ? 1 : 0); + files.add(obj); + } + return info.toString(); + } + + private void unzip(File file, String path) throws Exception { + try (ZipArchiveInputStream is = new ZipArchiveInputStream(new BufferedInputStream(new FileInputStream(file)))) { + ZipArchiveEntry entry; + while ((entry = is.getNextZipEntry()) != null) { + if (entry.isDirectory()) { + new File(path, entry.getName()).mkdirs(); + } else { + extractFile(is, path + File.separator + entry.getName()); + } + } + } + } + + private void extractFile(InputStream is, String path) { + try (OutputStream out = new FileOutputStream(path)) { + int len; + byte[] buf = new byte[2048]; + while ((len = is.read(buf)) > 0) out.write(buf, 0, len); + } catch (Exception e) { + e.printStackTrace(); + } + } + public static Response createPlainTextResponse(Response.IStatus status, String text) { - return newFixedLengthResponse(status, NanoHTTPD.MIME_PLAINTEXT, text); + return newFixedLengthResponse(status, MIME_PLAINTEXT, text); } public interface Listener { diff --git a/app/src/main/java/com/fongmi/android/tv/utils/FileUtil.java b/app/src/main/java/com/fongmi/android/tv/utils/FileUtil.java index 218411519..9efd33c40 100644 --- a/app/src/main/java/com/fongmi/android/tv/utils/FileUtil.java +++ b/app/src/main/java/com/fongmi/android/tv/utils/FileUtil.java @@ -1,6 +1,5 @@ package com.fongmi.android.tv.utils; -import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; @@ -31,8 +30,8 @@ public class FileUtil { return Environment.getExternalStorageDirectory().getAbsolutePath(); } - public static File getLibDir() { - return App.get().getDir("libs", Context.MODE_PRIVATE); + public static File getRootFile(String path) { + return new File(getRootPath() + File.separator + path); } public static File getCacheDir() { @@ -40,7 +39,7 @@ public class FileUtil { } public static File getCacheDir(String folder) { - return new File(getCachePath() + "/" + folder); + return new File(getCachePath() + File.separator + folder); } public static String getCachePath() { @@ -95,13 +94,14 @@ public class FileUtil { } } - public static void copy(File src, File dst) throws IOException { + public static void copy(File src, File dst) { try (InputStream in = new FileInputStream(src)) { try (OutputStream out = new FileOutputStream(dst)) { int len; byte[] buf = new byte[1024]; while ((len = in.read(buf)) > 0) out.write(buf, 0, len); } + } catch (Exception ignored) { } } diff --git a/app/src/main/res/raw/index.html b/app/src/main/res/raw/index.html index 3b27e7853..d60f3b12f 100644 --- a/app/src/main/res/raw/index.html +++ b/app/src/main/res/raw/index.html @@ -83,7 +83,7 @@
@@ -111,9 +111,7 @@
-
- 點擊「使用」時請打開接口視窗 -
+
請開啟接口設定
@@ -121,24 +119,11 @@
-
-
+
- -
-
-
-
-
-
-
-
-
- -
@@ -148,7 +133,7 @@
diff --git a/app/src/main/res/raw/script.js b/app/src/main/res/raw/script.js index 2ab3a5a6f..b71882096 100644 --- a/app/src/main/res/raw/script.js +++ b/app/src/main/res/raw/script.js @@ -1,9 +1,8 @@ let ic_dir = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAA7AAAAOwBeShxvQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIoSURBVFiF7Ze/T9RgGMc/T9/WO+74IXdRNKCJxETPQaOJi8LoxiouTurmv+DAYvwH3JzVhOhgSHByMTqY6GBCCBFMcJFwURC4O1rbvo8DoIg9rvHuYOGTdGjfb/J88vZpn1Z0bKw7JjsiaCcJqMiCKQ1OyuhonLTeLG5MZlLQq/UCooqd/nwfuNcOAUfgSorcbR0fN20RACRF7hgzc0PtEJB47IGmCSq8EXjbOKjG+VG54Buv531puK/WkfO2L0cY+ZI5ksVfD84vTt91U5vCEBvH7qxU0GqV18PX+Xg2+e6GFmZXhPlc3zOX5dW0Do2xMbIeEBuXmVMX68Y8BwxQzhXFlWqtdQKbzA+cIch0pMq630vgX55DTJgY+Bn18ql8hyAqphb4VjjeMLPVeG722nOKztddw0EPTARPUws0ohqB3TRw8w2KAwyaCRara/i2u+niCqxHf85TPQWe+Jw2L3kX3GhaYCdip5xU74FV28dSPNB8QZR8VEbCiCe1h+kFWopCVLH42oWz58Xh98s/y9o+CWzjQOBA4EBg3wX+mgWxdwlrTtSJRrjBK0T99glUC49R52jdcG75Fp7/on0C4BGGIeWFMtsHRL4zT2/hMCoerebfHkgYTWm+2/+XHTsQ4h3y6D/ZnxgWjRKvt0wgv3QTa+rN/I0mbDVipxxLe3c5kWjNgqIOSOO/nRajMVu99kF0hi4iM4LQtSfVrWbigHMa21k35NEvWSq4Cnb1Ay8AAAAASUVORK5CYII='; let ic_file = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAA7AAAAOwBeShxvQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAJdSURBVFiF7ZfLaxNRFMa/c+8MbZO0xAgmGIIxUmtsQCkobsSNO12I7v0PXAjuBDeCCN0I/gvdu3HhQlHBBwouTA1UUoulKXmYSDp59TEzx4XUpmFm0tsMTRd+u5l7uN9vvnvunRmCh+r1+km2+CWAlFedm5byP79PIHRx8tKk4VYjvCawTfvafs0BoN1qThWNYn7h3cK4EgDPI23P07Ox5pM7+zXfVqPRPFbulPO517nQngBmv3JwkwIPQXQDXDs9KAAANI1WtLpVXcx9yEU8AWY/WU95nRvL1tVbfhgLKXcgGq1oZe3XajabPdJdo3VfMOg2AAL7YQ8kUyewIiQs0wQAbG5sjHbqnUUARx0BAMi/IP4oEAxganpnFcuFEggU7q7x3AUHoaED9C6Bp5gZlmUpGQgSENL9OR0B2KUJCssFFAslJQAShPMXzkHX9b0DuCmRTCCRTCgB9JPzSeirhbeUEjDqBgyjoWQgiBCLxyCEcx8494DLZLZtwzbVmpAFeUaqlEA4EkY4Eu5fqKBDeg64bsNVVEoVJQMCITOTga47WznfJefJ4onjiMWjygBSk67jSgcRCYImlNqmr4beA0M/iP4ncDgT+BfBAUThmQDbpv+OPZ+8ngBa+xWEWfbFt9PqgJmh6/r7XR5OxduIkpcw9uMsWE4MZM7QMKoFq2uBF2dS6VO1vgArWzPIjDwHSUCOA2DXf0sFit9z6XS61nu7F8AEgLfrD/CtfQUhWR3YNzZS+ngzdPc+ps03TuO7Xjuzn61HzHQPDL1pAvaAu4AZJYvo+uPL9MWt5g/5NsVsHsMO8wAAAABJRU5ErkJggg=='; let current_root = ''; -let current_parent = ''; -let current_remote = ''; let current_file = ''; +let current_parent = ''; function search() { doAction('search', { text: $('#keyword').val() }); @@ -76,16 +75,12 @@ function selectFile(path, canDel) { if (canDel) $("#delFileBtn").show(); else $("#delFileBtn").hide(); $("#fileUrl1")[0].value = "file://" + current_file; - $("#fileUrl2")[0].value = current_remote + current_file; $("#fileInfoDialog").show(); } -function fileToApi(type) { - if (type === 1) { - doAction('api', { url: "file://" + current_file }); - } else { - doAction('api', { url: current_remote + current_file }); - } +function fileToApi() { + doAction('api', { text: "file://" + current_file }); + hideFileInfo(); } function hideFileInfo() { @@ -97,10 +92,9 @@ function listFile(path) { $.get('/file/' + path, function (res) { let info = JSON.parse(res); let parent = info.parent; - let canDel = info.del === 1; + let canDel = info.parent != '.'; current_root = path; current_parent = parent; - current_remote = info.remote; let array = info.files; if (path === '' && array.length == 0) { warnToast('可能沒有存儲權限'); @@ -200,22 +194,6 @@ function doDelFolder(yes) { } } -function delFolder() { - $('#delFolderContent').html('是否刪除 ' + current_root); - $('#delFolder').show(); -} - -function doDelFolder(yes) { - $('#delFolder').hide(); - if (yes == 1) { - $('#loadingToast').show(); - $.post('/delFolder', { path: current_root }, function (data) { - $('#loadingToast').hide(); - listFile(current_parent); - }); - } -} - function delFile() { hideFileInfo(); $('#delFileContent').html('是否刪除 ' + current_file); @@ -245,6 +223,7 @@ function showPanel(id) { let tab = $('#tab' + id)[0]; $(tab).attr('aria-selected', 'true').addClass('weui-bar__item_on'); $(tab).siblings('.weui-bar__item_on').removeClass('weui-bar__item_on').attr('aria-selected', 'false'); + if (id === 3) listFile('') var panelId = '#' + $(tab).attr('aria-controls'); $(panelId).css('display', 'block'); $(panelId).siblings('.weui-tab__panel').css('display', 'none');