分享一个简单的文件服务,可以在局域网上多设备同步文件。
我现有iPad、手机、Mac、Linux系统的笔记本、Windows系统的笔记本、台式机,如何在这些设备中快速共享文件。使用云服务及不方便尤其是共享大文件的时候。那么如何解决呢?可以在本地搭建一个文件共享服务,通过局域网共享文件。
接下来的实现满足最基本的需求。有Python版本的实现和Go语言的实现。
需求分析
各个设备通过下载、上传来共享文件,于是需要一个中心服务器提供下载、上传、保存文件的服务。把这个功能分解如下:
- 各个设备可以浏览共享的文件
- 各个设备可以下载共享的文件
- 各个设备可以上传自己的文件作为共享
架构设计和技术选型
- 采用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" )
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) file, handler, err := r.FormFile("uploadfile") if err != nil { fmt.Println(err) return } defer file.Close() fmt.Fprintf(w, "%v", handler.Header) 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/