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