by 雪隐 from juejin.cn/user/143341...
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权
概要
当我们深入了解了百度网盘的授权流程后,仿佛一扇神奇的大门已经为我们敞开。接下来,我们只需完成剩余的接口,这部分内容相当庞大,我将通过展示一些例子来简要介绍。不再深入细节,因为基本上都可以通过仔细研读百度的官方文档来轻松实现。此外,虽然一开始我对文件上传的分片上传并没有太大兴趣,觉得可能用不上,但实际使用后,发现实现这一功能是非常有必要的。虽然我暂时还未亲自实践,但会在未来抽出时间来深入了解并实现。
能力说明
在开始开发之前,请先阅读官方文档的能力说明。否则捣鼓了半天不成功然后回来看原因会郁闷的不行。最关键的还是要看下它的限制条件。
限制条件
- 目录限制
每个第三方应用在网盘只能拥有一个文件夹用于存储上传文件,该文件夹必须位于/apps
目录下,apps下的文件夹名称为申请接入时填写的申请接入的产品名称
。如申请接入的产品名称为云存储
,那么该文件夹为/apps/云存储
,用户看到的文件夹为/我的应用数据/云存储
。
这步骤非常重要,我们只能对这个文件夹里面的东西进行操作。
- 打开控制台,点击我的应用,查看自己的产品应用名称
- 打开网盘,创建对应的文件夹
- 大小限制
所有开发者均可接入使用接口,但可上传单个文件大小根据授权用户的身份有不同的限制:
-
普通用户单个上传文件大小上限为4GB
-
会员用户单个上传文件大小上限为10GB
-
超级会员用户单个上传文件大小上限为20GB
注:分片数量不得超过1024个
- 类型限制
普通用户在网盘APP端无法上传视频、Live Photo类型的文件。
- 频次限制
开放平台仅对异常行为进行相应限制,不会影响到您的正常使用。
接口事例范讲
接口文档可能有点复杂,但理解并参照官方的示例将会让一切变得清晰。下面,我将举例几个实际的操作,以帮助大家更好地理解。
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}`);
}
};
这段代码主要做了以下几件事情:
- 选择文件时更新状态: 在资料汇总页面中,你使用
onSelect
函数在选择文件时更新了当前路径currentPath
、文件 IDfsid
以及是否为目录isDir
的状态。 - 处理下载操作: 在操作组件中,通过
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; // 其他错误,例如权限问题
}
}
实现了从百度网盘获取文件信息,然后进行文件下载的整个过程。让我们逐一看一下:
-
获取文件信息:
- 使用
getFileMetas
函数根据给定的fsid
和access_token
获取文件信息。在这里,使用了百度网盘提供的 API 来获取文件的元数据,包括下载链接等。
- 使用
-
下载文件:
- 使用
downloadFile
函数,你首先通过调用getFileMetas
获取文件信息,然后判断文件是否已经存在。如果文件不存在,你通过创建可写流来将文件写入指定的本地路径。这个过程中使用了httpService
发送 GET 请求,并使用responseType: 'stream'
以流的方式获取文件内容。 - 使用
checkFileExists
函数检查文件是否已经存在,这样你可以决定是否需要重新下载。
- 使用
-
文件路径和前端展示:
- 最后,返回了一个包含下载链接的对象,这个链接在前端可以用于展示下载链接。构建了一个本地路径,以确保文件下载到
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>
通过这个简短的代码块,我们就实现了文件上传的前端部分。用户只需点击按钮,就能轻松上传文件,而antd
的Upload
组件则负责处理繁琐的细节。
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);
}
我们主要做了以下几个修改:
- 引入了
FileInterceptor
和diskStorage
,避免了多次引用multer
。 - 将回调函数中的文件名解码部分,合并到
filename
中,减少重复代码。 - 通过
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
,使代码更加清晰简洁。
总结
下一章进入测试章节,主要说明单元测试相关的内容,有兴趣的小伙伴,可以关注下,如果这篇文章的内容对您们有帮助,请多多点赞评论🙏!