惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
Stack Overflow Blog
Stack Overflow Blog
人人都是产品经理
人人都是产品经理
The GitHub Blog
The GitHub Blog
Engineering at Meta
Engineering at Meta
Vercel News
Vercel News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Y
Y Combinator Blog
The Cloudflare Blog
Last Week in AI
Last Week in AI
B
Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog
V
Visual Studio Blog
博客园 - 三生石上(FineUI控件)
小众软件
小众软件
Google DeepMind News
Google DeepMind News
D
DataBreaches.Net
博客园 - 司徒正美
B
Blog RSS Feed
Microsoft Azure Blog
Microsoft Azure Blog
罗磊的独立博客
Hugging Face - Blog
Hugging Face - Blog
L
LangChain Blog

博客园 - beautifulzzzz

[SDR] OFDM 收发,3 min 搞定通信学生的毕业设计 [SDR] OFDM RX 详解 [SDR] USRP 双通道与 MIMO 技术:手搓 1 发 2 收 AOA 阵列定位系统 [SDR] 两种方法可视化"打"飞机:ADS-B 信号接收与航班追踪 [SDR] 基于两个 hackrf 实现连续波测速雷达 [深度学习] 超长文,一篇讲完 NVIDIA Jetson Hello AI World 全部教程(推理 & 训练) [SDR] 蓝牙专项教程 —— 从 0 到 1 教小白基于 SDR 编写蓝牙协议栈 [SDR] GNU Radio 系列教程 —— GNU Radio RX PDU (接收据包操作)的基础知识(超全) [SDR] GNU Radio 系列教程 —— GNU Radio TX PDU (发送数据包操作)的基础知识(超全) [SDR] GNU Radio 系列教程(十五)—— GNU Radio GFSK 模块 [SDR] SDR 教程实战 —— 利用 GNU Radio + HackRF 手把手深入了解蓝牙协议栈(从电磁波 -> 01数据流 -> 蓝牙数据包) [SDR] GNU Radio 系列教程(十四) —— GNU Radio 低阶到高阶用法的分水岭 ZMQ 的使用详解 [Pyhton] SimPy 离散事件模拟框架详解 —— 以一个简单的汽车充电排队模拟为例 [SDR] GNU Radio 系列教程(二) —— 绘制第一个信号分析流程图 [SDR] GNU Radio 系列教程(一) —— 什么是 GNU Radio [Linux] 如何在 Linux 电脑上制作专业的视频教程 [LINUX] 像电影里的黑客一样用 terminal 作为日常开发 [python][flask] Flask 入门(以一个博客后台为例) [python][flask] Jinja 模板入门
[python][flask] Flask 图片上传与下载例子(支持漂亮的拖拽...
beautifulzzzz · 2022-05-31 · via 博客园 - beautifulzzzz


1、效果预览

我们基于 Flask 官方指导工程,增加一个图片拖拽上传功能,效果如下:


2、新增逻辑概览

我们在官方指导工程 https://github.com/pallets/flask/tree/2.1.1/examples/tutorial/flaskr 上进行增加代码,改动如下:

➜  flaskr git:(main) ✗ tree
.
├── static
│   ├── file
│   │   ├── css
│   │   │   └── upload.css           <-  增加图片上传的 CSS
│   │   ├── img
│   │   │   ├── 20220525004341_22.png
│   │   │   └── 20220529231518_76.png
│   │   └── js
│   │       └── upload.js            <- 增加图片上传的 JS
│   └── style.css
├── templates
│   ├── auth
│   │   ├── login.html
│   │   └── register.html
│   ├── base.html
│   ├── blog
│   │   ├── create.html
│   │   ├── index.html
│   │   └── update.html
│   └── tuchuang                     <- 增加图片上传的 html
│       └── upload.html
├── auth.py
├── blog.py
├── db.py
├── __init__.py
├── schema.sql
└── tuchuang.py                      <- 增加图床 python 蓝图

9 directories, 18 files

由于 flask 官方 Demo 基于蓝图设计,这给我们新增逻辑带来了很大的方便。关于官方 Demo 的介绍,可以参考我的《Flask 入门(以一个博客后台为例)


3、tuchuang.py 逻辑介绍

3.1 图片上传

1)该接口采用 POST 方法,需要登录;
2)接着,检查请求中是否有 'file' 关键词,然后取出文件,判断文件是否为空或是否合法;
3)最后,将上传的图片保存(采用秒级别的时间戳+随机数重命名);
4)该接口在上传图片成功后,返回该图片的链接;如果不成功,返回 upload.html 页面;

