【网盘项目日志】20210602:Seafile 离线下载系统开发(3)

今天主要来美化一下界面,优化用户体验。

文章目录

美化普通用户端

普通用户的界面就是前面展示的离线下载界面。我决定把它弄的宽一点,然后给状态加个颜色。

弄宽一点的话,我找到了 share dialog 的设置方式,偷了过来:

<Modal isOpen={true} style={{maxWidth: '760px'}} toggle={this.toggle}>

然后关于颜色问题的话,我找到了当时 Virus Scan 部分的列表显示代码,把它偷了过来:

let stateStr = <span className="text-red">Unknown</span>;
switch (item.status) {
  case 1:
    stateStr = <span className="text-purple">Waiting</span>;
    break;
  case 2:
    stateStr = <span className="text-azure">Downloading</span>;
    break;
  case 3:
    stateStr = <span className="text-green">OK</span>;
    break;
  case 4:
    stateStr = <span className="text-red">Error</span>;
    break;
}

做好的效果如图:

【网盘项目日志】20210602:Seafile 离线下载系统开发(3)

顺眼多了。

提交后自动删除输入框内容

啊,这其实是个小问题,提交文件后输入框内容还在,用户使用起来也不方便。加一条 JS 语句就好啦。

addOfflineDownloadTask(this.props.repoID, this.props.path, q).then((res) => {
  this.setState({ q: '' });		// Here it is!
  this.refreshList();
}).catch(error => {
  let errMessage = Utils.getErrorMsg(error);
  this.setState({
    errMessage: errMessage
  });
});

有个BUG导致 seafevents 自动退出

在测试的时候发现每过一段时间,离线下载功能就不好用了,卡在 Waiting 状态上。添加了中间输出以后,发现问题:

