原 型 开 发

引言

实验目的:用你熟悉的语言,开发一个基于网络(比如Socket)的简单的互操作程序:实现A机器的程序,可以管理(增加、删除、改等)B机器上的某个文件夹或者目录。

问题分析

由于题目并未限制编程语言和技术,在此直接选用了HTTP协议,利用Python的Flask框架开发Web应用。

在服务器端,设计必要的RESTful API,用于前端jQuery发送Ajax请求与远程服务器连接。

用户可以直接访问服务器端的Web地址,对服务器端的文件夹进行文件管理。

也可在本机启动客户端Web服务,通过指定服务器端的URL,客户端访问服务器端API,实现远程的文件管理。

项目仓库地址:https://github.com/unsioer/file-manager-flask

环境

开发环境:

  • Visual Studio Code

  • Python 3.8.5

  • Flask 1.1.2

具体设计

概要

本实验的服务器端和客户端均采用了Python的Flask这一Web框架。

为了演示方便,服务器端和客户端均在本机测试。服务器端绑定0.0.0.0:5000,操作的目录设置为test;客户端代码置于client文件夹下,绑定0.0.0.0:10000,直接请求http://127.0.0.1:5000的内容。这显然容易推广到“A机器管理B机器的指定文件夹”。

  • 服务器端的路由设计中,/api 用于RESTful API,/app 用于返回HTML界面。

  • 客户端的路由设计中,/app 用于返回HTML界面(实际与服务器端的内容一致)。

下面具体说明使用到的技术。

客户端

客户端直接利用了Flask的模板解析功能,实际用于交互的只有一个HTML页面。因为界面功能基本一致,为了减少耦合,与服务器端共用代码(提交的代码中,client/templates文件夹实际是templates文件夹的映射)。

HTML界面的各项交互功能主要靠jQuery和衍生插件jquery-confirm(jQuery版的确认消息提示框)完成;

还用到了bootstrap以及font-awesome美化界面。

我们知道,客户端与服务器端应该使用的是不同的套接字,所以jQuery的Ajax请求的URL应该是不同的。所以,在共用的HTML代码中,设计了{{ host_link }}这一字段。

客户端有一个字典remote_config = {'host_link':'http://127.0.0.1:5000'},直接填写服务器端的URL。

代码差异如下:

# client/app.py 客户端
@app.route("/app/<path:folderPath>", methods=['GET'])
def index(folderPath):
    return render_template('app.html', **remote_config)
# app.py 服务器端
@app.route("/app/<path:folderPath>", methods=['GET'])
def index(folderPath):
    return render_template('app.html')
// templates/app.html 共用的HTML模板
$.ajax({
    url: window.location.href{% if host_link %}.replace(/(https?:\/\/)?[^\/]+(\/.+)/i,'{{ host_link }}$2',1){% endif %}.replace('/app', '/api/folder/basic', 1),
    ...
});

服务器端对应的Python代码未提供host_link,则不会被解析;客户端提供了host_link,在对应位置会被替换为指定的服务器端URL,由JavaScript的字符串替换功能改为正确的服务器端API的URL。

服务器端

服务器端除了上文提及的HTML界面外,主要内容为一系列操作的API。

大体上的代码大致均如:

@app.route("/api/folder/basic/<path:folderPath>", methods=['GET'])
@cross_origin()
def get(folderPath):
    ...

@app.route()指定了路径和请求方法;

@cross_origin()则是为了实现API的跨域访问,使得客户端Web界面得以操作服务器端的API。

服务器端路由

服务器端的路由设计中,/api 用于RESTful API,/app 用于返回HTML界面。

需要注意的几点:

  1. 以下的API设计中,格式均对齐为/api/(file|folder)/[a-z]+/<path>?,这是为了防止路径名称与API产生冲突。作为简易实现的功能,严格来说不是严谨的设计。
  2. 作为服务器端,如果要让客户端利用API,需要提供跨域访问支持。这里使用了Python的flask_cors包,并对每个API添加@cross_origin()支持。
  3. 目前设计的API中,除发生未知的系统错误外,均返回JSON字串,HTTP状态码均为200(虽然大部分JSON字串有一个state属性,写法仿照HTTP返回状态码)。这只是简易实现功能,不是严谨的设计。在Flask中,返回错误状态码可以使用abort方法,可以通过from flask import abort引入。
  4. 如果是URL路径不存在,Flask默认返回404状态码。