@bp.route('/', methods=['GET', 'POST'])
@login_required
def upload_file():
    if request.method == 'POST':
        # check if the post request has the file part
        if 'file' not in request.files:
            flash('No file part')
            return redirect(request.url)
        file = request.files['file']
        # If the user does not select a file, the browser submits an
        # empty file without a filename.
        if file.filename == '':
            flash('No selected file')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            # 获取安全的文件名 正常文件名
            filename = secure_filename(file.filename)
            
            # 生成随机数
            random_num = random.randint(0, 100)
            # f.filename.rsplit('.', 1)[1] 获取文件的后缀
            filename = datetime.now().strftime("%Y%m%d%H%M%S") + "_" + str(random_num) + "." + filename.rsplit('.', 1)[1]
            file_path = app.config['UPLOAD_FOLDER']    # basedir 代表获取当前位置的绝对路径
            
            # 如果文件夹不存在,就创建文件夹
            if not os.path.exists(file_path):
	            os.makedirs(file_path)
	
            file.save(os.path.join(file_path, filename))
            return redirect(url_for('tuchuang.download_file', name=filename))
    return render_template("tuchuang/upload.html")

3.2 图片合法检查

上述代码中有一个合法检测的函数 allowed_file,用于检查上传图片的后缀是否在允许列表:

basedir = os.path.abspath(os.path.dirname(__file__))                 # 获取当前文件所在目录
UPLOAD_FOLDER = basedir+'/static/file/img'                           # 计算图片文件存放目录
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}     # 设置可上传图片后缀 

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
bp = Blueprint("tuchuang", __name__, url_prefix="/tuchuang")

def allowed_file(filename):                                          # 检查上传图片是否在可上传图片允许列表
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

3.3 图片下载

图片下载比较简单,就是调用 send_from_directory 函数,就能够把 static 目录下的对应文件发出:(我们一般把各种用于外面访问的静态图片、JS、CSS 等放在 static 文件中)

@bp.route('/download/<name>')
def download_file(name):
    return send_from_directory(app.config["UPLOAD_FOLDER"], name)

4、__init__.py 逻辑介绍

由于我们采用蓝图设计,因此需要稍微修改下 __init__.py 文件,来将 tuchuang.py 加入:

  • MAX_CONTENT_LENGTH=16 * 1000 * 1000 上传图片大小限制
  • from flaskr import auth, blog, tuchuang
  • app.register_blueprint(tuchuang.bp) 将 tuchuang 加入蓝图
  • app.add_url_rule("/download/<name>", endpoint="download_file", build_only=True)

5、upload.html 介绍

5.1 upload Jinja 模板介绍

  • Jinja 引用外部 css:<link rel="stylesheet" href="{{ url_for('static', filename='file/css/upload.css') }}">
  • Jinja 引用外部 js:<script type="text/javascript" src="{{ url_for('static', filename='file/js/upload.js') }}"></script>
  • 该 Jinja 模板实现了两种图片上传交互:
    • 普通版,采用 file select 框 + submit 按钮,实现图片上传:
      <form method=post enctype=multipart/form-data>
          <input type=file name=file>
          <input type=submit value=Upload>
      </form>
      
    • 拖拽版(需要借助 JS,CSS),在 <div id="drop-area"> 内实现

下面是 tuchuang/upload.html 完整代码:

<!doctype html>
<link rel="stylesheet" href="{{ url_for('static', filename='file/css/upload.css') }}">
<script type="text/javascript"  src="{{ url_for('static', filename='file/js/upload.js') }}"></script>
<title>Upload new File</title>
<h1>Upload new File</h1>
<form method=post enctype=multipart/form-data>
    <input type=file name=file>
    <input type=submit value=Upload>
</form>
<div id="drop-area">
    <form class="my-form">
        <p>Upload multiple files with the file dialog or by dragging and dropping images onto the dashed region</p>
        <input type="file" id="fileElem" multiple accept="image/*" onchange="handleFiles(this.files)">
        <label class="button" for="fileElem">Select some files</label>
        <div id="gallery"></div>
        <progress id="progress-bar" max=100 value=0></progress>
    </form>
</div>

5.2 upload css 介绍(虚线框)

下面是拖拽需要用到的 CSS,大家暂时浏览下,之后结合 JS 就明白了:

➜  css git:(main) ✗ cat upload.css            
#drop-area {
    border: 2px dashed #ccc;
    border-radius: 20px;
    width: 480px;
    font-family: sans-serif;
    margin: 100px auto;
    padding: 20px;
}
#drop-area.highlight {
    border-color: purple;
}
p {
    margin-top: 0;
}
.my-form {
    margin-bottom: 10px;
}
#gallery {
    margin-top: 10px;
}
#gallery img {
    width: 150px;
    margin-bottom: 10px;
    margin-right: 10px;
    vertical-align: middle;
}
.button {
    display: inline-block;
    padding: 10px;
    background: #ccc;
    cursor: pointer;
    border-radius: 5px;
    border: 1px solid #ccc;
}
.button:hover {
    background: #ddd;
}
#fileElem {
    display: none;
}

5.3 upload js 介绍(拖拽)

5.3.1 JS 拖拽框架

JS 代码主要基于 window.onload + 拖拽事件实现,大致框架如下:

➜  js git:(main) ✗ cat upload.js 
window.onload=function(){
    var dropArea = document.getElementById('drop-area')

    // 阻止默认行为
    ;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
        dropArea.addEventListener(eventName, preventDefaults, false)
    })

    function preventDefaults (e) {
        e.preventDefault()
        e.stopPropagation()
    }

    // 增加事件,鼠标拖入边框高亮,拖出边框变为原来样子
    ;['dragenter', 'dragover'].forEach(eventName => {
        dropArea.addEventListener(eventName, highlight, false)
    })

    ;['dragleave', 'drop'].forEach(eventName => {
        dropArea.addEventListener(eventName, unhighlight, false)
    })

    function highlight(e) {
        dropArea.classList.add('highlight')
    }

    function unhighlight(e) {
        dropArea.classList.remove('highlight')
    }

    // 增加事件,鼠标放下,之后准备上传图片
    dropArea.addEventListener('drop', handleDrop, false)

    function handleDrop(e) {
    	// 之后准备上传图片
    }
}

window.onload() 方法用于在网页加载完毕后立刻执行的操作,即当 HTML 文档加载完毕后,立刻执行某个方法。

为什么使用 window.onload()?

因为 JavaScript 中的函数方法需要在 HTML 文档渲染完成后才可以使用,如果没有渲染完成,此时的 DOM 树是不完整的,这样在调用一些 JavaScript 代码时就可能报出"undefined"错误。


5.3.2 JS 图片上传
function handleDrop(e) {
	// 从拖拽放下事件中获取拖拽的文件
    let dt = e.dataTransfer
    let files = dt.files

	// 调用图片处理函数,对图片进行处理
    handleFiles(files)
}

function handleFiles(files) {
	// 对于多个图片,循环调用 uploadFile 函数,进行上传
    ([...files]).forEach(uploadFile)
}

function uploadFile(file) {
	// JS 合成表单,利用 POST 方法,实现上传(部署在远端时,要改下下面的 url)
    let url = 'http://127.0.0.1:5000/tuchuang/'
    let formData = new FormData()

    formData.append('file', file)

    fetch(url, {
        method: 'POST',
        body: formData
    })
        .then(progressDone) // <- Add `progressDone` call here
        .catch(() => { /* Error. Inform the user */ })
}

Fetch API 提供了一个 JavaScript接口,用于访问和操纵HTTP管道的部分,例如请求和响应。它还提供了一个全局 fetch()方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。详细介绍参考《参考链接[8]》:

  • 1.进行 fetch 请求 参考;
  • 2.支持的请求参数参考;
  • 3.发送带凭据的请求参考;
  • 4.上传 JSON 数据参考;
  • 5.上传文件参考;
  • 6.上传多个文件参考;
  • 7.检测请求是否成功参考;
  • 8.自定义请求对象参考;
  • 9.Headers参考;
  • 10.Guard参考;
  • 11.Response 对象参考;
  • 12.Body参考;
  • 13.特性检测参考;

该文章讲的比较好,大家可以跳转过去学习下~


5.3.3 JS 图片上传进度条

想要带有进度条,我们需要修改下 handleFiles 函数:

var filesDone = 0
var filesToDo = 0
var progressBar = document.getElementById('progress-bar')

...

// 预览
function previewFile(file) {
    let reader = new FileReader()
    reader.readAsDataURL(file)
    reader.onloadend = function() {
        let img = document.createElement('img')
        img.src = reader.result
        document.getElementById('gallery').appendChild(img)
    }
}

// 进度条初始化,fileDone 置 0,filesToDo 置需要上传图片总数
function initializeProgress(numfiles) {
    progressBar.value = 0
    filesDone = 0
    filesToDo = numfiles
}

// 注意,该函作为 fetch 的返回回调函数,意思是每次传输完成一个图片,进度条进行相应变化
function progressDone() {
    filesDone++
    progressBar.value = filesDone / filesToDo * 100
}

function handleFiles(files) {
    files = [...files]
    initializeProgress(files.length) 
    files.forEach(uploadFile)
    files.forEach(previewFile)
}

6、后记

本文涉及到的源代码在 GITHUB,后续我会基于该工程加入各种有意思的功能。
此外,之前的两篇文章列在下面,可能对您理解本文有帮助:


参考链接

[1]. 本文代码 GITHUB
[2]. 在HTML中引入CSS的几种方式介绍
[3]. python Flask中html模版中如何引用css,js等资源
[4]. HTML引入JS的两种方法
[5]. 使用Flask引用HTML中的.js文件的静态资源问题
[6]. Flask 官方指导 Uploading Files
[7]. JavaScript window.onload
[8]. JavaScript使用 Fetch
[9]. 本文 JS+CSS 参考



: 这篇是在大家熟悉 flaskr 的指导项目之后,实现一个图片上传和下载的案例...

如果觉得不错,帮忙点个支持哈~