🌈 利用个人服务器搭建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盘。
截屏或者粘贴的图片插入到Typora里如果没有自动上传到远程服务器,如下图里链接被替换为HTTP图片地址了。
🔥具体步骤
💥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的完整路径)
截止到这里,图床服务就构建完成了。源代码也已分享,快乐使用吧。
📱 联系方式
更多文章或咨询关注:微信公众号(pwfocus) | 项目合作加个人微信 |
---|---|