GET /api/folder/basic/<path:folderPath>

获取指定目录下的文件(包含文件夹)信息。

返回JSON格式

{
    "files":[
        {
            "name": "FileName", //文件名
            "path": "pathx/pathy", //文件所在路径 
            "type": 1, //文件类型,分为三类:0为文件夹,1为文件,2为其他
            "size": 0, //文件大小,单位为字节。该属性不对type===0(即文件夹)有效
            "mtime": "2021-03-24 22:39:32" //文件修改时间
        },
        ...
    ]
}

GET /api/folder/basic/

获取根目录下的文件信息。

返回JSON格式:同上。

GET /api/file/check/<path:filePath>

检查给出完整路径的文件(夹)是否存在。

注:由于下文一些API针对文件(夹)名冲突做了不同处理,HTML界面实际并未利用到此API。

返回JSON格式

  • 存在:{"status": 200}

  • 不存在:{"status": 404}

POST /api/folder/basic/<path:folderPath>

在指定目录上传文件

HTTP请求体:提交内容为表单数据,file项为二进制(即上传的文件)内容。

冲突检测:如果该目录下已经有同名文件,则将上传的文件命名为[非后缀名] - 副本[.后缀名] 保存。如果仍然存在命名冲突,则尝试命名为[非后缀名] - 副本 (2)[.后缀名] , [非后缀名] - 副本 (3)[.后缀名] ……直至避开冲突。

返回JSON格式{"status": 200}

POST /api/folder/basic

在根目录上传文件

HTTP请求体、冲突检测、返回JSON格式:同上。

PUT /api/folder/basic/<path:folderPath>

为指定目录下的指定文件(夹)重命名

HTTP请求体{"src":"原名称","dst":"新名称"}

文件(夹)名称合法性检测:拒绝文件(夹)名出现\ / : ? < > * 等特殊字符。

冲突检测:在这里,不允许新名称与当前目录下其他文件的名称有冲突。

返回JSON格式

  • 重命名成功,或原名称与新名称相同:{"status": 200}
  • 请求体不合法:{"status": 400, "msg": "No data given"}{"status": 400, "msg": "No source file or destination file given"}
  • 文件(夹)名称有特殊字符:{"status": 403, "msg": "Illegal file or folder name"}
  • 源文件不存在:{"status": 404, "msg": "Source file does not exist"}
  • 新名称与当前目录下其他文件的名称冲突:"status": 400, "msg": "Destination file exists"}

PUT /api/folder/basic/

为根目录下的指定文件(夹)重命名

HTTP请求体、文件(夹)名称合法性检测、冲突检测、返回JSON格式:同上。

DELETE /api/folder/basic/<path:folderPath>

删除指定目录下的指定文件(夹)。

HTTP请求体{"src":"要删除的文件(夹)名称"}

返回JSON格式

  • 删除成功,或原名称与新名称相同:{"status": 200}
  • 请求体不合法:{"status": 400, "msg": "No data given"}{"status": 400, "msg": "No source file or destination file given"}
  • 源文件不存在:{"status": 404, "msg": "Source file does not exist"}
  • 没有删除权限:"status": 400, "msg": "Permission refused"}

DELETE /api/folder/basic/

删除根目录下的指定文件(夹)。

HTTP请求体、返回JSON格式:同上。

POST /api/folder/new/<path:folderPath>

在指定目录下新建文件夹

HTTP请求体{"src":"文件夹名称"}

文件夹名称合法性检测:拒绝文件夹名出现\ / : ? < > * 等特殊字符。

冲突检测:在这里,不允许文件夹名称与当前目录下其他文件夹的名称有冲突。

返回JSON格式

  • 删除成功,或原名称与新名称相同:{"status": 200}

  • 请求体不合法:{"status": 400, "msg": "No data given"}{"status": 400, "msg": "No folder name given"}

  • 文件夹名称有特殊字符:{"status": 403, "msg": "Illegal folder name"}

  • 文件夹名称与当前目录下其他文件夹的名称冲突:"status": 400, "msg": "Destination folder exists"}

POST /api/folder/new/

在根目录下新建文件夹

HTTP请求体文件夹名称合法性检测冲突检测返回JSON格式:同上。

PUT /api/file/copy/<path:filePath>

复制指定路径下的文件(夹)到指定位置文件夹下。

指定位置文件夹如不存在则新建。

HTTP请求体{"dst":"目标文件夹名称"}

路径名称合法性检测:目标文件夹路径不得出现 : ? < > * 等特殊字符。

冲突检测

以下内容也适用于目标文件夹名称就是当前文件(夹)所在文件夹的情况。

如果该目录下已经有同名文件,则将复制的文件命名为[非后缀名] - 副本[.后缀名] 保存。如果仍然存在命名冲突,则尝试命名为[非后缀名] - 副本 (2)[.后缀名] , [非后缀名] - 副本 (3)[.后缀名] ……直至避开冲突。

如果该目录下已经有同名文件夹,则将复制的文件夹命名为[文件夹名] - 副本 保存。如果仍然存在命名冲突,则尝试命名为[文件夹名] - 副本 (2) , [文件夹名] - 副本 (3)……直至避开冲突。

返回JSON格式

  • 复制成功:{"status": 200}
  • 要复制的文件(夹)不存在:{"status": 404, "msg": "File not found"}
  • 请求体不合法:{"status": 400, "msg": "No data given"}{"status": 400, "msg": "No destination folder given"}
  • 目标文件夹路径名称不合法:{"status": 403, "msg": "Illegal path"}
  • 复制失败:{"status": 400, "msg": "错误信息"}

PUT /api/file/move/<path:filePath>

移动指定路径下的文件(夹)到指定位置文件夹下。

指定位置文件夹如不存在则新建。

HTTP请求体{"dst":"目标文件夹名称"}

路径名称合法性检测:目标文件夹路径不得出现 : ? < > * 等特殊字符。

冲突检测

当目标文件夹名称就是当前文件(夹)所在文件夹时,不做处理,返回{"status": 200}

如果该目录下已经有同名文件,则将移动的文件命名为[非后缀名] - 副本[.后缀名] 保存。如果仍然存在命名冲突,则尝试命名为[非后缀名] - 副本 (2)[.后缀名] , [非后缀名] - 副本 (3)[.后缀名] ……直至避开冲突。

如果该目录下已经有同名文件夹,则将移动的文件夹命名为[文件夹名] - 副本 保存。如果仍然存在命名冲突,则尝试命名为[文件夹名] - 副本 (2) , [文件夹名] - 副本 (3)……直至避开冲突。

返回JSON格式

  • 移动成功,或目标文件夹名称就是当前文件(夹)所在文件夹:{"status": 200}
  • 要移动的文件(夹)不存在:{"status": 404, "msg": "File not found"}
  • 请求体不合法:{"status": 400, "msg": "No data given"}{"status": 400, "msg": "No destination folder given"}
  • 目标文件夹路径名称不合法:{"status": 403, "msg": "Illegal path"}
  • 移动失败:{"status": 400, "msg": "错误信息"}

以下为HTML界面路由,服务器端与客户端基本一致。

客户端路由

GET /app/<path:folderPath>及GET /app/

返回HTML界面,显示指定目录及根目录下的情况。界面效果详见下节。

Web界面展示

这里做了简要截图,也可详见演示视频。

基本界面

界面左上方为网页标题,下有四个按钮,分别为:根目录、刷新、新建文件夹、上传(文件)。

接着是文件列表。对于非根目录,还有一行..的链接返回上一级目录。

每个文件(夹)根据文件类型配套不同图标。文件夹有链接可以访问;文件则显示具体大小。文件和文件夹均显示最近修改时间。

对于每个文件(夹),均提供四种操作,分别为:复制、移动、重命名、删除。

最下方显示作者和创作年份。

基本界面

提示框

新建、重命名、复制、移动提示框(以重命名提示框为例):

重命名提示框

删除提示框:

删除提示框

技术总结

本次实验实现了一个最简单的基于网络的远程文件管理应用,完成了实验所有要求并做了一些合法性检测,设计了相对简洁美观的界面。

由于只是简易实现功能,尚有以下不足之处:

  • API命名规范

  • 请求和返回内容设计较为随意

  • 未进行充分测试,难免bug(如文件操作权限不足在API中没有全部体现)

  • 未运用Vue.js等前端框架,纯手写jQuery,代码较为繁琐