NestJS实战09-在NestJS中如何调用百度网盘API(下)

by 雪隐 from juejin.cn/user/143341...

本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权

概要

当我们深入了解了百度网盘的授权流程后,仿佛一扇神奇的大门已经为我们敞开。接下来,我们只需完成剩余的接口,这部分内容相当庞大,我将通过展示一些例子来简要介绍。不再深入细节,因为基本上都可以通过仔细研读百度的官方文档来轻松实现。此外,虽然一开始我对文件上传的分片上传并没有太大兴趣,觉得可能用不上,但实际使用后,发现实现这一功能是非常有必要的。虽然我暂时还未亲自实践,但会在未来抽出时间来深入了解并实现。

能力说明

在开始开发之前,请先阅读官方文档的能力说明。否则捣鼓了半天不成功然后回来看原因会郁闷的不行。最关键的还是要看下它的限制条件。

限制条件

  1. 目录限制

每个第三方应用在网盘只能拥有一个文件夹用于存储上传文件,该文件夹必须位于/apps目录下,apps下的文件夹名称为申请接入时填写的申请接入的产品名称。如申请接入的产品名称为云存储,那么该文件夹为/apps/云存储,用户看到的文件夹为/我的应用数据/云存储

这步骤非常重要,我们只能对这个文件夹里面的东西进行操作。

  • 打开控制台,点击我的应用,查看自己的产品应用名称
  • 打开网盘,创建对应的文件夹
  1. 大小限制

所有开发者均可接入使用接口,但可上传单个文件大小根据授权用户的身份有不同的限制:

  • 普通用户单个上传文件大小上限为4GB

  • 会员用户单个上传文件大小上限为10GB

  • 超级会员用户单个上传文件大小上限为20GB

    注:分片数量不得超过1024个

  1. 类型限制

普通用户在网盘APP端无法上传视频、Live Photo类型的文件。

  1. 频次限制

开放平台仅对异常行为进行相应限制,不会影响到您的正常使用。

接口事例范讲

接口文档可能有点复杂,但理解并参照官方的示例将会让一切变得清晰。下面,我将举例几个实际的操作,以帮助大家更好地理解。

1. 创建文件夹

在开始之前,请先仔细阅读创建文件夹的官方文档。创建文件夹的请求结构如下:

bash 复制代码
POST /rest/2.0/xpan/file?method=create&access_token=xxx HTTP/1.1
Host: pan.baidu.com

接下来是一个 TypeScript 代码的例子:

ts 复制代码
  async creatFolder(baiduListDto: BaiduListDto): Promise<any> {
    const access_token = await this.cacheManager.get('access_token');
    if (access_token) {
      const { path } = baiduListDto;
      const url = `/rest/2.0/xpan/file?method=create&access_token=${access_token}`;
      const data = `path=${path}&isdir=1&rtype=1`;
      this.logger.debug(url);
      this.logger.debug(data);
      const response = await firstValueFrom(
        this.httpService
          .post(url, data, {
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
          })
          .pipe(map((response) => response.data))
          .pipe(
            catchError(async (error) => {
              this.logger.error(error);
              await this.cacheManager.del('access_token');
              await this.cacheManager.del('refresh_token');
              throw '创建百度网盘文件夹失败!';
            }),
          ),
      );
      return response;
    }
    return this.noToken();
  }

2. 删除文件或文件夹

参照官方文档,删除文件的请求示例如下:

  • delete 删除 delete【/测试目录/test/西瓜书.pdf】:
less 复制代码
curl "http://pan.baidu.com/rest/2.0/xpan/file?method=filemanager&access_token=12.53c9523518840783bada572ddafc2831.YaT0UDVITrNy_UhBGlVLnzFXQLnsuXqWeKQwRVY.nupc9w&opera=delete" \
-d 'async=2&filelist=["%2F%E6%B5%8B%E8%AF%95%E7%9B%AE%E5%BD%95%2Ftest%2F%E8%A5%BF%E7%93%9C%E4%B9%A6.pdf"]'
  • 而在 Nest 中的实现为:
