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

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

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

需求分析

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

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

架构设计和技术选型

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

接口设计

设计三个HTTP API,

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

功能实现

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

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

@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)文件上传按钮

1
2
3
4
5
6
7
8
9
10
11
<!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>

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

1
2
3
4

<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中。

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

启动服务器

1
2
3
4
5
6
7
8
9
@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语言的好处是编译成一个可实行文件即可。这个实现除了标准库不需要第三方包。

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
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/