Skip to content

feat(cloud): 添加云盘音乐上传功能#1023

Open
577fkj wants to merge 3 commits intoimsyy:devfrom
577fkj:feat-cloud-upload
Open

feat(cloud): 添加云盘音乐上传功能#1023
577fkj wants to merge 3 commits intoimsyy:devfrom
577fkj:feat-cloud-upload

Conversation

@577fkj
Copy link
Copy Markdown
Contributor

@577fkj 577fkj commented Apr 2, 2026

  • 添加云盘音乐上传功能
image image image

Copilot AI review requested due to automatic review settings April 2, 2026 09:00
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a cloud music upload feature, adding backend support for multipart file uploads and a new frontend CloudUpload modal. The implementation includes file size limit increases, MD5 calculation for integrity, and a fallback mechanism between proxy and direct-to-cloud uploads. Feedback identifies a bug where the Content-MD5 header uses hex instead of Base64, an issue with filename sanitization that strips extensions, and a recommendation to move metadata from query parameters to the request body to prevent URL length issues.

Comment on lines +229 to +230
const ext = item.file.name.split(".").pop() || "mp3";
const filename = item.file.name.replace("." + ext, "").replace(/\s/g, "").replace(/\./g, "_");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

目前的 filename 处理逻辑会移除文件后缀名并将点替换为下划线(例如 test.mp3 变成 test)。这会导致上传到云盘的文件失去原始后缀,可能导致云盘无法正确识别音频格式。建议直接使用原始文件名。

  const filename = item.file.name;

