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,使代码更加清晰简洁。

总结

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

本章代码

代码

相关推荐
腾讯TNTWeb前端团队35 分钟前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰4 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪4 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪4 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy5 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom6 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom6 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom6 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom6 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom6 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试