# upload(文件上传及显示处理)

## 使用werkzeug进行处理

在Uliweb中对上传有一定的封装处理,下面先介绍一下,不使用Uliweb提供的功能,如何 使用底层的werkzeug来处理文件上传。

当用户在前端通过 <input type=”file”> 来上传一个文件时,在request对象中的request.files中可以得到相应的上传文件。其中是一个类dict的对象,存放多个文件对象属性,例如, <input type=”file” name=”docfile”> 定义了一个docfile字段,当上传后,可以通过:

` filename = request.files['docfile'].filename file_obj = request.files['docfile'].stream `

来分别处理上传的文件名和文件对象。后续你要保存到哪个目录,同时考虑到中文的问题, 你可能需要将上传的unicode文件名转为与操作系统对应的本身编码。因此,为了解决这些 问题,Uliweb提供了upload app来处理上传文件。同时upload app还提供上传后文件的访 问处理,包括X-Sendfile的支持。

## upload 的安装与配置

在apps/settings.ini中的INSTALLED_APPS中添加:

` 'uliweb.contrib.upload' `

upload缺省提供以下配置:

``` [UPLOAD] TO_PATH = ‘./uploads’ BUFFER_SIZE = 4096 FILENAME_CONVERTER = BACKEND =

#X-Sendfile type: nginx, apache X_SENDFILE = None

#if not set, then will be set a default value that according to X_SENDFILE #nginx will be ‘X-Accel-Redirect’ #apache will be ‘X-Sendfile’ X_HEADER_NAME = ‘’ X_FILE_PREFIX = ‘/files’

[EXPOSES] file_serving = ‘/uploads/<path:filename>’, ‘uliweb.contrib.upload.file_serving’ ```

其中:

TO_PATH –
为文件要保存的目录。缺省为当前路径下的uploads子目录。一般,当前路径就是你的项目目录。
BUFFER_SIZE –
保存文件时的块大小。
FILENAME_CONVERTER –
文件名转換类,它用来处理当文件上传后,在保存文件时使用的文件名。没有给出此配置时缺省是使用UUID来生成文件名,以保证文件名不重复。同时upload还定义了其它的几种转換类,可以根据需要来使用。更详细的说明参见下面具体的说明。
BACKEND –
upload中文件上传和下载的类定义。如果没有给出,则缺省使用 FileServing 类来处理。