[WARNING] Failed to get offline download tasks (1) from db: QueuePool limit of size 5 overflow 10 reached, connection timed out, timeout 30.00 (Background on this error at: http://sqlalche.me/e/14/3o7r).

是 SQLAlchemy 的连接线程过多了。既然刚开始的时候能用,那说明一定是处理的时候有什么线程没有关闭。

排查以后发现:

def set_record_status(self, odr_id, status, comment=None):
    session: Session = self.edb_session()
    try:
        q = session.query(OfflineDownloadRecord).filter(OfflineDownloadRecord.odr_id == odr_id)
        r = q.first()
        if not r:
            logger.error('No offline download record: %d.', odr_id)
            return
        else:
            r.status = status
            if comment is not None:
                r.comment = comment
            session.commit()
    except Exception as e:
        logger.warning('Failed to update offline download status from db: %s.', e)

罪魁祸首就是这个函数,应该在后面加上:

finally:
    session.close()

就好了。

添加用户界面本地化

本地化好说,还是那两个文件。

弄好本地化以后的效果:

【网盘项目日志】20210602:Seafile 离线下载系统开发(3)

BUG:中文导致 events 退出的问题

测试的时候发现,只要给出一个中文的地址,这玩意就会报错:

UnicodeEncodeError: 'ascii' codec can't encode characters in position 35-41: ordinal not in range(128)

但是我的文件全部设置了 utf-8 的编码,并且我尝试用 sys.getdefaultencoding,都是输出 utf-8,网上的方法一点用也没。

最后我想到,先前用 vim 的时候,也是完全没办法输入中文,会不会是系统不支持中文导致的?尤其是 docker 这种,它对中文的支持应该更差。

于是按照这篇教程:https://blog.csdn.net/llllllloooooo/article/details/102852027,我给容器安装了中文语言包,这样就 OK 了。

无法下载 ed2k 链接(暂未解决)

磁力链接理论上来说 Aria2 是支持的才对。。。不过看来 ed2k 的是不支持的,哈哈

我又放了一个 magnet 的链接上去,是可以下载的:

【网盘项目日志】20210602:Seafile 离线下载系统开发(3)

再优化一下 task 列表刷新逻辑

现在 task 列表刷新逻辑还可以再优化一下。咱们实时刷新是为了在 Task 完成的时候,用户可以及时看到。那么如果列表本身没有正在进行的任务了,那也就没有必要刷新列表了。

什么?如果用户在别的界面上创建了新任务怎么办?那关我什么事?用户闲的没事干非要这么玩吗?(呼呼,直接怼回去

intervalRefreshList() {
  if (this.state.ent_ongoing_num > 0) this.refreshList();
}

优化断点续传

断点续传功能在 Aria2 上也是支持的,只要开启 -c 选项即可。不过目前的程序逻辑来看,每次启动 Aria2 都需要重新生成一个新的临时文件夹,这样效率不高啊。

我们在写程序的时候预留了一个 comment 字段在数据库里,当时是为了保存错误原因。我们知道错误原因只有在出了错以后才会保存,那没有错误的时候呢?当然就可以随便用啦。

tdir = self.db_oper.get_record_comment(download_task.odr_id)
if tdir is None or len(tdir) == 0 or not os.path.isdir(tdir):
    tdir = tempfile.mkdtemp(dir=self.settings.temp_dir)
    self.db_oper.set_record_comment(download_task.odr_id, tdir)
    logger.debug("Created temp dir '%s' for task '%d'", tdir, download_task.odr_id)
else:
    logger.debug("Using old temp dir '%s' for task '%d'", tdir, download_task.odr_id)

加一个管理员界面

管理界面就是把所有的 task 全部显示在界面上就好。模仿一下 Virus Files 的界面吧。

【网盘项目日志】20210602:Seafile 离线下载系统开发(3)

看起来有些简单?咳咳,说简单其实也不是那么简单,一点一点来。

左侧列表按钮

就是左边那个离线上传的选项卡需要添加一个新条目,这个就在当时给 Virus Scan 和 审计系统解除 isPro 限制的地方(frontend/src/pages/sys-admin/side-panel.js):

{otherPermission &&
<li className="nav-item">
  <Link
      className={`nav-link ellipsis ${this.getActiveClass('offline-downloads')}`}
      to={siteRoot + 'sys/offline-downloads'}
      onClick={() => this.props.tabItemClick('offline-downloads')}
  >
    <span className="sf2-icon-link" aria-hidden="true"></span>
    <span className="nav-text">{gettext('Offline Upload')}</span>
  </Link>
</li>
}

主界面

主界面我是复制了一个 Virus Scan 的文件列表过来进行修改的。与 Virus Scan 的列表不同的是,我这个列表无需支持任何操作,因此只需要一个表格即可。


return (
  <tr className={this.state.highlight ? 'tr-highlight' : ''} onm ouseEnter={this.handleMouseEnter} onm ouseLeave={this.handleMouseLeave}>
    <td>{taskInfo.task_owner}</td>
    <td>{taskInfo.repo_name}</td>
    <td>{taskInfo.repo_owner}</td>
    <td>{taskInfo.file_path}</td>
    <td onClick={this.onUrlClick} title={taskInfo.url}>{this.state.showAllUrl ? taskInfo.url : shortenUrl}</td>
    <td>{taskInfo.status === 4 ? `${Utils.bytesToSize(taskInfo.size)}` : '-'}</td>
    <td>{stateStr}</td>
  </tr>
);

这里我做了一些小优化,在管理员界面默认不显示全部的 url,只有当点击到 url 的时候,才会去显示全部的 url。

大小显示这里,是学的 all repo 那个界面,调用了 Utils.bytesToSize 函数,可以用一个好看的格式把 byte 单位的大小信息显示出来。

Paginator BUG

我在写的时候发现一个 BUG,就是它给的 Paginator,点击下一页以后虽然加载了,但是底下的页码依然显示 1,并且无法返回上一页。

但是很奇怪,这部分我明明是完全按照 virus scan 的写法来写的,怎么会出错呢?我打开了 Paginator 的源代码:

goToNext = () => {
  const { currentPage, curPerPage } = this.props;
  this.updateURL(currentPage + 1, curPerPage);
  this.props.gotoNextPage();
}

很奇怪,这里根本没有修改 currentPage,也就是说底部的数字是取决于外面的 Component。

抱着尝试的心态,我又打开了 All Repo 的界面,结果就发现了 setState 中还有一个 currentPage 的设置:

seafileAPI.sysAdminListFileUpdateLogs(page, perPage).then((res) => {
  this.setState({
    logList: res.data.file_update_log_list,
    loading: false,
    currentPage: page,
    hasNextPage: res.data.has_next_page,
  });
})

然而在 Virus Scan 里并没有设置它!!!也就是说,原版的 Virus Scan 里面也是有 BUG 的!!!!!!

海文麻烦打一下钱,给你们找出来一个 BUG(

统一的 State Resolver

目前我们还是比较原始的做法,在两个界面的文件里直接用 switch-case 做状态码和文字之间的转换,并且判断是否成功之类的也是直接和数字判断。这样的坏处就是如果以后状态码修改了,那数字什么的很容易就乱了。

我在 Utils 文件里放了一个新字段:

offlineDownloadStatus: {
  UNKNOWN: 0,
  WAITING: 1,
  QUEUING: 2,
  DOWNLOADING: 3,
  OK: 4,
  ERROR: 5,
  toDisplayText: function toDisplayText(status) {
    switch (status) {
      case Utils.offlineDownloadStatus.WAITING:
        return <span className="text-purple">{gettext('Waiting')}</span>;
      case Utils.offlineDownloadStatus.QUEUING:
        return  <span className="text-purple">{gettext('Queuing')}</span>;
      ......
    }
  }
},

然后在代码里就可以这么用:

let is_not_ok = this.state.taskList[i].status === Utils.offlineDownloadStatus.WAITING ||
    this.state.taskList[i].status === Utils.offlineDownloadStatus.QUEUING ||
    this.state.taskList[i].status === Utils.offlineDownloadStatus.DOWNLOADING;

啊,虽然代码长度增加了,但是可读性也强了很多。

数据问题

就是要加一个新的接口。这里的接口定义我又模仿了 VirusScan 的获取列表,然后稍微改了一下,具体怎么改的就不再赘述了。

后记

好啦,离线下载功能也完成啦,我给自己安排的任务就基本完成啦。

整个离线下载功能其实满打满算的话,总共用了 3 天的时间。根据整个博客看下来,不算很难?当然不是,之所以这次开发这么顺利,是因为有先前的经验积累,我知道每个功能大致的实现方式,知道每个组件大致的位置和逻辑,所以我自己添加新功能的时候,每当我需要借鉴什么,我都能快速定位到对应的位置。整这种大项目不仅需要项目经验,更需要对【这个】项目有经验(毕竟这玩意劝退了我们小组的两名组员。。。

另外,千万不要觉得我前面“偷” pro 版本的功能很简单,我只是在某些地方轻描淡写,说“啊,pro 里有拿过来用就好啦”,但是至于我是怎么发现的,怎么拿来的,中间做了哪些繁琐的尝试和修改,提到的不多,但也是最难的。你想想,如果只是复制几个文件过来,随便改几行代码就把 pro 的功能弄好了,那海文公司还卖什么 pro 版?还赚什么钱?以后哪个公司想用 Seafile pro 的功能,随便找个本科生程序员来改改就有了,那还了得吗?

一些探索过程之类的东西没有写到博客里,一方面因为某些探索过程实在是比较长、比较繁杂,写到博客里也没意思,另外就是我一般找到头绪以后会顺着一直往前走,等都弄完以后才来写博客,有些也就忘了。

上一篇:tensorflow学习笔记二:入门基础 好教程 可用


下一篇:[Utils] Sublime Text 常用快捷键