Support upload zip

pull/123/head
FongMi 3 years ago
parent e26b2d8bd5
commit 33a3a3bdab
  1. 1
      app/build.gradle
  2. 3
      app/src/main/AndroidManifest.xml
  3. 189
      app/src/main/java/com/fongmi/android/tv/server/Nano.java
  4. 10
      app/src/main/java/com/fongmi/android/tv/utils/FileUtil.java
  5. 23
      app/src/main/res/raw/index.html
  6. 33
      app/src/main/res/raw/script.js

@ -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'

@ -8,8 +8,9 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name=".App"

@ -1,18 +1,33 @@
package com.fongmi.android.tv.server;
import android.os.Environment;
import com.fongmi.android.tv.R;
import com.fongmi.android.tv.api.ApiConfig;
import com.fongmi.android.tv.server.process.InputRequestProcess;
import com.fongmi.android.tv.server.process.RawRequestProcess;
import com.fongmi.android.tv.server.process.RequestProcess;
import com.fongmi.android.tv.utils.FileUtil;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -22,18 +37,20 @@ import fi.iki.elonen.NanoHTTPD;
public class Nano extends NanoHTTPD {
private List<RequestProcess> 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<String, String> 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<String, String> 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<String, String> files = new HashMap<>();
private void parseBody(IHTTPSession session, Map<String, String> 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<String, String> 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<String, String> params, Map<String, String> 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<String, String> 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<String, String> 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 {

@ -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) {
}
}

@ -83,7 +83,7 @@
<div class="button-sp-area" style="margin-top: 10px;">
<a href="javascript:void(0)" role="button" class="weui-btn weui-btn_mini weui-btn_default weui-wa-hotarea" href="javascript:void(0)" onclick="uploadFile()">上傳檔案</a>&nbsp;
<a href="javascript:void(0)" role="button" class="weui-btn weui-btn_mini weui-btn_default weui-wa-hotarea" href="javascript:void(0)" onclick="newFolder()">新增資料夾</a>&nbsp;
<a id="delCurFolder" href="javascript:void(0)" role="button" class="weui-btn weui-btn_mini weui-btn_warn weui-wa-hotarea" href="javascript:void(0)" onclick="delFolder()" style="display: none;">刪除當前文件</a>
<a id="delCurFolder" href="javascript:void(0)" role="button" class="weui-btn weui-btn_mini weui-btn_warn weui-wa-hotarea" href="javascript:void(0)" onclick="delFolder()" style="display: none;">刪除資料</a>
</div>
</div>
<div class="weui-cells" id="file_list">
@ -111,9 +111,7 @@
<div role="button" class="weui-mask"></div>
<div class="weui-half-screen-dialog">
<div class="weui-half-screen-dialog__hd">
<div class="weui-half-screen-dialog__hd__main">
點擊「使用」時請打開接口視窗
</div>
<div class="weui-half-screen-dialog__hd__main">請開啟接口設定</div>
</div>
<div class="weui-half-screen-dialog__bd" id="fileInfo">
<div class="weui-form__control-area" style="margin-bottom: 0px !important;">
@ -121,24 +119,11 @@
<div class="weui-cells">
<div class="weui-cell weui-cell_active weui-cell_vcode weui-cell_wrap">
<div class="weui-cell__hd"><label class="weui-label">本機地址</label></div>
<div class="weui-cell__bd weui-flex"><button onclick="fileToApi(1); return false;" class="weui-cell__control weui-btn weui-btn_default weui-vcode-btn">使用</button>
</div>
<div class="weui-cell__bd weui-flex"><button onclick="fileToApi(); return false;" class="weui-cell__control weui-btn weui-btn_default weui-vcode-btn">使用</button></div>
</div>
<div class="weui-cell weui-cell_active weui-cell_vcode weui-cell_wrap">
<div class="weui-cell__bd weui-flex">
<input id="fileUrl1" class="weui-input weui-cell__control weui-cell__control_flex" type="text" value="" readonly />
</div>
</div>
<div class="weui-cell weui-cell_active weui-cell_vcode weui-cell_wrap">
<div class="weui-cell__hd"><label class="weui-label">局域網地址</label></div>
<div class="weui-cell__bd weui-flex"><button onclick="fileToApi(2); return false;" class="weui-cell__control weui-btn weui-btn_default weui-vcode-btn">使用</button>
</div>
</div>
<div class="weui-cell weui-cell_active weui-cell_vcode weui-cell_wrap">
<div class="weui-cell__bd weui-flex">
<input id="fileUrl2" class="weui-input weui-cell__control weui-cell__control_flex" type="text" value="" readonly />
</div>
</div>
</div>
@ -148,7 +133,7 @@
<div class="weui-half-screen-dialog__ft" style="padding-bottom: 30px;">
<div class="weui-half-screen-dialog__btn-area">
<a href="javascript:void(0)" class="weui-btn weui-btn_default" onclick="hideFileInfo()">關閉</a>
<a href="javascript:void(0)" id="delFileBtn" class="weui-btn weui-btn_warn" onclick="delFile()">刪除檔案</a>
<a href="javascript:void(0)" id="delFileBtn" class="weui-btn weui-btn_warn" onclick="delFile()">刪除</a>
</div>
</div>
</div>

@ -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');

Loading…
Cancel
Save