基于Hexo与OwnCloud的自动化博客发布系统设计与实现
安装Docker
[root@localhost ~]# dnf -y install dnf-plugins-core
[root@localhost ~]# dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
[root@localhost ~]# dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
[root@localhost ~]# systemctl enable --now docker
[root@localhost ~]# yum update
[root@localhost ~]# yum install docker-compose-plugin
构建Hexo容器基础镜像
[root@localhost ~]# docker pull node
[root@localhost ~]# vi Dockerfile
# 将以下内容写入在Dockerfile中
FROM node:latest
MAINTAINER Shadowyingyan
EXPOSE 4000
WORKDIR /hexo
RUN npm install hexo-cli -g
RUN hexo init .
RUN npm install
CMD ["hexo", "server"]
# 文件截止
[root@localhost ~]# docker build -t 'hexo:latest' .
[+] Building 104.4s (9/9) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 250B 0.0s
=> WARN: MaintainerDeprecated: Maintainer instruction is deprecated in favor of using label (line 3) 0.0s
=> [internal] load metadata for docker.io/library/node:latest 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/5] FROM docker.io/library/node:latest 0.0s
=> [2/5] WORKDIR /hexo 0.0s
=> [3/5] RUN npm install hexo-cli -g 20.2s
=> [4/5] RUN hexo init . 81.3s
=> [5/5] RUN npm install 1.6s
=> exporting to image 1.3s
=> => exporting layers 1.3s
=> => writing image sha256:7b3f0e7f46c16a6b05d1202bff3041240c64c580c78ee1a9ebf15569f8fe951b 0.0s
=> => naming to docker.io/library/hexo:latest 0.0s
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hexo latest 7b3f0e7f46c1 3 minutes ago 1.24GB
node latest 71e2b415fa05 3 days ago 1.12GB
测试Hexo基础镜像
[root@localhost ~]# docker run -it --name="temp" -p 4000:4000 hexo:latest
INFO Validating config
INFO Start processing
INFO Hexo is running at http://localhost:4000/ . Press Ctrl+C to stop.
使用浏览器访问虚拟机的IP地址加4000端口可以访问到Hexo基础容器。
编写Owncloud容器的编排内容
[root@localhost ~]# mkdir owncloud-docker-server
[root@localhost owncloud-docker-server]# vi docker-compose.yml
# 将以下内容写入到文本中
version: "3"
volumes:
mysql:
driver: local
redis:
driver: local
services:
owncloud:
image: owncloud/server:${OWNCLOUD_VERSION}
container_name: owncloud_server
restart: always
ports:
- ${HTTP_PORT}:8080
depends_on:
- mariadb
- redis
environment:
- OWNCLOUD_DOMAIN=${OWNCLOUD_DOMAIN}
- OWNCLOUD_TRUSTED_DOMAINS=${OWNCLOUD_TRUSTED_DOMAINS}
- OWNCLOUD_DB_TYPE=mysql
- OWNCLOUD_DB_NAME=owncloud
- OWNCLOUD_DB_USERNAME=owncloud
- OWNCLOUD_DB_PASSWORD=owncloud
- OWNCLOUD_DB_HOST=mariadb
- OWNCLOUD_ADMIN_USERNAME=${ADMIN_USERNAME}
- OWNCLOUD_ADMIN_PASSWORD=${ADMIN_PASSWORD}
- OWNCLOUD_MYSQL_UTF8MB4=true
- OWNCLOUD_REDIS_ENABLED=true
- OWNCLOUD_REDIS_HOST=redis
healthcheck:
test: ["CMD", "/usr/bin/healthcheck"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- /data/owncloud/file:/mnt/data
mariadb:
image: mariadb:10.11 # minimum required ownCloud version is 10.9
container_name: owncloud_mariadb
restart: always
environment:
- MYSQL_ROOT_PASSWORD=owncloud
- MYSQL_USER=owncloud
- MYSQL_PASSWORD=owncloud
- MYSQL_DATABASE=owncloud
- MARIADB_AUTO_UPGRADE=1
command: ["--max-allowed-packet=128M", "--innodb-log-file-size=64M"]
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-u", "root", "--password=owncloud"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- mysql:/var/lib/mysql
redis:
image: redis:6
container_name: owncloud_redis
restart: always
command: ["--databases", "1"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- redis:/data
# 文件结束
[root@localhost owncloud-docker-server]# vi .env
# 将以下内容添加到文件中
OWNCLOUD_VERSION=10.15
OWNCLOUD_DOMAIN=localhost:8080
OWNCLOUD_TRUSTED_DOMAINS=192.168.17.128
ADMIN_USERNAME=admin
ADMIN_PASSWORD=admin
HTTP_PORT=8080
# 文件结束
[root@localhost owncloud-docker-server]# docker compose up -d
WARN[0000] /root/owncloud-docker-server/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
[+] Running 4/4
✔ Network owncloud-docker-server_default Created 0.0s
✔ Container owncloud_redis Started 0.2s
✔ Container owncloud_mariadb Started 0.2s
✔ Container owncloud_server Started 0.3s
[root@localhost owncloud-docker-server]#
使用浏览器访问虚拟机IP地址加8080端口发现可以访问到Owncloud页面。
编写hexo+owncloud整合编排文件
现在需要将Hexo容器的内容和Owncloud容器的内容整合到一个docker-compose.yml文件中。
[root@localhost ~]# mkdir hexo-owncloud
[root@localhost ~]# cp owncloud-docker-server/.env hexo-owncloud/.
[root@localhost ~]# cd hexo-owncloud/
[root@localhost hexo-owncloud]# vi docker-compose.yml
# 将以下内容写入到文件中
version: "3"
volumes:
mysql:
driver: local
redis:
driver: local
services:
hexo:
image: hexo:latest
container_name: hexo_server
restart: always
ports:
- 80:4000
volumes:
- /data/hexo/:/hexo/
tty: true
command: ["tail", "-f", "/dev/null"]
owncloud:
image: owncloud/server:${OWNCLOUD_VERSION}
container_name: owncloud_server
restart: always
ports:
- ${HTTP_PORT}:8080
depends_on:
- mariadb
- redis
environment:
- OWNCLOUD_DOMAIN=${OWNCLOUD_DOMAIN}
- OWNCLOUD_TRUSTED_DOMAINS=${OWNCLOUD_TRUSTED_DOMAINS}
- OWNCLOUD_DB_TYPE=mysql
- OWNCLOUD_DB_NAME=owncloud
- OWNCLOUD_DB_USERNAME=owncloud
- OWNCLOUD_DB_PASSWORD=owncloud
- OWNCLOUD_DB_HOST=mariadb
- OWNCLOUD_ADMIN_USERNAME=${ADMIN_USERNAME}
- OWNCLOUD_ADMIN_PASSWORD=${ADMIN_PASSWORD}
- OWNCLOUD_MYSQL_UTF8MB4=true
- OWNCLOUD_REDIS_ENABLED=true
- OWNCLOUD_REDIS_HOST=redis
healthcheck:
test: ["CMD", "/usr/bin/healthcheck"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- /data/owncloud/passwd:/etc/passwd
- /data/owncloud/file/:/mnt/data
mariadb:
image: mariadb:10.11 # minimum required ownCloud version is 10.9
container_name: owncloud_mariadb
restart: always
environment:
- MYSQL_ROOT_PASSWORD=owncloud
- MYSQL_USER=owncloud
- MYSQL_PASSWORD=owncloud
- MYSQL_DATABASE=owncloud
- MARIADB_AUTO_UPGRADE=1
command: ["--max-allowed-packet=128M", "--innodb-log-file-size=64M"]
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-u", "root", "--password=owncloud"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- mysql:/var/lib/mysql
redis:
image: redis:6
container_name: owncloud_redis
restart: always
command: ["--databases", "1"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- redis:/data
# 文件结束
创建hexo的文件夹目录
[root@localhost ~]# mkdir /data/hexo
[root@localhost ~]# chmod 777 /data/hexo
创建passwd文件内容,使www-data用户有权限操作owncloud容器内部内容。
[root@localhost ~]# vi /data/owncloud/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/bin/bash
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
删除刚刚测试的内容
[root@localhost ~]# cd owncloud-docker-server/
[root@localhost owncloud-docker-server]# docker compose down
WARN[0000] /root/owncloud-docker-server/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
[+] Running 4/4
✔ Container owncloud_server Removed 10.1s
✔ Container owncloud_redis Removed 0.1s
✔ Container owncloud_mariadb Removed 0.3s
✔ Network owncloud-docker-server_default Removed
[root@localhost owncloud-docker-server]# docker volume ls
DRIVER VOLUME NAME
local owncloud-docker-server_mysql
local owncloud-docker-server_redis
[root@localhost owncloud-docker-server]# rm -rf /data/owncloud/file/*
[root@localhost owncloud-docker-server]# docker volume rm owncloud-docker-server_mysql owncloud-docker-server_redis
owncloud-docker-server_mysql
owncloud-docker-server_redis
[root@localhost hexo-owncloud]# docker rm temp
temp
运行Docker compose编排文件
[root@localhost ~]# cd hexo-owncloud/
[root@localhost hexo-owncloud]# docker compose up -d
WARN[0000] /root/hexo-owncloud/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
[+] Running 4/4
✔ Container hexo_server Running 0.0s
✔ Container owncloud_redis Running 0.0s
✔ Container owncloud_mariadb Running 0.0s
✔ Container owncloud_server Running 0.0s
进入Hexo容器初始化Hexo文件夹
[root@localhost hexo-owncloud]# docker exec -it hexo_server bash
root@e6c4bceb9695:/hexo# hexo init
创建owncloud上的文件夹
访问owncloud,创建文件夹hexo_posts用来存放文档.
手动刷新owncloud数据命令:
[root@localhost hexo-owncloud]# docker exec -it owncloud_server entrypoint bash
su - www-data -c 'php /var/www/owncloud/occ files:scan --all'
Python代码
在/root下创建main.py
#!/usr/bin/env python3
import os
import time
import shutil
import subprocess
import fcntl
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
# 配置路径
HEXO_POSTS_DIR = "/data/owncloud/file/files/admin/files/hexo_posts"
HEXO_TARGET_DIR = "/data/hexo/source/_posts"
LOCK_FILE = "/tmp/hexo_sync.lock"
# 容器相关命令
HEXO_CONTAINER = "hexo_server"
HEXO_CLEAN_CMD = f"docker exec {HEXO_CONTAINER} hexo clean"
HEXO_GENERATE_CMD = f"docker exec {HEXO_CONTAINER} hexo generate"
HEXO_SERVER_CMD = f"docker exec -d {HEXO_CONTAINER} hexo server"
# 同步控制参数
BATCH_OPERATION_DELAY = 5 # 批量操作等待时间(秒)
MIN_SYNC_INTERVAL = 10 # 最小同步间隔(秒)
class FileSync:
def __init__(self):
self.last_sync_time = 0
self.pending_sync = False
self.batch_operation_detected = False
self.batch_operation_time = 0
self.lock_file = None
self.setup_dirs()
def setup_dirs(self):
"""确保所需目录存在"""
os.makedirs(HEXO_POSTS_DIR, exist_ok=True)
os.makedirs(HEXO_TARGET_DIR, exist_ok=True)
def is_markdown_file(self, filename):
"""检查文件是否是markdown文件"""
return filename.lower().endswith('.md') and not self.is_temp_file(filename)
def is_temp_file(self, filename):
"""检查是否是临时文件"""
return '.ocTransferId' in filename or filename.endswith('.part')
def is_file_empty(self, filepath):
"""检查文件是否为空"""
try:
return os.path.getsize(filepath) == 0
except:
return True
def run_command(self, cmd, background=False):
"""执行shell命令"""
try:
if background:
subprocess.Popen(cmd, shell=True, start_new_session=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
else:
subprocess.run(cmd, shell=True, check=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return True
except subprocess.CalledProcessError as e:
print(f"[ERROR] 命令执行失败: {cmd}\n错误: {e}")
return False
def acquire_lock(self):
"""获取文件锁防止多进程冲突"""
self.lock_file = open(LOCK_FILE, 'w')
try:
fcntl.flock(self.lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
return True
except IOError:
print("[WARN] 另一个同步操作正在进行,跳过本次操作")
return False
def release_lock(self):
"""释放文件锁"""
try:
if self.lock_file:
fcntl.flock(self.lock_file, fcntl.LOCK_UN)
self.lock_file.close()
except:
pass
def sync_hexo_posts(self, force=False):
"""同步hexo_posts到hexo目标目录"""
current_time = time.time()
# 检查同步间隔
if not force and current_time - self.last_sync_time < MIN_SYNC_INTERVAL:
print(f"[INFO] 同步操作过于频繁,跳过本次同步")
return False
# 获取文件锁
if not self.acquire_lock():
return False
try:
print("[INFO] 开始同步文件...")
# 获取当前有效的markdown文件列表
current_files = set()
for filename in os.listdir(HEXO_POSTS_DIR):
filepath = os.path.join(HEXO_POSTS_DIR, filename)
if (os.path.isfile(filepath) and \
self.is_markdown_file(filename) and \
not self.is_file_empty(filepath)):
current_files.add(filename)
# 获取目标目录现有文件列表
target_files = set(os.listdir(HEXO_TARGET_DIR))
# 需要删除的文件(在目标目录但不在源目录)
files_to_remove = target_files - current_files
# 需要添加/更新的文件
files_to_update = current_files - target_files
for filename in current_files & target_files:
src_path = os.path.join(HEXO_POSTS_DIR, filename)
dst_path = os.path.join(HEXO_TARGET_DIR, filename)
if os.path.getmtime(src_path) > os.path.getmtime(dst_path):
files_to_update.add(filename)
# 如果没有变化且不强制更新,则跳过
if not force and not files_to_remove and not files_to_update:
print("[INFO] 没有检测到文件变更,跳过同步")
return False
# 删除多余文件
for filename in files_to_remove:
try:
os.remove(os.path.join(HEXO_TARGET_DIR, filename))
print(f"[INFO] 删除文件: {filename}")
except Exception as e:
print(f"[ERROR] 删除文件失败: {filename}\n错误: {e}")
# 添加/更新文件
for filename in files_to_update:
src_path = os.path.join(HEXO_POSTS_DIR, filename)
dst_path = os.path.join(HEXO_TARGET_DIR, filename)
try:
shutil.copy2(src_path, dst_path)
os.chmod(dst_path, 0o777)
print(f"[INFO] 更新文件: {filename}")
except Exception as e:
print(f"[ERROR] 复制文件失败: {src_path} -> {dst_path}\n错误: {e}")
# 重新生成hexo
print("[INFO] 重新生成Hexo站点...")
self.run_command(HEXO_CLEAN_CMD)
self.run_command(HEXO_GENERATE_CMD)
self.run_command(HEXO_SERVER_CMD, background=True)
self.last_sync_time = time.time()
return True
finally:
self.release_lock()
def check_batch_operation(self):
"""检查是否处于批量操作状态"""
if self.batch_operation_detected:
if time.time() - self.batch_operation_time > BATCH_OPERATION_DELAY:
self.batch_operation_detected = False
print("[INFO] 批量操作结束,开始同步")
self.sync_hexo_posts()
else:
self.pending_sync = True
elif self.pending_sync:
self.pending_sync = False
self.sync_hexo_posts()
class HexoHandler(FileSystemEventHandler):
def __init__(self, file_sync):
super().__init__()
self.file_sync = file_sync
def on_any_event(self, event):
"""处理所有文件系统事件"""
if event.is_directory:
return
filename = os.path.basename(event.src_path)
# 忽略临时文件和非Markdown文件
if self.file_sync.is_temp_file(filename) or not self.file_sync.is_markdown_file(filename):
return
# 批量操作检测
current_time = time.time()
if not self.file_sync.batch_operation_detected:
self.file_sync.batch_operation_detected = True
self.file_sync.batch_operation_time = current_time
print("[INFO] 检测到批量操作,等待操作完成...")
# 重置批量操作计时器
self.file_sync.batch_operation_time = current_time
# 根据事件类型处理
if event.event_type == 'deleted':
print(f"[INFO] 检测到文件删除: {filename}")
else:
print(f"[INFO] 检测到文件变更: {filename} ({event.event_type})")
# 计划同步
self.file_sync.check_batch_operation()
def main():
file_sync = FileSync()
# 初始同步
print("[INFO] 初始处理已存在的文件...")
for filename in os.listdir(HEXO_POSTS_DIR):
filepath = os.path.join(HEXO_POSTS_DIR, filename)
if os.path.isfile(filepath):
if file_sync.is_temp_file(filename):
continue
if file_sync.is_markdown_file(filename) and file_sync.is_file_empty(filepath):
try:
os.remove(filepath)
except:
pass
file_sync.sync_hexo_posts(force=True)
# 设置文件监控
print("[INFO] 开始监控文件变化...")
event_handler = HexoHandler(file_sync)
observer = Observer()
observer.schedule(event_handler, HEXO_POSTS_DIR, recursive=False)
observer.start()
try:
while True:
file_sync.check_batch_operation()
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
if __name__ == "__main__":
main()
安装pip并安装watchdog模块
[root@localhost ~]# dnf install python3-pip
[root@localhost ~]# python3 -m pip install watchdog
注册为系统服务
[root@localhost ~]# vi /etc/systemd/system/hexo_owncloud.service
[Unit]
Description=Hexo and OwnCloud File Sync Service
After=network.target docker.service
[Service]
Type=simple
User=root
WorkingDirectory=/root
ExecStart=/usr/bin/python3 -u /root/hexo_sync.py
Restart=always
RestartSec=5s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=hexo_owncloud
[Install]
WantedBy=multi-user.target
# 文件结束
[root@localhost ~]# mv main.py hexo_sync.py
[root@localhost ~]# systemctl daemon-reload
[root@localhost ~]# systemctl start hexo_owncloud
[root@localhost ~]# systemctl enable hexo_owncloud
Created symlink /etc/systemd/system/multi-user.target.wants/hexo_owncloud.service → /etc/systemd/system/hexo_owncloud.service.
配置Hexo
主题
[root@localhost ~]# docker exec -it hexo_server bash
root@e6c4bceb9695:/hexo# git clone https://github.com/zoeingwingkei/frame.git
Cloning into 'frame'...
remote: Enumerating objects: 535, done.
remote: Counting objects: 100% (169/169), done.
remote: Compressing objects: 100% (98/98), done.
remote: Total 535 (delta 88), reused 122 (delta 71), pack-reused 366 (from 1)
Receiving objects: 100% (535/535), 2.14 MiB | 2.28 MiB/s, done.
Resolving deltas: 100% (284/284), done.
root@e6c4bceb9695:/hexo# exit
[root@localhost ~]# vi /data/hexo/_config.yml
# 将文件中以下选项进行修改
theme: fram
[root@localhost ~]# docker restart hexo_server
hexo_server
[root@localhost ~]# systemctl restart hexo_owncloud
再次访问就会发现主题已经更改完成。
更改样式
进入到主题配置文件中修改一些内容:
[root@localhost hexo]# cd /data/hexo/themes/frame/
[root@localhost frame]# vi _config.yml
修改条目:
site_brand_name: 毕业设计
profile:
title: 基于Hexo与OwnCloud的自动化博客发布系统设计与实现
body: 22Cloud_Computing1
image: /szpulogo.png
links:
主页: /
文章: /archives/
powered_by: # Powered by Hexo & Frame
enable: false
进入到Hexo主配置文件中修改相关内容:
[root@localhost frame]# cd /data/hexo/
[root@localhost hexo]# vi _config.yml
修改条目:
title: 22Cloud_Computing1
author: 22Cloud_Computing1
上传图片
使用相关软件将主页Logo图片放到该目录下:
功能演示
基础上传文章功能
将毕设实现步骤文档以markdown格式上传到owncloud中。
再刷新hexo网站页面,会发现网站内容已经更新。
查看文章内容,文章内容可以正常显示
批量上传文章功能
一次性提交12篇文章,测试是否能正常生成十二篇文章在网页中
发现新上传的12篇文章都已经正常生成网页。
批量删除文章
选择如下文件进行批量删除
回到网页发现网页中的内容已经删除完毕
更改文章内容
在owncloud上打开一篇文章,在开头添加内容并进行保存。
回到网页发现已经成功修改文章内容
版本控制
在owncloud中更改文章以后会有版本控制,点击回退版本可以回退到指定的版本。
文章的内容也会同步更改。
文章回收站
在owncloud中删除文件有回收站的功能,可以在其中对文件进行还原。
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 Shadowyingyan