主要介绍了Python中使用Flask、MongoDB搭建简易图片服务器,本文是一个详细完整的教程,须要的朋友能够参考下html
一、前期准备web
经过 pip 或 easy_install 安装了 pymongo 以后, 就能经过 Python 调教 mongodb 了.
接着安装个 flask 用来当 web 服务器.mongodb
固然 mongo 也是得安装的. 对于 Ubuntu 用户, 特别是使用 Server 12.04 的同窗, 安装最新版要略费些周折, 具体说是shell
1
2
3
4
|
sudo apt
-
key adv
-
-
keyserver hkp:
/
/
keyserver.ubuntu.com:
80
-
-
recv
7F0CEB10
echo
'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen'
| sudo tee
/
etc
/
apt
/
sources.
list
.d
/
mongodb.
list
sudo apt
-
get update
sudo apt
-
get install mongodb
-
10gen
|
若是你跟我同样以为让经过上传文件名的后缀判别用户上传的什么文件彻底是捏着山药当小黄瓜同样欺骗本身, 那么最好还准备个 Pillow 库数据库
或 (更适合 Windows 用户)flask
二、正片ubuntu
2.1 Flask 文件上传浏览器
Flask 官网上那个例子竟然分了两截让人无从吐槽. 这里先弄个最简单的, 不管什么文件都先弄上来缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import
flask
app
=
flask.Flask(__name__)
app.debug
=
True
@app
.route(
'/upload'
, methods
=
[
'POST'
])
def
upload():
f
=
flask.request.files[
'uploaded_file'
]
print
f.read()
return
flask.redirect(
'/'
)
@app
.route(
'/'
)
def
index():
return
'''
<!doctype html>
<html>
<body>
<form action='/upload' method='post' enctype='multipart/form-data'>
<input type='file' name='uploaded_file'>
<input type='submit' value='Upload'>
</form>
'''
if
__name__
=
=
'__main__'
:
app.run(port
=
7777
)
|
注: 在 upload 函数中, 使用 flask.request.files[KEY] 获取上传文件对象, KEY 为页面 form 中 input 的 name 值服务器
由于是在后台输出内容, 因此测试最好拿纯文本文件来测.
2.2 保存到 mongodb
若是不那么讲究的话, 最快速基本的存储方案里只须要
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import
pymongo
import
bson.binary
from
cStringIO
import
StringIO
app
=
flask.Flask(__name__)
app.debug
=
True
db
=
pymongo.MongoClient(
'localhost'
,
27017
).test
def
save_file(f):
content
=
StringIO(f.read())
db.files.save(
dict
(
content
=
bson.binary.Binary(content.getvalue()),
))
@app
.route(
'/upload'
, methods
=
[
'POST'
])
def
upload():
f
=
flask.request.files[
'uploaded_file'
]
save_file(f)
return
flask.redirect(
'/'
)
|
把内容塞进一个 bson.binary.Binary 对象, 再把它扔进 mongodb 就能够了.
如今试试再上传个什么文件, 在 mongo shell 中经过 db.files.find() 就能看到了.
不过 content 这个域几乎肉眼没法分辨出什么东西, 即便是纯文本文件, mongo 也会显示为 Base64 编码.
2.3 提供文件访问
给定存进数据库的文件的 ID (做为 URI 的一部分), 返回给浏览器其文件内容, 以下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
def
save_file(f):
content
=
StringIO(f.read())
c
=
dict
(content
=
bson.binary.Binary(content.getvalue()))
db.files.save(c)
return
c[
'_id'
]
@app
.route(
'/f/<fid>'
)
def
serve_file(fid):
f
=
db.files.find_one(bson.objectid.ObjectId(fid))
return
f[
'content'
]
@app
.route(
'/upload'
, methods
=
[
'POST'
])
def
upload():
f
=
flask.request.files[
'uploaded_file'
]
fid
=
save_file(f)
return
flask.redirect(
'/f/'
+
str
(fid))
|
上传文件以后, upload 函数会跳转到对应的文件浏览页. 这样一来, 文本文件内容就能够正常预览了, 若是不是那么挑剔换行符跟连续空格都被浏览器吃掉的话.
2.4 当找不到文件时
有两种状况, 其一, 数据库 ID 格式就不对, 这时 pymongo 会抛异常 bson.errors.InvalidId ; 其二, 找不到对象 (!), 这时 pymongo 会返回 None .
简单起见就这样处理了
1
2
3
4
5
6
7
8
9
10
|
@app
.route(
'/f/<fid>'
)
def
serve_file(fid):
import
bson.errors
try
:
f
=
db.files.find_one(bson.objectid.ObjectId(fid))
if
f
is
None
:
raise
bson.errors.InvalidId()
return
f[
'content'
]
except
bson.errors.InvalidId:
flask.abort(
404
)
|
2.5 正确的 MIME
从如今开始要对上传的文件严格把关了, 文本文件, 狗与剪刀等皆不能上传.
判断图片文件以前说了咱们动真格用 Pillow
1
2
3
4
5
6
7
8
9
10
11
12
13
|
from
PIL
import
Image
allow_formats
=
set
([
'jpeg'
,
'png'
,
'gif'
])
def
save_file(f):
content
=
StringIO(f.read())
try
:
mime
=
Image.
open
(content).
format
.lower()
if
mime
not
in
allow_formats:
raise
IOError()
except
IOError:
flask.abort(
400
)
c
=
dict
(content
=
bson.binary.Binary(content.getvalue()))
db.files.save(c)
return
c[
'_id'
]
|
而后试试上传文本文件确定虚, 传图片文件才能正常进行. 不对, 也不正常, 由于传完跳转以后, 服务器并无给出正确的 mimetype, 因此仍然以预览文本的方式预览了一坨二进制乱码.
要解决这个问题, 得把 MIME 一并存到数据库里面去; 而且, 在给出文件时也正确地传输 mimetype
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
def
save_file(f):
content
=
StringIO(f.read())
try
:
mime
=
Image.
open
(content).
format
.lower()
if
mime
not
in
allow_formats:
raise
IOError()
except
IOError:
flask.abort(
400
)
c
=
dict
(content
=
bson.binary.Binary(content.getvalue()), mime
=
mime)
db.files.save(c)
return
c[
'_id'
]
@app
.route(
'/f/<fid>'
)
def
serve_file(fid):
try
:
f
=
db.files.find_one(bson.objectid.ObjectId(fid))
if
f
is
None
:
raise
bson.errors.InvalidId()
return
flask.Response(f[
'content'
], mimetype
=
'image/'
+
f[
'mime'
])
except
bson.errors.InvalidId:
flask.abort(
404
)
|
固然这样的话原来存进去的东西可没有 mime 这个属性, 因此最好先去 mongo shell 用 db.files.drop() 清掉原来的数据.
2.6 根据上传时间给出 NOT MODIFIED
利用 HTTP 304 NOT MODIFIED 能够尽量压榨与利用浏览器缓存和节省带宽. 这须要三个操做
1)、记录文件最后上传的时间
2)、当浏览器请求这个文件时, 向请求头里塞一个时间戳字符串
3)、当浏览器请求文件时, 从请求头中尝试获取这个时间戳, 若是与文件的时间戳一致, 就直接 304
体现为代码是
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
import
datetime
def
save_file(f):
content
=
StringIO(f.read())
try
:
mime
=
Image.
open
(content).
format
.lower()
if
mime
not
in
allow_formats:
raise
IOError()
except
IOError:
flask.abort(
400
)
c
=
dict
(
content
=
bson.binary.Binary(content.getvalue()),
mime
=
mime,
time
=
datetime.datetime.utcnow(),
)
db.files.save(c)
return
c[
'_id'
]
@app
.route(
'/f/<fid>'
)
def
serve_file(fid):
try
:
f
=
db.files.find_one(bson.objectid.ObjectId(fid))
if
f
is
None
:
raise
bson.errors.InvalidId()
if
flask.request.headers.get(
'If-Modified-Since'
)
=
=
f[
'time'
].ctime():
return
flask.Response(status
=
304
)
resp
=
flask.Response(f[
'content'
], mimetype
=
'image/'
+
f[
'mime'
])
resp.headers[
'Last-Modified'
]
=
f[
'time'
].ctime()
return
resp
except
bson.errors.InvalidId:
flask.abort(
404
)
|
而后, 得弄个脚本把数据库里面已经有的图片给加上时间戳.
顺带吐个槽, 其实 NoSQL DB 在这种环境下根本体现不出任何优点, 用起来跟 RDB 几乎没两样.
2.7 利用 SHA-1 排重
与冰箱里的可乐不一样, 大部分状况下你确定不但愿数据库里面出现一大波彻底同样的图片. 图片, 连同其 EXIFF 之类的数据信息, 在数据库中应该是唯一的, 这时使用略强一点的散列技术来检测是再合适不过了.
达到这个目的最简单的就是创建一个 SHA-1 唯一索引, 这样数据库就会阻止相同的东西被放进去.
在 MongoDB 中表中创建唯一 索引 , 执行 (Mongo 控制台中)
若是你的库中有多条记录的话, MongoDB 会给报个错. 这看起来很和谐无害的索引操做被告知数据库中有重复的取值 null (实际上目前数据库里已有的条目根本没有这个属性). 与通常的 RDB 不一样的是, MongoDB 规定 null, 或不存在的属性值也是一种相同的属性值, 因此这些幽灵属性会致使唯一索引没法创建.
解决方案有三个:
1)删掉如今全部的数据 (必定是测试数据库才用这种不负责任的方式吧!)
2)创建一个 sparse 索引, 这个索引不要求幽灵属性唯一, 不过出现多个 null 值仍是会断定重复 (无论现有数据的话能够这么搞)
3)写个脚本跑一次数据库, 把全部已经存入的数据翻出来, 从新计算 SHA-1, 再存进去
具体作法随意. 假定如今这个问题已经搞定了, 索引也弄好了, 那么剩是 Python 代码的事情了.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import
hashlib
def
save_file(f):
content
=
StringIO(f.read())
try
:
mime
=
Image.
open
(content).
format
.lower()
if
mime
not
in
allow_formats:
raise
IOError()
except
IOError:
flask.abort(
400
)
sha1
=
hashlib.sha1(content.getvalue()).hexdigest()
c
=
dict
(
content
=
bson.binary.Binary(content.getvalue()),
mime
=
mime,
time
=
datetime.datetime.utcnow(),
sha1
=
sha1,
)
try
:
db.files.save(c)
except
pymongo.errors.DuplicateKeyError:
pass
return
c[
'_id'
]
|
在上传文件这一环就没问题了. 不过, 按照上面这个逻辑, 若是上传了一个已经存在的文件, 返回 c['_id'] 将会是一个不存在的数据 ID. 修正这个问题, 最好是返回 sha1 , 另外, 在访问文件时, 相应地修改成用文件 SHA-1 访问, 而不是用 ID.
最后修改的结果及本篇完整源代码以下 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
import
hashlib
import
datetime
import
flask
import
pymongo
import
bson.binary
import
bson.objectid
import
bson.errors
from
cStringIO
import
StringIO
from
PIL
import
Image
app
=
flask.Flask(__name__)
app.debug
=
True
db
=
pymongo.MongoClient(
'localhost'
,
27017
).test
allow_formats
=
set
([
'jpeg'
,
'png'
,
'gif'
])
def
save_file(f):
content
=
StringIO(f.read())
try
:
mime
=
Image.
open
(content).
format
.lower()
if
mime
not
in
allow_formats:
raise
IOError()
except
IOError:
flask.abort(
400
)
sha1
=
hashlib.sha1(content.getvalue()).hexdigest()
c
=
dict
(
content
=
bson.binary.Binary(content.getvalue()),
mime
=
mime,
time
=
datetime.datetime.utcnow(),
sha1
=
sha1,
)
try
:
db.files.save(c)
except
pymongo.errors.DuplicateKeyError:
pass
return
sha1
@app
.route(
'/f/<sha1>'
)
def
serve_file(sha1):
try
:
f
=
db.files.find_one({
'sha1'
: sha1})
if
f
is
None
:
raise
bson.errors.InvalidId()
if
flask.request.headers.get(
'If-Modified-Since'
)
=
=
f[
'time'
].ctime():
return
flask.Response(status
=
304
)
resp
=
flask.Response(f[
'content'
], mimetype
=
'image/'
+
f[
'mime'
])
resp.headers[
'Last-Modified'
]
=
f[
'time'
].ctime()
return
resp
except
bson.errors.InvalidId:
flask.abort(
404
)
@app
.route(
'/upload'
, methods
=
[
'POST'
])
def
upload():
f
=
flask.request.files[
'uploaded_file'
]
sha1
=
save_file(f)
return
flask.redirect(
'/f/'
+
str
(sha1))
@app
.route(
'/'
)
def
index():
return
'''
<!doctype html>
<html>
<body>
<form action='/upload' method='post' enctype='multipart/form-data'>
<input type='file' name='uploaded_file'>
<input type='submit' value='Upload'>
</form>
'''
if
__name__
=
=
'__main__'
:
app.run(port
=
7777
)
|
三、REF
Developing RESTful Web APIs with Python, Flask and MongoDB