Files
my_site/front/src/lib/upload-session.ts

221 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { apiBinaryUploadRequest, apiUploadRequest, apiV2Request, ApiError } from './api';
import type {
PreparedUploadResponse,
UploadSessionResponse,
UploadSessionStrategy,
} from './types';
export interface UploadFileToNetdiskOptions {
onProgress?: (progress: {loaded: number; total: number}) => void;
signal?: AbortSignal;
}
export interface UploadedNetdiskFileRef {
sessionId: string;
filename: string;
path: string;
}
interface CreateUploadSessionRequest {
path: string;
filename: string;
contentType: string | null;
size: number;
}
interface UploadSessionPartRecordRequest {
etag: string;
size: number;
}
function replacePartIndex(template: string, partIndex: number) {
return template.replace('{partIndex}', String(partIndex));
}
function toInternalApiPath(path: string) {
return path.startsWith('/api/') ? path.slice('/api'.length) : path;
}
function getRequiredStrategyValue(value: string | null | undefined, key: keyof UploadSessionStrategy) {
if (!value) {
throw new Error(`上传会话缺少 ${key},无法继续上传`);
}
return value;
}
export function createUploadSession(request: CreateUploadSessionRequest) {
return apiV2Request<UploadSessionResponse>('/files/upload-sessions', {
method: 'POST',
body: request,
});
}
export function getUploadSession(sessionId: string) {
return apiV2Request<UploadSessionResponse>(`/files/upload-sessions/${sessionId}`);
}
export function cancelUploadSession(sessionId: string) {
return apiV2Request<UploadSessionResponse>(`/files/upload-sessions/${sessionId}`, {
method: 'DELETE',
});
}
export function prepareSingleUploadSession(sessionId: string) {
return apiV2Request<PreparedUploadResponse>(`/files/upload-sessions/${sessionId}/prepare`);
}
export function uploadUploadSessionContent(
sessionId: string,
file: File,
options: UploadFileToNetdiskOptions = {},
) {
const formData = new FormData();
formData.append('file', file);
return apiUploadRequest<UploadSessionResponse>(`/v2/files/upload-sessions/${sessionId}/content`, {
body: formData,
onProgress: options.onProgress,
signal: options.signal,
});
}
export function prepareUploadSessionPart(sessionId: string, partIndex: number) {
return apiV2Request<PreparedUploadResponse>(`/files/upload-sessions/${sessionId}/parts/${partIndex}/prepare`);
}
export function recordUploadSessionPart(
sessionId: string,
partIndex: number,
request: UploadSessionPartRecordRequest,
) {
return apiV2Request<UploadSessionResponse>(`/files/upload-sessions/${sessionId}/parts/${partIndex}`, {
method: 'PUT',
body: request,
});
}
export function completeUploadSession(sessionId: string) {
return apiV2Request<UploadSessionResponse>(`/files/upload-sessions/${sessionId}/complete`, {
method: 'POST',
});
}
async function runProxyUpload(session: UploadSessionResponse, file: File, options: UploadFileToNetdiskOptions) {
const proxyFormField = getRequiredStrategyValue(session.strategy.proxyFormField, 'proxyFormField');
const formData = new FormData();
formData.append(proxyFormField, file);
await apiUploadRequest<UploadSessionResponse>(
toInternalApiPath(getRequiredStrategyValue(session.strategy.proxyContentUrl, 'proxyContentUrl')),
{
body: formData,
onProgress: options.onProgress,
signal: options.signal,
},
);
}
async function runDirectSingleUpload(session: UploadSessionResponse, file: File, options: UploadFileToNetdiskOptions) {
const prepared = await prepareSingleUploadSession(session.sessionId);
await apiBinaryUploadRequest(prepared.uploadUrl, {
method: prepared.method,
headers: prepared.headers,
body: file,
onProgress: options.onProgress,
signal: options.signal,
});
}
async function runMultipartUpload(session: UploadSessionResponse, file: File, options: UploadFileToNetdiskOptions) {
const partPrepareUrlTemplate = getRequiredStrategyValue(session.strategy.partPrepareUrlTemplate, 'partPrepareUrlTemplate');
const partRecordUrlTemplate = getRequiredStrategyValue(session.strategy.partRecordUrlTemplate, 'partRecordUrlTemplate');
let uploadedBytes = 0;
for (let partIndex = 0; partIndex < session.chunkCount; partIndex += 1) {
const partStart = partIndex * session.chunkSize;
const partEnd = Math.min(file.size, partStart + session.chunkSize);
const partBlob = file.slice(partStart, partEnd);
const prepared = await apiV2Request<PreparedUploadResponse>(
toInternalApiPath(replacePartIndex(partPrepareUrlTemplate, partIndex)).replace('/v2', ''),
);
const uploadResult = await apiBinaryUploadRequest(prepared.uploadUrl, {
method: prepared.method,
headers: prepared.headers,
body: partBlob,
responseHeaders: ['etag'],
signal: options.signal,
onProgress: options.onProgress
? ({loaded, total}) => {
options.onProgress?.({
loaded: uploadedBytes + loaded,
total: Math.max(file.size, uploadedBytes + total),
});
}
: undefined,
});
const etag = uploadResult.headers.etag;
if (!etag) {
throw new Error('分片上传成功但未返回 etag无法完成上传');
}
await apiV2Request<UploadSessionResponse>(
toInternalApiPath(replacePartIndex(partRecordUrlTemplate, partIndex)).replace('/v2', ''),
{
method: 'PUT',
body: {
etag,
size: partBlob.size,
},
},
);
uploadedBytes += partBlob.size;
options.onProgress?.({
loaded: uploadedBytes,
total: file.size,
});
}
}
export async function uploadFileToNetdiskViaSession(
file: File,
path: string,
options: UploadFileToNetdiskOptions = {},
): Promise<UploadedNetdiskFileRef> {
const session = await createUploadSession({
path,
filename: file.name,
contentType: file.type || null,
size: file.size,
});
let shouldCancelSession = true;
try {
switch (session.uploadMode) {
case 'PROXY':
await runProxyUpload(session, file, options);
break;
case 'DIRECT_SINGLE':
await runDirectSingleUpload(session, file, options);
break;
case 'DIRECT_MULTIPART':
await runMultipartUpload(session, file, options);
break;
default:
throw new Error(`不支持的上传模式:${String(session.uploadMode)}`);
}
await completeUploadSession(session.sessionId);
shouldCancelSession = false;
return {
sessionId: session.sessionId,
filename: file.name,
path,
};
} catch (error) {
if (shouldCancelSession && !(error instanceof ApiError && error.message === '上传已取消')) {
await cancelUploadSession(session.sessionId).catch(() => undefined);
}
if (shouldCancelSession && error instanceof ApiError && error.message === '上传已取消') {
await cancelUploadSession(session.sessionId).catch(() => undefined);
}
throw error;
}
}