revert: undo HuggingFace range streaming changes

This commit is contained in:
MarSeventh
2026-05-30 17:25:21 +08:00
parent 8c083db63a
commit ef9c14163b
2 changed files with 18 additions and 377 deletions
+14 -34
View File
@@ -879,55 +879,31 @@ async function handleHuggingFaceFile(context, metadata, encodedFileName, fileTyp
}
// 构建文件 URL
const huggingfaceAPI = new HuggingFaceAPI(hfToken, hfRepo, hfIsPrivate);
let fileUrl = metadata.HfFileUrl || huggingfaceAPI.getFileURL(hfFilePath);
fileUrl = await huggingfaceAPI.resolveFileURL(hfFilePath, { fileUrl });
const fileSize = await huggingfaceAPI.getRemoteFileSize(hfFilePath, { fileUrl, metadata });
// 构建响应头
const headers = new Headers();
setCommonHeaders(headers, encodedFileName, fileType, getFileCacheControl(context));
if (fileSize) {
headers.set('Content-Length', fileSize.toString());
}
const fileUrl = metadata.HfFileUrl || `https://huggingface.co/datasets/${hfRepo}/resolve/main/${hfFilePath}`;
// 处理 HEAD 请求
if (request.method === 'HEAD') {
const headers = new Headers();
setCommonHeaders(headers, encodedFileName, fileType, getFileCacheControl(context));
return handleHeadRequest(headers);
}
// 构建请求头
const fetchHeaders = {};
// 私有仓库需要 Authorization
if (hfIsPrivate && hfToken) {
fetchHeaders['Authorization'] = `Bearer ${hfToken}`;
}
// 支持 Range 请求
const range = request.headers.get('Range');
if (range) {
fetchHeaders['Range'] = range;
if (fileSize) {
const proxyRange = huggingfaceAPI.getProxyRange(range, fileSize);
if (!proxyRange) {
return new Response('Range Not Satisfiable', {
status: 416,
headers: {
'Content-Range': `bytes */${fileSize}`,
'Accept-Ranges': 'bytes'
}
});
}
const rangeResponse = await huggingfaceAPI.fetchRange(hfFilePath, proxyRange.start, proxyRange.end, { fileUrl });
setRangeHeaders(headers, rangeResponse.start, rangeResponse.end, fileSize);
return new Response(rangeResponse.body, {
status: 206,
headers
});
}
}
const response = await huggingfaceAPI.fetchFile(hfFilePath, {
fileUrl,
const response = await fetch(fileUrl, {
method: 'GET',
headers: fetchHeaders
});
@@ -935,6 +911,10 @@ async function handleHuggingFaceFile(context, metadata, encodedFileName, fileTyp
return new Response(`Error: Failed to fetch from HuggingFace - ${response.status}`, { status: response.status });
}
// 构建响应头
const headers = new Headers();
setCommonHeaders(headers, encodedFileName, fileType, getFileCacheControl(context));
// 复制相关头部
if (response.headers.get('Content-Length')) {
headers.set('Content-Length', response.headers.get('Content-Length'));
+4 -343
View File
@@ -12,9 +12,6 @@
* SHA256 由前端预计算传入,避免后端 CPU 超时
*/
const HF_PROXY_RANGE_CHUNK_SIZE = 4 * 1024 * 1024;
const HF_UPSTREAM_RANGE_CHUNK_SIZE = 4 * 1024 * 1024;
export class HuggingFaceAPI {
constructor(token, repo, isPrivate = false) {
this.token = token;
@@ -508,7 +505,10 @@ export class HuggingFaceAPI {
* 获取文件内容(用于私有仓库代理)
*/
async getFileContent(filePath) {
return await this.fetchFile(filePath);
const fileUrl = `${this.baseURL}/datasets/${this.repo}/resolve/main/${filePath}`;
return await fetch(fileUrl, {
headers: this.isPrivate ? { 'Authorization': `Bearer ${this.token}` } : {}
});
}
/**
@@ -517,343 +517,4 @@ export class HuggingFaceAPI {
getFileURL(filePath) {
return `${this.baseURL}/datasets/${this.repo}/resolve/main/${filePath}`;
}
getRequestHeaders(extraHeaders = {}) {
const headers = new Headers(extraHeaders);
if (this.isPrivate && this.token && shouldSendAuthorization(headers.get('X-HF-Target-URL')) && !headers.has('Authorization')) {
headers.set('Authorization', `Bearer ${this.token}`);
}
headers.delete('X-HF-Target-URL');
return headers;
}
async fetchFile(filePath, options = {}) {
const fileUrl = options.fileUrl || this.getFileURL(filePath);
return await fetch(fileUrl, {
method: options.method || 'GET',
headers: this.getRequestHeaders({
...(options.headers || {}),
'X-HF-Target-URL': fileUrl,
}),
});
}
async resolveFileURL(filePath, options = {}) {
const fileUrl = options.fileUrl || this.getFileURL(filePath);
try {
const response = await fetch(fileUrl, {
method: 'HEAD',
headers: this.getRequestHeaders({ 'X-HF-Target-URL': fileUrl }),
redirect: 'manual',
});
const location = response.headers.get('Location');
if (response.status >= 300 && response.status < 400 && location) {
return new URL(location, fileUrl).toString();
}
} catch (error) {
console.warn('HuggingFace download URL resolve failed:', error.message);
}
if (shouldSendAuthorization(fileUrl)) {
try {
const response = await fetch(fileUrl, {
method: 'GET',
headers: this.getRequestHeaders({
Range: 'bytes=0-0',
'X-HF-Target-URL': fileUrl,
}),
redirect: 'manual',
});
try {
const location = response.headers.get('Location');
if (response.status >= 300 && response.status < 400 && location) {
return new URL(location, fileUrl).toString();
}
} finally {
if (response.body) {
await response.body.cancel().catch(() => {});
}
}
} catch (error) {
console.warn('HuggingFace range URL resolve failed:', error.message);
}
}
return fileUrl;
}
async getRemoteFileSize(filePath, options = {}) {
const metadataSize = getExactMetadataFileSize(options.metadata);
if (metadataSize) {
return metadataSize;
}
try {
const headResponse = await this.fetchFile(filePath, {
fileUrl: options.fileUrl,
method: 'HEAD',
});
if (headResponse.ok) {
const contentLength = Number(headResponse.headers.get('Content-Length'));
if (Number.isFinite(contentLength) && contentLength > 0) {
return Math.floor(contentLength);
}
}
} catch (error) {
console.warn('HuggingFace HEAD size probe failed:', error.message);
}
try {
const rangeResponse = await this.fetchFile(filePath, {
fileUrl: options.fileUrl,
headers: { Range: 'bytes=0-0' },
});
try {
const contentRange = parseContentRange(rangeResponse.headers.get('Content-Range'));
if (contentRange?.total) {
return contentRange.total;
}
if (rangeResponse.status === 200) {
const contentLength = Number(rangeResponse.headers.get('Content-Length'));
if (Number.isFinite(contentLength) && contentLength > 0) {
return Math.floor(contentLength);
}
}
} finally {
if (rangeResponse.body) {
await rangeResponse.body.cancel().catch(() => {});
}
}
} catch (error) {
console.warn('HuggingFace range size probe failed:', error.message);
}
return getApproximateMetadataFileSize(options.metadata);
}
getProxyRange(rangeHeader, totalSize) {
const range = parseRangeHeader(rangeHeader, totalSize);
if (!range) {
return null;
}
const rangeEnd = range.openEnded
? Math.min(range.start + HF_PROXY_RANGE_CHUNK_SIZE - 1, range.end)
: range.end;
return {
start: range.start,
end: rangeEnd,
openEnded: range.openEnded,
};
}
async fetchRange(filePath, rangeStart, rangeEnd, options = {}) {
const fileUrl = options.fileUrl || this.getFileURL(filePath);
const chunks = [];
let nextStart = rangeStart;
let totalBytes = 0;
while (nextStart <= rangeEnd) {
const upstreamEnd = Math.min(nextStart + HF_UPSTREAM_RANGE_CHUNK_SIZE - 1, rangeEnd);
const response = await this.fetchFile(filePath, {
fileUrl,
headers: { Range: `bytes=${nextStart}-${upstreamEnd}` },
});
if (!response.ok && response.status !== 206) {
if (totalBytes > 0) {
break;
}
throw new Error(`HuggingFace range fetch failed: ${response.status}`);
}
const contentRange = parseContentRange(response.headers.get('Content-Range'));
let readableBytes = upstreamEnd - nextStart + 1;
if (contentRange) {
if (contentRange.start !== nextStart || contentRange.end < nextStart) {
if (totalBytes > 0) {
break;
}
throw new Error(`Unexpected HuggingFace Content-Range: ${response.headers.get('Content-Range')}`);
}
readableBytes = Math.min(contentRange.end - contentRange.start + 1, rangeEnd - nextStart + 1);
} else if (response.status === 200 && nextStart !== 0) {
if (totalBytes > 0) {
break;
}
throw new Error('HuggingFace did not honor range request');
} else {
const contentLength = Number(response.headers.get('Content-Length'));
if (Number.isFinite(contentLength) && contentLength > 0) {
readableBytes = Math.min(contentLength, rangeEnd - nextStart + 1);
}
}
const bytes = await readResponseBytes(response.body, readableBytes);
if (bytes.byteLength === 0) {
break;
}
chunks.push(bytes);
totalBytes += bytes.byteLength;
nextStart += bytes.byteLength;
}
if (totalBytes <= 0) {
throw new Error('Empty HuggingFace range response');
}
return {
body: concatUint8Arrays(chunks, totalBytes),
start: rangeStart,
end: rangeStart + totalBytes - 1,
contentLength: totalBytes,
};
}
}
function shouldSendAuthorization(fileUrl) {
if (!fileUrl) {
return true;
}
try {
return new URL(fileUrl).hostname === 'huggingface.co';
} catch {
return true;
}
}
function parseContentRange(contentRange) {
if (!contentRange) {
return null;
}
const match = contentRange.match(/^bytes\s+(\d+)-(\d+)\/(\d+|\*)$/i);
if (!match) {
return null;
}
return {
start: parseInt(match[1], 10),
end: parseInt(match[2], 10),
total: match[3] === '*' ? null : parseInt(match[3], 10),
};
}
function parseRangeHeader(rangeHeader, totalSize) {
if (!rangeHeader || !totalSize) {
return null;
}
const match = String(rangeHeader).trim().match(/^bytes=(\d*)-(\d*)$/i);
if (!match || (match[1] === '' && match[2] === '')) {
return null;
}
let start;
let end;
let openEnded = false;
if (match[1] === '') {
const suffixLength = parseInt(match[2], 10);
if (!Number.isFinite(suffixLength) || suffixLength <= 0) {
return null;
}
start = Math.max(totalSize - suffixLength, 0);
end = totalSize - 1;
} else {
start = parseInt(match[1], 10);
openEnded = match[2] === '';
end = openEnded ? totalSize - 1 : parseInt(match[2], 10);
}
if (!Number.isFinite(start) || !Number.isFinite(end) || start >= totalSize || start > end) {
return null;
}
return {
start,
end: Math.min(end, totalSize - 1),
openEnded,
};
}
function getExactMetadataFileSize(metadata = {}) {
const fileSizeBytes = Number(metadata?.FileSizeBytes);
if (Number.isFinite(fileSizeBytes) && fileSizeBytes > 0) {
return Math.floor(fileSizeBytes);
}
return null;
}
function getApproximateMetadataFileSize(metadata = {}) {
const fileSizeMB = Number(metadata?.FileSize);
if (Number.isFinite(fileSizeMB) && fileSizeMB > 0) {
return Math.floor(fileSizeMB * 1024 * 1024);
}
return null;
}
async function readResponseBytes(body, maxBytes) {
if (!body || maxBytes <= 0) {
return new Uint8Array(0);
}
const reader = body.getReader();
let remaining = maxBytes;
let totalBytes = 0;
const chunks = [];
try {
while (remaining > 0) {
const { done, value } = await reader.read();
if (done) {
break;
}
const chunk = value.byteLength > remaining ? value.slice(0, remaining) : value;
chunks.push(chunk);
totalBytes += chunk.byteLength;
remaining -= chunk.byteLength;
if (chunk.byteLength < value.byteLength) {
await reader.cancel();
break;
}
}
} finally {
reader.releaseLock();
}
return concatUint8Arrays(chunks, totalBytes);
}
function concatUint8Arrays(chunks, totalBytes) {
if (chunks.length === 1 && chunks[0].byteLength === totalBytes) {
return chunks[0];
}
const result = new Uint8Array(totalBytes);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.byteLength;
}
return result;
}