You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Firebase存储上传缩略图卡在0.0%无报错,求排查方案

问题

我正在实现一个包含以下步骤的图片上传函数:

  1. 使用sharp.js压缩本地图片并存入缓存文件夹;
  2. 将原图上传至Firebase Storage指定位置;
  3. 将压缩后的缩略图上传至Firebase Storage另一位置。

当前步骤1、2可正常执行,但步骤3始终停滞:缩略图上传进度卡在0.0%,且Firebase无任何错误抛出。我已添加大量日志和catch语句仍无法定位原因。

实现代码

// appends a filename to the end of a storageReference
export function addFilename(filename: string, reference: StorageReference) {
    return ref(storage, `${reference}/${path.basename(filename)}`);
}

// -- file uploading
export async function uploadFromDisk(
    filePath: string,
    destination: StorageReference,
    metadata?: UploadMetadata,
) {
    console.log("initiating upload - " + JSON.stringify(filePath));

    const taskPromise: Promise<UploadTaskSnapshot> = new Promise(((resolve, reject) => {
        const task = uploadBytesResumable(
            addFilename(filePath, destination),
            fs.readFileSync(filePath),
            metadata
        );
    
        task.on("state_changed", (snapshot) => {
            if (snapshot.state == "running") {
                console.log(`File ${filePath} status: ${(snapshot.bytesTransferred / snapshot.totalBytes * 100).toPrecision(2)}%`);
            }

            if (snapshot.bytesTransferred >= snapshot.totalBytes ) {
                printCyan(`File ${filePath} available @ ${snapshot.ref.bucket}/${snapshot.ref.fullPath}`);
                resolve(snapshot);
            }
            
            if (snapshot.state == "error") {
                reject(new Error("There was an error uploading file " + filePath));
            }
        });
    }));
    
    return taskPromise;
}

export async function uploadGalleryImage(
    imagePath: string,
    site: string,
    gallery?: string,
) {
    const metadata = {
        name: path.basename(imagePath),
        contentType: path.extname(imagePath) == ".jpg" ? "image/jpeg" : "image/png"
    };

    const imageFolder = `galleries/${gallery}/full`;
    const thumbPath = (await compressImages([imagePath]))[0];
    const thumbFolder = `galleries/${gallery}/thumbs`;

    const results: UploadResult[] = [];
    const imageResult = await uploadFromDisk(
        imagePath,
        getAssetFolder(site, imageFolder),
        metadata,
    );

    const thumbResult = await uploadFromDisk(
        thumbPath,
        getAssetFolder(site, thumbFolder),
        metadata,
    );
    
    results.push(imageResult);
    results.push(thumbResult);

    return results;
}

控制台输出示例

File images/bulksplash-charleingracia--Ux5mdMJNEA.jpg status: 77% 
// original image is uploading fine

...

File images/bulksplash-charleingracia--Ux5mdMJNEA.jpg status: 1.0e+2%
File images/bulksplash-charleingracia--Ux5mdMJNEA.jpg available ... 
// fully uploaded original image

...

File thumbs/bulksplash-charleingracia--Ux5mdMJNEA.jpg status: 0.0% 
// stuck at 0.0%
File images/bulksplash-davieees-uJDWga_IOck.jpg status: 35% 
// the other, previously initiated image uploads are chugging along just fine
File images/bulksplash-ferminrp-a1dpvJv45To.jpg status: 79%
File images/bulksplash-aliciasteels-mrB45cHDsrM.jpg status: 55%

排查方向与修复建议

1. 验证缩略图文件的完整性

  • 在获取thumbPath后添加日志,打印文件路径和fs.statSync(thumbPath).size的结果。如果文件大小为0,说明sharp.js压缩未正确生成文件,需排查compressImages函数的异步写入逻辑,确保等待sharp().toFile()完成后再返回路径。
  • 手动检查thumbPath指向的文件是否可正常打开,排除文件损坏问题。

2. 修复上传Promise的逻辑漏洞

当前uploadFromDisk的Promise仅在snapshot.bytesTransferred >= snapshot.totalBytes时resolve,若Firebase的上传完成状态未触发该条件,会导致Promise永久pending。改用state_changed的完整回调参数处理:

export async function uploadFromDisk(
    filePath: string,
    destination: StorageReference,
    metadata?: UploadMetadata,
) {
    console.log("initiating upload - " + JSON.stringify(filePath));

    return new Promise<UploadTaskSnapshot>((resolve, reject) => {
        const task = uploadBytesResumable(
            addFilename(filePath, destination),
            fs.readFileSync(filePath),
            metadata
        );
    
        task.on("state_changed", 
            (snapshot) => {
                if (snapshot.state === "running") {
                    const progress = (snapshot.bytesTransferred / snapshot.totalBytes * 100).toPrecision(2);
                    console.log(`File ${filePath} status: ${progress}%`);
                }
            },
            (error) => {
                reject(new Error(`Upload failed for ${filePath}: ${error.message}`));
            },
            () => {
                const snapshot = task.snapshot;
                printCyan(`File ${filePath} available @ ${snapshot.ref.bucket}/${snapshot.ref.fullPath}`);
                resolve(snapshot);
            }
        );
    });
}

3. 为缩略图单独生成元数据

当前复用原图的metadata可能导致contentType不匹配(比如压缩后格式变化),为缩略图单独生成元数据:

// 在uploadGalleryImage中修改
const thumbMetadata = {
    name: path.basename(thumbPath),
    contentType: path.extname(thumbPath) === ".jpg" ? "image/jpeg" : "image/png"
};

// 上传缩略图时使用thumbMetadata
const thumbResult = await uploadFromDisk(
    thumbPath,
    getAssetFolder(site, thumbFolder),
    thumbMetadata,
);

4. 检查Firebase存储路径与权限

  • 打印getAssetFolder(site, thumbFolder)的fullPath属性,确认存储路径拼写、大小写是否正确。
  • 验证Firebase Storage安全规则是否允许写入galleries/${gallery}/thumbs路径,排除权限拦截问题。

5. 排查文件读取的时序问题

fs.readFileSync是同步读取,若compressImages未完全写入缩略图就返回路径,会读取到空内容。确保compressImages函数内部等待sharp的写入操作完成后再返回thumbPath。

内容的提问来源于stack exchange,提问作者ajazz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 17:24:30