ts 复制代码
  /**
   * 删除文件
   * @param file
   */
  async deleteFile(baiduListDto: BaiduListDto) {
    const access_token = await this.cacheManager.get('access_token');
    if (access_token) {
      const { path } = baiduListDto;
      const baseUrl = '/rest/2.0/xpan/file';
      const url = `${baseUrl}?method=filemanager&access_token=${access_token}&opera=delete`;
      const data = {
        async: '2',
        filelist: JSON.stringify([path]),
      };
      const headers = {
        'User-Agent': 'pan.baidu.com',
        'Content-Type': 'application/x-www-form-urlencoded',
      };
      this.logger.debug(url);
      this.logger.debug(data);
      const response = await firstValueFrom(
        this.httpService
          .post(url, data, { headers })
          .pipe(map((response) => response.data))
          .pipe(
            catchError(async (error) => {
              this.logger.error(error);
              await this.cacheManager.del('access_token');
              await this.cacheManager.del('refresh_token');
              throw '百度网盘删除失败!';
            }),
          ),
      );
      return response;
    }
    return this.noToken();
  }

3. 下载信息获取

参照官方文档,下载有点麻烦,下载流程如下:

我按照官方的流程,先获取百度列表,然后选择对应的文件,获取对应的fsid,然后请求文件下载,然后把文件下载到public公共目录,向前端展示连接。然后通过浏览器来打开或者下载这个文件。

代码如下:

ts 复制代码
// 资料汇总页面
const [fsid, setFsid] = useState("");
// ...其他代码

const onSelect: TreeProps["onSelect"] = async (selectedKeys, info) => {
  setCurrentPath((info.node as any).path);
  setFsid((info.node as any).fs_id);
  setIsDir((info.node as any).isdir);
};

// 操作组件
const handleDownload = async () => {
  if (currentPath === "" || currentPath == "/apps/个人管理系统") {
    notification.error({ message: "请选择合适的路径" });
  } else if (isDir) {
    notification.error({ message: "不能下载文件夹" });
  } else {
    const res: any = await getDownloadFileInfo(fsid);
    const { downloadUrl } = res;
    setDownloadUrl(`${process.env.API_DOWNLOAD_URL}${downloadUrl}`);
  }
};

这段代码主要做了以下几件事情:

  1. 选择文件时更新状态: 在资料汇总页面中,你使用 onSelect 函数在选择文件时更新了当前路径 currentPath、文件 ID fsid 以及是否为目录 isDir 的状态。
  2. 处理下载操作: 在操作组件中,通过 handleDownload 函数处理下载操作。首先,它检查了当前路径是否为空或为特定路径,如果是,则显示错误通知。接着,检查是否为文件夹,如果是则同样显示错误通知。最后,如果是一个可下载的文件,使用 getDownloadFileInfo 函数获取下载信息,然后构建下载链接,并将其设置到 downloadUrl 状态中。

这是一个清晰而有效的下载文件的流程。在前端中,你还需要在 UI 上呈现下载链接,并提供用户点击或复制的选项。另外,确保在后端的下载逻辑中,你能够成功从百度网盘获取文件并提供给前端进行下载。

后端根据官方文档来操作就可以了。然后把它写入到public文件夹里面。

ts 复制代码
  async getFileMetas(fsid: string, access_token: string) {
    const url = `http://pan.baidu.com/rest/2.0/xpan/multimedia?method=filemetas&access_token=${access_token}&fsids=[${fsid}]&thumb=1&dlink=1&extra=1`;
    const response = await firstValueFrom(
      this.httpService
        .get(url)
        .pipe(map((response) => response.data))
        .pipe(
          catchError(async (error) => {
            this.logger.error(error);
            await this.cacheManager.del('access_token');
            await this.cacheManager.del('refresh_token');
            throw '百度网盘获取文件信息失败!';
          }),
        ),
    );
    return response;
  }

  async downloadFile(fsid: string) {
    const access_token = await this.cacheManager.get('access_token');
    if (access_token) {
      const data = await this.getFileMetas(fsid, access_token);
      if (data.list.length > 0) {
        const { dlink, filename } = data.list[0];
        // 指定图像文件的保存路径和文件名,以及图像格式(例如,.png、.jpg)
        const outputPath = join(
          process.cwd(),
          `./public/downloads/${filename}`,
        );
        const fileExists = await this.checkFileExists(outputPath);
        // 文件不存在
        if (!fileExists) {
          if (dlink) {
            const url = `${dlink}&access_token=${access_token}`;
            const headers = {
              'User-Agent': 'pan.baidu.com',
              Host: 'd.pcs.baidu.com',
            };
            this.logger.debug(url);

            const writer = createWriteStream(outputPath);
            await firstValueFrom(
              this.httpService
                .get(url, { headers, responseType: 'stream' })
                .pipe(map((response) => response.data.pipe(writer)))
                .pipe(
                  catchError(async (error) => {
                    this.logger.error(error);
                    await this.cacheManager.del('access_token');
                    await this.cacheManager.del('refresh_token');
                    throw '百度网盘文件下载失败!';
                  }),
                ),
            );
            // 使用 promisify 来监听写入完成事件
            await promisify(writer.on).call(writer, 'finish');

            return { downloadUrl: `/downloads/${filename}` };
          }
        } else {
          return { downloadUrl: `/downloads/${filename}` };
        }
      }
    }
    return this.noToken();
  }
  
  async checkFileExists(filePath) {
    try {
      await fs.access(filePath, fs.constants.F_OK);
      return true; // 文件存在
    } catch (error) {
      if (error.code === 'ENOENT') {
        return false; // 文件不存在
      }
      return false; // 其他错误,例如权限问题
    }
  }