目前upload还支持X-Sendfile的处理方式,这是目前apache, nginx中都有的一种方法,不过细节上有所差异。关于X-Sendfile这里有一篇 [文章](http://www.kuigg.com/xiazai-kongzhi) 可以参考。简单地说就是在下载文件时可以对下载的过程进行控制,详情可参加下面的Nginx配置示例。

X_SENDFILE –
X-Sendfile处理类型,目前只支持Nginx和Apache。根据需要可以输入’nginx’或’apache’。缺省为None,则表示不启动,则文件读取及下载是由Uliweb本身提供的。
X_HEADER_NAME –
当X_SENDFILE生效时,此选项用于指明将返回web server的头。目前已经知道Nginx和Apache要使用的头标识,其它的web server可以在这里指定。当保持为空时,则根据X_SENDFILE的值自动使用相应的头标识,对于Nginx则为 X-Accel-Redirect ,对于Apache则为 X-Sendfile。
X_FILE_PREFIX –
传给web server的头中新的URL的前缀。因为这个URL与原始的URL将不一样,所以利用这个前缀可以方便生成新的URL。这个前缀需要与web server中的内部URL相对应。不过对于Nginx和Apache的处理机制不完全相同,对于Nginx的方式,是通过返回一个新的URL,而对于Apache来说,则需要返回一个文件路径,不是一个URL。所以这个项的设置要根据所使用的web server而有所不同。对于Apache则可以认为是目录的前缀。对于Nginx则可以认为是新的URL的前缀。
EXPOSES/file_serving –
用于定义一个缺省的View函数,以处理上传后的文件的下载。

其实这个配置中,真正与上传有关的就是前两项。后几项都是和下载有关的。在upload中,不仅处理了上传还为了方便处理了下载。只不过,它与静态文件的下载不同,它的下载是可以在非static目录下,并且可以有view的控制参与的。而静态文件是不需要进行控制处理的。

## 文件上传的处理

其实在Uliweb有不同级别的文件上传处理,比如最原始的就是手工处理、然后就是利用upload来处理、再有就是通过generic.py来处理。在处理时,有使用Form来处理的,也可以不使用Form来处理,而是使用request,它们之间有一些差异。generic.py会专门在generic.py的文档进行讲解,这里将根据几种情景进行说明。

### 上传Form的定义

其实使用手工HTML或利用Uliweb提供的Form类来生成Form代码都没有太大关系,基本上是一样的。 简单的话,就是使用Form类了,例如:

``` from uliweb.form import *

class F(Form):
file = FileField(label=’file’)

@expose(‘/show_upload’) def show_upload():

form = F(action=’/upload’) return {‘form’:form}

```

上面定义了一个Form类,然后我们在show_upload()中将返回一个dict,用于模板的渲染。这个view方法只处理了显示,上传还没有处理。在F创建时,我们传入action的值用于指定上传文件后的处理URL。

### 不使用upload app进行上传处理

当用户选择了文件,并提交上传后,信息将提交到/upload来。则对应的处理代码示例为:

``` import os

@expose(‘/upload’) def upload():

form = F(action=’/upload’) if form.validate(request.params, request.files):

filename = request.files[‘file’].filename target = os.path.join(‘./uploads’, filename) with open(target, ‘wb’) as f:

f.write(request.files[‘file’].stream.read())

return redirect(‘/ok’)

else:
#指定将要使用的模板文件名 response.template = ‘show_upload.html’ #如果校验失败,则再次返回Form,将带有错误信息 return {‘form’:form}

```

先生成保存目标的文件名,然后手工将上传的内容进行保存。不过,这里如果文件名有中文有可能会报错。request中得到的文件名是unicode,你需要将其转为与操作系统相匹配的编码。在Uliweb的全局配置项中提供了一个:

` [GLOBAL] FILESYSTEM_ENCODING = None `

你可以考虑先对其进行配置,然后使用它来处理文件的编码。因此,你需要做的处理主要就是:

1. 生成目标文件名(可能要处理文件名编码的问题) 1. 保存文件

下面再看一看使用upload app的做法

### 使用upload app进行上传处理

首先安装upload app。

然后设置配置项,比如TO_PATH的值,缺省是./uploads。

将上面的代码修改一下:

``` import os

@expose(‘/upload’) def upload():

from uliweb import functions

form = F(action=’/upload’) if form.validate(request.params, request.files):

functions.save_file(form.file.data.filename, form.file.data.file) return redirect(‘/ok’)
else:
#指定将要使用的模板文件名 response.template = ‘show_upload.html’ #如果校验失败,则再次返回Form,将带有错误信息 return {‘form’:form}

```

这里使用了upload中提供的save_file函数,它的原型为:

` save_file(filename, fobj, replace=False, convert=True) `

这里只提供了两个参数,一个是文件名,一个是文件对象。第三个没有提供,因此如果存在 同名的文件,将不会覆盖,而是自动添加象(1), (2)这样的内容。在save_file中会自动根 据相关的配置项:文件系统编码、保存目录信息来自动生成目标文件名并转換成合适的编码, 然后保存。第4个参数用来控制是否自动进行文件名转換,缺省会转換。其目的是为了让文件 名不重复,没有乱码。

这里save_file是通过functions对象来引用的。在upload中还定义了其它类似的通用方法, 后面我们会看到。

为了方便处理Form字段,upload app还提供了save_file_field函数,具体使用参见下面的 函数说明。

### 放在一起的处理方式

我们可以考虑把显示和上传后的处理放在一起,也可以象这个例子一样,分开不同的URL。如果放在一起,逻辑可以是:

``` def upload():

from uliweb import functions

form = F() #GET是显示用,POST是提交用 if request.method == ‘GET’:

return {‘form’:form}
else:

#如果提交,则先进行校验,这里是使用Form的方式 #form有一个validate方法,可以传入多个值,这里将request.files传入 #以便形成完整的数据集,如果validate返回True,表示校验成功,并且 #上传的数据将按照Form字段定义的类型已经做了转換 if form.validate(request.params, request.files):

functions.save_file(form.file.data.filename, form.file.data.file) return redirect(‘/ok’)
else:
#如果校验失败,则再次返回Form,将带有错误信息 return {‘form’:form}

```

## FileServing 类

upload 把文件和下载的管理组织成了类的形式。这个类就是FileServing,你可以根据需要 从这个类进行派生。在缺省情况下,upload app会自动创建一个default_fileserving,而 前面所看到的UPLOAD的配置项就是这个缺省的文件服务类。同时,基于这个缺省的实例,提 供了下面的一些方法。在简单的情况下,你可以只使用缺省的文件服务对象就够了。

FileServing的说明:

``` class FileServing(object):

default_config = ‘UPLOAD’ options = {

‘x_sendfile’ : (‘X_SENDFILE’, None), ‘x_header_name’: (‘X_HEADER_NAME’, ‘’), ‘x_file_prefix’: (‘X_FILE_PREFIX’, ‘/files’), ‘to_path’: (‘TO_PATH’, ‘./uploads’), ‘buffer_size’: (‘BUFFER_SIZE’, 4096), ‘_filename_converter’: (‘FILENAME_CONVERTER’, None),

}

#每个FileServing类有相应的settings配置项。因此FileServing的所有方法 #都是根据这些配置项计算来的 #options中的每个值后面都可以使用 obj.xxx 来访问 # #default_config表示会从哪里读配置信息。它会和下面的配置项合成,如,可以 #和TO_PATH合成为 UPLOAD/TO_PATH 。如果配置项中间有 ‘/’ ,则认为不要进行合成。

def __init__(self, default_filename_converter_cls=UUIDFilenameConverter, config=None):
“”” 初始化函数。第一个参数表示文件名转換类,缺省使用UUID方式生成文件名 第二个参数表示读取配置项的名字,与Settings.ini中的section对应 “”“
def filename_convert(self, filename):
“”” 对文件名进行转換 “”“
def get_filename(self, filename, filesystem=False, convert=False)

“”” 用于获得一个文件的实际路径。它是根据to_path计算得到的。如果 filesystem为True,则会将生成的文件名按settings中配置的文件 系统编码来进行转換。convert参数用于处理是否要进行文件名的转換。 因此根据参数的不同,它有几种用法:

  1. 根据传入的filename得到对应的实际路径,但文件名不转換为文件系统 的编码:

    get_filename(filename)

  2. 根据传入的filename得到对应的实际路径,但是文件名转換为文件系统 的编码:

    get_filename(filename, filestystem=True)

  3. 得到filiename的实际路径,同时进行文件名转換,这样得到的文件名将 不是原来的文件名:

    get_filename(filename, convert=True)

前两种主要是用在上传文件后的显示上,这时一般使用的是转換后的文件名。 第三种是用在上传后保存文件时,先对文件名进行转換。 “”“

def download(self, filename, action=’download’, x_filename=’‘, real_filename=’‘)

“”” 提供下载处理,支持X-Sendfile的处理。action取值为’download’或 ‘inline’,它们分别对应不同的应答头:

download
Content-Disposition:attachment; filename=<filename>
inline
Content-Disposition:inline; filename=<filename>

如果action为None,则不显示上面的头信息。

在这里,我们看到有三个文件名,都有什么用?

filename一般是从数据库中取出来的文件名,比如我们将文件名保存到 FileProperty中,当取出来时是Unicode格式的,并且是相对于上传路径 的相对路径,所以我们要进行转換。

如果不考虑X-Sendfile的情况,一般我们只提供filename就足够了,因 为可以自动根据to_path来计算出实际文件路径。不过当文件名并不存在 于to_path所指定的目录下时,我们还可以提供real_filename参数来指 明文件实际的路径。

对于使用了X-Sendfile的情况,又复杂了一些。我们可能还需要指出 x_filename参数,比如在nginx下,它用来指明X-Accel-Redirect中的 文件名,而这个文件路径是一个URL,提供Nginx可以找到真正的文件。 所以x_sendfile其实是一个中间路径。

所以x_sendfile和real_filename其实不会同时使用。在更底层的filedown 函数中会进行确实的处理。对于用户来说,如果想实现根据配置不同, 使用不同的下载方式,则么这些参数最好都提供。 “”“

def save_file(self, filename, fobj, replace=False, convert=True)

“”” 将文件保存在to_path路径下。

使用convert可以设置要不要转換文件名。 “”“

def save_file_field(self, field, replace=False, filename=None, convert=True)
“”” 根据文件字段来保存。路径处理同save_file “”“
def save_image_field(self, field, resize_to=None, replace=False, filename=None, convert=True)
“”” 根据图片字段来保存。路径处理同save_file “”“
def delete_filename(self, filename)
“”” 删除保存在to_path下的文件。 “”“
def get_href(self, filename)
“”” 获取filename对应的URL地址,不是真正的URL信息 “”“
def get_url(self, filename, query_para=None, **url_args)

“”” 获取filename对应的URL。注意,这是一个真正的URL,如果只是想得到URL的 地址,要使用get_href(filename)

如果url_args中传入了 title 和 text,则生成的URL形式为

<a href=’xxx’ title=’title’>text</a>

如果没有传入,则使用filename代替title和text。

如果传入了query_para,则它的值将写在href对应的链接后面。query_para 是一个dict值,如: query_para={'alt':'filename.txt'}

那么生成的URL可能为:

<a href=’xxx?alt=filename.txt’ title=’title’>text</a>

它有什么用,在后面的download你会看到 “”“

```

## 自定义FileServing类

如果需要自定义FileServing类,可以从这个类派生。如果没有新増的配置项,只要定义 default_config 即可。它表示一个完整的section,用来定义options中所有的值。因此 我们需要在settings.ini中定义完整的配置信息,可以直接拷贝UPLOAD的定义。如果有些值 想采用UPLOAD的值,并且希望和UPLOAD值一起变化,那么可以在定义值的时候引用UPLOAD的值,如:

` [UPLOAD_TEST] TO_PATH = UPLOAD.TO_PATH BUFFER_SIZE = UPLOAD.BUFFER_SIZE FILENAME_CONVERTER = BACKEND = X_SENDFILE = None X_HEADER_NAME = '' X_FILE_PREFIX = '/files' `

上面前两项使用了UPLOAD中的定义值。

简单示例:

``` class FileServingTest(FileServing):

default_config = ‘UPLOAD_TEST’

```

## 如何获取FileServing对象

想要获得FileServing实例,可以先导入这个类,然后创建它的实例。创建时可以传入对应的 配置section的名字,以获得完整的配置,否则使用类中定义的配置。

另一种方法是使用 get_fileserving(config) 函数。它会使用FileServing作为BACKEND类,但 是配置项使用你指定的section的名字。这样不会创建新的类,但是实例的参数是可以不同。

## upload app提供方法说明

以下方法都是基于缺省的default_fileserving对象来处理的。

get_fileserving(config) –
获得缺省的文件上传下载对象。缺省使用UPLOAD的配置。如果传入其它的配置名,将 使用这个配置来创建FileServing对象。
file_serving(filename) –

缺省的文件下载函数。它是通过在 settings.ini 中配置了:

` [EXPOSES] file_serving = '/uploads/<path:filename>', 'uliweb.contrib.upload.file_serving' `

这样所有以 /uploads 开头的 URL都会被 file_serving 处理,从而提供服务。

{% alert class=info %} 在这里还有特殊的扩展处理。在缺省情况下,上传后的文件为了保证唯一性会自动 对文件名进行转換,具体用什么要看使用哪个文件名生成器处理的。详见下面 FilenameConverter 的有关说明。因此,当下载文件名还是使用转換后的文件 名会非常不方便。所以这里有一个扩展,就是在传入的URL上添加一个特殊的 query_string,如:

` xxxxxxxxx.txt?alt=中文.txt `

这样alt对应的就是想另存为的文件名。这样只要 <a> 标签加上 alt 信息就可以 以想要的文件名来保存。 {% endalert %}

get_filename(filename, filesystem=False, convert=False) –
用于获得目标文件,即将TO_PATH与filename进行连接。同时,如果给出filesystem为 True,则将文件名转为文件系统的编码。否则返回的将是unicode。 convert=False 表示不对文件名进行转換
save_file(filename, fobj, replace=False, convert=True) –
用于保存一个文件。需要传入文件名和文件对象,这些都可以从request或form字段中 获得。如果replace设置为True,则表示当存在同名文件时自动覆盖,否则将自动添加 (1), (2)等内容,以保证文件不重名。save_file会把文件保存到指定的目录下,并根 据配置项进行相应的文件名编码的转換。
save_file_field(field, replace=False, filename=None, convert=True) –
用于处理Form中的FileField字段。将自动从FileField中获得对应的文件名和文件对象。 也可以将文件保存为filename参数指定的文件名。
save_image_field(field, resize_to=None, replace=False, filename=None, convert=True) –
和save_file_field类似,是用来处理ImageField(图像字段)的。不过,如果你设置了 resize_to参数的话,它还可以自动对图像进行缩放处理。
delete_filename(filename) –
删除上传目录下的某个文件。
get_url(filename, query_para=None, **url_args) –
获得上传目录下某个文件的URL,以便可以让浏览器进行访问。 query_para 将传入到href属性后面成为query_string.
get_href(filename, **kwargs) –
获取filename对应的URL地址,不是真正的URL信息

{% alert class=info %} 如果上面的文件名使用的是相对路径,则会根据当前的FileServing对象来决定使用 什么配置信息,比如文件保存的路径。但是如果使用绝对路径,则将使用绝对路径进 行处理。

{% endalert %}

## FilenameConverter类的说明

upload提供了几个用于文件名上传后转换的类,并且可以在settings.ini进行配置,分别说明如下:

  1. FilenameConverter:

    ``` class FilenameConverter(object):

    @staticmethod def convert(filename):

    return filename

    ```

最基本的类,对文件名不作任何转換 1. UUIDFilenameConverter 使用UUID方法生成文件名。文件名将保证唯一。 1. MD5FilenameConverter 使用MD5算法生成文件名。具体算法:

``` f = md5(

md5(“%f%s%f%s” % (time.time(), id({}), random.random(),
getpid())).hexdigest(),

).hexdigest()

```

## BACKEND配置说明

upload在启动时会缺省按照 UPLOAD/BACKEND 的定义来生成缺省的fileserving类。如果 没给出,则使用FileServing。通过修改 BACKEND ,用户就可以定义自已缺省的文件上传处理类。

## X-Sendfile Nginx配置说明

简单的处理流程可以表示为:

![image](_static/upload_01.png)

以上的处理可以理解为:

1. 用户请求的url在后台经过处理后,由后台处理添加一个内部的头信息,头信息带有一个新的URL,并且返回内容为空,因此真正的内容将由Nginx完成,所以只要添加相应的头信息即可。同时你也可能会返回其它的头信息,如: ‘Content-Disposition’, ‘Content-Type’ 等。 1. Nginx在发现 ‘X-Accel-Redirect’ 头之后会自动删除,并且根据URL的信息去对应的目录下查找相应的文件,然后返回。 1. 因此用户看到的文件路径有可能和真正存放文件的路径不同。并且,允许后台处理根据需要来决定返回 ‘X-Accel-Redirect’ 还是其它的信息,从而可以控制是否真正进行文件下载。一方面可以进行下载控制,另一方面可以对后台文件进行保护。

Nginx的配置如下:

``` location /files {

internal; alias /path/to/files;

}

在Nginx的conf文件中添加上面的内容,需要根据需要进行修改。其中:

/files –
为你将在后台处理中要重新生成的URL的前缀。
internal –
表示内部使用,用户将无法直接通过URL来访问这个路径。
alias –

指明/files后对应的文件信息存放的路径。这里还可以考虑使用root,它们的区别就是:

> 例如URL为 /files/filename,如果配置为 alias /download,则将要读取的文件应该是 /download/filename,而如果配置为 root /download,则将要读取的文件将是 /download/files/filename

启用Nginx进行文件下载处理的配置项应设置为:

` [UPLOAD] X_SENDFILE = 'nginx' `

## functions定义说明

为了方便使用,在settings.ini中的FUNCTIONS中定义了一些全局函数,可以方便使用:

  • get_filename
  • save_file
  • save_file_field
  • save_image_field
  • delete_filename
  • get_url
  • get_href
  • download
  • filename_convert
  • get_fileserving