上下文

上周,我正在使用的Ghost实例开始出现异常行为。我无法上传新图片,一段时间后,由于504 Gateway Timeout... 😭 我完全无法访问该网站

由于我仅将 Ghost 用作后端(我正在使用Eleventy构建我的网站),因此我没有惊慌,因为我可以简单地从静态版本重新创建每个帖子。这需要一点时间,但这是可能的。 ⏱️

不幸的是,有一个问题:未发布的帖子(草稿或预定的)不会生成。Ghost Content API不会返回它们,所以我有点担心它们,因为我通常会保留 3 或 4 个帖子准备发布。所以如果网站不回来,我会永远失去他们......😟

幸运的是,我去睡觉了,第二天,网站又回来了。呸! 😌

我快速导出我的内容以保存它,但整个事件让我思考......也许是时候通过自托管它来完全拥有我的 Ghost 实例了。 🦚

指南

本指南是对 Ghost 官方文档的补充:如何将数据从 Ghost 迁移到 Ghost和Ghost 中的导入和导出:访问您的内容和数据 - FAQ。

先决条件

对于以下指南,我假设:

  • 你有一个版本Node.js支持顶级等待(如_16.1.0_)

  • 你已经配置了一个新的 Ghost 实例

  • 您是 Ghost 实例的所有者

  • 您可以通过 SSH 直接访问新 Ghost 实例的托管服务器

第一步——下载数据

如果您是 Ghost 实例的所有者,您应该可以访问“Labs”菜单,您可以在其中找到一个按钮来导出您的 bloc 内容。单击它,瞧,您的整个站点都有一个 JSON 文件。 🤗

[实验室视图的屏幕截图,带有导出按钮](https://res.cloudinary.com/practicaldev/image/fetch/s--bShVOP51--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/ https://www.benjaminrancourt.ca/images/export-content-ghost-3.jpg)

Ghost 3.42 中实验室视图中的 Export 按钮。

如果您使用的是最新的 Ghost 版本(当前为 4.5),则 Labs 选项菜单不再位于主菜单中,它现在隐藏在 Settings (⚙️) 选项菜单中!

不幸的是,该按钮仅导出数据,图像尚未导出......如果图像已作为绝对 URL(使用https://)插入到数据中,如果您想删除您的以前的 Ghost 实例...🥴

由于图像是公开的,因此您可以直接从 Web 下载它们,而您可以将它们的 URL 从绝对地址更改为相对地址(例如/content/images)。 😉

第二步——下载图片

如果您查看导出文件的数据,您可能会看到您的图像像我一样托管在另一台服务器上。就我而言,所有图像都在https://digitalpress.fra1.cdn.digitaloceanspaces.com/rec0035/下提供。

为了找到图像的所有 URL,我创建了以下 Node.js 脚本,将它们提取出来并将它们下载到我的计算机上。

import escapeStringRegExp from 'escape-string-regexp'
import fetch from 'node-fetch';
import fs from 'fs/promises';
import path from 'path'

// Configuration
const oldServerURL = "https://digitalpress.fra1.cdn.digitaloceanspaces.com/rec0035/"
const newServerURL = '/content/images/'
const oldGhostContentFile = 'ma-maison-sherby.ghost.2021-05-15-17-33-42.json'
const newGhostContentFile = oldGhostContentFile.replace('.json', '-output.json')
const imageDirectory = 'images/'

// Regexes
const filenameRegex = /(.*?\.(?:avif|ico|gif|jpe?g|png))/
const serverURLRegex = new RegExp(escapeStringRegExp(oldServerURL), 'gi')
const imageRegex = new RegExp(serverURLRegex.source + filenameRegex.source, 'gi')

// Utility functions
const stringify = object => JSON.stringify(object)
const parse = object => JSON.parse(object)
const returnBoolean = bool => () => bool

// Utility filesystem functions
const fileExists = async filePath => await fs.access(filePath, fs.F_OK)
.then(returnBoolean(true))
.catch(returnBoolean(false))

console.log(`Loading the Ghost content into memory...`)
const content = await fs.readFile(oldGhostContentFile, 'utf8')

const extractInformationsFromImageMatch = match => ({
  absoluteURL: match[0],
  directory: imageDirectory + path.dirname(match[1]),
  fileName: path.basename(match[1]),
  filePath: imageDirectory + match[1],
})

console.log(`Finding all unique images...`)
let images = [...content.matchAll(imageRegex)].map(extractInformationsFromImageMatch);
images = new Set(images.map(stringify))
images = Array.from(images).map(parse)

console.log(`${images.length} images found!`)

// Download an image inside a match object
const download = async (matchObject) => {
  // Check if we have already download the image on our disk
  const skipDownload = await fileExists(matchObject.filePath)
  if (skipDownload) {
    return
  }

  console.log(`Downloading ${matchObject.filePath}...`)

  // Download the image
  const response = await fetch(matchObject.absoluteURL);

  // Check if everything is ok
  if (!response.ok) {
    throw new Error(`Unexpected response ${response.statusText}`);
  }

  // Transform the response to buffer
  const buffer = await response.buffer();

  // Make sure the directory of the file exists and create it otherwise
  await fs.mkdir(matchObject.directory, {recursive: true})

  // Write the image on disk
  await fs.writeFile(matchObject.filePath, buffer)

  console.log(`Download finished for ${matchObject.filePath}!`)
}

console.log(`Downloading all images found...`)
for (let i = 0; i < images.length; ++i) {
  await download(images[i])
}

// Replace all occurrences of the old server to the new one
const newContent = content.replaceAll(serverURLRegex, newServerURL)

// Write the output inside a new file that can be imported
await fs.writeFile(newGhostContentFile, newContent)

进入全屏模式 退出全屏模式

download.mjs文件,用于下载所有图像并创建一个新的 JSON 文件。

{
  "dependencies": {
    "escape-string-regexp": "^5.0.0",
    "node-fetch": "^2.6.1"
  }
}

进入全屏模式 退出全屏模式

package.json文件,只有两个依赖项。

调整文件顶部的四个配置变量后,您可以简单地使用node download.mjs命令运行脚本,并且图像通常应该下载到images文件夹中。

如果您仔细阅读了我的脚本,您可能还会看到我的脚本创建了一个新文件,该文件的内容与导出的数据几乎相同,但具有相对图像 URL。 😮

第三步——上传图片

请放心,这一步没有上一步复杂。 😅

就我而言,因为我可以直接访问我的新 Ghost 实例的服务器,所以我能够使用以下命令通过 SSH 连接将图像上传到正确的位置:

scp -r images IP_ADDRESS:/var/opt/ghost/mamaisonsherby

进入全屏模式 退出全屏模式

使用scp(安全副本)将images目录下的所有内容上传到我的远程服务器。

最后一步——导入内容

我们现在可以使用Import content按钮在SettingsLabs中导入在步骤 2 中生成的新内容文件。

[实验室视图的屏幕截图,带有导入按钮](https://res.cloudinary.com/practicaldev/image/fetch/s--0JTnjJI__--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/ https://www.benjaminrancourt.ca/images/import-content-ghost-3.jpg)

Ghost 3.42 中实验室视图中的 Import 按钮。

如果您有任何问题,您可以随时单击 Export 按钮下方的 Delete 按钮来重新启动程序并更正您的数据。该按钮将删除所有帖子和标签。 😈

如果一切顺利,您应该会看到如下所示的确认消息。就我而言,有一些警告,但它们只是次要的。

[导入后实验室视图的屏幕截图](https://res.cloudinary.com/practicaldev/image/fetch/s--7_OTxlXK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https ://www.benjaminrancourt.ca/images/ghost-4-5-successful-import.jpg)

成功导入消息的示例,带有轻微警告。

验证步骤

什么?还有一步吗?是的,如果您不关心最终结果,这只是可选的。所以也许,在这种情况下这是强制性的......🤔

我建议您检查所有网页(帖子、标签、作者和页面),看看是否缺少图像。

而且,与往常一样,您还可以更新站点的全局 CSS (SettingsCode injectionSite header),以直观地标记需要更多注意的图像。

img[src*="digitaloceanspaces"] {
  border: 5px dashed darkred;
}

进入全屏模式 退出全屏模式

为我以前服务器上的所有图像添加红色边框的 CSS 代码。

结论

不是每天都移动 Ghost 实例,但下次需要它时,路径就会被清除。 🌾

Logo

更多推荐