实现了从百度网盘获取文件信息,然后进行文件下载的整个过程。让我们逐一看一下:

  1. 获取文件信息:

    • 使用 getFileMetas 函数根据给定的 fsidaccess_token 获取文件信息。在这里,使用了百度网盘提供的 API 来获取文件的元数据,包括下载链接等。
  2. 下载文件:

    • 使用 downloadFile 函数,你首先通过调用 getFileMetas 获取文件信息,然后判断文件是否已经存在。如果文件不存在,你通过创建可写流来将文件写入指定的本地路径。这个过程中使用了 httpService 发送 GET 请求,并使用 responseType: 'stream' 以流的方式获取文件内容。
    • 使用 checkFileExists 函数检查文件是否已经存在,这样你可以决定是否需要重新下载。
  3. 文件路径和前端展示:

    • 最后,返回了一个包含下载链接的对象,这个链接在前端可以用于展示下载链接。构建了一个本地路径,以确保文件下载到 public/downloads 文件夹中。前端可以通过访问 /downloads/${filename} 来下载文件。

文件上传:从前端到百度网盘

在开发过程中,文件上传是一个常见而又关键的操作。我将分享我的文件上传流程,以及一些我在实践中的经验。

我的步骤是先把文件上传到服务器(Nest官方的文件上传),再通过服务器上传到百度网盘(百度网盘官方文档文件上传)。

百度网盘官方文档文件上传有2种方式,分片上传和单片上传,我这里只实现了单步上传。

1. 前端轻松搞定

前端这边,我选择了antd的组件,这让文件上传变得异常简单。通过Upload组件,我们可以方便地选择文件,然后将文件和存储路径发送给后端。以下是一个简单的示例代码:

ts 复制代码
// resource/components/operation/index.tsx
<Upload
  maxCount={1}
  name="file"
  headers={{
    Authorization: `Bearer ${localStorage.getItem("token")}`,
  }}
  data={{ path: currentPath }}
  action={`${process.env.API_BASE_URL}/baidu-api/upload`}
  multiple={false}
  capture={undefined}
  onChange={handleChange}
  showUploadList={false}
>
  <Button icon={<UploadOutlined />}>上传文件</Button>
</Upload>

通过这个简短的代码块,我们就实现了文件上传的前端部分。用户只需点击按钮,就能轻松上传文件,而antdUpload组件则负责处理繁琐的细节。

2. 后端精简文件上传流程

首先,关于安装类型包的部分,我们可以将它添加到 package.json 中的 devDependencies 中,然后使用 npm install 进行安装。

shall 复制代码
$ npm install --save-dev @types/multer

接下来,让我们来改进控制器中的文件上传部分。

ts 复制代码
// src/baidu-api/baidu-api.controller.ts
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';

@Post('upload')
@UseInterceptors(
  FileInterceptor('file', {
    fileFilter(req, file, callback) {
      // 解决中文名乱码的问题,使用 Buffer 转码
      file.originalname = Buffer.from(file.originalname, 'latin1').toString('utf8');
      callback(null, true);
    },
    storage: diskStorage({
      destination: './uploads', // 设置上传文件存储目录
      filename: (req, file, cb) => {
        // 使用原始文件名字进行传递
        cb(null, decodeURI(file.originalname));
      },
    }),
  }),
)
async uploadFile(
  @UploadedFile() file,
  @Body('path') path: string,
): Promise<any> {
  return await this.baiduApiService.uploadFile(file, path);
}

我们主要做了以下几个修改:

  1. 引入了 FileInterceptordiskStorage,避免了多次引用 multer
  2. 将回调函数中的文件名解码部分,合并到 filename 中,减少重复代码。
  3. 通过 decodeURI 来解码文件名,替代原有的 Buffer 操作。
  • 接下来,我们看一下文件上传的实现部分,可以进一步简化。
  • 代码实现
