这几天访问博客总感觉打开图片比较慢,应该是服务器带宽不够,加上我上传图片也不怎么注意大小导致的,搜索了一下 Ghost 官方文档,可以手动更改博客图片的存储方式,感觉不是很复杂,心想替换上去不是美滋滋,所以直接换了,没想到直接翻车。记得上一次是弄熊掌号的时候同样遇见麻烦,后来博客直接倒闭了……这一次记录一下细节,希望可以帮助其他人避免这些坑爹情况。

Ghost 官方文档上说,用户可以自定义一个存储适配器,该适配器本质上是一个类,Ghost 在运行过程中会根据用户上传图片的动作调用这个类的若干函数。默认情况下 Ghost 会调用本地存储的适配器,该适配器是 Ghost 自带的,假如你已经实现了自己的存储适配器,只需要更改一下配置即可,具体的步骤如下:

  1. 在 Ghost 程序的根目录,有一个 content 文件夹,第一步需要创建存储适配器的目录,然后将存储适配器的代码放进入。该目录的位置:content/adapters/storage
  2. 实现自己的存储适配器代码,官方有一个第三方适配器的列表,我选了其中一个名字叫做 ghost-upyun-store 的适配器,这个适配器可以将图片自动的保存到又拍云上。

安装好后,需要重启 Ghost 才能生效。但是我万万没想到,修改了配置之后,原有的图片路径竟然全部失效,博客上所有的图片都无法打开,全部 404 ,我也是醉了……

我立刻登陆服务器,进入数据库,将数据库中 posts 表中的 feature_image 以及 html 两个字段中的链接全部更新,参考语句如下:

update posts set feature_image = CONCAT('https://img.wangzhechao.com/upload', substr(feature_image, 9))

update posts set html = replace(html, "/content/", "https://img.wangzhechao.com/upload/") 

本来以为一下搞定,但是又发现了一个奇葩 bug:

500

就知道事情没这么简单,继续研究,查看 Ghost 日志:

TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be one of type string, TypedArray, or DataView. Received type boolean
    at Hash.update (internal/crypto/hash.js:58:11)
    at buildContentResponse (/var/www/ghost/versions/3.2.0/core/server/web/shared/middlewares/serve-favicon.js:17:48)
    at storage.getStorage.read.then (/var/www/ghost/versions/3.2.0/core/server/web/shared/middlewares/serve-favicon.js:56:35)
    at tryCatcher (/var/www/ghost/content/adapters/storage/ghost-upyun-store/node_modules/bluebird/js/release/util.js:16:23)
    at Promise._settlePromiseFromHandler (/var/www/ghost/content/adapters/storage/ghost-upyun-store/node_modules/bluebird/js/release/promise.js:512:31)
    at Promise._settlePromise (/var/www/ghost/content/adapters/storage/ghost-upyun-store/node_modules/bluebird/js/release/promise.js:569:18)
    at Promise._settlePromise0 (/var/www/ghost/content/adapters/storage/ghost-upyun-store/node_modules/bluebird/js/release/promise.js:614:10)
    at Promise._settlePromises (/var/www/ghost/content/adapters/storage/ghost-upyun-store/node_modules/bluebird/js/release/promise.js:693:18)
    at Async._drainQueue (/var/www/ghost/content/adapters/storage/ghost-upyun-store/node_modules/bluebird/js/release/async.js:133:16)
    at Async._drainQueues (/var/www/ghost/content/adapters/storage/ghost-upyun-store/node_modules/bluebird/js/release/async.js:143:10)
    at Immediate.Async.drainQueues [as _onImmediate] (/var/www/ghost/content/adapters/storage/ghost-upyun-store/node_modules/bluebird/js/release/async.js:17:14)
    at runCallback (timers.js:705:18)
    at tryOnImmediate (timers.js:676:5)
    at processImmediate (timers.js:658:5)
    at process.topLevelDomainCallback (domain.js:126:23)

日志提示信息感觉像是读图标失败的表现,尼玛,我的图标呢?

wtf

没办法只能读代码研究一下:

const buildContentResponse = (ext, buf) => {
    content = {
        headers: {
            'Content-Type': `image/${ext}`,
            'Content-Length': buf.length,
            ETag: `"${crypto.createHash('md5').update(buf, 'utf8').digest('hex')}"`,
            'Cache-Control': `public, max-age=${config.get('caching:favicon:maxAge')}`
        },
        body: buf
    };

    return content;
};

初步看应该是 buf 返回不正常,导致 md5 调用 update 方法失败报异常,再次看之前的调用,也是这个文件:

