🌈 利用个人服务器搭建Typora图床(Typora+Ngnix+Python)

☀️ 背景知识

我的博客网站pwfocus.com 没有搞管理后台,平时博文是在Typora里写Markdown,博客网站只需要将Typora的markdown博文在前端渲染为HTML就可以。在写Markdown博文过程中,经常会粘贴一些图片或者截图到Typora软件里,在Typora里直接插入图片,都是直接存储在电脑本地路径,不是存储在远程服务器上。例如:

# Typora在Windows下的路径
C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20241013204136053.png

Typora里插入和粘贴的图片保存在电脑本地: 不是远程HTTP连接,有两个缺点:

❌ 如果不在服务器上,有一天需要迁移到其他软件里,例如: Obsidian。文章里的图片地址需要重新编辑。

❌ 图片保存在本地,例如: 保存在C盘,没有自动生成HTTP链接,则需要手动将每个图片地址贴到博客里。

网上介绍使用PicGo等插件上传至云厂商OSS存储服务,都无法满足我的需求,我的需求主要有三点:

🔥博文里的图片粘贴到Typora软件,同时自动上传到个人服务器并生成HTTP链接。

🔥每个上传到服务器的图片地址,都使用该图片MD5进行命名。

🔥截图通常是PNG图片,要转换为JPEG,因为JPEG比PNG小很多,有利于网页加载速度。

🍄 预期效果

开始之前先看下最终想要的效果。下面分别是保存在电脑本地和自动上传图片博客服务器的效果演示。

截屏或者粘贴的图片插入到Typora里如果没有自动上传到远程服务器,就如下图里链接显示的C盘。

65dc25a96d589ef28368bf35c2b07426

截屏或者粘贴的图片插入到Typora里如果没有自动上传到远程服务器,如下图里链接被替换为HTTP图片地址了。

52d96a9b5b5711a5c87ecc5cfb3060a8

🔥具体步骤

💥1. 自动连接到博客服务器

将图片自动上传到服务器上,有两种方式:

1️⃣服务器提供API,图片通过API方式POST到服务器上,保存在服务器某个目录下。

2️⃣通过SSH的方式,使用公钥和私钥或用户名和密码两种认证,将图片保存到服务器某个目录下。

这里选择第二种方式,因为维护服务器需要高频率登录远程服务器,就是通过SSH加密登录,非常方便。

SSH加密认证登录可以参考Windows11 WSL配置VsCode Remote开发环境 文中的第五步: Windows11配置SSH密钥,用于VsCode免密登录。MacOS和Linux的方式类似。如果有必要可以单开一篇文章单独介绍。

💥2. 确定上传路径

我的博客服务器采用Nginx做转发,所以配置图片静态资源比较简单。可以在nginx.conf里配置assets目录:

server {
    listen       8090;
    server_name  localhost;

    location / {
        root   html;
        index  index.html index.htm;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }

    ## 主要关注这里,将所有的文件都存储在/home/work/apps/nginx/assets路径下。
    ## 注意这里是root方式
    location /assets/ {
        root /home/work/apps/nginx;
    }
}

💥3. 生成图片自动上传脚本

图片自动上传脚本是Python实现,原因是Python库生态太好。可以参考网络其他文章。图片自动上传脚本中会用到requests、paramiko、Pillow三个第三方库, 需要额外安装, 安装命令如下:

pip install paramiko
pip install requests
pip install Pillow

图片自动上传脚本主要实现以下功能:

1️⃣ 将Typora服务器存储在本地搬到另外一个目录下,并计算该文件的md5。copy_file函数实现。

2️⃣ 将PNG格式转为JPEG格式,由convert函数实现。

3️⃣SSH连接到服务器上,通过sftp将转化后的JPEG图片上传远程服务器的/home/work/app/nginx/assets目录下,由upload函数实现。

4️⃣上传之后,验证远程服务器的链接是否可访问,由verify函数实现。

5️⃣如果上传成功,输出该URL,就会自动替换Typora的本地图片存储路径为该HTTP图片地址。

Python代码如下, 完整代码参考pichub.py Github地址:

## pichub.py
import sys
import os
import json
import shutil
import hashlib
import paramiko
import requests
from io import BytesIO
from PIL import Image

def cal_md5(file_path):
    # 检查路径是否存在
    if not os.path.exists(file_path):
        return None

    # 确保路径是一个文件,而不是目录
    if not os.path.isfile(file_path):
        return None

    # 计算文件的MD5哈希值
    md5_hash = hashlib.md5()
    with open(file_path, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            md5_hash.update(chunk)

    # 返回MD5哈希值的十六进制表示
    return md5_hash.hexdigest()

def upload(pic_path, config):
    private_key_path = config["private_key_path"]
    hostname = config["hostname"]
    username = config["username"]
    remote_path = config["remote_dir"]

    private_key = paramiko.RSAKey.from_private_key_file(private_key_path)
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())  # 自动添加主机密钥,生产环境请慎用

    try:
        ssh.connect(hostname=hostname, port=22, username=username, pkey=private_key)
        #print(f"Connected to {hostname}")

        # 创建SFTP客户端
        sftp = ssh.open_sftp()
        try:
            # 上传文件到远程路径
            remote_file_path = os.path.join(remote_path, os.path.basename(pic_path))
            sftp.put(pic_path, remote_file_path)
            return True
        finally:
            sftp.close()
    finally:
        ssh.close()
    return False

def convert(pic_path):
    image = Image.open(pic_path)
    if image.format == "PNG":
        jpeg_image = image.convert('RGB')

        # 使用BytesIO保存临时的JPEG数据
        jpeg_io = BytesIO()
        jpeg_image.save(jpeg_io, format="JPEG")
        jpeg_data = jpeg_io.getvalue()
        md5_hash = hashlib.md5(jpeg_data).hexdigest()

        # 获取原始图片所在的目录,并构造输出文件的完整路径
        output_dir = os.path.dirname(pic_path)
        output_path = os.path.join(output_dir, f'{md5_hash}.jpeg')

        # 保存转换后的JPEG图片到文件
        with open(output_path, 'wb') as output_file:
            output_file.write(jpeg_data)

        # 返回转换后的JPEG图片的完整路径
        return output_path
    else:
        return pic_path

def copy_file(pic_path, dst_dir):
    ## 将pic_path移动到一个dst目录下,并按照md5sum.png的方式进行存储
    md5sum = cal_md5(pic_path)
    if not os.path.exists(dst_dir):
        os.makedirs(dst_dir)

    _, ext = os.path.splitext(pic_path)
    dst_path = os.path.join(dst_dir, f"{md5sum}{ext}")
    shutil.copy2(pic_path, dst_path)
    out_path = convert(dst_path)
    return out_path

def compose_url(pic_path, config):
    hostname = config["hostname"]
    #port = config["http_port"]
    filename = os.path.basename(pic_path)
    #url = f"http://{hostname}:{port}/assets/{filename}"
    url = f"https://pwfocus.com/assets/{filename}"
    return url

def verify(pic_path, config):
    url = compose_url(pic_path, config)
    response = requests.get(url)
    if response.status_code >= 200 and response.status_code < 400:
        print(url)
        return True

def main():
    # Typora测试的两个文件路径
    # C:\Users\Administrator\AppData\Local\Temp\Typora\typora-icon2.png
    # C:\Users\Administrator\AppData\Local\Temp\Typora\typora-icon.png

    json_file_path = "D:\\python\\config.json"
    with open(json_file_path, 'r') as json_file:
        config = json.load(json_file)

    if len(sys.argv) > 1:
        for pic_path in sys.argv[1:]:
            dst_path = copy_file(pic_path, config["local_assets_dir"])
            upload(dst_path, config)
            verify(dst_path, config)
    else:
        print("no args")

if __name__ == "__main__":
    main()

配置文件如下:

{
    "private_key_path" : "C:\\Users\\Administrator\\.ssh\\id_rsa",
    "local_assets_dir" : "D:\\Typora\\assets", // rename存储的路径
    "hostname" : "172.20.95.125",
    "username" : "work",
    "remote_dir" : "/home/work/apps/nginx/assets/",
    "ssl_port" : 22,
    "http_port" : 8090
}

💥4. 配置自动上传脚本

打开Typora软件

1️⃣找到 文件->偏好设置->图像

2️⃣修改插入图片的动作为上传图片

3️⃣上传服务设定,选择自定义命令

4️⃣命令了输入: python pichub.py(注意这里需要输入完整的pichub的完整路径)

d9cbd9464c55150426927a03517f0d6a

截止到这里,图床服务就构建完成了。源代码也已分享,快乐使用吧。

📱 联系方式

更多文章或咨询关注:微信公众号(pwfocus) 项目合作加个人微信
pwfocus 个人微信