ts 复制代码
  async uploadFile(file, path) {
    const access_token = await this.cacheManager.get('access_token');
    if (access_token) {
      const formData = new FormData();
      this.logger.debug('开始读取文件!');
      // 读取文件内容并附加到formData
      const fileContent = await fs.readFile(file.path);
      // 转换 Buffer 为 Blob
      const fileBlob = this.bufferToBlob(fileContent, file.mimetype);

      formData.append('file', fileBlob, file.filename);

      const url = `https://d.pcs.baidu.com/rest/2.0/pcs/file?method=upload&access_token=${access_token}&path=${path}/${encodeURIComponent(
        file.filename,
      )}`;
      this.logger.debug(url);
      const response = await firstValueFrom(
        this.httpService
          .post(url, formData, {
            headers: { 'Content-Type': 'multipart/form-data' },
          })
          .pipe(map((response) => response.data))
          .pipe(
            catchError(async (error) => {
              this.logger.error(error);
              await this.cacheManager.del('access_token');
              await this.cacheManager.del('refresh_token');
              throw '百度网盘上传失败!';
            }),
          ),
      );

      return response;
    }
    return this.noToken();
  }

在这里,我们通过使用 file.buffer 直接获取文件内容,避免了读取整个文件的操作,同时通过 FormData 直接附加文件内容,减少了中间步骤。

希望这些调整使得你的文件上传代码更加清晰和优雅。如果有其他方面需要优化或修改,请随时告诉我。

定期清理服务器空间,释放磁盘资源

在服务器应用中,随着时间的推移,文件可能会不断积累,占据大量磁盘空间。为了确保服务器的稳定运行,我们需要定期清理这些不再需要的文件。在这篇文章中,我将向你展示如何通过定时任务来自动化这个过程,释放磁盘资源。

1. 设置定时任务

首先,我们使用 Nest.js 中的 @Cron 装饰器,设定一个每天午夜执行的清理任务。这确保了在最低使用时进行文件清理,不影响系统正常运行

ts 复制代码
  @Cron('0 0 * * *') // 每天午夜执行清理任务
  async deleteUploads() {
    this.logger.debug('开始每天的清除任务');

    const currentDate = new Date();
    const cutoffDate = new Date(currentDate);
    cutoffDate.setDate(currentDate.getDate() - this.cleanupDaysAgo);

    this.cleanDirectory(this.uploadDirectory, cutoffDate);
    this.cleanDirectory(this.downloadDirectory, cutoffDate);
  }

2. 清理文件目录

ts 复制代码
private async cleanDirectory(directoryPath: string, cutoffDate: Date) {
  try {
    const files = await fs.promises.readdir(directoryPath);

    for (const file of files) {
      const filePath = path.join(directoryPath, file);
      const stats = await fs.promises.stat(filePath);
      const fileCreationDate = new Date(stats.birthtime);

      if (fileCreationDate < cutoffDate) {
        await fs.promises.unlink(filePath);
        this.loggerNomal('删除文件', filePath);
      }
    }
  } catch (error) {
    this.loggerError('清理文件夹失败', error);
  }
}

接下来,我们编写了 cleanDirectory 函数,用于删除指定日期之前的文件。在这里,我们使用异步的 fs.promises,使代码更加清晰简洁。

总结

下一章进入测试章节,主要说明单元测试相关的内容,有兴趣的小伙伴,可以关注下,如果这篇文章的内容对您们有帮助,请多多点赞评论🙏!

本章代码

代码

相关推荐
Myli_ing10 分钟前
考研倒计时-配色+1
前端·javascript·考研
余道各努力,千里自同风13 分钟前
前端 vue 如何区分开发环境
前端·javascript·vue.js
PandaCave20 分钟前
vue工程运行、构建、引用环境参数学习记录
javascript·vue.js·学习
软件小伟22 分钟前
Vue3+element-plus 实现中英文切换(Vue-i18n组件的使用)
前端·javascript·vue.js
醉の虾43 分钟前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧1 小时前
TypeScript 的发展与基本语法
前端·javascript·typescript
hummhumm1 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
asleep7011 小时前
第8章利用CSS制作导航菜单
前端·css
hummhumm1 小时前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架
幼儿园的小霸王2 小时前
通过socket设置版本更新提示
前端·vue.js·webpack·typescript·前端框架·anti-design-vue