mirror of
https://github.com/ZSCGR/CloudFlare-ImgBed.git
synced 2026-08-13 04:03:42 +08:00
revert: undo HuggingFace range streaming changes
This commit is contained in:
+14
-34
@@ -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'));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user