headers: {
"x-nos-token": uploadData.uploadToken,
"Content-Type": item.file.type || "audio/mpeg",
"Content-MD5": fileMd5,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Content-MD5 请求头要求使用 Base64 编码的 MD5 哈希值,而当前的 fileMd5 是十六进制字符串。这会导致存储服务器(NOS)校验失败并返回 403 或 400 错误。如果不需要强制校验,建议移除此 Header,或者将其转换为 Base64 格式。

Comment on lines +134 to +143
params: {
songId,
filename,
resourceId,
md5,
song,
artist,
album,
timestamp: Date.now(),
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

completeCloudUpload 中,歌曲名、艺术家和专辑等信息作为 params(查询参数)发送。由于这些字符串可能较长且包含特殊字符,建议改用 data 将其放在 POST 请求体中,以避免触及 URL 长度限制或引起编码问题。同理,getCloudUploadToken 也建议进行类似修改。

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

此 PR 为“我的云盘”页面新增音乐上传能力,包含前端上传弹窗与上传流程(后端代理上传失败时回退到客户端直传),并在 Electron 内置 API 服务侧补充 multipart 文件上传支持及上传大小限制。

Changes:

  • 在云盘页面的“更多操作”中新增“上传音乐”入口,打开上传弹窗并在成功后刷新云盘列表
  • 新增 CloudUpload 弹窗组件,实现上传队列、进度展示、失败重试,以及“代理上传 → 直传”回退逻辑
  • 补充云盘直传所需的 token/complete API 封装,并在 Electron Fastify 服务端支持 multipart 文件转发与 100MB 限制

Reviewed changes

Copilot reviewed 7 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/views/Cloud.vue 增加“上传音乐”菜单项,调用上传弹窗并在成功后刷新云盘数据
src/utils/modal.ts 新增 openCloudUpload,以 modal 方式加载并展示上传组件
src/components/Modal/CloudUpload.vue 新增上传弹窗:队列/进度/重试、代理上传失败回退直传、元数据提取
src/api/cloud.ts 新增直传所需的 /cloud/upload/token/cloud/upload/complete 请求封装
electron/server/netease/index.ts 动态 API 路由增加 multipart 文件读取并映射为 songFile 参数
electron/server/index.ts 配置 Fastify bodyLimit 与 multipart fileSize 为 100MB
components.d.ts 注册 CloudUpload 组件与 Naive UI Upload 相关组件类型
package.json 升级 @neteasecloudmusicapienhanced/api 依赖版本
pnpm-lock.yaml 锁文件更新以匹配依赖升级(含传递依赖变更)
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +95 to +102

// 检查是否为音频文件
if (!file.type.startsWith("audio/")) {
window.$message.warning(`${file.name} 不是音频文件`);
return false;
}

return true;
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

beforeUpload 仅依赖 file.type.startsWith('audio/') 判断音频类型;但在部分环境(尤其本地文件/Electron 场景)File.type 可能为空字符串,导致合法音频被误拒绝。建议在 file.type 为空时回退用扩展名判断,或交由后端校验并前端仅做提示。

Suggested change
// 检查是否为音频文件
if (!file.type.startsWith("audio/")) {
window.$message.warning(`${file.name} 不是音频文件`);
return false;
}
return true;
const mimeType = file.type;
// 1. 优先使用 MIME 类型判断
if (mimeType && mimeType.startsWith("audio/")) {
return true;
}
// 2. 当 MIME 为空时回退使用扩展名判断(兼容部分本地/Electron 场景)
if (!mimeType || mimeType === "") {
const name = file.name || "";
const dotIndex = name.lastIndexOf(".");
const ext = dotIndex !== -1 ? name.slice(dotIndex + 1).toLowerCase() : "";
const audioExts = ["mp3", "wav", "flac", "aac", "ogg", "m4a", "wma", "aiff", "alac"];
if (ext && audioExts.includes(ext)) {
return true;
}
}
// 非音频文件给出提示
window.$message.warning(`${file.name} 不是音频文件`);
return false;

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +101
// 文件上传前校验
const beforeUpload = (data: { file: { file: File | null }; fileList: any[] }) => {
const file = data.file.file;
if (!file) return false;

// 检查是否为音频文件
if (!file.type.startsWith("audio/")) {
window.$message.warning(`${file.name} 不是音频文件`);
return false;
}

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

前端未限制上传文件大小;但 Electron 侧 Fastify 已把 bodyLimit/multipart fileSize 设为 100MB,超出时会失败且提示不友好。建议在 beforeUpload 增加与服务端一致的 size 校验(提示中说明上限),并避免 FileReader 对超大文件做全量读取造成内存压力。

Suggested change
// 文件上传前校验
const beforeUpload = (data: { file: { file: File | null }; fileList: any[] }) => {
const file = data.file.file;
if (!file) return false;
// 检查是否为音频文件
if (!file.type.startsWith("audio/")) {
window.$message.warning(`${file.name} 不是音频文件`);
return false;
}
// 最大上传大小:100MB(需与后端 Fastify bodyLimit / multipart fileSize 保持一致)
const MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
// 文件上传前校验
const beforeUpload = (data: { file: { file: File | null }; fileList: any[] }) => {
const file = data.file.file;
if (!file) return false;
// 大小限制校验
if (file.size > MAX_UPLOAD_SIZE) {
window.$message.warning(
`${file.name} 超出大小限制,最大仅支持 100MB`
);
return false;
}
// 检查是否为音频文件
if (!file.type.startsWith("audio/")) {
window.$message.warning(`${file.name} 不是音频文件`);
return false;
}

Copilot uses AI. Check for mistakes.
Comment on lines +163 to +187
for (const item of uploadQueue.value) {
if (item.status !== "pending") continue;

try {
// 先尝试后端代理,失败则回退到客户端直传
await uploadFileWithFallback(item);
item.status = "success";
item.statusText = "上传完成!";
item.percent = 100;
} catch (error: any) {
console.error(`${item.file.name} 上传失败:`, error);
item.status = "error";
item.error = true;
const errorMsg = error.response?.data?.msg || error.message || "未知错误";
item.statusText = `上传失败: ${errorMsg}`;
item.percent = 0;
}
}

// 检查是否有成功的上传
const hasSuccess = uploadQueue.value.some(item => item.status === "success");
if (hasSuccess) {
// 通知刷新
emit("success");

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

processUploadQueue() 使用 for (const item of uploadQueue.value) 仅遍历一次当前队列;上传过程中若继续添加文件(isUploading 为 true 时不会触发新一轮处理),新加入的 pending 项可能在本轮结束后一直不被处理。建议改成“循环直到不存在 pending”的处理方式,或结束前检测仍有 pending 时继续跑一轮。

Suggested change
for (const item of uploadQueue.value) {
if (item.status !== "pending") continue;
try {
// 先尝试后端代理,失败则回退到客户端直传
await uploadFileWithFallback(item);
item.status = "success";
item.statusText = "上传完成!";
item.percent = 100;
} catch (error: any) {
console.error(`${item.file.name} 上传失败:`, error);
item.status = "error";
item.error = true;
const errorMsg = error.response?.data?.msg || error.message || "未知错误";
item.statusText = `上传失败: ${errorMsg}`;
item.percent = 0;
}
}
// 检查是否有成功的上传
const hasSuccess = uploadQueue.value.some(item => item.status === "success");
if (hasSuccess) {
// 通知刷新
emit("success");
// 循环处理,直到队列中不存在 pending 状态的任务
while (true) {
const nextItem = uploadQueue.value.find(item => item.status === "pending");
if (!nextItem) {
break;
}
try {
// 先尝试后端代理,失败则回退到客户端直传
await uploadFileWithFallback(nextItem);
nextItem.status = "success";
nextItem.statusText = "上传完成!";
nextItem.percent = 100;
} catch (error: any) {
console.error(`${nextItem.file.name} 上传失败:`, error);
nextItem.status = "error";
nextItem.error = true;
const errorMsg = error.response?.data?.msg || error.message || "未知错误";
nextItem.statusText = `上传失败: ${errorMsg}`;
nextItem.percent = 0;
}
}
// 检查是否有成功的上传
const hasSuccess = uploadQueue.value.some(item => item.status === "success");
if (hasSuccess) {
// 通知刷新
emit("success");

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +140
const handleUpload = async (options: UploadCustomRequestOptions) => {
const file = options.file.file;
if (!file) return;

// 检查文件是否已存在于队列中
if (isFileInQueue(file as File)) {
window.$message.warning(`${file.name} 已在上传队列中`);
return;
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n-upload 使用 custom-request 时通常需要在上传成功/失败后调用 options.onFinish / options.onError(以及可选的 onProgress),否则 Upload 组件内部状态可能一直处于 uploading,影响后续选择/事件触发。建议在队列任务完成时回调对应 handler(即使 show-file-list=false 也应同步状态)。

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants