- 背景:自己搭了一个技术博客,少不了图片展示,不想特地买个图片服务器用于图片存储和访问,于是自己实现了一个图片上传和访问的服务。
- 你需要:
- 了解Python,Django [django 官方文档 : https://docs.djangoproject.com/zh-hans/2.0/]
- 有一台云服务器
- 实现的内容:
- 图片上传
- 图片存储
- 图片访问
数据库表&模型
- 我们需要使用数据库(mysql)保存图片的上传记录,有两个目的,第一是保存图片的基本信息如图片名,图片大小,图片的md5值(用于区分图片,防止重复上传相同的图片),图片类型等等;第二是为以后做对已上传的图片进行管理做铺垫。
create table upload_image(
id int unsigned auto_increment,
filename varchar(252) not null default "" comment "文件名",
file_md5 varchar(128) not null comment "文件的MD5值",
file_type varchar(32) not null default "" comment "文件类型",
file_size int unsigned not null default 0 comment "文件大小",
created_at datetime not null default current_timestamp,
updated_at datetime not null default current_timestamp on update current_timestamp,
primary key(id),
unique index(file_md5)
)engine=innodb default charset=utf8 comment "文件上传表";
- 我们使用django的orm(对象关系映射)做数据库访问的方式,模型定义如下:
from django.db import models
from datetime import datetime
from django.conf import settings
class UploadImage(models.Model):
class Meta:
db_table = "upload_image"
id = models.IntegerField(primary_key=True)
filename = models.CharField(max_length=252, default="")
file_md5 = models.CharField(max_length=128)
file_type = models.CharField(max_length=32)
file_size = models.IntegerField()
created_at = models.DateTimeField(default=datetime.now)
updated_at = models.DateTimeField(default=datetime.now)
# 我们还定义了通过文件md5值获取模型对象的类方法
@classmethod
def getImageByMd5(cls, md5):
try:
return UploadImage.objects.filter(file_md5=md5).first()
except Exception as e:
return None
# 获取本图片的url,我们可以通过这个url在浏览器访问到这个图片
# 其中settings.WEB_HOST_NAME 是常量配置,指你的服务器的域名
# settings.WEB_IMAGE_SERVER_PATH 也是常量配置,指你的静态图片资源访问路径
# 这些配置项我在Django项目的settings.py文件中进行配置
def getImageUrl(self):
filename = self.file_md5 + "." + self.file_type
url = settings.WEB_HOST_NAME + settings.WEB_IMAGE_SERVER_PATH + filename
return url
# 获取本图片在本地的位置,即你的文件系统的路径,图片会保存在这个路径下
def getImagePath(self):
filename = self.file_md5 + "." + self.file_type
path = settings.IMAGE_SAVING_PATH + filename
return path
def __str__(self):
s = "filename:" + str(self.filename) + " - " + "filetype:" + str(self.file_type) \
+ " - " + "filesize:" + str(self.file_size) + " - " + "filemd5:" + str(self.file_md5)
return s
视图方法
- 视图方法即是你的服务接口的实现,几乎所有业务逻辑都放在视图方法里,目前我们只需要一个服务接口,用于上传图片,并返回访问该图片的url 供我们访问。
- 在这个视图方法中,我们做的业务逻辑有:从请求中获取上传表单中的文件数据,检测文件的大小是否超出我们的限制,计算文件的md5值并判断该文件是否已经上传过(如果上传过我们直接返回该图片的url),判断该文件的类型是否为图片类型(我们只允许上传图片类型的文件),到目前为止通过所有的验证,插入一条数据库记录到数据库,保存图片到服务器磁盘,做一下日志记录,返回图片的url以供访问。
- 代码有详细的注释
rom django.views.decorators.http import require_http_methods
import filetype, hashlib
from app.views import returnOk, returnForbidden, returnBadRequest
from app.models import UploadImage
from django.conf import settings
# 上传文件的视图
@require_http_methods(["POST"])
def uploadImage(request):
# 从请求表单中获取文件对象
file = request.FILES.get("img", None)
if not file: # 文件对象不存在, 返回400请求错误
return returnBadRequest("need file.")
# 图片大小限制
if not pIsAllowedFileSize(file.size):
return returnForbidden("文件太大")
# 计算文件md5
md5 = pCalculateMd5(file)
uploadImg = UploadImage.getImageByMd5(md5)
if uploadImg: # 图片文件已存在, 直接返回
return returnOk({'url': uploadImg.getImageUrl()})
# 获取扩展类型 并 判断
ext = pGetFileExtension(file)
if not pIsAllowedImageType(ext):
return returnForbidden("文件类型错误")
# 检测通过 创建新的image对象
# 文件对象即上一小节的UploadImage模型
uploadImg = UploadImage()
uploadImg.filename = file.name
uploadImg.file_size = file.size
uploadImg.file_md5 = md5
uploadImg.file_type = ext
uploadImg.save() # 插入数据库
# 保存 文件到磁盘
with open(uploadImg.getImagePath(), "wb+") as f:
# 分块写入
for chunk in file.chunks():
f.write(chunk)
# 记录日志,这一步可有可无,可定制
FileLogger.log_info("upload_image", uploadImg, FileLogger.IMAGE_HANDLER)
# 返回图片的url以供访问
return returnOk({"url": uploadImg.getImageUrl()})
# 检测文件类型
# 我们使用第三方的库filetype进行检测,而不是通过文件名进行判断
# pip install filetype 即可安装该库
def pGetFileExtension(file):
rawData = bytearray()
for c in file.chunks():
rawData += c
try:
ext = filetype.guess_extension(rawData)
return ext
except Exception as e:
# todo log
return None
# 计算文件的md5
def pCalculateMd5(file):
md5Obj = hashlib.md5()
for chunk in file.chunks():
md5Obj.update(chunk)
return md5Obj.hexdigest()
# 文件类型过滤 我们只允许上传常用的图片文件
def pIsAllowedImageType(ext):
if ext in ["png", "jpeg", "jpg"]:
return True
return False
# 文件大小限制
# settings.IMAGE_SIZE_LIMIT是常量配置,我设置为10M
def pIsAllowedFileSize(size):
limit = settings.IMAGE_SIZE_LIMIT
if size < limit:
return True
return False
- 到目前为止我们实现了所有的业务逻辑,接下来将该视图方法配置到路由中即可(一般是urls.py文件)
# 上传图片的接口
path("upload/image", upload_views.uploadImage),
部署
- 我买了一台腾讯云的服务器,1核2GB内存,三年的使用时间,费用2000,这样的配置自己玩一玩一般是足够了的。
- 我们使用ngnix来做代理服务器,使用supervisor作为服务的管理器,使用gunicorn作为http 服务器(即让django跑起来)
- nginx和supervisor的安装有大把教程,gunicorn直接使用pip进行安装即可(pip install gunicorn)
- nginx的配置可以参考:
upstream image-server{
server unix:/tmp/image-server.sock;
}
server {
listen 80;
# 你的服务器域名,对应代码配置中的settings.WEB_HOST_NAME
server_name your_host_name;
charset utf-8;
# 这是就是图片服务提供的静态资源访问的路径,对应代码配置中的settings.WEB_IMAGE_SERVER_PATH
location /images/ {
# 这个路径就是你上传图片的保存的文件夹路径 对应代码配置中的settings.IMAGE_SAVING_PATH
alias /path/to/your/images/directory;
}
location / {
include proxy_params;
proxy_pass http://image-server;
}
}
- supervisor 的配置可以参考:
[program:image-server]
command=/path/to/your/virtualenv/bin/gunicorn --bind unix:/tmp/image-server.sock project.wsgi
directory=/path/to/your/project/directory
user=ubuntu
autostart=true
autorestart=true
stderr_logfile=/data/log/supervisor/image-server.err.log
stdout_logfile=/data/log/supervisor/image-server.out.log
- 重启nginx 和 supervisor 即可
sudo service nginx restart
sudo service supervisor restart
测试
-
我使用insomnia作为测试工具,它是类似postman的api测试工具,风格简约,功能强大;
-
测试上传接口,选择一张图片并上传,如果成功,会返回该图片的url地址提供访问
接口测试.png -
上传图片的url , 它现在就是一个网络资源了: http://littlewulu.cn/images/29d3472dc80a645f1f92646968c5afe4.jpg
-
完整的项目代码在我的github上面:https://github.com/littlewulu/blog/blob/master/app/upload_views.py
-
可以关注我的公众号及时接收推送~
公众号.jpg
网友评论