docker搭建个人随机图片API
注意:若要为随机图片api配置域名并设置反向代理时,不要开启反向代理的缓存设置(会导致一直拿到同一张图)!!!
起因
在搭建个人博客时,希望文章封面可以不用每次手动设计,而是随机获取。而恰好butterfly主题提供了对应设置,于是就想自己部署一个自己的api。
将想法提供个chatgpt,它提出采用flask框架搭建。为了防止被其他人直接访问,又配置了api key的验证方式(虽然很简陋)。由于最近在学习docker,且本博客也是采用1panel面板搭建,于是将api配置成dockerfile,从而实现在docker中部署。
项目地址:random_image_api
(目前,发现一些不合理的地方,不应该每次访问都扫描获取图片列表,实际上只需要在修改图片名称等操作后更新图片列表。此外,docker的一些细节上,也有带改进,例如使用数据卷实现添加图片,后续学完docker知识后有时间会更正。)
项目结构
./image-api
├── app
│ ├── api_key.txt(初始api_key)
│ ├── app.py(服务)
│ ├── convert_images.sh(将/images图片转为webp格式,非图片删除,可能会误删,建议图片格式png、jpg、webp)
│ └── update_api_key.sh(生成或更新key)
├── Dockerfile(docker镜像构建文件)
└── README.md
构建
项目文档中有详细的使用步骤,请参考项目文档构建使用...
app.py介绍
使用 Python 的 Flask 框架来快速构建一个简单的
API
服务: 这里相较于最初版本,添加更新图片的功能。在最初版本,在每一次请求时都需要获取一次图片列表和 api 秘钥,显然这样是十分低效的。实际上对于图片列表和 api 秘钥,大部分时间都是处于不变状态,因此只需要在更新的时候动态维护即可。此外靠虑了很久,我仍然不认可由 url 控制 api 秘钥和图片列表的更新,这并不安全,即使这样可以简化操作。故而 url 只负责对数据的更新维护,不具有脚本执行的权利,更新脚本仍然需要用户手动执行。
实际上在最初的时候,我并不太看好 url 控制更新。我采用的是 flask cli,这期间也出现一些问题,在此记录一下:
使用 flask run 启动应用时,Flask 会自己管理应用的启动,而不会执行 app.py 的
if __name__ == '__main__':
中的初始化代码。这是因为flask run
会启动 Flask 服务器,而不是通过python app.py
或其他方式直接启动应用。使用 flask cli 需要使用
CMD ["flask run --host=0.0.0.0"]
启动(并配置启动环境ENV FLASK_APP=app.py
),但是官方不建议在生产环境中使用 flask 内置服务器(即使用 flask run 启动服务)。
后来我又采用
CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:5000", "--timeout", "37", "app:app"]
启动(2.0版本),不知是否是参数设置的问题,响应速度很慢,最终还是改回CMD ["python","app.py"]
(3.0版本)。具体代码及其部分注释如下:
from flask import Flask, request, jsonify, send_file from functools import wraps from flask_cors import CORS import os import secrets import random # import subprocess import logging from logging.handlers import RotatingFileHandler app = Flask(__name__) CORS(app) """ 启用跨域支持,默认允许所有来源 如果想限制允许访问的域名,在CORS中传入特定参数,如: CORS(app, origins=['https://example.com']) """ # api秘钥文件 API_KEY_FILE = '/app/api_key.txt' # 图片目录 IMAGE_DIR = '/app/images' # API密钥 app.config['API_KEY'] = None # 维护图片列表 app.config['IMAGE_LIST'] = [] # 检查文件是否存在 def check_file_exists(path): if not os.path.exists(path): logging.error(f'{path} not found. Exiting...') return False return True # 装饰器:验证API密钥 def require_api_key(f): @wraps(f) def decorated_function(*args, **kwargs): # 从url参数中获取API密钥 token = kwargs.get('key') # 若从请求头中获取API密钥:request.args.get('key') if not token or not verify_api_key(token): return jsonify({"error": "Unauthorized"}), 401 return f(*args, **kwargs) return decorated_function # 更新API密钥(docker exec -it <container_name> update_api_key.sh) @app.route('/update-key/<key>', methods=['GET']) @require_api_key def update_api_key(key): # 维护API密钥 app.config['API_KEY'] = read_api_key() if not app.config['API_KEY']: return jsonify({"error": "API key not found or invalid"}), 404 return jsonify({"message": "API key updated successfully"}), 200 # 从文件中读取API密钥 def read_api_key(): if os.path.exists(API_KEY_FILE): with open(API_KEY_FILE, 'r') as f: key = f.read().strip() if key: return key logging.error(f'API key file {API_KEY_FILE} not found or empty.Try run "update-api-key" command.') return None # 验证请求中提供的API密钥 def verify_api_key(token): stored_key = app.config['API_KEY'] return stored_key and secrets.compare_digest(stored_key, token) # 解析图片列表 def parse_image_list(): # 检查目录是否存在,不存在则创建 if not os.path.exists(IMAGE_DIR): os.makedirs(IMAGE_DIR) app.config['IMAGE_LIST'] = [f for f in os.listdir(IMAGE_DIR) if f.endswith('.webp')] logging.info(f"Initialized with {len(app.config['IMAGE_LIST'])} images.") # 更新并转换图片(docker exec -it <container_name> convert_images.sh) @app.route('/update-images/<key>', methods=['GET']) @require_api_key def update_images(key): # 维护图片列表 parse_image_list() # 判断维护结果并做出回应 if not app.config['IMAGE_LIST']: return jsonify({"error": "No images found"}), 404 return jsonify({"message": "Images list updated"}), 200 # 随机获取一张图片 @app.route('/random-image/<key>', methods=['GET']) @require_api_key def random_image(key): if not app.config['IMAGE_LIST']: return jsonify({"error": "No images found"}), 404 selected_image = random.choice(app.config['IMAGE_LIST']) return send_file(os.path.join(IMAGE_DIR, selected_image), mimetype='image/webp') if __name__ == '__main__': # 初始化日志 handler = RotatingFileHandler('/app/log/app.log', maxBytes=10*1024*1024, backupCount=10) handler.setLevel(logging.INFO) # 设置信息级别 formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) logging.getLogger().addHandler(handler) logging.getLogger().addHandler(logging.StreamHandler()) # 确保标准输出也记录日志 logging.getLogger().setLevel(logging.INFO) # 初始化图片列表 parse_image_list() # 初始化API密钥 app.config['API_KEY'] = read_api_key() logging.info(f"API key init:{app.config['API_KEY']}") if not app.config['API_KEY']: logging.error("API key not found or invalid. Exiting.") exit(1) # 运行Flask应用 app.run(host='0.0.0.0', port=5000)
gpt建议,在启动时判断一些文件或者目录是否存在并处理,后续可以不再判断。未采用,不排除容器运行过程中失手删除某些文件的可能。但在此记录一下,可以作为以后其他项目优化的一个思路。
convert_images.sh
#!/bin/bash
cd /app/images || exit
for file in *; do
# 跳过已经是 WEBP 格式的文件
if [[ "$file" == *.webp ]]; then
echo "$file is already in WEBP format. Skipping..."
continue
fi
# 检查文件是否为常见图片格式(jpg, png, gif等)
if file "$file" | grep -qE 'image|bitmap'; then
# 转换图片为 WEBP 格式
echo "Converting $file to WEBP format..."
cwebp -quiet "$file" -o "${file%.*}.webp"
# 检查转换是否成功
if [[ -f "${file%.*}.webp" ]]; then
echo "$file successfully converted. Deleting original..."
rm "$file"
else
echo "Failed to convert $file. Skipping deletion."
fi
else
echo "$file is not a supported image format. Deleting..."
rm "$file"
fi
done
update_api_key.sh
#!/bin/bash
# 定义API密钥存储文件路径
API_KEY_FILE="/app/api_key.txt"
# 生成新的API密钥
new_key=$(openssl rand -hex 32)
# 将新密钥覆盖写入文件
echo "$new_key" > "$API_KEY_FILE"
# 打印新生成的API密钥,使用颜色和格式使其更显眼
echo -e "\n\033[1;32m========================================"
echo -e "New API key generated:"
echo -e "\033[1;31m$new_key\033[0m" # 红色加粗显示API密钥
echo -e "\033[1;32m========================================\033[0m\n"
Dockerfile
接下来,需要创建一个
Dockerfile
来定义Docker镜像的构建过程。但是在此过程中,还是遇到了一些问题:开始时我打算在容器构建时将图片加入到容器并进行格式转换,但是这样会导致容器构建及其缓慢,并且最重要的是后续将系统目录挂载到该位置时,容器的图片文件夹文件夹会被覆盖,功亏一篑。
于是我又打算不使用系统目录挂载,而是使用数据卷,虽然通过命令很容易知道数据卷位置,但是后续实际操作发现没有权限,根本进不去。
于是只能退而求其次,先启动容器挂载系统目录,然后再添加图片和装换格式。
Dockerfile 文件及其部分注释内容如下:
# 使用python基础镜像 FROM python:3.11-slim # 换源并安装必要的软件包和工具(apt-utils略) RUN echo "deb http://mirrors.aliyun.com/debian/ bookworm main non-free contrib" > /etc/apt/sources.list \ && echo "deb-src http://mirrors.aliyun.com/debian/ bookworm main non-free contrib" >> /etc/apt/sources.list \ && echo "deb http://mirrors.aliyun.com/debian-security bookworm-security main" >> /etc/apt/sources.list \ && echo "deb-src http://mirrors.aliyun.com/debian-security bookworm-security main" >> /etc/apt/sources.list \ && echo "deb http://mirrors.aliyun.com/debian/ bookworm-updates main" >> /etc/apt/sources.list \ && echo "deb-src http://mirrors.aliyun.com/debian/ bookworm-updates main" >> /etc/apt/sources.list \ && pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ \ && apt-get update \ && apt-get install -y \ imagemagick \ # 图片修复时使用 libwebp-dev \ openssl \ file \ webp \ && pip install --upgrade pip \ && pip install Flask \ && pip install flask-cors \ && pip install gunicorn \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # 设置工作目录并复制文件 WORKDIR /app COPY ./app /app # 使脚本可执行并执行 RUN chmod +x /app/convert_images.sh /app/update_api_key.sh # 暴露应用运行的端口 EXPOSE 5000 # 指定运行应用的命令 CMD ["python","app.py"]
gpt 建议将 /app/convert_images.sh 执行过程放到 CMD 中,即容器执行过程中,以简化容器构建过程。但我觉得会导致容器启动及其缓慢,未采用。但在此记录一下,可以作为以后其他项目优化的一个思路。