function serveFavicon() {
    let iconType;
    let filePath;

    return function serveFavicon(req, res, next) {
        if (req.path.match(/^\/favicon\.(ico|png)/i)) {
        	
        	//......
            
            filePath = imageLib.blogIcon.getIconPath();

            let originalExtension = path.extname(filePath).toLowerCase();
            const requestedExtension = path.extname(req.path).toLowerCase();

            // CASE: custom favicon exists, load it from local file storage
            if (settingsCache.get('icon')) {
                // depends on the uploaded icon extension
                if (originalExtension !== requestedExtension) {
                    return res.redirect(302, urlUtils.urlFor({relativeUrl: `/favicon${originalExtension}`}));
                }

                storage.getStorage()
                    .read({path: filePath})
                    .then((buf) => {
                        iconType = imageLib.blogIcon.getIconType();
                        content = buildContentResponse(iconType, buf);

                        res.writeHead(200, content.headers);
                        res.end(content.body);
                    })
                    .catch(next);
            } else {
            	//......
            }
        } else {
            return next();
        }
    };
}

这里应该就是加载图标的函数,只不过貌似做了一下缓存,其中调用了 storeage.getStorage.read 函数,这个函数正是自定义存储适配器中的函数,难道这个类实现有问题?

我打印了一下这个 filePath 路径,确认路径是:

/2020/01/bitbug_favicon.ico

我有点懵逼了,为毛图标的路径在这里?我又看了看存储适配器中的代码:

read(options) {
    options = options || {};
    const client = this.client;
    const key = urlParse(options.path).pathname.slice(1);

    return new Promise(function (resolve, reject) {
        client.getFile(key).then(function (result) {
            resolve(result);
        }).catch(function (error) {
            reject('Could not read image');
        });
    });
}

这里 client 是又拍云提供的官方 SDK,我去查了一下 getFile 函数:

getFile(remotePath, saveStream = null)

下载保存在又拍云服务的文件

参数

  • remotePath: 需要下载的文件路径
  • saveStream: 可选值,一个可以写入的流。若传递该参数,下载的文件将直接写入该流。该参数不支持浏览器端使用

第一个参数是完整的图片路径,SDK 既然是官方提供的,参数说明肯定不会错,反推回来,也就是说这个图标的路径有问题,并且我有一个更奇怪的问题,为什么图标的名称叫做 bitbug_favicon.icobitbug_ 这个前缀是什么鬼?

我想了很久,并且去看是否有 ico 的后台配置,找着找着,突然我想起个问题,为什么这个图标的位置会在 2020/01 这个目录?这个目录貌似是因为我 1 月建立博客才会存在的,我突然想起应该看看博客后台,看过之后,我恨不得给我自己一巴掌……

ghost_1

我竟然忘记了查看后台配置信息:

ghost_1

破案了,原来是因为本地存储器保存图片的路径是 2020/01/bitbug_favicon.ico ,所以一直加载这个路径,最后导致显示 404,删除重新上传即可。

看了几篇文章,貌似一切都很完美,不过等一等,尼玛这是什么?

ghost_2

所有引用文章的图片貌似显示都是有问题的,我看了一下路径:

https://wangzhechao.comhttps://img.wangzhechao.com/upload/images/2020/02/sdl-windows.jpg

😰,很明显是因为前面我 SQL 语句替换导致的,文章中有绝对路径,也有相对路径,你统一用一种能死么?

继续写 SQL 替换:

update posts set html = replace(html, "https://wangzhechao.comhttps://img.wangzhechao.com", "https://img.wangzhechao.com") 

除了这个之外,我还发现 posts 表中还有个 mobiledoc 字段,同样替换之:

update posts set mobiledoc = replace(mobiledoc, "/content/images/", "https://img.wangzhechao.com/upload/images/") 

update posts set mobiledoc = replace(mobiledoc, "https://wangzhechao.comhttps://img.wangzhechao.com", "https://img.wangzhechao.com") 

我接着查看其它的表结构,发现一个 mobiledoc 的表,其中的链接貌似都是相对链接,感觉最好还是更改一下:

update mobiledoc_revisions set mobiledoc = replace(mobiledoc, "/content/images/", "https://img.wangzhechao.com/upload/images/") 

貌似改了好多行,不知道会不会有啥问题……

到此总算结束了,我点开大部分的文章,感觉应该是可以了,如果大家发现其它问题请及时告诉我,最后奉劝大家修改需谨慎,不要瞎折腾!