fix(canvas): 画布与素材同步 - 修复批量缩略图、远程资源回退与重命名同步
This commit is contained in:
@@ -96,6 +96,9 @@ export class ResourceUploadError extends Error {
|
||||
const resourceCache = new Map<string, RemoteResource>();
|
||||
const resourceRequests = new Map<string, Promise<RemoteResource>>();
|
||||
const missingResourceIds = new Set<string>();
|
||||
const ossUrlCache = new Map<string, { url: string; expiresAt: number }>();
|
||||
const ossUrlRequests = new Map<string, Promise<string>>();
|
||||
const OSS_URL_CACHE_TTL_MS = 4 * 60 * 1000;
|
||||
|
||||
export function resourceStorageKey(id: string) {
|
||||
return `resource:${id}`;
|
||||
@@ -271,14 +274,25 @@ export function refreshResource(id: string): Promise<RemoteResource> {
|
||||
export async function getResourceOSSUrl(storageKey?: string) {
|
||||
const id = resourceIdFromStorageKey(storageKey);
|
||||
if (!id) throw new Error("当前媒体尚未上传到后端资源存储");
|
||||
try {
|
||||
const data = await http.get<{ url: string }>(`/resources/${encodeURIComponent(id)}/oss-url`);
|
||||
if (!data.url) throw new Error("后端未返回对象存储地址");
|
||||
return data.url;
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) throw new Error(error.message || "获取对象存储地址失败");
|
||||
throw error;
|
||||
}
|
||||
const cached = ossUrlCache.get(resourceCacheKey(id));
|
||||
if (cached && cached.expiresAt > Date.now()) return cached.url;
|
||||
const pending = ossUrlRequests.get(resourceCacheKey(id));
|
||||
if (pending) return pending;
|
||||
const request = (async () => {
|
||||
try {
|
||||
const data = await http.get<{ url: string }>(`/resources/${encodeURIComponent(id)}/oss-url`);
|
||||
if (!data.url) throw new Error("后端未返回对象存储地址");
|
||||
ossUrlCache.set(resourceCacheKey(id), { url: data.url, expiresAt: Date.now() + OSS_URL_CACHE_TTL_MS });
|
||||
return data.url;
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) throw new Error(error.message || "获取对象存储地址失败");
|
||||
throw error;
|
||||
} finally {
|
||||
ossUrlRequests.delete(resourceCacheKey(id));
|
||||
}
|
||||
})();
|
||||
ossUrlRequests.set(resourceCacheKey(id), request);
|
||||
return request;
|
||||
}
|
||||
|
||||
function resourceCacheKey(id: string) {
|
||||
@@ -287,7 +301,7 @@ function resourceCacheKey(id: string) {
|
||||
|
||||
export function resourceFileUrl(id: string) {
|
||||
const base = String(apiBaseURL).replace(/\/+$/, "");
|
||||
return `${base}/resources/${encodeURIComponent(id)}/file`;
|
||||
return `${base}/resources/${encodeURIComponent(id)}/file?direct=1`;
|
||||
}
|
||||
|
||||
function resourceProxyFileUrl(id: string) {
|
||||
@@ -306,16 +320,25 @@ export function resolveResourceUrl(storageKey?: string, fallback = "") {
|
||||
// 副本就绪前由后端回退原件,调用方再按需降级。
|
||||
export function playbackVariantUrl(id: string) {
|
||||
const base = String(apiBaseURL).replace(/\/+$/, "");
|
||||
return `${base}/resources/${encodeURIComponent(id)}/file?variant=playback`;
|
||||
return `${base}/resources/${encodeURIComponent(id)}/file?variant=playback&direct=1`;
|
||||
}
|
||||
|
||||
export async function getResourceBlob(storageKey: string) {
|
||||
export async function getResourceBlob(storageKey: string, options?: { allowProxyFallback?: boolean }) {
|
||||
const id = resourceIdFromStorageKey(storageKey);
|
||||
if (!id) return null;
|
||||
const url = resourceProxyFileUrl(id);
|
||||
const response = await fetch(url, { credentials: isResourceUrl(url) ? "include" : "same-origin" });
|
||||
if (!response.ok) return null;
|
||||
return response.blob();
|
||||
// A direct OSS response needs CORS to be readable as a Blob. Native <img>/<video>
|
||||
// can still display it without CORS, so background cache fills must not proxy it.
|
||||
try {
|
||||
const ossUrl = await getResourceOSSUrl(storageKey);
|
||||
const response = await fetch(ossUrl, { credentials: "omit", mode: "cors" });
|
||||
if (response.ok) return response.blob();
|
||||
if (!options?.allowProxyFallback || response.status === 401 || response.status === 403 || response.status === 404) return null;
|
||||
} catch (error) {
|
||||
if (!options?.allowProxyFallback) throw error;
|
||||
}
|
||||
if (!options?.allowProxyFallback) return null;
|
||||
const response = await fetch(resourceProxyFileUrl(id), { credentials: "include" });
|
||||
return response.ok ? response.blob() : null;
|
||||
}
|
||||
|
||||
function extensionFromMime(mimeType: string, kind: string) {
|
||||
|
||||
Reference in New Issue
Block a user