filecache

This commit is contained in:
cansnow
2025-12-27 07:08:30 +08:00
parent 974d149d25
commit 09c7889525
54 changed files with 10485 additions and 164 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
import Dns from 'okhttp3.Dns';
import UnknownHostException from 'java.net.UnknownHostException';
import InetAddress from 'java.net.InetAddress';
import Inet4Address from 'java.net.Inet4Address';
export class OKDns implements Dns {
public override lookup(hostName: string): kotlin.collections.MutableList<InetAddress> {
if (hostName == null) {
throw UnknownHostException("hostname == null");
} else {
try {
let inetAddressesList: Array<InetAddress> = [];
let inetAddresses = InetAddress.getAllByName(hostName);
for (inetAddress in inetAddresses) {
if (inetAddress instanceof Inet4Address) {
inetAddressesList.unshift(inetAddress)
} else {
inetAddressesList.push(inetAddress);
}
}
return inetAddressesList;
} catch (e: Exception) {
let unknownHostException = new UnknownHostException("error");
unknownHostException.initCause(e);
throw unknownHostException;
}
}
}
}
@@ -0,0 +1,80 @@
class StatusCode {
public static statusCodeMap : Map<string, string> | null = null;
private static initStatusCodeMap() {
let map = new Map<string, string>();
this.statusCodeMap = map;
map.set('100', "Continue");
map.set('101', "Switching Protocol");
map.set('200', "OK");
map.set('201', "Created");
map.set('202', "Accepted");
map.set('203', "Non-Authoritative Information");
map.set('204', "No Content");
map.set('205', "Reset Content");
map.set('206', "Partial Content");
map.set('300', "Multiple Choice");
map.set('301', "Moved Permanently");
map.set('302', "Found");
map.set('303', "See Other");
map.set('304', "Not Modified");
map.set('305', "Use Proxy");
map.set('306', "unused");
map.set('307', "Temporary Redirect");
map.set('308', "Permanent Redirect");
map.set('400', "Bad Request");
map.set('401', "Unauthorized");
map.set('402', "Payment Required");
map.set('403', "Forbidden");
map.set('404', "Not Found");
map.set('405', "Method Not Allowed");
map.set('406', "Not Acceptable");
map.set('407', "Proxy Authentication Required");
map.set('408', "Request Timeout");
map.set('409', "Conflict");
map.set('410', "Gone");
map.set('411', "Length Required");
map.set('412', "Precondition Failed");
map.set('413', "Payload Too Large");
map.set('414', "URI Too Long");
map.set('415', "Unsupported Media Type");
map.set('416', "Requested Range Not Satisfiable");
map.set('417', "Expectation Failed");
map.set('418', "I'm a teapot");
map.set('421', "Misdirected Request");
map.set('426', "Upgrade Required");
map.set('428', "Precondition Required");
map.set('429', "Too Many Requests");
map.set('431', "Request Header Fields Too Large");
map.set('500', "Internal Server Error");
map.set('501', "Not Implemented");
map.set('502', "Bad Gateway");
map.set('503', "Service Unavailable");
map.set('504', "Gateway Timeout");
map.set('505', "HTTP Version Not Supported");
map.set('506', "Variant Also Negotiates");
map.set('507', "Variant Also Negotiates");
map.set('511', "Network Authentication Required");
}
public static getStatus(code : string) : string {
let map = this.statusCodeMap;
if (map == null) {
this.initStatusCodeMap();
}
let tmp = this.statusCodeMap!;
if (!(tmp.has(code))) {
return 'unknown status';
} else {
return tmp.get(code)! as string;
}
}
}
export {
StatusCode
}
@@ -0,0 +1,507 @@
import { DownloadFileOptions, DownloadTask, DownloadFileProgressUpdateCallback, OnProgressDownloadResult } from '../../../interface.uts';
import { NetworkDownloadFileListener } from '../NetworkManager.uts'
import OkHttpClient from 'okhttp3.OkHttpClient';
import TimeUnit from 'java.util.concurrent.TimeUnit';
import ExecutorService from 'java.util.concurrent.ExecutorService';
import Executors from 'java.util.concurrent.Executors';
import Dispatcher from 'okhttp3.Dispatcher';
import Callback from 'okhttp3.Callback';
import Response from 'okhttp3.Response';
import Request from 'okhttp3.Request';
import Call from 'okhttp3.Call';
import IOException from 'java.io.IOException';
import ResponseBody from 'okhttp3.ResponseBody';
import File from 'java.io.File';
import BufferedSink from 'okio.BufferedSink';
import BufferedSource from 'okio.BufferedSource';
import Okio from 'okio.Okio';
import TextUtils from 'android.text.TextUtils';
import StringTokenizer from 'java.util.StringTokenizer';
import MimeTypeMap from 'android.webkit.MimeTypeMap';
import URLDecoder from 'java.net.URLDecoder';
import CookieManager from 'android.webkit.CookieManager';
import KotlinArray from 'kotlin.Array';
import Context from 'android.content.Context';
import Environment from 'android.os.Environment';
import { CookieInterceptor } from '../interceptor/CookieInterceptor.uts'
class NetworkDownloadTaskImpl implements DownloadTask {
private call : Call | null = null;
private listener : NetworkDownloadFileListener | null = null;
constructor(call : Call | null, listener : NetworkDownloadFileListener) {
this.call = call;
this.listener = listener;
}
public abort() {
if (this.call != null) {
this.call?.cancel();
}
}
public onProgressUpdate(option : DownloadFileProgressUpdateCallback) {
const kListener = this.listener;
if (kListener != null) {
kListener.progressListeners.add(option);
}
}
}
export class DownloadController {
private static instance : DownloadController | null = null
/**
* 上传的线程池
*/
private downloadExecutorService : ExecutorService | null = null;
public static getInstance() : DownloadController {
if (this.instance == null) {
this.instance = new DownloadController();
}
return this.instance!;
}
public downloadFile(options : DownloadFileOptions, listener : NetworkDownloadFileListener) : DownloadTask {
const client = this.createDownloadClient(options);
let request = this.createDownloadRequest(options, listener);
if (request == null) {
return new NetworkDownloadTaskImpl(null, listener);
}
let call : Call = client.newCall(request);
call.enqueue(new SimpleDownloadCallback(listener, options.filePath ?? ""));
let task = new NetworkDownloadTaskImpl(call, listener);
return task;
}
private createDownloadClient(option : DownloadFileOptions) : OkHttpClient {
let clientBuilder = OkHttpClient.Builder();
const timeout : Long = option.timeout != null ? option.timeout!.toLong() : 120000;
clientBuilder.connectTimeout(timeout, TimeUnit.MILLISECONDS);
clientBuilder.readTimeout(timeout, TimeUnit.MILLISECONDS);
clientBuilder.writeTimeout(timeout, TimeUnit.MILLISECONDS);
clientBuilder.addInterceptor(new CookieInterceptor());
if (this.downloadExecutorService == null) {
this.downloadExecutorService = Executors.newFixedThreadPool(10);
}
clientBuilder.dispatcher(new Dispatcher(this.downloadExecutorService));
return clientBuilder.build();
}
private createDownloadRequest(options : DownloadFileOptions, listener : NetworkDownloadFileListener) : Request | null {
let requestBilder = new Request.Builder();
try {
requestBilder.url(options.url);
} catch (exception : Exception) {
let option: UTSJSONObject = {};
option['statusCode'] = '-1';
option['errorCode'] = '-1';
option['errorMsg'] = "invalid URL";
let cause = exception.cause.toString();
option['cause'] = new SourceError(cause);
if (listener != null) {
listener.onComplete(option);
}
return null;
}
let ua = UTSAndroid.getWebViewInfo(UTSAndroid.getAppContext()!)["ua"].toString();
requestBilder.header("User-Agent", ua);
const headers = options.header?.toMap();
if (headers != null) {
for (entry in headers) {
const key = entry.key;
const value = entry.value;
if (value != null) {
requestBilder.addHeader(key, "" + value);
} else {
continue;
}
}
}
return requestBilder.build();
}
}
class SimpleDownloadCallback implements Callback {
private downloadFilePath = "/uni-download/";
private listener : NetworkDownloadFileListener | null = null;
private specifyPath = "";
constructor(listener : NetworkDownloadFileListener, specifyPath : string) {
this.listener = listener;
if(specifyPath.startsWith("unifile://")){
this.specifyPath = UTSAndroid.convert2AbsFullPath(specifyPath);
}else{
this.specifyPath = specifyPath
}
}
override onFailure(call : Call, exception : IOException) {
let option: UTSJSONObject = {};
option['statusCode'] = '-1';
option['errorCode'] = '-1';
option['errorMsg'] = exception.message;
let cause = exception.cause.toString();
option['cause'] = new SourceError(cause);
this.listener?.onComplete(option);
}
override onResponse(call : Call, response : Response) {
if (response.isSuccessful()) {
const source = response.body()?.source()
if (source != null) {
let mime_type = response.body()?.contentType();
let extension = "data";
if (mime_type != null) {
let mime_type1 = mime_type.toString(); // 例如: "application/json; charset=utf-8"
extension = mime_type.subtype(); // 子类型,例如: "json"
}
const tempFile = this.getTempFile()
let tempSink : BufferedSink | null = null;
let tempSource : BufferedSource | null = null;
let targetSink : BufferedSink | null = null;
try {
tempSink = Okio.buffer(Okio.sink(tempFile));
let totalBytesRead : Long = 0;
const contentLength = response.body()!!.contentLength();
const bufferSize : Int = 8 * 1024;
const buffer = ByteArray(bufferSize);
do {
let bytesRead = source.read(buffer);
if (bytesRead == -1) {
break;
}
tempSink.write(buffer, 0, bytesRead)
totalBytesRead += bytesRead.toLong();
const progress = (totalBytesRead.toFloat() / contentLength) * 100
let downloadProgressUpdate : OnProgressDownloadResult = {
progress: progress,
totalBytesWritten: totalBytesRead,
totalBytesExpectedToWrite: contentLength
}
this.listener?.onProgress(downloadProgressUpdate);
} while (true)
tempSink.flush()
tempSource = Okio.buffer(Okio.source(tempFile));
this.specifyPath = this.specifyPath.replace('{{ext}}',extension);
let targetFile = this.getFile(response)
targetSink = Okio.buffer(Okio.sink(targetFile));
targetSink.writeAll(tempSource);
targetSink.flush();
let option = {};
option['statusCode'] = response.code() + "";
if (targetFile.exists()) {
option['tempFilePath'] = targetFile.getPath();
}
this.listener?.onComplete(option);
} catch (e: Exception){
let option: UTSJSONObject = {};
const code = "-1"
const errorMsg = e.message
option['statusCode'] = code;
option['errorCode'] = code;
option['errorMsg'] = errorMsg;
const sourceError = new SourceError(errorMsg ?? "");
option['cause'] = sourceError;
this.listener?.onComplete(option);
} finally {
tempSink?.close()
targetSink?.close()
tempSource?.close()
source?.close()
tempFile.delete()
}
}
} else {
let option: UTSJSONObject = {};
const code = response.code() + "";
const errorMsg = response.body()?.string();
option['statusCode'] = code;
option['errorCode'] = code;
option['errorMsg'] = errorMsg;
const sourceError = new SourceError(errorMsg ?? "");
sourceError.code = response.code();
option['cause'] = sourceError;
this.listener?.onComplete(option);
}
}
//ext:string
getTempFile() : File {
return new File(UTSAndroid.getAppContext()!.getExternalCacheDir(), "temp_" + System.currentTimeMillis())
}
getRealPath() : string {
var path = UTSAndroid.getAppTempPath() ?? "";
return path + this.downloadFilePath;
}
getFile(response : Response) : File {
let targetPath = "";
if (this.specifyPath != "") {
const sourcePath = UTSAndroid.convert2AbsFullPath("/");
const sourceFileDir = new File(sourcePath);
if (this.isDescendant(sourceFileDir, new File(this.specifyPath))) {
let option = {};
option['statusCode'] = '-1';
option['errorCode'] = '602001';
option['errorMsg'] = "This path is not supported";
option['cause'] = null;
this.listener?.onComplete(option);
return new File("")
}
const pos = this.specifyPath.lastIndexOf("/")
if (pos == this.specifyPath.length - 1) {
//如果filePath是目录
if (this.isAbsolute(this.specifyPath)) {
targetPath = this.specifyPath;
} else {
targetPath = UTSAndroid.getAppTempPath()!! + "/" + this.specifyPath
}
} else {
let path = "";
if (this.isAbsolute(this.specifyPath)) {
path = this.specifyPath;
} else {
path = UTSAndroid.getAppTempPath()!! + "/" + this.specifyPath;
}
var file = new File(path)
const parentFile = file.getParentFile()
if (parentFile != null) {
if (!parentFile.exists()) {
parentFile.mkdirs()
}
}
if (file.exists() && file.isDirectory()) {
let option = {};
option['statusCode'] = '-1';
option['errorCode'] = '602001';
option['errorMsg'] = "The target file path is already a directory file, and file creation failed.";
option['cause'] = null;
this.listener?.onComplete(option);
}
if (file.exists()) {
const index = path.lastIndexOf(".");
let tFileName = path;
let tFileType = "";
if (index >= 0) {
tFileName = path.substring(0, index)
tFileType = path.substring(index)
}
var number = 1
while (new File(path).exists()) {
path = tFileName + "(" + number + ")" + tFileType;
number++
}
file = new File(path)
}
if (!file.exists()) {
try {
file.createNewFile()
} catch (exception : Exception) {
let option = {};
option['statusCode'] = '-1';
option['errorCode'] = '602001';
option['errorMsg'] = exception.message;
let cause = exception.cause.toString();
option['cause'] = new SourceError(cause);
this.listener?.onComplete(option);
}
}
return file
}
} else {
targetPath = this.getRealPath();
}
let fileName = "";
let remoteFileName = response.header("content-disposition");
if (!TextUtils.isEmpty(remoteFileName)) {
// form-data; name="file"; filename="xxx.pdf"
const segments : KotlinArray<String | null> | null = this.stringSplit(remoteFileName, ";")
if (segments != null) {
for (let i : Int = 0; i < segments.size; i++) {
const segment = segments[i];
if (segment != null) {
if (segment.contains("filename")) {
const pair = this.stringSplit(segment.trim(), "=") //目前认为只存在一个键值对
if (pair != null && pair.size > 1) {
let key = pair[0];
let value = pair[1];
const reg = new RegExp("^\"|\"$","g")
if (key != null) {
key = key.replace(reg, "");
}
if (value != null) {
value = value.replace(reg, "");
}
if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(value) && key!.equals("filename", true)) {
if (value != null) {
fileName = value;
}
}
}
}
}
}
}
}
if (TextUtils.isEmpty(fileName)) {
let path = response.request().url().encodedPath()
let pos = path.lastIndexOf('/')
if (pos >= 0) {
path = path.substring(pos + 1)
if (path.indexOf('.') >= 0 || path.length > 0) { //存在类型后缀或者没有文件格式后缀的情况,取最后LastPathComponent的名称当做文件名。
if (path.contains("?")) {
path = path.substring(0, path.indexOf("?"))
}
fileName = path
}
}
}
if (TextUtils.isEmpty(fileName)) {
fileName = System.currentTimeMillis().toString()
const contentType = response.header("content-type")
let type = MimeTypeMap.getSingleton().getExtensionFromMimeType(contentType);
if (type != null) {
fileName += "." + type;
}
}
fileName = URLDecoder.decode(fileName, "UTF-8")
fileName = fileName.replace(File.separator.toRegex(), "")
if (fileName.contains("?")) {
fileName = fileName.replace("\\?".toRegex(), "0")
}
if (fileName.length > 80) {
const subFileName : String = fileName.substring(0, 80)
fileName = subFileName + System.currentTimeMillis()
}
if (new File(targetPath + fileName).exists()) {
const index = fileName.lastIndexOf(".");
let tFileName = fileName;
let tFileType = "";
if (index >= 0) {
tFileName = fileName.substring(0, index)
tFileType = fileName.substring(index)
//fileName是 .xxx的情况
if(tFileName == ""){
tFileName = tFileType
tFileType = ""
}
} else {
tFileName = fileName
}
var number = 1
while (new File(targetPath + fileName).exists()) {
fileName = tFileName + "(" + number + ")" + tFileType;
number++
}
}
targetPath += fileName
const file = new File(targetPath)
const parentFile = file.getParentFile()
if (parentFile != null) {
if (!parentFile.exists()) {
parentFile.mkdirs()
}
}
if (!file.exists()) {
try {
file.createNewFile()
} catch (exception : Exception) {
let option: UTSJSONObject = {};
option['statusCode'] = '-1';
option['errorCode'] = '602001';
option['errorMsg'] = exception.message;
let cause = exception.cause.toString();
option['cause'] = new SourceError(cause);
this.listener?.onComplete(option);
}
}
return file
}
isAbsolute(path : string) : boolean {
const context = UTSAndroid.getAppContext()!! as Context;
if (path.startsWith(context.getFilesDir().getParent())) {
return true;
}
const exPath = context.getExternalFilesDir(null)?.getParent();
if (exPath != null && path.startsWith(exPath)) {
return true;
}
return false;
}
/**
* 判断两个文件的上下级关系
*/
isDescendant(parent : File, child : File) : boolean {
//有可能开发者传入的是/sdcard 或者/storage/emulated/ 这样的文件路径, 所以要用软连接的实际文件路径进行对比.
if (child.getCanonicalPath() == parent.getCanonicalPath()) {
return true;
}
let parentFile = child.getParentFile();
if (parentFile == null) {
return false;
}
return this.isDescendant(parent, parentFile);
}
stringSplit(str : String | null, delim : String | null) : KotlinArray<String | null> | null {
if (!TextUtils.isEmpty(str) && !TextUtils.isEmpty(delim)) {
const stringTokenizer = new StringTokenizer(str, delim, false);
const result = arrayOfNulls<String>(stringTokenizer.countTokens())
var index : Int = 0
while (stringTokenizer.hasMoreElements()) {
result[index] = stringTokenizer.nextToken().trim()
index += 1
}
return result
}
return null
}
}
@@ -0,0 +1,82 @@
import Interceptor from 'okhttp3.Interceptor';
import Response from 'okhttp3.Response';
import CookieHandler from 'java.net.CookieHandler';
import TreeMap from 'java.util.TreeMap';
import Headers from 'okhttp3.Headers';
import Request from 'okhttp3.Request';
class CookieInterceptor implements Interceptor {
override intercept(chain : Interceptor.Chain) : Response {
let request = chain.request()
let headerCookie = request.header("cookie")
let uri = request.url().uri()
let cookieHandler = CookieHandler.getDefault()
if (headerCookie == null) {
let requestBuilder = request.newBuilder()
try {
let currentHeaders = this.toMap(request.headers())
let localCookie = cookieHandler.get(uri, currentHeaders)
this.addCookies(requestBuilder, localCookie)
} catch (e : Exception) {
}
request = requestBuilder.build()
}
let response = chain.proceed(request)
try {
cookieHandler.put(uri, this.toMap(response.headers()))
} catch (e : Exception) {
}
return response
}
private toMap(headers : Headers) : MutableMap<String, MutableList<String>> {
let result : MutableMap<String, MutableList<String>> = new TreeMap(String.CASE_INSENSITIVE_ORDER)
let size = headers.size()
for (let i:Int = 0; i < size; i++) {
let name = headers.name(i)
let values = result[name]
if (values == null) {
values = arrayListOf()
result[name] = values
}
values.add(headers.value(i))
}
return result
}
private addCookies(builder : Request.Builder, localCookie : MutableMap<String, MutableList<String>>) : void {
let totalList = mutableListOf<String>()
let flagList = mutableListOf<String>()
for (key in localCookie.keys) {
if (flagList.size == 2) {
break
}
if ("cookie".equals(key, true) || "cookie2".equals(key, true)) {
flagList.add(key)
let cookieList = localCookie[key]
if (!cookieList.isNullOrEmpty()) {
totalList.addAll(cookieList)
}
}
}
let headerStr = new StringBuilder()
for (let str in totalList) {
headerStr.append(str)
headerStr.append("; ")
}
if (headerStr.toString().endsWith("; ")) {
headerStr.deleteRange(headerStr.length - 2, headerStr.length - 1)
}
if (!headerStr.toString().isEmpty()){
builder.addHeader("Cookie", headerStr.toString())
}
}
}
export {
CookieInterceptor
}
@@ -0,0 +1,48 @@
import Arrays from 'java.util.Arrays';
import KotlinArray from 'kotlin.Array'
class SSLConfig {
private keystore ?: string = null;
private storePass ?: string = null;
private ca ?: KotlinArray<String> = null;
public getKeystore() : string | null {
return this.keystore;
}
public setKeystore(ks : string) {
if (ks == null) {
ks = "";
}
this.keystore = ks;
}
public getStorePass() : string | null {
return this.storePass;
}
public setStorePass(sp : string) {
if (sp == null) {
sp = "";
}
this.storePass = sp;
}
public getCa() : KotlinArray<String> | null {
return this.ca;
}
public setCa(ca : KotlinArray<String>) {
if (ca == null) {
ca = emptyArray();
}
this.ca = ca;
}
}
export {
SSLConfig
}
@@ -0,0 +1,65 @@
import SSLSocketFactory from 'javax.net.ssl.SSLSocketFactory';
import { SSLConfig } from './SSLConfig.uts'
import SSLContext from 'javax.net.ssl.SSLContext';
import KeyStore from 'java.security.KeyStore';
import KeyManagerFactory from 'javax.net.ssl.KeyManagerFactory';
import CertificateFactory from 'java.security.cert.CertificateFactory';
import TextUtils from 'android.text.TextUtils';
class SSLFactoryManager {
private static instance?: SSLFactoryManager = null;
private cacheSSLFactory: Map<SSLConfig, SSLSocketFactory> = new Map<SSLConfig, SSLSocketFactory>();
public static getInstance(): SSLFactoryManager {
if (this.instance == null) {
this.instance = SSLFactoryManager();
}
return this.instance!;
}
public getSSLSocketFactory(sslConfig: SSLConfig): SSLSocketFactory | null {
if (sslConfig == null) {
return null;
}
if (this.cacheSSLFactory.has(sslConfig)){
let sslFactory = this.cacheSSLFactory.get(sslConfig);
if (sslConfig != null){
return sslFactory;
}
}
try{
let sslContext = SSLContext.getInstance('TLS');
let keyStore = KeyStore.getInstance('PKCS12');
let keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
if (!TextUtils.isEmpty(sslConfig.getKeystore()) && !TextUtils.isEmpty(sslConfig.getStorePass())){
//todo 1. 这里需要解析keystore
// 2. 如果是文件需要转换一下路径,然后读出来。
// resolve 原生层会提供bundleurl和运行模式的接口。
}else{
keyManagerFactory = null;
}
let certificateFactory = CertificateFactory.getInstance('X.509');
let caKeyStore = KeyStore.getInstance('PKCS12');
}catch(e : Exception){
}
return null;
}
}
export {
SSLFactoryManager
}
@@ -0,0 +1,47 @@
import RequestBody from 'okhttp3.RequestBody';
import MediaType from 'okhttp3.MediaType';
import InputStream from 'java.io.InputStream';
import BufferedSink from 'okio.BufferedSink';
import Source from 'okio.Source';
import Okio from 'okio.Okio';
import Util from 'okhttp3.internal.Util';
export class InputStreamRequestBody extends RequestBody {
private mediaType : MediaType | null = null;
private length : Long = -1;
private inputStream : InputStream | null = null;
constructor(mediaType : MediaType, length : Long, inputStream : InputStream) {
super()
this.mediaType = mediaType;
this.length = length;
this.inputStream = inputStream;
}
override contentLength() : Long {
return this.length;
}
override contentType() : MediaType {
const type = this.mediaType;
if (type == null) {
return MediaType.parse("application/octet-stream")!;
} else {
return type;
}
}
override writeTo(sink : BufferedSink) {
let source : Source | null = null;
try {
source = Okio.source(this.inputStream);
sink.writeAll(source);
} catch (e) {
}
Util.closeQuietly(source);
}
}
@@ -0,0 +1,62 @@
import RequestBody from 'okhttp3.RequestBody';
import MediaType from 'okhttp3.MediaType';
import BufferedSink from 'okio.BufferedSink';
import ForwardingSink from 'okio.ForwardingSink';
import Sink from 'okio.Sink';
import Buffer from 'okio.Buffer';
import Okio from 'okio.Okio';
export interface UploadProgressListener {
onProgress(bytesWritten : number, contentLength : number) : void;
}
class CountingSink extends ForwardingSink {
private listener : UploadProgressListener | null = null;
private bytesWritten : number = 0;
private total : number = 0;
constructor(sink : Sink, total : number, listener : UploadProgressListener) {
super(sink)
this.listener = listener;
this.total = total;
}
override write(source : Buffer, byteCount : Long) {
super.write(source, byteCount);
this.bytesWritten += byteCount;
this.listener?.onProgress(this.bytesWritten, this.total);
}
}
export class ProgressRequestBody extends RequestBody {
private requestBody : RequestBody | null = null;
private listener : UploadProgressListener | null = null;
constructor(requestBody : RequestBody, listener : UploadProgressListener) {
super();
this.requestBody = requestBody;
this.listener = listener;
}
override contentLength() : Long {
return this.requestBody?.contentLength() ?? 0;
}
override contentType() : MediaType {
const body = this.requestBody;
if (body == null) {
return MediaType.parse("application/octet-stream")!;
} else {
return body.contentType()!;
}
}
override writeTo(sink : BufferedSink) {
const countingSink = new CountingSink(sink, this.contentLength(), this.listener!);
const bufferedSink = Okio.buffer(countingSink);
this.requestBody?.writeTo(bufferedSink);
bufferedSink.flush();
}
}
@@ -0,0 +1,430 @@
import { UploadFileOptions, UploadTask, UploadFileProgressUpdateCallback, UploadFileOptionFiles, OnProgressUpdateResult } from '../../../interface.uts';
import { NetworkUploadFileListener } from '../NetworkManager.uts'
import OkHttpClient from 'okhttp3.OkHttpClient';
import TimeUnit from 'java.util.concurrent.TimeUnit';
import ExecutorService from 'java.util.concurrent.ExecutorService';
import Executors from 'java.util.concurrent.Executors';
import RequestBody from 'okhttp3.RequestBody';
import MediaType from 'okhttp3.MediaType';
import MultipartBody from 'okhttp3.MultipartBody';
import Call from 'okhttp3.Call';
import Dispatcher from 'okhttp3.Dispatcher';
import Request from 'okhttp3.Request';
import MimeTypeMap from 'android.webkit.MimeTypeMap';
import TextUtils from 'android.text.TextUtils';
import File from 'java.io.File';
import Uri from 'android.net.Uri';
import InputStream from 'java.io.InputStream';
import MediaStore from 'android.provider.MediaStore';
import FileInputStream from 'java.io.FileInputStream';
import { InputStreamRequestBody } from './InputStreamRequestBody.uts';
import { UploadProgressListener, ProgressRequestBody } from './ProgressRequestBody.uts'
import Callback from 'okhttp3.Callback';
import Response from 'okhttp3.Response';
import IOException from 'java.io.IOException';
import Retention from 'java.lang.annotation.Retention';
import URI from 'java.net.URI';
import Build from 'android.os.Build';
import Environment from 'android.os.Environment';
import UUID from 'java.util.UUID';
import Context from 'android.content.Context';
import FileOutputStream from 'java.io.FileOutputStream';
import { CookieInterceptor } from '../interceptor/CookieInterceptor.uts'
class FileInformation {
public inputStream : InputStream | null = null;
public size : Long = -1;
public mime : string | null = null;
public name : string | null = null;
}
class NetworkUploadTaskImpl implements UploadTask {
private call : Call | null = null;
private listener : NetworkUploadFileListener | null = null;
constructor(call : Call | null, listener : NetworkUploadFileListener) {
this.call = call;
this.listener = listener;
}
public abort() {
if (this.call != null) {
this.call?.cancel();
}
}
public onProgressUpdate(option : UploadFileProgressUpdateCallback) {
const kListener = this.listener;
if (kListener != null) {
kListener.progressListeners.add(option);
}
}
}
class NetworkUploadProgressListener implements UploadProgressListener {
private listener : NetworkUploadFileListener | null = null;
constructor(listener : NetworkUploadFileListener) {
this.listener = listener;
}
onProgress(bytesWritten : number, contentLength : number) {
const progress = (bytesWritten.toFloat() / contentLength) * 100
const progressUpdate : OnProgressUpdateResult = {
progress: progress.toInt(),
totalBytesSent: bytesWritten,
totalBytesExpectedToSend: contentLength
}
this.listener?.onProgress(progressUpdate);
}
}
class UploadController {
private static instance : UploadController | null = null
/**
* 上传的线程池
*/
private uploadExecutorService : ExecutorService | null = null;
public static getInstance() : UploadController {
if (this.instance == null) {
this.instance = new UploadController();
}
return this.instance!;
}
public uploadFile(options : UploadFileOptions, listener : NetworkUploadFileListener) : UploadTask {
const client = this.createUploadClient(options);
let request = this.createUploadRequest(options, listener);
if (request == null) {
return new NetworkUploadTaskImpl(null, listener);;
}
let call : Call = client.newCall(request);
call.enqueue(new SimpleUploadCallback(listener));
let task = new NetworkUploadTaskImpl(call, listener);
return task;
}
private createUploadClient(option : UploadFileOptions) : OkHttpClient {
let clientBuilder = OkHttpClient.Builder();
const timeout : Long = option.timeout != null ? option.timeout!.toLong() : 120000;
clientBuilder.connectTimeout(timeout, TimeUnit.MILLISECONDS);
clientBuilder.readTimeout(timeout, TimeUnit.MILLISECONDS);
clientBuilder.writeTimeout(timeout, TimeUnit.MILLISECONDS);
clientBuilder.addInterceptor(new CookieInterceptor());
if (this.uploadExecutorService == null) {
this.uploadExecutorService = Executors.newFixedThreadPool(10);
}
clientBuilder.dispatcher(new Dispatcher(this.uploadExecutorService));
return clientBuilder.build();
}
private createUploadRequest(options : UploadFileOptions, listener : NetworkUploadFileListener) : Request | null {
let requestBilder = new Request.Builder();
try {
requestBilder.url(options.url);
} catch (exception : Exception) {
let option = {};
option['statusCode'] = '-1';
option['errorCode'] = '600009';
option['errorMsg'] = "invalid URL";
let cause = exception.cause.toString();
option['cause'] = new SourceError(cause);
if (listener != null) {
listener.onComplete(option);
}
return null;
}
let multiPartBody = (new MultipartBody.Builder("----" + UUID.randomUUID().toString())).setType(MultipartBody.FORM);
const formData = options.formData?.toMap();
if (formData != null) {
for (entry in formData) {
const key = entry.key;
const value = entry.value;
if (value != null) {
multiPartBody.addFormDataPart(key, "" + value);
} else {
continue;
}
}
}
const tempFiles = options.files;
if (tempFiles != null && tempFiles!.length > 0) {
const files : UploadFileOptionFiles[] = tempFiles;
for (let i = 0; i < files.length; i++) {
const file = files[i];
const path = file.uri;
const fileInformation = this.getFileInformation(path)
const name = file.name ?? "file";
const inputStream = fileInformation?.inputStream;
if (fileInformation != null && inputStream != null) {
let requestBody = new InputStreamRequestBody(MediaType.parse(fileInformation.mime ?? "*/*")!, fileInformation.size, inputStream);
multiPartBody.addFormDataPart(name, fileInformation.name, requestBody);
} else {
let option = {};
option['statusCode'] = '-1';
option['errorCode'] = '-1';
option['errorMsg'] = "Illegal file";
option['cause'] = null;
if (listener != null) {
listener.onComplete(option);
}
return null;
}
}
} else {
const filePath = options.filePath;
if (filePath == null) {
let option = {};
option['statusCode'] = '-1';
option['errorCode'] = '-1';
option['errorMsg'] = "filePath is null";
option['cause'] = null;
if (listener != null) {
listener.onComplete(option);
}
return null;
}
const fileInformation = this.getFileInformation(filePath);
const name = options.name ?? "file";
const inputStream = fileInformation?.inputStream;
if (fileInformation != null && inputStream != null) {
let requestBody = new InputStreamRequestBody(MediaType.parse(fileInformation.mime ?? "*/*")!, fileInformation.size, inputStream);
multiPartBody.addFormDataPart(name, fileInformation.name, requestBody);
} else {
let option = {};
option['statusCode'] = '-1';
option['errorCode'] = '-1';
option['errorMsg'] = "Illegal file";
option['cause'] = null;
if (listener != null) {
listener.onComplete(option);
}
return null;
}
}
let ua = UTSAndroid.getWebViewInfo(UTSAndroid.getAppContext()!)["ua"].toString();
requestBilder.header("User-Agent", ua);
const headers = options.header?.toMap();
if (headers != null) {
for (entry in headers) {
const key = entry.key;
const value = entry.value;
if (value != null) {
requestBilder.addHeader(key, "" + value);
} else {
continue;
}
}
}
requestBilder.post(new ProgressRequestBody(multiPartBody.build(), new NetworkUploadProgressListener(listener)));
return requestBilder.build();
}
/**
* 获取文件信息对象
*/
private getFileInformation(uri : string) : FileInformation | null {
let result : FileInformation | null = null;
if (uri.startsWith("content://")) {
const contentUri = Uri.parse(uri);
const context = UTSAndroid.getAppContext();
let cursor = context!.getContentResolver().query(contentUri, null, null, null, null);
if (cursor != null) {
cursor.moveToFirst();
let fileInformation = new FileInformation();
fileInformation.inputStream = context.getContentResolver().openInputStream(contentUri);
fileInformation.size = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media.SIZE)).toLong();
fileInformation.name = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME));
fileInformation.mime = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE))
result = fileInformation;
cursor.close()
}
} else {
if (uri.startsWith("file://")) {
uri = uri.substring("file://".length)
} else if(uri.startsWith("unifile://")){
uri = UTSAndroid.convert2AbsFullPath(uri)
}else {
// 如果不是file://开头的,就说明是相对路径。
uri = UTSAndroid.convert2AbsFullPath(uri)
if(uri.startsWith("/android_asset/")){
const filePath = uri.replace("/android_asset/", "")
const context = UTSAndroid.getAppContext();
const apkFile = this.copyAssetFileToPrivateDir(context!!, filePath)
if(apkFile != null){
uri = apkFile.getPath()
}
}
}
let file = new File(uri);
let fileInputStream = new FileInputStream(file);
let size = file.length();
let name = file.getName();
let mime = this.getMimeType(name);
let fileInformation = new FileInformation();
fileInformation.inputStream = fileInputStream;
fileInformation.size = size;
fileInformation.name = name;
fileInformation.mime = mime;
result = fileInformation;
}
return result;
}
private copyAssetFileToPrivateDir(context: Context, fileName: string): File| null {
try {
const destPath = context.getCacheDir().getPath() + "/uploadFiles/" + fileName
const outFile = new File(destPath)
const parentFile = outFile.getParentFile()
if (parentFile != null) {
if (!parentFile.exists()) {
parentFile.mkdirs()
}
}
if(!outFile.exists()){
outFile.createNewFile()
}
const inputStream = context.getAssets().open(fileName)
const outputStream = new FileOutputStream(outFile)
let buffer = new ByteArray(1024);
do {
let len = inputStream.read(buffer);
if (len == -1) {
break;
}
outputStream.write(buffer, 0, len)
} while (true)
inputStream.close()
outputStream.close()
return outFile
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
private checkPrivatePath(path : string) : boolean {
if (Build.VERSION.SDK_INT > 29 && Environment.isExternalStorageManager()) {
return true;
}
if (path.startsWith("file://")) {
path = path.replace("file://", "");
}
const context = UTSAndroid.getAppContext()!;
let cache = context.getExternalCacheDir();
let sPrivateExternalDir = ""
if (cache == null) {
sPrivateExternalDir = Environment.getExternalStorageDirectory().getPath() + "/Android/data/" + context.getPackageName();
} else {
sPrivateExternalDir = cache.getParent();
}
const sPrivateDir = context.getFilesDir().getParent();
if (sPrivateExternalDir.startsWith("/") && !path.startsWith("/")) {
path = "/" + path;
}
if ((path.contains(sPrivateDir) || path.contains(sPrivateExternalDir))//表示应用私有路径
|| this.isAssetFile(path) //表示apk的assets路径文件
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.Q//表示当前手机属于可正常访问路径系统
) {
//文件路径在私有路径下或手机系统版符合非分区存储逻辑
return true;
}
return false;
}
private isAssetFile(filePath : string) : boolean {
let isAsset = false;
if (filePath.startsWith("apps/")) {
isAsset = true;
} else if (filePath.startsWith("/android_asset/") || filePath.startsWith("android_asset/")) {
isAsset = true;
}
return isAsset;
}
/**
* 获取文件mime
*/
private getMimeType(filename : string) : string {
let map = MimeTypeMap.getSingleton()
var extType = MimeTypeMap.getFileExtensionFromUrl(filename)
if (extType == null && filename.lastIndexOf(".") >= 0) {
extType = filename.substring(filename.lastIndexOf(".") + 1)
}
let ret = map.getMimeTypeFromExtension(extType);
if (TextUtils.isEmpty(ret)) {
if (TextUtils.isEmpty(extType)) {
ret = "*/*"
} else {
ret = "application/" + extType
}
}
return ret!;
}
}
class SimpleUploadCallback implements Callback {
private listener : NetworkUploadFileListener | null = null;
constructor(listener : NetworkUploadFileListener) {
this.listener = listener;
}
override onFailure(call : Call, exception : IOException) {
let option = {};
option['statusCode'] = '-1';
option['errorCode'] = '-1';
option['errorMsg'] = exception.message;
let cause = exception.cause.toString();
option['cause'] = new SourceError(cause);
this.listener?.onComplete(option);
}
override onResponse(call : Call, response : Response) {
const result = {};
result["statusCode"] = response.code() + "";
result["data"] = response.body()?.string();
this.listener?.onComplete(result);
}
}
export {
UploadController
}
@@ -0,0 +1,61 @@
export class NetworkUtil {
public static convertHeaders(headers: MutableMap<String, MutableList<String>>): UTSJSONObject {
let simpleHeaders = {};
if (headers != null) {
let it = headers.iterator();
while (it.hasNext()) {
let entry = it.next();
let key = entry.key;
let value = entry.value;
let tmpKey = '_';
if (key == null) {
key = tmpKey;
}
if (value.size == 0) {
continue;
} else if (value.size == 1) {
simpleHeaders[key] = value.get(0);
} else {
simpleHeaders[key] = value.toString();
}
}
}
return simpleHeaders;
}
public static parseCookie(header: UTSJSONObject | null): string[] {
if (header == null) {
return []
}
let cookiesStr = header.getString('Set-Cookie')
if (cookiesStr == null) {
cookiesStr = header.getString('set-cookie')
}
if (cookiesStr == null) {
return []
}
let cookiesArr = new Array<string>()
if (cookiesStr.charAt(0) == "[" && cookiesStr.charAt(cookiesStr.length - 1) == "]") {
cookiesStr = cookiesStr.slice(1, -1)
}
const handleCookiesArr = cookiesStr.split(';')
for (let i = 0; i < handleCookiesArr.length; i++) {
if (handleCookiesArr[i].indexOf('Expires=') != -1 || handleCookiesArr[i].indexOf('expires=') != -1) {
cookiesArr.push(handleCookiesArr[i].replace(',', ''))
} else {
cookiesArr.push(handleCookiesArr[i])
}
}
cookiesArr = cookiesArr.join(';').split(',')
return cookiesArr
}
}