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,412 @@
import {
RequestOptions,
RequestTask,
UploadTask,
UploadFileOptions,
OnProgressUpdateResult,
UploadFileProgressUpdateCallback,
OnProgressDownloadResult,
DownloadTask,
DownloadFileOptions,
DownloadFileProgressUpdateCallback,
RequestTaskOnHeadersReceivedCallback,
RequestTaskOnHeadersReceivedListenerResult,
RequestTaskOnChunkReceivedCallback,
RequestTaskOnChunkReceivedListenerResult
}
from '../../interface.uts';
import { UTSiOS } from "DCloudUTSFoundation";
import { NSNumber, URLSessionDataDelegate, URL, CharacterSet, URLSession, URLSessionConfiguration, OperationQueue, URLSessionTask, URLResponse, URLSessionDataTask, URLAuthenticationChallengeSender, URLAuthenticationChallenge, URLCredential, URLSessionTaskMetrics, Data, HTTPURLResponse, NSError, URLRequest, ComparisonResult } from 'Foundation';
import { UploadController } from './upload/UploadController.uts';
import { DownloadController } from './download/DownloadController.uts';
import { Int } from 'Swift';
import { NetworkUtil } from './utils/NetworkUtil.uts';
class NetworkRequestListener {
@UTSiOS.keyword("weak")
public task: AnyObject | null = null;
public onStart() : void { }
public onHeadersReceived(statusCode : number, headers : Map<string, any>) : void { }
public onDataReceived(data : Data) : void { }
public onFinished(response : HTTPURLResponse) : void { }
public onFail(error : NSError) : void { }
}
class NetworkUploadFileListener {
@UTSiOS.keyword("weak")
public task: AnyObject | null = null;
public progressListeners : Array<UploadFileProgressUpdateCallback> = new Array<UploadFileProgressUpdateCallback>();
public onProgress(progressUpdate : OnProgressUpdateResult) : void { };
public onDataReceived(data : Data) : void { };
public onFinished(response : HTTPURLResponse) : void { };
public onFail(error : NSError) : void { };
}
class NetworkDownloadFileListener {
@UTSiOS.keyword("weak")
public task: AnyObject | null = null;
public options : DownloadFileOptions | null = null;
public progressListeners : Array<DownloadFileProgressUpdateCallback> = new Array<DownloadFileProgressUpdateCallback>();
public onProgress(progressUpdate : OnProgressDownloadResult) : void { };
public onFinished(response : HTTPURLResponse, filePath : string) : void { };
public onFail(error : NSError) : void { };
}
class NetworkRequestTaskImpl implements RequestTask {
public headersReceivedListeners = new Map<number, RequestTaskOnHeadersReceivedCallback>()
public chunkReceivedListeners = new Map<number, RequestTaskOnChunkReceivedCallback>()
private requestTaskOnHeadersReceivedCallbackCount: number = 0
private requestTaskOnChunkReceivedCallbackCount: number = 0
private semaphore = DispatchSemaphore(value = 1)
private task : URLSessionDataTask | null = null;
constructor(task : URLSessionDataTask | null) {
this.task = task;
super();
}
public abort() {
this.task?.cancel()
UTSiOS.destroyInstance(this)
}
public onHeadersReceived(listener: RequestTaskOnHeadersReceivedCallback): number {
this.semaphore.wait()
this.requestTaskOnHeadersReceivedCallbackCount += 1
this.semaphore.signal()
this.headersReceivedListeners.set(this.requestTaskOnHeadersReceivedCallbackCount, listener)
return this.requestTaskOnHeadersReceivedCallbackCount
}
public offHeadersReceived(listener ?: number | RequestTaskOnHeadersReceivedCallback | null): void {
if (listener != null && typeof listener! == "number") {
const id = listener as number
this.headersReceivedListeners.delete(id)
} else {
this.headersReceivedListeners.clear()
}
}
public onChunkReceived(listener: RequestTaskOnChunkReceivedCallback): number {
this.semaphore.wait()
this.requestTaskOnChunkReceivedCallbackCount += 1
this.semaphore.signal()
this.chunkReceivedListeners.set(this.requestTaskOnChunkReceivedCallbackCount, listener)
return this.requestTaskOnChunkReceivedCallbackCount
}
public offChunkReceived(listener ?: number | RequestTaskOnChunkReceivedCallback | null): void {
if (listener != null && typeof listener! == "number") {
const id = listener as number
this.chunkReceivedListeners.delete(id)
} else {
this.chunkReceivedListeners.clear()
}
}
}
class NetworkManager implements URLSessionDataDelegate {
private static instance : NetworkManager | null = null;
private enableChunked: boolean = false;
private session : URLSession | null = null;
private taskMap : Map<URLSessionTask, NetworkRequestListener> = new Map<URLSessionDataTask, NetworkRequestListener>();
public static getInstance() : NetworkManager {
if (this.instance == null) {
this.instance = new NetworkManager();
}
return this.instance!;
}
public request<T>(param : RequestOptions<T>, listener : NetworkRequestListener) : RequestTask {
let request = this.createRequest(param);
this.enableChunked = param.enableChunked ?? false
if (request == null) {
let error = new NSError(domain = "invalid URL", code = 600009);
listener.onFail(error);
let task = new NetworkRequestTaskImpl(null);
listener.task = task;
return task;
}
if (this.session == null) {
let urlSessionConfig = URLSessionConfiguration.default;
this.session = new URLSession(configuration = urlSessionConfig, delegate = this, delegateQueue = OperationQueue.current);
}
let task = this.session?.dataTask(with = request!);
task?.resume();
if (task != null) {
this.taskMap.set(task!, listener);
}
let requestTask = new NetworkRequestTaskImpl(task);
listener.task = requestTask;
return requestTask;
}
public uploadFile(options : UploadFileOptions, listener : NetworkUploadFileListener) : UploadTask {
return UploadController.getInstance().uploadFile(options, listener);
}
public downloadFile(options : DownloadFileOptions, listener : NetworkDownloadFileListener) : DownloadTask {
return DownloadController.getInstance().downloadFile(options, listener);
}
public createRequest<T>(param : RequestOptions<T>) : URLRequest | null {
const encodeUrl = this.percentEscapedString(param.url)
let url = new URL(string = encodeUrl);
if (url == null) {
return null
}
let timeout = param.timeout == null ? 120000 : param.timeout;
let timeoutInterval = new Double(timeout!) / 1000;
let request = new URLRequest(url = url!, cachePolicy = URLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval = timeoutInterval);
request.httpShouldHandleCookies = true;
let method = param.method;
if (method == null || method!.trimmingCharacters(in = CharacterSet.whitespacesAndNewlines).count == 0) {
method = "GET";
}
request.httpMethod = method!;
let ua = UTSiOS.getUserAgent();
request.setValue(ua, forHTTPHeaderField = "User-Agent");
if (param.header == null) {
param.header = {}
}
let headers = param.header?.toMap();
let hasContentType = false;
if (headers != null) {
for (entry in headers!) {
let key = entry.key;
let value = entry.value;
if (key.caseInsensitiveCompare("Content-Type") == ComparisonResult.orderedSame) {
hasContentType = true;
}
let valueStr = "";
if (value instanceof UTSJSONObject) {
valueStr = JSON.stringify(value) ?? ""
} else if (value instanceof Map<string, any>) {
valueStr = JSON.stringify(new UTSJSONObject(value)) ?? ""
} else {
valueStr = `${value}`
}
request.setValue(valueStr, forHTTPHeaderField = key);
}
}
if (!hasContentType) {
if ("GET" != method) {
request.setValue("application/json", forHTTPHeaderField = "Content-Type");
}
}
if ("GET" == method) {
const data = param.data;
if (data != null) {
let json : UTSJSONObject | null = null;
if (typeof (data) == 'string') {
json = JSON.parseObject(data as string);
} else if (data instanceof UTSJSONObject) {
json = data as UTSJSONObject;
} else if (data instanceof Map<string, any>) {
json = new UTSJSONObject(data!)
}
if (json != null) {
let urlWithQuery = this.stringifyQuery(encodeUrl, json!)
let url = new URL(string = urlWithQuery);
request.url = url;
}
}
} else if (param.data != null) {
let bodyData : Data | null = null;
if (typeof (param.data) == 'string') {
bodyData = (param.data as string).data(using = String.Encoding.utf8);
} else if (param.data instanceof Map<string, any>) {
let body : string | null = "";
const contentType = request.value(forHTTPHeaderField = "Content-Type")
const data = new UTSJSONObject(param.data!);
if (contentType != null) {
if (contentType!.indexOf("application/x-www-form-urlencoded") == 0) {
const map : Map<string, any | null> = data.toMap();
const bodyArray = new Array<string>();
map.forEach((value, key) => {
bodyArray.push(key + "=" + `${value ?? "null"}`);
})
body = bodyArray.join("&");
} else {
body = JSON.stringify(data);
}
bodyData = body?.data(using = String.Encoding.utf8);
}
} else if (param.data instanceof ArrayBuffer) {
const arrayBuffer = param.data as ArrayBuffer
bodyData = arrayBuffer.toData()
} else if (param.data instanceof UTSJSONObject) {
let body : string | null = "";
const contentType = request.value(forHTTPHeaderField = "Content-Type")
if (contentType != null) {
if (contentType!.indexOf("application/x-www-form-urlencoded") == 0) {
const data = param.data as UTSJSONObject;
const map : Map<string, any | null> = data.toMap();
const bodyArray = new Array<string>();
map.forEach((value, key) => {
bodyArray.push(key + "=" + `${value ?? "null"}`);
})
body = bodyArray.join("&");
} else {
body = JSON.stringify(param.data);
}
bodyData = body?.data(using = String.Encoding.utf8);
}
}
if (bodyData == null) {
return null;
}
request.httpBody = bodyData;
}
return request;
}
/**
* data拼接到url上
*/
private stringifyQuery(url : string, data : UTSJSONObject) : string {
let newUrl = url;
//http:xxx/xxx?a=b&c=d#123
let str = url.split('#')
let hash = ''
if (str.length > 1) {
hash = str[1] //123
}
str = str[0].split('?')
let query = ''
if (str.length > 1) {
query = str[1] //a=b&c=d
}
newUrl = str[0] // http:xxx/xxx
const pairs = query.split('&')
const queryMap = new Map<string, string>();
pairs.forEach((item : string, index : number) => {
const temp = item.split('=')
if (temp.length > 1) {
queryMap[temp[0]] = temp[1]
}
});
const dataMap : Map<string, any | null> = data.toMap();
dataMap.forEach((value, key) => {
if (value == null) {
value = "";
}
let encodeKey = encodeURIComponent(key)!
if (value instanceof UTSJSONObject || value instanceof Array<any | null>) {
queryMap[encodeKey] = encodeURIComponent(JSON.stringify(value)!)!
} else if (value instanceof Map<string, any>) {
queryMap[encodeKey] = encodeURIComponent(JSON.stringify(new UTSJSONObject(value))!)!
} else {
queryMap[encodeKey] = encodeURIComponent(`${value!}`)!;
}
})
let queryStr = "";
queryMap.forEach((value, key) => {
queryStr += key + "=" + value + "&"
});
queryStr = queryStr.slice(0, -1);
if (queryStr.length > 0) {
newUrl += "?" + queryStr;
}
if (hash.length > 0) {
newUrl += "#" + hash;
}
return newUrl;
}
private percentEscapedString(str : string) : string {
//如果url已经有部分经过encode,那么需要先decode再encode。
return str.removingPercentEncoding?.addingPercentEncoding(withAllowedCharacters = CharacterSet.urlQueryAllowed) ?? str
}
//mark --- URLSessionDataDelegate
urlSession(session : URLSession, @argumentLabel("") task : URLSessionTask, @argumentLabel("didSendBodyData") bytesSent : Int64, @argumentLabel("") totalBytesSent : Int64, @argumentLabel("") totalBytesExpectedToSend : Int64) {
//todo 原生的onDataSent貌似没实现 ,考虑删掉这个回调。
}
urlSession(session : URLSession, @argumentLabel("") dataTask : URLSessionDataTask, @argumentLabel("didReceive") response : URLResponse, @argumentLabel("") @escaping completionHandler : (dis : URLSession.ResponseDisposition) => void) {
// response开始的时候的header回调
let listener = this.taskMap.get(dataTask);
if (listener != null) {
let httpResponse : HTTPURLResponse = response as HTTPURLResponse;
let statusCode = new NSNumber(value = httpResponse.statusCode);
listener?.onHeadersReceived(statusCode, httpResponse.allHeaderFields as Map<string, any>);
const headers = NetworkUtil.convertHeaders(httpResponse.allHeaderFields as Map<string, any>))
if (listener!.task != null && listener!.task instanceof NetworkRequestTaskImpl) {
const task = listener!.task! as NetworkRequestTaskImpl
task.headersReceivedListeners.forEach((value : RequestTaskOnHeadersReceivedCallback, key : number) => {
const result: RequestTaskOnHeadersReceivedListenerResult = {
cookies: NetworkUtil.parseCookie(headers),
header: headers,
statusCode: statusCode
}
value(result)
})
}
}
completionHandler(URLSession.ResponseDisposition.allow);
}
urlSession(session : URLSession, @argumentLabel("") dataTask : URLSessionDataTask, @argumentLabel("didReceive") data : Data) {
let listener = this.taskMap.get(dataTask);
listener?.onDataReceived(data);
if (this.enableChunked && listener != null) {
const task = listener!.task! as NetworkRequestTaskImpl
task.chunkReceivedListeners.forEach((value : RequestTaskOnChunkReceivedCallback, key : number) => {
const result : RequestTaskOnChunkReceivedListenerResult = {
data: ArrayBuffer.fromData(data)
}
value(result)
})
}
}
urlSession(session : URLSession, @argumentLabel("") task : URLSessionTask, @argumentLabel("didCompleteWithError") error : NSError | null) {
let listener = this.taskMap.get(task);
if (error != null) {
listener?.onFail(error as NSError);
} else {
listener?.onFinished(task.response as HTTPURLResponse);
}
this.taskMap.delete(task);
}
//todo 暂时证书验证先不实现。
// urlSession( session: URLSession, @argumentLabel("didReceive") challenge: URLAuthenticationChallenge, @escaping completionHandler:(dis:URLSession.AuthChallengeDisposition, credentiual:URLCredential)=>void) {
// console.log("didReceivechallenge");
// }
}
export {
NetworkManager,
NetworkRequestListener,
NetworkUploadFileListener,
NetworkDownloadFileListener
}
@@ -0,0 +1,81 @@
class StatusCode {
private static statusCodeMap : Map<string, string> | null = null;
private static initStatusCodeMap() {
let map = new Map<string, string>();
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");
this.statusCodeMap = map;
}
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,20 @@
/**
* 弱引用的包装类
*/
export class WeakRef<T extends Any>{
@UTSiOS.keyword("weak")
private value: T | null = null
constructor(value: T | null){
this.value = value;
}
public get(): T | null{
return this.value
}
}
@@ -0,0 +1,287 @@
import { DownloadFileOptions, DownloadTask, DownloadFileProgressUpdateCallback, OnProgressDownloadResult } from '../../../interface.uts';
import { NetworkDownloadFileListener } from '../NetworkManager.uts';
import { UUID, Data, URL, URLResourceKey, URLSessionDataTask, URLSessionTask, URLSession, URLSessionConfiguration, OperationQueue, URLSessionDataDelegate, URLSessionDownloadTask, NSError, URLSessionDownloadDelegate, URLRequest, FileManager, NSString, NSTemporaryDirectory, NSHomeDirectory , CharacterSet , HTTPURLResponse } from 'Foundation';
import { } from 'CommonCrypto';
import { Int, UnsafeBufferPointer, UnsafeRawBufferPointer } from 'Swift';
import { ObjCBool } from "ObjectiveC";
class NetworkDownloadTaskImpl implements DownloadTask {
private task : URLSessionDownloadTask | null = null;
private listener : NetworkDownloadFileListener | null = null;
constructor(task : URLSessionDownloadTask | null, listener : NetworkDownloadFileListener) {
this.task = task;
this.listener = listener;
super();
}
public abort() {
this.task?.cancel()
UTSiOS.destroyInstance(this)
}
public onProgressUpdate(option : DownloadFileProgressUpdateCallback) {
const kListener = this.listener;
if (kListener != null) {
kListener!.progressListeners.add(option);
}
}
}
export class DownloadController implements URLSessionDownloadDelegate {
private static instance : DownloadController | null = null
private session : URLSession | null = null;
private taskMap : Map<URLSessionTask, NetworkDownloadFileListener> = new Map<URLSessionTask, NetworkDownloadFileListener>();
public static getInstance() : DownloadController {
if (this.instance == null) {
this.instance = new DownloadController();
}
return this.instance!;
}
public downloadFile(options : DownloadFileOptions, listener : NetworkDownloadFileListener) : DownloadTask {
let request = this.createDownloadRequest(options, listener);
if (request == null) {
const task = new NetworkDownloadTaskImpl(null, listener)
listener.task = task;
return task;
}
if (this.session == null) {
let urlSessionConfig = URLSessionConfiguration.default;
this.session = new URLSession(configuration = urlSessionConfig, delegate = this, delegateQueue = OperationQueue.current);
}
let task = this.session?.downloadTask(with = request!);
task?.resume();
if (task != null) {
this.taskMap.set(task!, listener);
}
let requestTask = new NetworkDownloadTaskImpl(task, listener);
listener.task = requestTask
return requestTask;
}
private createDownloadRequest(options : DownloadFileOptions, listener : NetworkDownloadFileListener) : URLRequest | null {
const encodeUrl = this.percentEscapedString(options.url)
let url = new URL(string = encodeUrl);
if (url == null) {
let error = new NSError(domain = "invalid URL", code = 600009);
listener.onFail(error);
return null
}
let timeout = options.timeout == null ? 120000 : options.timeout;
let timeoutInterval = new Double(timeout!) / 1000;
let request = new URLRequest(url = url!, cachePolicy = URLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval = timeoutInterval);
request.httpShouldHandleCookies = true;
request.httpMethod = "GET";
let ua = UTSiOS.getUserAgent();
request.setValue(ua, forHTTPHeaderField = "User-Agent");
let headers = options.header?.toMap();
if (headers != null) {
for (entry in headers!) {
let key = entry.key;
let value = entry.value;
let valueStr = "";
if (value instanceof UTSJSONObject) {
valueStr = JSON.stringify(value) ?? ""
}else if(value instanceof Map<string, any>){
valueStr = JSON.stringify(new UTSJSONObject(value)) ?? ""
}else{
valueStr = `${value}`
}
request.setValue(valueStr, forHTTPHeaderField = key);
}
}
return request;
}
private percentEscapedString(str: string): string {
//如果url已经有部分经过encode,那么需要先decode再encode。
return str.removingPercentEncoding?.addingPercentEncoding(withAllowedCharacters= CharacterSet.urlQueryAllowed) ?? str
}
private convertToMD5(param : string) : string {
const strData = param.data(using = String.Encoding.utf8)!
let digest = new Array<UInt8>(repeating = 0, count = new Int(CC_MD5_DIGEST_LENGTH))
strData.withUnsafeBytes((body : UnsafeRawBufferPointer) => {
CC_MD5(body.baseAddress, new UInt32(strData.count), UTSiOS.getPointer(digest))
})
let md5String = ""
digest.forEach((byte : UInt8) => {
md5String += new String(format = "%02x", new UInt8(byte))
})
return md5String
}
private isSandBoxPath(path : string) : boolean {
return path.startsWith(NSHomeDirectory())
}
private getTempPath() : string {
let cacheDirectory = FileManager.default.urls(for = FileManager.SearchPathDirectory.cachesDirectory, in = FileManager.SearchPathDomainMask.userDomainMask).first!
return cacheDirectory.path
}
private getRealPath() : string {
return this.getTempPath() + "/uni-download/"
}
private getTargetPath(options : DownloadFileOptions | null, fileName : string, listener : NetworkDownloadFileListener | null) : string | null {
let targetPath = ""
let specifyPath = options?.filePath ?? ""
if(specifyPath.startsWith("unifile://")){
specifyPath = UTSiOS.getResourceAbsolutePath(specifyPath, null);
}
let hasFileName = false;
if (specifyPath != "") {
const pos = specifyPath.lastIndexOf("/")
if (pos == specifyPath.length - 1) {
//如果filePath是目录
if (this.isSandBoxPath(specifyPath)) {
targetPath = specifyPath
} else {
targetPath = this.getTempPath() + "/" + specifyPath
}
} else {
let path = "";
if (this.isSandBoxPath(specifyPath)) {
path = specifyPath;
} else {
path = this.getTempPath() + "/" + specifyPath
}
let fileManager = FileManager.default
var isDirectory : ObjCBool = false
if (fileManager.fileExists(atPath = path, isDirectory = UTSiOS.getPointer(isDirectory))) {
if (isDirectory.boolValue) {
let error = new NSError(domain = "The target file path is already a directory file, and file creation failed.", code = 602001);
listener?.onFail(error);
return null
}
}
targetPath = path
hasFileName = true
}
} else {
targetPath = this.getRealPath()
}
if(!hasFileName){
targetPath += fileName
}
let fileManager = FileManager.default
if (fileManager.fileExists(atPath = targetPath)) {
const index = targetPath.lastIndexOf(".");
let tFileName = targetPath;
let tFileType = "";
if (index >= 0) {
tFileName = targetPath.substring(0, index as Int)
tFileType = targetPath.substring(index as Int)
}
var number = 1
while (fileManager.fileExists(atPath = targetPath)) {
targetPath = tFileName + `(${number})` + tFileType;
number++
}
}
return targetPath
}
private getFileName(fileName : string, url : URL | null) : string {
var suggestedFilename = fileName
if (suggestedFilename != "") {
let cString = suggestedFilename.cString(using = String.Encoding.isoLatin1)
if (cString != null) {
suggestedFilename = new String(cString = cString!, encoding = String.Encoding.utf8) ?? suggestedFilename
}
let cleanUri = suggestedFilename.removingPercentEncoding
if (cleanUri != null && cleanUri!.length > 0) {
suggestedFilename = cleanUri!
}
suggestedFilename = suggestedFilename.replacingOccurrences(of = "/", with = "")
suggestedFilename = suggestedFilename.replacingOccurrences(of = "\\", with = "")
} else {
if (url == null) {
suggestedFilename = ""
} else {
suggestedFilename = this.convertToMD5(url!.absoluteString)
}
}
if (suggestedFilename.length > 255) {
let extensionType = (suggestedFilename as NSString).pathExtension
suggestedFilename = this.convertToMD5((suggestedFilename as NSString).deletingPathExtension)
if (extensionType != "") {
suggestedFilename = (suggestedFilename as NSString).appendingPathExtension(extensionType) ?? suggestedFilename
}
}
return suggestedFilename
}
//mark --- URLSessionDownloadDelegate
urlSession(session : URLSession, @argumentLabel("") downloadTask : URLSessionDownloadTask, @argumentLabel("didWriteData") bytesWritten : Int64, @argumentLabel("") totalBytesWritten : Int64, @argumentLabel("") totalBytesExpectedToWrite : Int64) {
let listener = this.taskMap.get(downloadTask);
const progress = (Number.from(totalBytesWritten) / totalBytesExpectedToWrite) * 100
const progressUpdate : OnProgressDownloadResult = {
progress: progress.toInt(),
totalBytesWritten: totalBytesWritten,
totalBytesExpectedToWrite: totalBytesExpectedToWrite
}
listener?.onProgress(progressUpdate)
}
urlSession(session : URLSession, @argumentLabel("") downloadTask : URLSessionDownloadTask, @argumentLabel("didFinishDownloadingTo") location : URL) {
let listener = this.taskMap.get(downloadTask);
let suggestedFilename = downloadTask.response?.suggestedFilename
const statusCode = (downloadTask.response as HTTPURLResponse).statusCode
if(statusCode - 200 < 100 && statusCode - 200 >= 0) {
const fileName = this.getFileName(suggestedFilename ?? "", downloadTask.response?.url)
let destPath = this.getTargetPath(listener?.options, fileName, listener);
if (destPath != null) {
let fileManager = FileManager.default
try {
let directoryPath = (destPath as NSString).deletingLastPathComponent
if (!fileManager.fileExists(atPath = directoryPath)) {
UTSiOS.try(fileManager.createDirectory(atPath = directoryPath, withIntermediateDirectories = true, attributes = null), "?")
}
UTSiOS.try(fileManager.moveItem(atPath = location.path, toPath = destPath!), "?")
listener?.onFinished(downloadTask.response as HTTPURLResponse, destPath!);
} catch {
let error = new NSError(domain = "file move fail", code = 602001);
listener?.onFail(error);
}
}
} else {
let error = new NSError(domain = "request fail", code = statusCode);
listener?.onFail(error);
}
this.taskMap.delete(downloadTask);
}
urlSession(session : URLSession, @argumentLabel("") task : URLSessionTask, @argumentLabel("didCompleteWithError") error : NSError | null) {
if(error != null){
let listener = this.taskMap.get(task);
listener?.onFail(error as NSError);
this.taskMap.delete(task);
}
}
}
@@ -0,0 +1,261 @@
import { UploadFileOptions, UploadTask, UploadFileProgressUpdateCallback, UploadFileOptionFiles, OnProgressUpdateResult } from '../../../interface.uts';
import { NetworkUploadFileListener } from '../NetworkManager.uts'
import { CharacterSet, HTTPURLResponse, UUID, Data, URL, URLResourceKey, URLSessionDataTask, URLSessionTask, URLSession, URLSessionConfiguration, OperationQueue, URLSessionDataDelegate, URLSessionUploadTask, NSError , NSMutableData , NSMutableSet , URLRequest} from 'Foundation';
import { UTTypeCreatePreferredIdentifierForTag, UTTypeCopyPreferredTagWithClass, kUTTagClassFilenameExtension, kUTTagClassMIMEType } from 'MobileCoreServices';
import { CFString } from 'CoreFoundation';
class NetworkUploadTaskImpl implements UploadTask {
private task : URLSessionDataTask | null = null;
private listener : NetworkUploadFileListener | null = null;
constructor(task : URLSessionDataTask | null, listener : NetworkUploadFileListener) {
this.task = task;
this.listener = listener;
super();
}
public abort() {
this.task?.cancel()
UTSiOS.destroyInstance(this)
}
public onProgressUpdate(option : UploadFileProgressUpdateCallback) {
const kListener = this.listener;
if (kListener != null) {
kListener!.progressListeners.add(option);
}
}
}
class UploadController implements URLSessionDataDelegate {
private static instance : UploadController | null = null
private session : URLSession | null = null;
private taskMap : Map<URLSessionTask, NetworkUploadFileListener> = new Map<URLSessionTask, NetworkUploadFileListener>();
public static getInstance() : UploadController {
if (this.instance == null) {
this.instance = new UploadController();
}
return this.instance!;
}
public uploadFile(options : UploadFileOptions, listener : NetworkUploadFileListener) : UploadTask {
let boundary = `----${new UUID().uuidString}`
let request = this.createRequest(options, listener, boundary);
if (request == null) {
const task = new NetworkUploadTaskImpl(null, listener)
listener.task = task
return task;
}
if (this.session == null) {
let urlSessionConfig = URLSessionConfiguration.default;
this.session = new URLSession(configuration = urlSessionConfig, delegate = this, delegateQueue = OperationQueue.current);
}
const bodyData = this.createBody(boundary, options, listener);
if (bodyData == null) {
const task = new NetworkUploadTaskImpl(null, listener);
listener.task = task
return task
}
let task = this.session?.uploadTask(with = request!, from = bodyData!);
task?.resume();
if (task != null) {
this.taskMap.set(task!, listener);
}
let requestTask = new NetworkUploadTaskImpl(task, listener);
listener.task = requestTask
return requestTask;
}
private createRequest(param : UploadFileOptions, listener : NetworkUploadFileListener, boundary : string) : URLRequest | null {
const encodeUrl = this.percentEscapedString(param.url)
let url = new URL(string = encodeUrl);
if (url == null) {
let error = new NSError(domain = "invalid URL", code = 600009);
listener.onFail(error);
return null
}
let timeout = param.timeout == null ? 120000 : param.timeout;
let timeoutInterval = new Double(timeout!) / 1000;
let request = new URLRequest(url = url!, cachePolicy = URLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval = timeoutInterval);
request.httpShouldHandleCookies = true;
request.httpMethod = "POST";
request.setValue(`multipart/form-data; boundary=${boundary}`, forHTTPHeaderField = "Content-Type");
let ua = UTSiOS.getUserAgent();
request.setValue(ua, forHTTPHeaderField = "User-Agent");
let headers = param.header?.toMap();
if (headers != null) {
for (entry in headers!) {
let key = entry.key;
let value = entry.value;
let valueStr = "";
if (value instanceof UTSJSONObject) {
valueStr = JSON.stringify(value) ?? ""
}else if(value instanceof Map<string, any>){
valueStr = JSON.stringify(new UTSJSONObject(value)) ?? ""
}else{
valueStr = `${value}`
}
request.setValue(valueStr, forHTTPHeaderField = key);
}
}
return request;
}
private createBody(boundary : string, options : UploadFileOptions, listener : NetworkUploadFileListener) : Data | null {
let body = new NSMutableData();
let formData = options.formData?.toMap();
if (formData != null) {
for (entry in formData!) {
const key = entry.key;
const value = entry.value;
if (value != null && typeof (key) == 'string') {
if (value instanceof UTSJSONObject) {
let valueStr = JSON.stringify(value)
if (valueStr != null) {
this.fillTextPart(body, boundary, key, valueStr as string)
}
}else if(value instanceof Map<string, any>){
let valueStr = JSON.stringify(new UTSJSONObject(value))
if (valueStr != null) {
this.fillTextPart(body, boundary, key, valueStr as string)
}
}else{
this.fillTextPart(body, boundary, 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 item = files[i]
const filePath = item.uri;
const name = item.name ?? "file";
if (!this.fillFilePart(body, boundary, name, filePath!, listener)) {
return null;
}
}
} else {
const filePath = options.filePath;
const name = options.name ?? "file";
if (filePath == null) {
let error = new NSError(domain = "filePath is null", code = -1);
listener.onFail(error)
return null;
}
if (!this.fillFilePart(body, boundary, name, filePath!, listener)) {
return null
}
}
body.append(`--${boundary}--\r\n`.data(using = String.Encoding.utf8)!)
return body.copy() as Data;
}
private percentEscapedString(str: string): string {
//如果url已经有部分经过encode,那么需要先decode再encode。
return str.removingPercentEncoding?.addingPercentEncoding(withAllowedCharacters= CharacterSet.urlQueryAllowed) ?? str
}
private fillTextPart(body : NSMutableData, boundary : string, key : string, value : string) {
body.append(`--${boundary}\r\n`.data(using = String.Encoding.utf8)!)
body.append(`Content-Disposition: form-data; name=\"${key}\"\r\n`.data(using = String.Encoding.utf8)!)
body.append("\r\n".data(using = String.Encoding.utf8)!)
body.append(value.data(using = String.Encoding.utf8)!)
body.append("\r\n".data(using = String.Encoding.utf8)!)
}
private fillFilePart(body : NSMutableData, boundary : string, name : string, filePath : string, listener : NetworkUploadFileListener) : boolean {
const absolutePath = new URL(fileURLWithPath = UTSiOS.getResourceAbsolutePath(filePath, null))
const fileData = UTSiOS.try(new Data(contentsOf = absolutePath), "?")
if (fileData == null) {
let error = new NSError(domain = "Illegal file", code = -1);
listener.onFail(error)
return false
}
const mimeType = this.getMimeType(absolutePath)
const fileName = absolutePath.lastPathComponent
const keys = new Swift.Set([URLResourceKey.fileSizeKey])
const resourceValue = UTSiOS.try(absolutePath.resourceValues(forKeys = keys), "?")
const fileSize = resourceValue?.fileSize
body.append(`--${boundary}\r\n`.data(using = String.Encoding.utf8)!)
body.append(`Content-Disposition: form-data; name=\"${name}\"; filename=\"${fileName}\"\r\n`.data(using = String.Encoding.utf8)!)
body.append(`Content-Type: ${mimeType}\r\n`.data(using = String.Encoding.utf8)!)
if (fileSize != null) {
body.append(`Content-Length: ${fileSize!}\r\n`.data(using = String.Encoding.utf8)!)
}
body.append("\r\n".data(using = String.Encoding.utf8)!)
body.append(fileData!)
body.append("\r\n".data(using = String.Encoding.utf8)!)
return true
}
private getMimeType(url : URL) : string {
let pathExtension = url.pathExtension
const uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, null)?.takeRetainedValue()
if (uti != null) {
let mimetype = UTTypeCopyPreferredTagWithClass(uti!, kUTTagClassMIMEType)?.takeRetainedValue()
if (mimetype != null) {
return mimetype as string;
}
}
return "application/octet-stream"
}
//mark --- URLSessionDataDelegate
urlSession(session : URLSession, @argumentLabel("") task : URLSessionTask, @argumentLabel("didSendBodyData") bytesSent : Int64, @argumentLabel("") totalBytesSent : Int64, @argumentLabel("") totalBytesExpectedToSend : Int64) {
let listener = this.taskMap.get(task);
const progress = (Number.from(totalBytesSent) / totalBytesExpectedToSend) * 100
const progressUpdate : OnProgressUpdateResult = {
progress: progress.toInt(),
totalBytesSent: totalBytesSent,
totalBytesExpectedToSend: totalBytesExpectedToSend
}
listener?.onProgress(progressUpdate)
}
urlSession(session : URLSession, @argumentLabel("") dataTask : URLSessionDataTask, @argumentLabel("didReceive") data : Data) {
let listener = this.taskMap.get(dataTask);
listener?.onDataReceived(data);
}
urlSession(session : URLSession, @argumentLabel("") task : URLSessionTask, @argumentLabel("didCompleteWithError") error : NSError | null) {
let listener = this.taskMap.get(task);
if (error != null) {
listener?.onFail(error as NSError);
} else {
listener?.onFinished(task.response as HTTPURLResponse);
}
this.taskMap.delete(task);
}
}
export {
UploadController
}
@@ -0,0 +1,48 @@
export class NetworkUtil {
public static convertHeaders(headers: Map<string, any> | null): UTSJSONObject {
let simpleHeaders = new UTSJSONObject()
if (headers != null) {
headers!.forEach((value: any | null, key: string) => {
if (value == null) {
simpleHeaders.set(key, '')
} else {
simpleHeaders.set(key, value)
}
})
}
return simpleHeaders;
}
public static parseCookie(header : UTSJSONObject | null) : string[] {
if (header == null) {
return []
}
let cookiesStr = header!.getString('Set-Cookie') as string | null
if (cookiesStr == null) {
cookiesStr = header!.getString('set-cookie') as string | null
}
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
}
}