8

10分钟编写文件共享服务

 2 years ago
source link: https://allenwind.github.io/blog/2305/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

分享一个简单的文件服务,可以在局域网上多设备同步文件。

我现有iPad、手机、Mac、Linux系统的笔记本、Windows系统的笔记本、台式机,如何在这些设备中快速共享文件。使用云服务及不方便尤其是共享大文件的时候。那么如何解决呢?可以在本地搭建一个文件共享服务,通过局域网共享文件。

接下来的实现满足最基本的需求。有Python版本的实现和Go语言的实现。

各个设备通过下载、上传来共享文件,于是需要一个中心服务器提供下载、上传、保存文件的服务。把这个功能分解如下:

  1. 各个设备可以浏览共享的文件
  2. 各个设备可以下载共享的文件
  3. 各个设备可以上传自己的文件作为共享

架构设计和技术选型

  • 采用Web的方式,通过浏览器完成文件的上传、浏览、下载
  • MVC架构模式
  • 采用Flask框架,使用gevent作Flask应用的HTTP容器

设计三个HTTP API,

GET / # 浏览文件
GET /file?name=filename # 下载文件
POST / # 上传文件

上面三个接口的实现,代码并不完整,详细代码参看我的github

@app.route('/', methods=['GET'])
def list_files():
files = os.listdir(app.config["UPLOAD_FOLDER"])
r = []
for file in files:
item = '<li><a href="/file?path={}">{}</a></li>'.format(base64.b64encode(file.encode('utf-8')).decode('utf-8'), file)
r.append(item)
return HTML_TEMPLATE.format('\n'.join(r))

@app.route('/', methods=['POST'])
def upload_file():
if 'file' not in request.files:
flash('not file part')
return redirect(url_for('list_files'))
file = request.files['file']
if file:
file.save(os.path.join(app.config['UPLOAD_FOLDER'], file.filename))
flash('file uploading is ok!')
return redirect(url_for('list_files'))

@app.route('/file')
def download():
path = request.args.get("path", default=None)
if path is None:
return redirect(url_for('list_files'))
filename = base64.b64decode(path.encode('utf-8')).decode('utf-8')
path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if os.path.exists(path):
fp = open(path, 'rb')
return send_file(fp, mimetype='application/octet-stream', as_attachment=True,
attachment_filename=filename)
else:
flash("file not found")
return redirect(url_for('list_files'))

模板和配置

html表单提供三种编码方案:

  • application/x-www-form-urlencoded (the default)
  • multipart/form-data
  • text/plain

当需要上传文件时,我们使用multipart/form-data这种编码方案。它允许整个文件包含在上传数据中。application/x-www-form-urlencoded类似于URL的请求部分。text/plain就是纯文本编码,当传输的数据就是HTML时,使用该方案。这三种方案的更详细信息参考W3C文档

html模板很简单,包括两部分:(1)文件列表(2)文件上传按钮

<!DOCTYPE html>
<title>File Service</title>
<h2>file list</h2>
{} <!--文件列表部分-->
<br>
<h2>upload file</h2>
<form method=post enctype=multipart/form-data>
<p><input type=file name=file>
<p><input type=submit value=upload>
</form>
</html>

有一点要注意的是文件列表的下载路径特点如下:

<li><a href="/file?path={}">{}</a></li>
<li><a href="/file?path=bGl0dGxlZmVuZy5weQ==">littlefeng.py</a></li>

bGl0dGxlZmVuZy5weQ==其实就是文件名的base64编码。

为什么这样做?这样做的一个限制是,字符串只能是ASCII字符,中文字符会出错。这个年头,一般文件命名都是英文。

功能测试、接口测试、单元测试

使用requests编写简单的接口测试+几个测试用例。done!

配置和部署

配置只需要在一个类中指定上传文件的目录即可。然后通过app.config.from_object导入到app中。

class Config(object):
DEBUG = False
UPLOAD_FOLDER = './uploads'

启动服务器

@app.after_request
def add_headers(response):
response.headers['Server'] = 'File Service/1.0'
return response

def start_app(address, app=app):
server = WSGIServer(address, app)
server.serve_forever()

使用和体验

终于,我的Mac、iPad、各台电脑可以共享文件了。项目的详细代码看我github.

补充Go语言的实现

使用Go语言的好处是编译成一个可实行文件即可。这个实现除了标准库不需要第三方包。

package main

import (
"crypto/md5"
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"
"strconv"
"time"
)

/*
application/x-www-form-urlencoded 表示在发送前编码所有字符(默认)
multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。
text/plain 空格转换为 "+" 加号,但不对特殊字符编码。
*/

func upload(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.Method, r.URL)
if r.Method == "GET" {
current := time.Now().Unix()
h := md5.New()
io.WriteString(h, strconv.FormatInt(current, 10))
token := fmt.Sprintf("%x", h.Sum(nil))

t, _ := template.ParseFiles("upload.tpl")
t.Execute(w, token)
} else {
r.ParseMultipartForm(32 << 10) // set max memory
file, handler, err := r.FormFile("uploadfile") // get file handle
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
fmt.Fprintf(w, "%v", handler.Header) // response
f, err := os.OpenFile("./upload/"+handler.Filename,
os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
io.Copy(f, file)
}
}

func main() {
http.HandleFunc("/upload", upload)
log.Fatal(http.ListenAndServe(":8080", nil))
}

项目的详细代码看我github

转载请包括本文地址:https://allenwind.github.io/blog/2305
更多文章请参考:https://allenwind.github.io/blog/archives/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK