feat: 适配新uploadFn,并修复小程序端使用PUT的错误判断

This commit is contained in:
Pan Qiancheng 2025-12-26 17:07:55 +08:00
parent 56b07dc4cb
commit 95b506b0eb
26 changed files with 325 additions and 307 deletions

7
es/types/Cos.d.ts vendored
View File

@ -1,11 +1,8 @@
import { EntityDict } from '../oak-app-domain'; import { EntityDict } from '../oak-app-domain';
import { BRC } from '..'; import { BRC } from '..';
import { Cache } from 'oak-frontend-base/es/features/cache'; import { Cache } from 'oak-frontend-base/es/features/cache';
export type UploadFn = (file: File | string | Blob, name: string, // 文件的part name import { UploadInterface } from 'oak-frontend-base/es/types/Upload';
uploadUrl: string, // 上传的url export type UploadFn = UploadInterface['uploadFile'];
formData: Record<string, any>, // 上传的其它part参数
autoInform?: boolean, // 上传成功是否会自动通知server若不会则需要前台显式通知
getPercent?: Function, uploadId?: string, method?: "POST" | "PUT") => Promise<any>;
export type UploadToAspect = (file: File | string, name: string, // 文件的part name export type UploadToAspect = (file: File | string, name: string, // 文件的part name
aspectName: string, // 上传的aspect名 aspectName: string, // 上传的aspect名
formData: Record<string, any>, // 上传的其它part参数 formData: Record<string, any>, // 上传的其它part参数

View File

@ -43,12 +43,20 @@ export default class ALiYun {
else { else {
let response; let response;
try { try {
response = await uploadFn(file, 'file', uploadMeta.uploadHost, { response = await uploadFn({
key: uploadMeta.key, file: file,
policy: uploadMeta.policy, name: 'file',
ossAccessKeyId: uploadMeta.accessKey, uploadUrl: uploadMeta.uploadHost,
signature: uploadMeta.signature, formData: {
}, true, getPercent, extraFile.id); key: uploadMeta.key,
policy: uploadMeta.policy,
ossAccessKeyId: uploadMeta.accessKey,
signature: uploadMeta.signature,
},
autoInform: true,
getPercent,
uploadId: extraFile.id,
});
} }
catch (err) { catch (err) {
// 网络错误 // 网络错误

View File

@ -1,5 +1,4 @@
import { sliceFile, cleanTempFiles } from '../files/slice'; import { sliceFile } from '../files/slice';
import assert from 'assert';
import { OakUploadException } from '../../types/Exception'; import { OakUploadException } from '../../types/Exception';
export function isAbortError(error) { export function isAbortError(error) {
return error instanceof DOMException && error.name === 'AbortError'; return error instanceof DOMException && error.name === 'AbortError';
@ -37,25 +36,33 @@ export async function chunkUpload(options) {
let lastError; let lastError;
for (let attempt = 0; attempt <= retryTimes; attempt++) { for (let attempt = 0; attempt <= retryTimes; attempt++) {
try { try {
const response = await uploadFn(chunk, 'file', part.uploadUrl, part.formData || {}, true, (percent) => { let data;
// 更新每个分片的进度 if (chunk.type === 'getter') {
updateChunkPercent(part.partNumber, percent); data = await chunk.getFile();
}, `${extraFile.id}:${part.partNumber}`, "PUT");
// 验证上传是否成功
let isSuccess = false;
if (process.env.OAK_PLATFORM === 'wechatMp') {
if (response.errMsg === 'request:ok') {
const data = JSON.parse(response.data);
isSuccess = !!(data.status === 204 || data.status === 200);
}
} }
else { else {
isSuccess = !!(response.status === 200 || response.status === 204); data = chunk;
} }
const response = await uploadFn({
file: data,
name: 'file',
uploadUrl: part.uploadUrl,
formData: part.formData || {},
autoInform: true,
getPercent: (percent) => {
// 更新每个分片的进度
updateChunkPercent(part.partNumber, percent);
},
uploadId: `${extraFile.id}:${part.partNumber}`,
method: "PUT"
});
// 验证上传是否成功
let isSuccess = false;
isSuccess = !!(response.status === 200 || response.status === 204);
if (isSuccess) { if (isSuccess) {
// 标记该分片已完成 // // 标记该分片已完成
part.etag = response.headers?.get("ETag") || response.headers?.get("etag") || response.headers?.get("eTag"); // part.etag = (response as Response).headers?.get("ETag") || response.headers?.get("etag") || response.headers?.get("eTag")
assert(part.etag, `无法获取分片 ${part.partNumber} 的 ETag`); // assert(part.etag, `无法获取分片 ${part.partNumber} 的 ETag`);
return; return;
} }
throw new OakUploadException(`分片 ${part.partNumber} 上传失败`); throw new OakUploadException(`分片 ${part.partNumber} 上传失败`);
@ -122,9 +129,5 @@ export async function chunkUpload(options) {
// if (onChunkSuccess) { // if (onChunkSuccess) {
// await onChunkSuccess(chunkInfo); // await onChunkSuccess(chunkInfo);
// } // }
// 清理小程序环境下的临时文件
if (process.env.OAK_PLATFORM === 'wechatMp' && typeof file === 'string') {
await cleanTempFiles(chunks);
}
return; return;
} }

View File

@ -30,12 +30,20 @@ export default class CTYun {
assert(extraFile.enableChunkedUpload !== true, '天翼云暂不支持分片上传'); assert(extraFile.enableChunkedUpload !== true, '天翼云暂不支持分片上传');
let response; let response;
try { try {
response = await uploadFn(file, 'file', uploadMeta.uploadHost, { response = await uploadFn({
key: uploadMeta.key, file: file,
Policy: uploadMeta.policy, name: 'file',
AWSAccessKeyId: uploadMeta.accessKey, uploadUrl: uploadMeta.uploadHost,
signature: uploadMeta.signature, formData: {
}, true, getPercent, extraFile.id); key: uploadMeta.key,
Policy: uploadMeta.policy,
AWSAccessKeyId: uploadMeta.accessKey,
signature: uploadMeta.signature,
},
autoInform: true,
getPercent,
uploadId: extraFile.id,
});
} }
catch (err) { catch (err) {
// 网络错误 // 网络错误

View File

@ -30,10 +30,18 @@ export default class Qiniu {
assert(extraFile.enableChunkedUpload !== true, '暂不支持分片上传'); assert(extraFile.enableChunkedUpload !== true, '暂不支持分片上传');
let response; let response;
try { try {
response = await uploadFn(file, 'file', uploadMeta.uploadHost, { response = await uploadFn({
key: uploadMeta.key, file,
token: uploadMeta.uploadToken, name: 'file',
}, true, getPercent, extraFile.id); uploadUrl: uploadMeta.uploadHost,
formData: {
key: uploadMeta.key,
token: uploadMeta.uploadToken,
},
autoInform: true,
getPercent,
uploadId: extraFile.id,
});
} }
catch (err) { catch (err) {
// 网络错误 // 网络错误

View File

@ -46,22 +46,23 @@ export default class S3 {
let response; let response;
try { try {
// S3 使用预签名 URL 直接上传,不需要额外的 formData // S3 使用预签名 URL 直接上传,不需要额外的 formData
response = await uploadFn(file, 'file', uploadMeta.uploadUrl, {}, true, getPercent, extraFile.id, "PUT"); response = await uploadFn({
file,
name: 'file',
uploadUrl: uploadMeta.uploadUrl,
formData: {},
autoInform: true,
getPercent,
uploadId: extraFile.id,
method: "PUT",
isFilePath: true,
});
} }
catch (err) { catch (err) {
throw new OakNetworkException('网络异常,请求失败'); throw new OakNetworkException('网络异常,请求失败');
} }
let isSuccess = false; let isSuccess = false;
if (process.env.OAK_PLATFORM === 'wechatMp') { isSuccess = response.status === 200 || response.status === 204;
// 小程序端上传
if (response.errMsg === 'uploadFile:ok') {
const statusCode = response.statusCode;
isSuccess = statusCode === 200 || statusCode === 204;
}
}
else {
isSuccess = response.status === 200 || response.status === 204;
}
if (isSuccess) { if (isSuccess) {
return; return;
} }

View File

@ -31,15 +31,23 @@ export default class TencentYun {
assert(extraFile.enableChunkedUpload !== true, '暂不支持分片上传'); assert(extraFile.enableChunkedUpload !== true, '暂不支持分片上传');
let response; let response;
try { try {
response = await uploadFn(file, 'file', uploadMeta.uploadHost, { response = await uploadFn({
key: uploadMeta.key, file,
policy: uploadMeta.policy, name: 'file',
signature: uploadMeta.signature, uploadUrl: uploadMeta.uploadHost,
'q-sign-algorithm': uploadMeta.algorithm, formData: {
'q-ak': uploadMeta.accessKey, key: uploadMeta.key,
'q-key-time': uploadMeta.keyTime, policy: uploadMeta.policy,
'q-signature': uploadMeta.signature, signature: uploadMeta.signature,
}, true, getPercent, extraFile.id); 'q-sign-algorithm': uploadMeta.algorithm,
'q-ak': uploadMeta.accessKey,
'q-key-time': uploadMeta.keyTime,
'q-signature': uploadMeta.signature,
},
autoInform: true,
getPercent,
uploadId: extraFile.id
});
} }
catch (err) { catch (err) {
// 网络错误 // 网络错误

View File

@ -1,3 +1,7 @@
export declare function sliceFile(file: string | File, chunkSize: number, partCount: number): Promise<(File | Blob | string)[]>; export type MpFileGetter = {
export declare function sliceFileForMp(filePath: string, chunkSize: number, partCount: number): Promise<string[]>; type: 'getter';
getFile: () => Promise<string | ArrayBuffer>;
};
export declare function sliceFile(file: string | File, chunkSize: number, partCount: number): Promise<(File | Blob | MpFileGetter)[]>;
export declare function sliceFileForMp(filePath: string, chunkSize: number, partCount: number): Promise<MpFileGetter[]>;
export declare function cleanTempFiles(tempFiles: string[]): Promise<void>; export declare function cleanTempFiles(tempFiles: string[]): Promise<void>;

View File

@ -42,34 +42,17 @@ export async function sliceFileForMp(filePath, chunkSize, partCount) {
const start = i * chunkSize; const start = i * chunkSize;
const length = Math.min(chunkSize, fileSize - start); const length = Math.min(chunkSize, fileSize - start);
try { try {
// 读取分片数据 chunks.push({
const data = await new Promise((resolve, reject) => { type: 'getter',
fs.readFile({ getFile: async () => {
filePath: filePath, // 读取分片数据
encoding: 'binary', const data = fs.readFileSync(filePath, 'binary', start, length);
position: start, return data;
length: length, }
success: (res) => resolve(res.data),
fail: reject
});
}); });
// 写入临时文件
const tempFilePath = `${tempDir}/chunk_${Date.now()}_${i}.temp`;
await new Promise((resolve, reject) => {
fs.writeFile({
filePath: tempFilePath,
encoding: 'binary',
data: data,
success: () => resolve(),
fail: reject
});
});
chunks.push(tempFilePath);
} }
catch (err) { catch (err) {
console.error('分片读取失败:', err); console.error('分片读取失败:', err);
// 清理已创建的临时文件
await cleanTempFiles(chunks);
throw new OakUploadException(`分片 ${i + 1} 读取失败: ${err}`); throw new OakUploadException(`分片 ${i + 1} 读取失败: ${err}`);
} }
} }

7
lib/types/Cos.d.ts vendored
View File

@ -1,11 +1,8 @@
import { EntityDict } from '../oak-app-domain'; import { EntityDict } from '../oak-app-domain';
import { BRC } from '..'; import { BRC } from '..';
import { Cache } from 'oak-frontend-base/es/features/cache'; import { Cache } from 'oak-frontend-base/es/features/cache';
export type UploadFn = (file: File | string | Blob, name: string, // 文件的part name import { UploadInterface } from 'oak-frontend-base/es/types/Upload';
uploadUrl: string, // 上传的url export type UploadFn = UploadInterface['uploadFile'];
formData: Record<string, any>, // 上传的其它part参数
autoInform?: boolean, // 上传成功是否会自动通知server若不会则需要前台显式通知
getPercent?: Function, uploadId?: string, method?: "POST" | "PUT") => Promise<any>;
export type UploadToAspect = (file: File | string, name: string, // 文件的part name export type UploadToAspect = (file: File | string, name: string, // 文件的part name
aspectName: string, // 上传的aspect名 aspectName: string, // 上传的aspect名
formData: Record<string, any>, // 上传的其它part参数 formData: Record<string, any>, // 上传的其它part参数

View File

@ -45,12 +45,20 @@ class ALiYun {
else { else {
let response; let response;
try { try {
response = await uploadFn(file, 'file', uploadMeta.uploadHost, { response = await uploadFn({
key: uploadMeta.key, file: file,
policy: uploadMeta.policy, name: 'file',
ossAccessKeyId: uploadMeta.accessKey, uploadUrl: uploadMeta.uploadHost,
signature: uploadMeta.signature, formData: {
}, true, getPercent, extraFile.id); key: uploadMeta.key,
policy: uploadMeta.policy,
ossAccessKeyId: uploadMeta.accessKey,
signature: uploadMeta.signature,
},
autoInform: true,
getPercent,
uploadId: extraFile.id,
});
} }
catch (err) { catch (err) {
// 网络错误 // 网络错误

View File

@ -2,9 +2,7 @@
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.isAbortError = isAbortError; exports.isAbortError = isAbortError;
exports.chunkUpload = chunkUpload; exports.chunkUpload = chunkUpload;
const tslib_1 = require("tslib");
const slice_1 = require("../files/slice"); const slice_1 = require("../files/slice");
const assert_1 = tslib_1.__importDefault(require("assert"));
const Exception_1 = require("../../types/Exception"); const Exception_1 = require("../../types/Exception");
function isAbortError(error) { function isAbortError(error) {
return error instanceof DOMException && error.name === 'AbortError'; return error instanceof DOMException && error.name === 'AbortError';
@ -42,25 +40,33 @@ async function chunkUpload(options) {
let lastError; let lastError;
for (let attempt = 0; attempt <= retryTimes; attempt++) { for (let attempt = 0; attempt <= retryTimes; attempt++) {
try { try {
const response = await uploadFn(chunk, 'file', part.uploadUrl, part.formData || {}, true, (percent) => { let data;
// 更新每个分片的进度 if (chunk.type === 'getter') {
updateChunkPercent(part.partNumber, percent); data = await chunk.getFile();
}, `${extraFile.id}:${part.partNumber}`, "PUT");
// 验证上传是否成功
let isSuccess = false;
if (process.env.OAK_PLATFORM === 'wechatMp') {
if (response.errMsg === 'request:ok') {
const data = JSON.parse(response.data);
isSuccess = !!(data.status === 204 || data.status === 200);
}
} }
else { else {
isSuccess = !!(response.status === 200 || response.status === 204); data = chunk;
} }
const response = await uploadFn({
file: data,
name: 'file',
uploadUrl: part.uploadUrl,
formData: part.formData || {},
autoInform: true,
getPercent: (percent) => {
// 更新每个分片的进度
updateChunkPercent(part.partNumber, percent);
},
uploadId: `${extraFile.id}:${part.partNumber}`,
method: "PUT"
});
// 验证上传是否成功
let isSuccess = false;
isSuccess = !!(response.status === 200 || response.status === 204);
if (isSuccess) { if (isSuccess) {
// 标记该分片已完成 // // 标记该分片已完成
part.etag = response.headers?.get("ETag") || response.headers?.get("etag") || response.headers?.get("eTag"); // part.etag = (response as Response).headers?.get("ETag") || response.headers?.get("etag") || response.headers?.get("eTag")
(0, assert_1.default)(part.etag, `无法获取分片 ${part.partNumber} 的 ETag`); // assert(part.etag, `无法获取分片 ${part.partNumber} 的 ETag`);
return; return;
} }
throw new Exception_1.OakUploadException(`分片 ${part.partNumber} 上传失败`); throw new Exception_1.OakUploadException(`分片 ${part.partNumber} 上传失败`);
@ -127,9 +133,5 @@ async function chunkUpload(options) {
// if (onChunkSuccess) { // if (onChunkSuccess) {
// await onChunkSuccess(chunkInfo); // await onChunkSuccess(chunkInfo);
// } // }
// 清理小程序环境下的临时文件
if (process.env.OAK_PLATFORM === 'wechatMp' && typeof file === 'string') {
await (0, slice_1.cleanTempFiles)(chunks);
}
return; return;
} }

View File

@ -32,12 +32,20 @@ class CTYun {
(0, assert_1.assert)(extraFile.enableChunkedUpload !== true, '天翼云暂不支持分片上传'); (0, assert_1.assert)(extraFile.enableChunkedUpload !== true, '天翼云暂不支持分片上传');
let response; let response;
try { try {
response = await uploadFn(file, 'file', uploadMeta.uploadHost, { response = await uploadFn({
key: uploadMeta.key, file: file,
Policy: uploadMeta.policy, name: 'file',
AWSAccessKeyId: uploadMeta.accessKey, uploadUrl: uploadMeta.uploadHost,
signature: uploadMeta.signature, formData: {
}, true, getPercent, extraFile.id); key: uploadMeta.key,
Policy: uploadMeta.policy,
AWSAccessKeyId: uploadMeta.accessKey,
signature: uploadMeta.signature,
},
autoInform: true,
getPercent,
uploadId: extraFile.id,
});
} }
catch (err) { catch (err) {
// 网络错误 // 网络错误

View File

@ -32,10 +32,18 @@ class Qiniu {
(0, assert_1.assert)(extraFile.enableChunkedUpload !== true, '暂不支持分片上传'); (0, assert_1.assert)(extraFile.enableChunkedUpload !== true, '暂不支持分片上传');
let response; let response;
try { try {
response = await uploadFn(file, 'file', uploadMeta.uploadHost, { response = await uploadFn({
key: uploadMeta.key, file,
token: uploadMeta.uploadToken, name: 'file',
}, true, getPercent, extraFile.id); uploadUrl: uploadMeta.uploadHost,
formData: {
key: uploadMeta.key,
token: uploadMeta.uploadToken,
},
autoInform: true,
getPercent,
uploadId: extraFile.id,
});
} }
catch (err) { catch (err) {
// 网络错误 // 网络错误

View File

@ -48,22 +48,23 @@ class S3 {
let response; let response;
try { try {
// S3 使用预签名 URL 直接上传,不需要额外的 formData // S3 使用预签名 URL 直接上传,不需要额外的 formData
response = await uploadFn(file, 'file', uploadMeta.uploadUrl, {}, true, getPercent, extraFile.id, "PUT"); response = await uploadFn({
file,
name: 'file',
uploadUrl: uploadMeta.uploadUrl,
formData: {},
autoInform: true,
getPercent,
uploadId: extraFile.id,
method: "PUT",
isFilePath: true,
});
} }
catch (err) { catch (err) {
throw new Exception_2.OakNetworkException('网络异常,请求失败'); throw new Exception_2.OakNetworkException('网络异常,请求失败');
} }
let isSuccess = false; let isSuccess = false;
if (process.env.OAK_PLATFORM === 'wechatMp') { isSuccess = response.status === 200 || response.status === 204;
// 小程序端上传
if (response.errMsg === 'uploadFile:ok') {
const statusCode = response.statusCode;
isSuccess = statusCode === 200 || statusCode === 204;
}
}
else {
isSuccess = response.status === 200 || response.status === 204;
}
if (isSuccess) { if (isSuccess) {
return; return;
} }

View File

@ -33,15 +33,23 @@ class TencentYun {
(0, assert_1.assert)(extraFile.enableChunkedUpload !== true, '暂不支持分片上传'); (0, assert_1.assert)(extraFile.enableChunkedUpload !== true, '暂不支持分片上传');
let response; let response;
try { try {
response = await uploadFn(file, 'file', uploadMeta.uploadHost, { response = await uploadFn({
key: uploadMeta.key, file,
policy: uploadMeta.policy, name: 'file',
signature: uploadMeta.signature, uploadUrl: uploadMeta.uploadHost,
'q-sign-algorithm': uploadMeta.algorithm, formData: {
'q-ak': uploadMeta.accessKey, key: uploadMeta.key,
'q-key-time': uploadMeta.keyTime, policy: uploadMeta.policy,
'q-signature': uploadMeta.signature, signature: uploadMeta.signature,
}, true, getPercent, extraFile.id); 'q-sign-algorithm': uploadMeta.algorithm,
'q-ak': uploadMeta.accessKey,
'q-key-time': uploadMeta.keyTime,
'q-signature': uploadMeta.signature,
},
autoInform: true,
getPercent,
uploadId: extraFile.id
});
} }
catch (err) { catch (err) {
// 网络错误 // 网络错误

View File

@ -1,3 +1,7 @@
export declare function sliceFile(file: string | File, chunkSize: number, partCount: number): Promise<(File | Blob | string)[]>; export type MpFileGetter = {
export declare function sliceFileForMp(filePath: string, chunkSize: number, partCount: number): Promise<string[]>; type: 'getter';
getFile: () => Promise<string | ArrayBuffer>;
};
export declare function sliceFile(file: string | File, chunkSize: number, partCount: number): Promise<(File | Blob | MpFileGetter)[]>;
export declare function sliceFileForMp(filePath: string, chunkSize: number, partCount: number): Promise<MpFileGetter[]>;
export declare function cleanTempFiles(tempFiles: string[]): Promise<void>; export declare function cleanTempFiles(tempFiles: string[]): Promise<void>;

View File

@ -48,34 +48,17 @@ async function sliceFileForMp(filePath, chunkSize, partCount) {
const start = i * chunkSize; const start = i * chunkSize;
const length = Math.min(chunkSize, fileSize - start); const length = Math.min(chunkSize, fileSize - start);
try { try {
// 读取分片数据 chunks.push({
const data = await new Promise((resolve, reject) => { type: 'getter',
fs.readFile({ getFile: async () => {
filePath: filePath, // 读取分片数据
encoding: 'binary', const data = fs.readFileSync(filePath, 'binary', start, length);
position: start, return data;
length: length, }
success: (res) => resolve(res.data),
fail: reject
});
}); });
// 写入临时文件
const tempFilePath = `${tempDir}/chunk_${Date.now()}_${i}.temp`;
await new Promise((resolve, reject) => {
fs.writeFile({
filePath: tempFilePath,
encoding: 'binary',
data: data,
success: () => resolve(),
fail: reject
});
});
chunks.push(tempFilePath);
} }
catch (err) { catch (err) {
console.error('分片读取失败:', err); console.error('分片读取失败:', err);
// 清理已创建的临时文件
await cleanTempFiles(chunks);
throw new Exception_1.OakUploadException(`分片 ${i + 1} 读取失败: ${err}`); throw new Exception_1.OakUploadException(`分片 ${i + 1} 读取失败: ${err}`);
} }
} }

View File

@ -1,17 +1,9 @@
import { EntityDict } from '../oak-app-domain'; import { EntityDict } from '../oak-app-domain';
import { BRC } from '..'; import { BRC } from '..';
import { Cache } from 'oak-frontend-base/es/features/cache' import { Cache } from 'oak-frontend-base/es/features/cache'
import { UploadInterface } from 'oak-frontend-base/es/types/Upload';
export type UploadFn = ( export type UploadFn = UploadInterface['uploadFile']
file: File | string | Blob,
name: string, // 文件的part name
uploadUrl: string, // 上传的url
formData: Record<string, any>, // 上传的其它part参数
autoInform?: boolean, // 上传成功是否会自动通知server若不会则需要前台显式通知
getPercent?: Function,
uploadId?: string,
method?: "POST" | "PUT" //默认POST
) => Promise<any>
export type UploadToAspect = ( export type UploadToAspect = (
file: File | string, file: File | string,

View File

@ -70,18 +70,20 @@ export default class ALiYun implements Cos<EntityDict> {
let response; let response;
try { try {
response = await uploadFn( response = await uploadFn(
file,
'file',
uploadMeta.uploadHost,
{ {
key: uploadMeta.key, file: file,
policy: uploadMeta.policy, name: 'file',
ossAccessKeyId: uploadMeta.accessKey, uploadUrl: uploadMeta.uploadHost,
signature: uploadMeta.signature, formData: {
}, key: uploadMeta.key,
true, policy: uploadMeta.policy,
getPercent, ossAccessKeyId: uploadMeta.accessKey,
extraFile.id!, signature: uploadMeta.signature,
},
autoInform: true,
getPercent,
uploadId: extraFile.id!,
}
); );
} catch (err: any) { } catch (err: any) {
// 网络错误 // 网络错误

View File

@ -1,4 +1,4 @@
import { sliceFile, cleanTempFiles } from '../files/slice'; import { MpFileGetter, sliceFile, cleanTempFiles } from '../files/slice';
import { OpSchema } from '../../oak-app-domain/ExtraFile/Schema'; import { OpSchema } from '../../oak-app-domain/ExtraFile/Schema';
import { UploadFn } from "../../types/Cos"; import { UploadFn } from "../../types/Cos";
import { EntityDict } from '../../oak-app-domain'; import { EntityDict } from '../../oak-app-domain';
@ -59,39 +59,41 @@ export async function chunkUpload(
}, 500); }, 500);
// 上传单个分片的函数,带重试 // 上传单个分片的函数,带重试
const uploadPart = async (part: typeof chunkInfo.parts[0], chunk: File | Blob | string) => { const uploadPart = async (part: typeof chunkInfo.parts[0], chunk: File | Blob | MpFileGetter) => {
let lastError; let lastError;
for (let attempt = 0; attempt <= retryTimes; attempt++) { for (let attempt = 0; attempt <= retryTimes; attempt++) {
try { try {
let data: File | Blob | string;
if (chunk.type === 'getter') {
data = await (chunk as MpFileGetter).getFile() as string;
} else {
data = chunk as File | Blob;
}
const response = await uploadFn( const response = await uploadFn(
chunk, {
'file', file: data,
part.uploadUrl, name: 'file',
part.formData || {}, uploadUrl: part.uploadUrl,
true, formData: part.formData || {},
(percent: number) => { autoInform: true,
// 更新每个分片的进度 getPercent: (percent: number) => {
updateChunkPercent(part.partNumber, percent); // 更新每个分片的进度
}, updateChunkPercent(part.partNumber, percent);
`${extraFile.id}:${part.partNumber}`, },
"PUT" uploadId: `${extraFile.id}:${part.partNumber}`,
method: "PUT"
}
); );
// 验证上传是否成功 // 验证上传是否成功
let isSuccess = false; let isSuccess = false;
if (process.env.OAK_PLATFORM === 'wechatMp') { isSuccess = !!(response.status === 200 || response.status === 204);
if (response.errMsg === 'request:ok') {
const data = JSON.parse(response.data);
isSuccess = !!(data.status === 204 || data.status === 200);
}
} else {
isSuccess = !!(response.status === 200 || response.status === 204);
}
if (isSuccess) { if (isSuccess) {
// 标记该分片已完成 // // 标记该分片已完成
part.etag = (response as Response).headers?.get("ETag") || response.headers?.get("etag") || response.headers?.get("eTag") // part.etag = (response as Response).headers?.get("ETag") || response.headers?.get("etag") || response.headers?.get("eTag")
assert(part.etag, `无法获取分片 ${part.partNumber} 的 ETag`); // assert(part.etag, `无法获取分片 ${part.partNumber} 的 ETag`);
return; return;
} }
@ -167,10 +169,5 @@ export async function chunkUpload(
// await onChunkSuccess(chunkInfo); // await onChunkSuccess(chunkInfo);
// } // }
// 清理小程序环境下的临时文件
if (process.env.OAK_PLATFORM === 'wechatMp' && typeof file === 'string') {
await cleanTempFiles(chunks as string[]);
}
return; return;
} }

View File

@ -53,18 +53,20 @@ export default class CTYun implements Cos<EntityDict> {
let response; let response;
try { try {
response = await uploadFn( response = await uploadFn(
file,
'file',
uploadMeta.uploadHost,
{ {
key: uploadMeta.key, file: file,
Policy: uploadMeta.policy, name: 'file',
AWSAccessKeyId: uploadMeta.accessKey, uploadUrl: uploadMeta.uploadHost,
signature: uploadMeta.signature, formData: {
}, key: uploadMeta.key,
true, Policy: uploadMeta.policy,
getPercent, AWSAccessKeyId: uploadMeta.accessKey,
extraFile.id, signature: uploadMeta.signature,
},
autoInform: true,
getPercent,
uploadId: extraFile.id!,
}
); );
} catch (err) { } catch (err) {
// 网络错误 // 网络错误

View File

@ -53,16 +53,18 @@ export default class Qiniu implements Cos<EntityDict> {
let response; let response;
try { try {
response = await uploadFn( response = await uploadFn(
file,
'file',
uploadMeta.uploadHost,
{ {
key: uploadMeta.key, file,
token: uploadMeta.uploadToken, name: 'file',
}, uploadUrl: uploadMeta.uploadHost,
true, formData: {
getPercent, key: uploadMeta.key,
extraFile.id!, token: uploadMeta.uploadToken,
},
autoInform: true,
getPercent,
uploadId: extraFile.id!,
}
); );
} catch (err) { } catch (err) {
// 网络错误 // 网络错误

View File

@ -75,29 +75,24 @@ export default class S3 implements Cos<EntityDict> {
try { try {
// S3 使用预签名 URL 直接上传,不需要额外的 formData // S3 使用预签名 URL 直接上传,不需要额外的 formData
response = await uploadFn( response = await uploadFn(
file, {
'file', file,
uploadMeta.uploadUrl, name: 'file',
{}, uploadUrl: uploadMeta.uploadUrl,
true, formData: {},
getPercent, autoInform: true,
extraFile.id, getPercent,
"PUT" uploadId: extraFile.id,
method: "PUT",
isFilePath: true,
}
); );
} catch (err) { } catch (err) {
throw new OakNetworkException('网络异常,请求失败'); throw new OakNetworkException('网络异常,请求失败');
} }
let isSuccess = false; let isSuccess = false;
if (process.env.OAK_PLATFORM === 'wechatMp') { isSuccess = response.status === 200 || response.status === 204;
// 小程序端上传
if (response.errMsg === 'uploadFile:ok') {
const statusCode = response.statusCode;
isSuccess = statusCode === 200 || statusCode === 204;
}
} else {
isSuccess = response.status === 200 || response.status === 204;
}
if (isSuccess) { if (isSuccess) {
return; return;

View File

@ -51,25 +51,27 @@ export default class TencentYun implements Cos<EntityDict> {
const { extraFile, uploadFn, file, uploadToAspect, getPercent } = options; const { extraFile, uploadFn, file, uploadToAspect, getPercent } = options;
const uploadMeta = extraFile.uploadMeta! as TencentYunUploadInfo; const uploadMeta = extraFile.uploadMeta! as TencentYunUploadInfo;
assert(extraFile.enableChunkedUpload !== true, '暂不支持分片上传'); assert(extraFile.enableChunkedUpload !== true, '暂不支持分片上传');
let response; let response;
try { try {
response = await uploadFn( response = await uploadFn(
file,
'file',
uploadMeta.uploadHost,
{ {
key: uploadMeta.key, file,
policy: uploadMeta.policy, name: 'file',
signature: uploadMeta.signature, uploadUrl: uploadMeta.uploadHost,
'q-sign-algorithm': uploadMeta.algorithm, formData: {
'q-ak': uploadMeta.accessKey, key: uploadMeta.key,
'q-key-time': uploadMeta.keyTime, policy: uploadMeta.policy,
'q-signature': uploadMeta.signature, signature: uploadMeta.signature,
}, 'q-sign-algorithm': uploadMeta.algorithm,
true, 'q-ak': uploadMeta.accessKey,
getPercent, 'q-key-time': uploadMeta.keyTime,
extraFile.id! 'q-signature': uploadMeta.signature,
},
autoInform: true,
getPercent,
uploadId: extraFile.id!
}
); );
} catch (err) { } catch (err) {
// 网络错误 // 网络错误

View File

@ -1,8 +1,14 @@
import assert from "assert"; import assert from "assert";
import { OakUploadException } from '../../types/Exception'; import { OakUploadException } from '../../types/Exception';
// 小程序专用类型避免下方writeFile报错
export type MpFileGetter = {
type: 'getter',
getFile: () => Promise<string | ArrayBuffer>
};
// 辅助方法:将文件切片 // 辅助方法:将文件切片
export async function sliceFile(file: string | File, chunkSize: number, partCount: number): Promise<(File | Blob | string)[]> { export async function sliceFile(file: string | File, chunkSize: number, partCount: number): Promise<(File | Blob | MpFileGetter)[]> {
if (process.env.OAK_PLATFORM === 'wechatMp') { if (process.env.OAK_PLATFORM === 'wechatMp') {
// 微信小程序环境下file 可能是临时文件路径字符串 // 微信小程序环境下file 可能是临时文件路径字符串
if (typeof file === 'string') { if (typeof file === 'string') {
@ -23,7 +29,7 @@ export async function sliceFile(file: string | File, chunkSize: number, partCoun
} }
// 小程序环境下的文件分片 // 小程序环境下的文件分片
export async function sliceFileForMp(filePath: string, chunkSize: number, partCount: number): Promise<string[]> { export async function sliceFileForMp(filePath: string, chunkSize: number, partCount: number): Promise<MpFileGetter[]> {
const fs = wx.getFileSystemManager(); const fs = wx.getFileSystemManager();
const tempDir = `${wx.env.USER_DATA_PATH}/oak_upload_temp`; const tempDir = `${wx.env.USER_DATA_PATH}/oak_upload_temp`;
@ -36,7 +42,7 @@ export async function sliceFileForMp(filePath: string, chunkSize: number, partCo
fs.mkdirSync(tempDir, false); fs.mkdirSync(tempDir, false);
} }
const chunks: string[] = []; const chunks: MpFileGetter[] = [];
// 读取文件信息 // 读取文件信息
const fileStat = fs.statSync(filePath); const fileStat = fs.statSync(filePath);
@ -49,35 +55,16 @@ export async function sliceFileForMp(filePath: string, chunkSize: number, partCo
const length = Math.min(chunkSize, fileSize - start); const length = Math.min(chunkSize, fileSize - start);
try { try {
// 读取分片数据 chunks.push({
const data = await new Promise<string>((resolve, reject) => { type: 'getter',
fs.readFile({ getFile: async () => {
filePath: filePath, // 读取分片数据
encoding: 'binary', const data = fs.readFileSync(filePath, 'binary', start, length);
position: start, return data;
length: length, }
success: (res) => resolve(res.data as string),
fail: reject
});
}); });
// 写入临时文件
const tempFilePath = `${tempDir}/chunk_${Date.now()}_${i}.temp`;
await new Promise<void>((resolve, reject) => {
fs.writeFile({
filePath: tempFilePath,
encoding: 'binary',
data: data,
success: () => resolve(),
fail: reject
});
});
chunks.push(tempFilePath);
} catch (err) { } catch (err) {
console.error('分片读取失败:', err); console.error('分片读取失败:', err);
// 清理已创建的临时文件
await cleanTempFiles(chunks);
throw new OakUploadException(`分片 ${i + 1} 读取失败: ${err}`); throw new OakUploadException(`分片 ${i + 1} 读取失败: ${err}`);
} }
} }