表單中的文件上傳javascript
基本的表單渲染,表單類設置等等就很少說了,參看另外一個文章便可。可是那篇文章裏沒有提到對於FileField,也就是上傳文件的表單字段是如何處理,後端又是如何實現接受上傳過來的文件的。由於看到了一篇很好的文章【https://zhuanlan.zhihu.com/p/23731819?refer=flask】,因此我決定仔細學習一下。下面將按照那篇文章的脈絡,由簡至繁地說明表單中文件上傳的辦法。css
■ 利用Flask原生的機制進行文件上傳html
首先在前端確定有一個帶有文件上傳功能的表單。這個表單若是利用wtfoms來實現的話那就是在forms裏面得有FileField這個字段。FileField還能夠加上validators=[DataRequired()]參數來保證沒有文件選擇時不進行POST動做。在表單點擊肯定以後,後端能夠這麼操做來獲得文件對象:前端
uploaded_file = request.files['file'] #其中'file'是指表單中type是file的那個input的name屬性的值。另外也能夠: uploaded_file = uploadForm.file.data
能夠經過request對象的files屬性的file字段的值來得到,也能夠和其餘表單字段獲取數據的方式同樣經過form對象來得到,二者取得的是同一個對象。這個對象是一個FileStorage對象,這個對象的屬性有下面這些:java
filename 上傳上來的文件的名字linux
headers 上傳文件的頭信息git
name 表單字段的名字github
其中filename值得一用,好比在後面我能夠寫filename = os.path.join(app.config['UPLOAD_FOLDER'],uploaded_file.filename)來獲得一個上傳後文件的絕對路徑。而後調用uploaded_file.save(filename)就能夠把上傳上來的文件的內容固化到服務器的硬盤中了。web
由於涉及到文件,仍是須要注意相對路徑和絕對路徑這個坑。好比我以前的路徑都寫的是static/download_data,意思是讓文件所有都保存到項目的static目錄下面,可是並無什麼用,由於相對路徑是從工做目錄而非腳本所在目錄開始尋找的。爲了保證程序獲得絕對路徑的信息同時又不想在程序裏面寫死的話,就要活用os.path.abspath,os.path.join,os.path.dirname等這些方法了。ajax
可能已經注意到,對於一些配置咱們能夠放到app.config中去,好比UPLOAD_FOLDER能夠指定上傳上來的文件放到哪一個目錄下,ALLOWED_EXTENDSIONS能夠賦它一個集合的值,對於不屬於這個集合的後綴名的文件就不容許上傳,只是這個功能還須要咱們手動實現;MAX_CONTENT_LENGTH能夠指定文件最大能夠有多少字節等等。
在咱們的後臺獲得了上傳上來的文件以後,也許咱們還要考慮開放一個接口讓訪問者能供訪問到這些文件。這可能會用到flask自帶的一個send_file或者send_from_directory兩個方法。之後者爲例,接受兩個參數,第一個是本地的一個目錄名,第二個是文件名。函數會自動讀取這個文件並返回文件的內容:
@app.route('/upload/<filename>') def uploaded_file(filename): return send_from_directory(app.config['UPLOAD_FOLDER'],filename)
這樣咱們在表單中上傳後的文件,能夠經過 /upload/文件名 來訪問文件的內容了。
順帶一提,若是在send_from_directory中加上as_attachment=True的參數的話,那麼就能夠將文件做爲一個待下載的文件發送給客戶端了
■ 利用flask-uploads擴展進行文件上傳
以上雖然利用flask原生的一些工具進行了文件上傳功能的實現,可是不夠完善,並且文件擴展名驗證之類的工做仍是要咱們本身手動編碼來完成。根據以往其餘一些功能的經驗,咱們天然想到了有沒有一個擴展是能夠支持flask方便地實現上傳文件的功能呢?答案是有的,就是flask-uploads。
用flask-uploads上傳文件主要用到flask_uploads中的這些類:
from flask_uploads import UploadSet,configure_uploads,AUDIO,IMAGE
其中最重要的是UploadSet。這個方法返回了一個UploadSet對象,即上傳文件集合。他能夠這麼用 myfile = UploadSet('MYFILE')。參數的字符串爲這個上傳文件集取了一個名字,這個名字在後續還將出現再等號左邊,這是比較神奇的一個設定。好比按照上面那樣有了myfile這個對象以後,能夠在合適的地方爲app對象配置:
from flask_uploads import UploadSet,configure_upload,AUDIO #... myfile = UploadSet('MYFILE') app.config['UPLOADED_MYFILE_DEST'] = os.path.join(os.path.dirname(__file__),'static','download_data') app.config['UPLOADED_MYFILE_ALLOW'] = AUDIO configure_upload(app,myfile) #別忘了這一步,把上傳文件集的設置和app關聯起來
能夠看到,在爲app進行config配置的時候,UPLOADED_%s_DEST和UPLOADED_%s_ALLOW中間的那串字符串是由以前UploadSet建立的時候定義的,二者分別規定了這個上傳文件集存放的位置以及容許哪些後綴名的文件。那麼就能夠推斷AUDIO的具體內容是什麼了,其實就是一些後綴名的元組。由於是AUDIO,因此是相似於('.mp3','.mp4','.avi'...)之類的。在flask_uploads中還有不少這種預設的後綴名元組,好比IMAGE,TEXT等等,就不舉例了。當相關配置所有完成後,記得最後還要configure_upload(app,myfile)來使得myfile和app關聯起來。以前看到網上有人採坑,把這一步沒有寫到views裏面而寫在了forms裏面去,這致使了報沒有app上下文的錯,須要注意。
*若是以爲上面的UPLOAD_%s_ALLOWd的方式來配置容許的格式太麻煩的話,也能夠在myfile=Upload('MYFILE',AUDIO)這樣第二個參數來直接指定某個UploadSet對象限定的後綴名。
那麼他爲何要把這個放到forms裏面去?由於在forms中,在flask_wtf中存在一個file子模塊,裏面含有一些文件相關的表單提交前驗證方法(就像表單那篇中提到的DataReqiured,EqualTo等validator同樣)。flask.wtf.file裏面的這個FileAllowed驗證方法接受的參數第一個就是一個UploadSet對象,第二個和其餘validator相似是messasge。由於須要這個UploadSet對象,因此爲了方便,就把建立UploadSet對象的工做放到了forms裏。可是爲了不缺乏上下文的那個錯誤,建議仍是放在views裏面建立,而後讓forms導入;或者把configure_upload和UploadSet兩個方法分在兩個文件中寫,注意引用關係不報錯便可。
這樣一來,在views和forms以及前端表單的配合下,一個較爲完善,模塊化程度較高的上傳文件功能就作好了。:
##########views.py中部分代碼########### from forms import myfile app.config['UPLOAD_MYFILE_DEST'] = os.path.join(os.path.dirname(__file__),'static','downloaddata') app.config['UPLOAD_MYFILE_ALLOW'] = AUDIO configure_upload(app,myfile) @app.route('/form',methods=['GET','POST']) def form(): uploadForm = UploadForm() if uploadForm.validate_on_submit(): myfile.save(uploadForm.file.data,name=uploadForm.file.data.filename) #請注意保存文件時是用了UploadSet對象調用了save方法,並且這個save方法的第一個參數是文件對象,第二個參數是文件名 return render_template('form.html',form=uploadForm) @app.route('/show/<filename>') def show(filename): return '<a href="{0}">文件</a>'.format(myfile.url(filename)) ##########forms.py中部分代碼######### from flask_wtf.file import FileField,FileAllowed,FileRequired #請注意,若是要在FileField中加上FileAllowed等驗證函數的話,就不能從wtforms中導入Field類,而是必須從flask_wtf.file中,不然會報錯沒有has_file方法。 myfile = UploadSet('MYFILE') class uploadForm(): file = FileField(u'上傳文件',validators=[FileAllowed(myfile,u'文件格式不對'),FileRequired()]) submit = SubmitField(u'提交')
關於視圖路由中的/show部分,調用了UploadSet對象的url方法,其實對於一個UploadSet對象有指定的存放文件的目錄,這也就是說,經這個對象處理的文件不會有重名(經試驗即便傳了重名的,後一個文件會被命名成filename_1.ext這樣子被存放到相關目錄中)。那麼就表名,UploadSet對象其實能夠維護文件名和完整文件路徑的關係。url方法返回的就是從http://開始包括域名在內的完整文件路徑。
*在測試的時候還發現了一個flash_upload的小缺陷,就是後綴名大寫的文件將不被識別爲可接受文件。好比1.png能夠被ALLOW是IMAGES的UploadSet對象接收,可是1.PNG就不行。個人解決辦法是在UploadSet類中的extension_allowed方法中,加上一句ext = ext.lower()。
若是咱們根據上傳過來的文件的格式不一樣而進行不一樣的存儲和處理,那麼用UploadSet會是一個比較不錯的辦法。
■ 文件的管理
在架設了文件上傳的功能以後,可能須要留出接口讓用戶能夠經過web界面來進行文件管理。下面介紹一下一些方法來方便咱們進行文件管理工做。
展現文件能夠用os.listdir或者walk之類的方法,來獲取後臺全部文件的列表,而後和前端配合將這些文件列表展示出來。
若是咱們須要留出刪除這些文件的接口,那麼在後臺可調用os.remove(path)方法來刪除一個文件。路徑參數path能夠經過myfile.path(filename)來得到。由於一個文件集中文件名是不能重複的,因此UploadSet對象的path方法能夠返回一個完整的絕對路徑。path方法和前面提到的url方法很容易混淆,二者都是經過一個filename來返回一串定位用的串,其中url返回的是經過web和HTTP方法來訪問,因此是相似於http://ip:port/path/to/file這樣的,而path方法返回的是相似於C:\path\to\file或者linux上的/path/to/file這樣的。
從新再來看save方法,通過上面的調用能夠看到save方法除了FileStorage對象以外還能夠傳一個name參數指定保存到服務器上文件的名字。除此以外save方法其實還有一個folder參數,能夠指定在UPLOAD_XXX_DEST中文件還要存放到什麼子級目錄中去。好比folder=os.path.join('subdir','tempdata')的話,這個文件就會被放到UPLOAD_XXX_DEST指定目錄下的subdir/tempdata目錄中,若以前路徑還不存在程序會自動建立沒有的目錄。
● 文件名點竄
關於上面對文件名的默認處理是保持原名,可是實際上這並非很安全。能夠參考的一個作法是提取myfile.path(filename)的md5散列值,而後把這個值的前N位或者整個值做爲文件名保存下來。而後把真實文件名的path和這個文件名對應起來存到數據庫中。這樣用戶在調取文件的時候能夠從數據庫中定位相關的真實文件名。
■ 多文件上傳
在以上實現中,咱們的FileField一次只能接受至多一個文件上傳,也就是說點擊瀏覽按鈕彈出來的對話框中咱們只能單選一個文件。
若是要多選,首先在前端的input標籤中應該是要有multiple='multiple'這個屬性。至於到了後端,該如何接受這些文件?按照上面的說明,後端接受文件能夠有兩種方式,第一種是經過flask.request對象,接受多個文件時用request.files.getlist('file'),這個方法返回一個FileStorage對象的列表。遍歷這個列表,針對每個FileStorage對象調用UploadSet對象對其執行save方法便可保存它們。getlist中的'file'實際上是表單中文件上傳的那個input的name屬性的值,若是用的是wtforms實現的表單的話那麼也是文件上傳field的對象的變量名。
■ 拖曳上傳
上面實現的文件上傳表單,界面是很傳統的那種一個瀏覽按鈕加上一串提示字符串。稍微時髦一點的可使用拖曳文件到瀏覽器窗口進行上傳。要實現拖曳上傳,須要用到Dropzone.js這個擴展JS。
【如何將這個JS整合進咱們的項目我本身沒有研究。。偷了個懶,上面那篇文章的做者寫了一個flask擴展flask-dropzone,咱們能夠直接pip install之避免重複造輪子。這個擴展的用法:http://greyli.com/flask-dropzone-add-file-upload-capabilities-for-your-project/。這個擴展的github地址:https://github.com/wyzypa/flask-dropzone。】
簡單來講,咱們只要像其餘擴展那樣初始化:dropzone = Dropzone(app),而後去前端作一些相關的修改便可。前端的修改能夠參考下面:
{% block head %}{# 注意,必定要在head中load,不然dropzone.js和相關css文件沒法經過CDN方法導入#}
{{ dropzone.load() }}
{% endblock %}
{% block page_content %}
{{ dropzone.create(action_view='form') }}
{{ dropzone.style('border:5px spotted black;width:100%;background:grey') }}
{# create方法接收一個action_view參數。dropzone建立的實際上是一個表單,這個表單標籤form的action屬性就由這個參數的值決定,
也就是說要寫相關視圖函數的endpoint才行。這裏寫了form,意味着在views中必定存在@app.route('/something')下的def form():...#}
{# style方法容許手工調整dropzone區域的CSS樣式 #}
{% endblock %}
這樣一個基本的dropzone就已經完成了。若是想要對提示文字等做出更多調整能夠參考上面那個用法說明中給出的能夠配置哪些app.config的項。好比通過以下config設置以後
app.config['DROPZONE_DEFAULT_MESSAGE'] = u'點擊或將文件拖曳到此區域來上傳文件' 就能夠改變默認的提示文字。
app.config['DROPZONE_MAX_FILE_SIZE'] = 3 設置了上傳文件最大隻能到3MB
app.config['DROPZONE_TOO_BIG'] = u'上傳文件大小超過限制' 設置當上傳文件大小超過限制時的提示文字
app.config['DROPZONE_INVALID_FILE_TYPE'] = u'文件類型不符合規定'
app.config['DROPZONE_SERVER_ERROR'] = u'服務器內部發生錯誤:{{ statusCode }}'
*若是要對上傳文件的格式作出限制,以前一直傻傻地還去該uploadForm裏面的FileField的FileAllowed的validator,後來忽然意識到,如今已經和form不要緊了。。經過flask-dropzone來設置格式限制是這樣的:
app.config['DROPZONE_ALLOWED_FILE_CUSTOM'] = True
app.config['DROPZONE_ALLOWED_FILE_TYPE'] = '一些字符串值'。字符串的格式由dropzone.js規定。基本格式是相似於這樣的:'.jpg, .png, .zip, .rar',請注意每一個格式前面到逗號爲止是要空出一格的。dropzone還提供了一些預設的好比image/*,audio/*等囊括一些後綴名的集合,也能夠用。好比混用二者:'image/*, .rar, .zip'。
由於經過dropzone.js來實現的文件上傳表單不經過wtforms渲染,因此就不能再views裏面經過咱們熟悉的from = XXForm(),而後if form.validate_on_submit()這樣的方式在視圖中控制了。因此咱們要回歸原始,採用if request.method == 'POST' and request.files.get('file')以後,利用UploadSet對象的save方法來進行文件存儲。
順便一提,最開始dropzone.js被開發出來時好像專門是針對圖片上傳用的,因此對圖片的支持特別好。圖片格式被上傳以後,還會再頁面上顯示縮略圖。好比下面這樣:
■ 進度條!
進度條之於上傳的重要性想必不用多講了。其實在上面的dropzone.js中已經實現了一個小進度條了,不過考慮到更加自由的進度條設置方式,仍是有必要來看下這塊內容。
網上現成的進度條解決方案很少,我搜到了這篇文章【http://www.jianshu.com/p/716d470d6434】(怎麼感受總是在copy而後總結別人的文章,果真仍是本身水平太~低了【笑哭】)
利用了flask+jQuery+Bootstrap實現的進度條。正好這幾個技術都還算熟悉,就用這個了。首先來講說他實現的原理,首先確定是不能用dropzone.js之類已經包裝好上傳方法的組件了,記得paramiko的sftp在上傳下載的時候提供了一個回調函數主要就是用來給開發人員作進度條用的,因此咱們要本身實現上傳方法。這裏用了jQuery中的ajax的POST方法來實現文件的上傳。另外還有必要來複習一下Bootstrap的進度條組件,BS中的進度條就是一個div包着另外一個div。外面的div是.progress,裏面的div是.progress-bar,經過改變.progress-bar的width CSS屬性來實現進度條的走動。
至於文件上傳方法,咱們要作的是在ajax中設置XMLHttpRequest對象,並以此爲基礎添加事件來監聽上傳的進度。具體的前端代碼以下(一部分):
{% block page_content %}
<form action="{{ url_for('form') }}" role="form" method="POST" enctype="multipart/form-data"> <input type="file" id="file" name="file" /> <input type="submit"> </form> //進度條 <div class="process" style="display:none;"> <div class="process-bar" style="width:0%;" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div> </div> {% endblock %}
{% block scripts %}
{{ super() }} //千萬別忘了super,不然jQuery和bootstrap.js就不被CDN方式導入了 <script type="text/javascript"> $("form").on("submit",function(event){ $(".progress").css("display","block"); //顯示進度條 event.preventDefault(); //不是很懂這裏是幹嗎的,原文說是爲了阻止表單提交 var formData = new FormData(this);
//若是須要爲表單添加一些其餘字段的數據能夠調用formData.append('key','value')來實現 //開始用ajax上傳文件 $.ajax({ xhr : function(){ var xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress' ,function(e){ if (e.lengthComputable){ var percent = Math.round(e.loaded * 100 / e.total); $(".progress-bar").attr("aria-valuenow",percent).css("width",percent+"%"); } }); return xhr; }, type : 'POST', url : '/form', cache : false, data : formData, processData : false, //這條主要是指出了jQuery不要去處理髮送的數據 contentType : false})
//這裏是說明不要ajax去設置Content-Type請求頭,緣由我也不懂。。
.done( //接在整個ajax請求方法後面,表示處理完成或失敗時調用的函數
function(){alert('success');}).fail(
function(){alert('failed');}); }); </script>
{% endblock %}
經過這樣的方法構造出來的一個上傳部件就實現了進度條的功能了。要注意在ajax的參數列表中必須設置processData : false和contentType : false這兩條。不然極可能在前端控制檯報錯說append方法用到了沒有實現FormData接口的對象上。
另外爲了提升用戶體驗,增強重複上傳時的工做性能,能夠在done和fail兩個回調函數中再添加上好比$(".progress").hide("slow") //上傳結束後進度條從新隱藏。$("progress-bar").css("width","0%").attr("aria-valuenow","0"); //上傳結束後把進度條歸零。
按照上面的代碼打造出來的進度條很好,可是可能不能附帶表單數據。若是須要表單數據那麼須要額外在JS中爲formData對象添加一些K-V對來表示表單數據。好比formData.append('name',$('input#formName').val());就是把當前頁面表單中的某個字段的值賦予formData對象的某個Key。而後經過ajax請求上傳的這個表單就既有文件上傳又有表單數據了。還要提醒一句,formData是一個FormData原型的對象而不是一個簡單的Object。因此在控制檯裏log出來的也是一個空對象,不要覺得沒有append進去,其實只有經過formData.get方法才能順利取出數據的 。