Skip to content
实现百度网盘上传文件和分享链接功能

需要前往百度开放平台申请key和token

js
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const axios = require('axios');
const FormData = require('form-data');

// 配置信息
const config = {
  accessToken: '121.6c4468c8f1c6f9c73bba9bd7e21e6f63.YHp2C6F7X5XfCRClRllUF-A9Q2HWK5idwgwkXTD.4n8CSQ',
  appKey: 'cT9I7SLq7m8wUhQjKfOdMccp3p6ysB8L'
};

// 配置分片大小,保持4MB不变
const BLOCK_SIZE = 4 * 1024 * 1024; // 4MB

// 计算文件MD5
function calculateMD5(filePath, start = 0, end = Infinity) {
  return new Promise((resolve, reject) => {
    const hash = crypto.createHash('md5');
    const stream = fs.createReadStream(filePath, { start, end });
    
    stream.on('data', (data) => {
      hash.update(data);
    });
    
    stream.on('end', () => {
      resolve(hash.digest('hex'));
    });
    
    stream.on('error', (error) => {
      reject(error);
    });
  });
}

// 计算文件所有分片的MD5
async function calculateBlockList(filePath) {
  const stats = fs.statSync(filePath);
  const fileSize = stats.size;
  const totalBlocks = Math.ceil(fileSize / BLOCK_SIZE);
  
  const blockList = [];
  
  for (let i = 0; i < totalBlocks; i++) {
    const start = i * BLOCK_SIZE;
    const end = Math.min(start + BLOCK_SIZE, fileSize);
    const md5 = await calculateMD5(filePath, start, end);
    blockList.push(md5);
  }
  
  return blockList;
}

// 预上传函数
async function precreate(filePath, remotePath) {
  try {
    // 获取文件信息
    const stats = fs.statSync(filePath);
    const fileSize = stats.size;
    
    // 计算所有分片的MD5
    console.log('计算文件分片MD5...');
    const blockList = await calculateBlockList(filePath);
    console.log('分片MD5列表:', blockList);
    
    // 预上传请求参数
    const precreateParams = {
      path: remotePath,
      size: fileSize,
      isdir: 0,
      block_list: JSON.stringify(blockList), // 转换为JSON字符串
      autoinit: 1,
      rtype: 1 // 覆盖同名文件
    };
    
    console.log('\n发送预上传请求...');
    console.log('请求参数:', precreateParams);
    
    // 发送预上传请求
    const response = await axios.post(
      'https://pan.baidu.com/rest/2.0/xpan/file?method=precreate&access_token=' + config.accessToken,
      precreateParams,
      { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } // 设置正确的Content-Type
    );
    
    console.log('\n预上传请求结果:');
    console.log('状态码:', response.status);
    console.log('响应数据:', response.data);
    
    return response.data;
  } catch (error) {
    console.error('预上传失败:', error);
    if (error.response) {
      console.error('响应数据:', error.response.data);
      console.error('响应状态:', error.response.status);
    }
    throw error;
  }
}

// 获取上传域名函数
async function getUploadDomain(path, uploadid) {
  try {
    const params = {
      method: 'locateupload',
      appid: 250528, // 文档中指定的固定值
      access_token: config.accessToken,
      path: path,
      uploadid: uploadid,
      upload_version: '2.0' // 文档中指定的固定值
    };
    
    console.log('\n发送获取上传域名请求...');
    console.log('请求参数:', params);
    
    const response = await axios.get('https://d.pcs.baidu.com/rest/2.0/pcs/file', { params });
    
    console.log('\n获取上传域名请求结果:');
    console.log('状态码:', response.status);
    console.log('响应数据:', response.data);
    
    // 优先使用https的服务器
    const httpsServers = response.data.servers.filter(server => server.server.startsWith('https://'));
    if (httpsServers.length > 0) {
      return httpsServers[0].server;
    }
    
    // 如果没有https服务器,使用第一个服务器
    if (response.data.servers.length > 0) {
      return response.data.servers[0].server;
    }
    
    throw new Error('未获取到有效的上传域名');
  } catch (error) {
    console.error('获取上传域名失败:', error);
    if (error.response) {
      console.error('响应数据:', error.response.data);
      console.error('响应状态:', error.response.status);
    }
    throw error;
  }
}

// 分片上传函数
  async function uploadChunk(filePath, uploadDomain, remotePath, uploadid, partseq) {
    try {
      const start = partseq * BLOCK_SIZE;
      const end = Math.min(start + BLOCK_SIZE, fs.statSync(filePath).size);
      
      // 使用与calculateMD5相同的方式读取分片内容
      // 这样可以确保读取的内容与预上传时完全一致,MD5值也会匹配
      const readChunk = () => {
        return new Promise((resolve, reject) => {
          const chunks = [];
          const stream = fs.createReadStream(filePath, { start, end });
          
          stream.on('data', (chunk) => {
            chunks.push(chunk);
          });
          
          stream.on('end', () => {
            resolve(Buffer.concat(chunks));
          });
          
          stream.on('error', (error) => {
            reject(error);
          });
        });
      };
      
      const chunkBuffer = await readChunk();
      
      // 打印实际分片大小,验证是否正确读取了分片
      console.log(`实际读取分片大小: ${chunkBuffer.length}字节`);
      
      // 验证当前分片的MD5是否与预上传时一致
      const chunkMD5 = crypto.createHash('md5').update(chunkBuffer).digest('hex');
      console.log(`当前分片计算的MD5: ${chunkMD5}`);
      
      // 准备表单数据
      const formData = new FormData();
      formData.append('file', chunkBuffer, { filename: 'chunk' + partseq });
      
      console.log(`
发送分片${partseq}上传请求...`);
      console.log('上传域名:', uploadDomain);
      console.log(`分片范围: ${start}-${end}字节`);
      console.log(`分片大小: ${chunkBuffer.length}字节`);
      
      // 发送分片上传请求,增加超时设置
      const response = await axios.post(
        `${uploadDomain}/rest/2.0/pcs/superfile2`,
        formData,
        {
          params: {
            access_token: config.accessToken,
            method: 'upload',
            type: 'tmpfile',
            path: remotePath,
            uploadid: uploadid,
            partseq: partseq
          },
          headers: formData.getHeaders(),
          timeout: 60000, // 增加到60秒超时
          maxContentLength: Infinity,
          maxBodyLength: Infinity
        }
      );
      
      console.log(`
分片${partseq}上传结果:`);
      console.log('状态码:', response.status);
      console.log('响应数据:', response.data);
      
      return response.data;
    } catch (error) {
      console.error(`分片${partseq}上传失败:`, error);
      if (error.response) {
        console.error('响应数据:', error.response.data);
        console.error('响应状态:', error.response.status);
      } else if (error.request) {
        console.error('请求已发送但未收到响应:', error.request);
      } else {
        console.error('请求配置错误:', error.message);
      }
      throw error;
    }
  }

// 创建文件函数
async function createFile(remotePath, fileSize, blockList, uploadid) {
  try {
    // 创建文件请求参数
    const createParams = {
      path: remotePath,
      size: fileSize,
      isdir: 0,
      block_list: JSON.stringify(blockList),
      uploadid: uploadid,
      rtype: 1 // 覆盖同名文件,与预上传保持一致
    };
    
    console.log('\n发送创建文件请求...');
    console.log('请求参数:', createParams);
    
    // 发送创建文件请求
    const response = await axios.post(
      'https://pan.baidu.com/rest/2.0/xpan/file?method=create&access_token=' + config.accessToken,
      createParams,
      {
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        timeout: 60000
      }
    );
    
    console.log('\n创建文件结果:');
    console.log('状态码:', response.status);
    console.log('响应数据:', response.data);
    
    return response.data;
  } catch (error) {
    console.error('创建文件失败:', error);
    if (error.response) {
      console.error('响应数据:', error.response.data);
      console.error('响应状态:', error.response.status);
    }
    throw error;
  }
}

// 主函数
async function main() {
  try {
    // 本地文件路径
    const localFilePath = path.join(__dirname, 'files', '1768820155275.txt');
    
    // 检查文件是否存在
    if (!fs.existsSync(localFilePath)) {
      console.error('文件不存在:', localFilePath);
      return;
    }
    
    // 获取文件信息
    const stats = fs.statSync(localFilePath);
    console.log('文件信息:');
    console.log('  路径:', localFilePath);
    console.log('  大小:', stats.size, '字节');
    console.log('  修改时间:', stats.mtime);
    
    // 远程文件路径 (apps/{appname}/filename)
    // 动态拿文件名
    const remoteFilePath = '/小说资源/' + path.basename(localFilePath);
    
    // 执行预上传
    const precreateResult = await precreate(localFilePath, remoteFilePath);
    
    // 如果预上传成功,获取上传域名
    if (precreateResult.errno === 0) {
      const uploadDomain = await getUploadDomain(remoteFilePath, precreateResult.uploadid);
      
      // 计算所有分片的MD5(用于创建文件时使用)
      const blockList = await calculateBlockList(localFilePath);
      
      // 执行分片上传
      const totalBlocks = Math.ceil(stats.size / BLOCK_SIZE);
      
      console.log(`\n开始分片上传,共${totalBlocks}个分片...`);
      
      // 保存所有分片的上传结果
      const uploadResults = [];
      
      for (let i = 0; i < totalBlocks; i++) {
        const result = await uploadChunk(localFilePath, uploadDomain, remoteFilePath, precreateResult.uploadid, i);
        uploadResults.push(result);
      }
      
      console.log('\n所有分片上传完成!');
      
      // 检查所有分片是否上传成功
      const allChunksUploaded = uploadResults.every(result => result && result.request_id);
      
      if (allChunksUploaded) {
        console.log('\n开始创建文件...');
        // 调用创建文件接口
        const createResult = await createFile(remoteFilePath, stats.size, blockList, precreateResult.uploadid);
        
        if (createResult.errno === 0) {
          console.log('\n🎉 文件上传完成!');
          console.log('文件信息:');
          console.log('  文件名:', createResult.server_filename);
          console.log('  文件大小:', createResult.size, '字节');
          console.log('  文件路径:', createResult.path);
          console.log('  文件ID:', createResult.fs_id);
          console.log('  文件MD5:', createResult.md5);
          
          // 调用创建分享链接函数
          console.log('\n开始创建分享链接...');
          await createShareLink(createResult.fs_id);
        } else {
          console.error('\n❌ 创建文件失败!');
        }
      } else {
        console.error('\n❌ 存在分片上传失败,无法创建文件!');
      }
    }
    
  } catch (error) {
    console.error('程序执行错误:', error);
  }
}

// 执行主程序
main();


// 生成随机四位数分享口令(数字+小写字母)
function generateSharePassword() {
  const chars = '0123456789abcdefghijklmnopqrstuvwxyz';
  let password = '';
  for (let i = 0; i < 4; i++) {
    password += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return password;
}

// 创建分享链接函数
async function createShareLink(fsid) {
  try {
    // 应用ID为121883393
    const appId = '121883393';
    // 生成随机四位数分享口令
    const pwd = generateSharePassword();
    // 分享有效期默认7天
    const period = 7;
    
    // 创建分享请求参数
    const shareParams = {
      fsid_list: JSON.stringify([fsid.toString()]), // 分享文件id列表,只包含当前上传的文件
      period: period,
      pwd: pwd,
      remark: '上传文件分享' // 可选备注
    };
  
    
    console.log('\n发送创建分享链接请求...');
    console.log('请求参数:', shareParams);
    
    // 发送创建分享链接请求
    const response = await axios.post(
      `https://pan.baidu.com/apaas/1.0/share/set?product=netdisk&appid=${appId}&access_token=${config.accessToken}`,
      shareParams,
      {
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        timeout: 60000
      }
    );
    
    console.log('\n创建分享链接结果:');
    console.log('状态码:', response.status);
    console.log('响应数据:', response.data);
    
    if (response.data.errno === 0) {
      console.log('\n🎉 分享链接创建成功!');
      console.log('分享信息:');
      console.log('  分享长链接:', response.data.data.link);
      console.log('  分享短链接:', response.data.data.short_url);
      console.log('  分享提取码:', response.data.data.pwd);
      console.log('  分享有效期:', response.data.data.period, '天');
      console.log('  分享ID:', response.data.data.share_id);
      
      return response.data.data;
    } else {
      console.error('\n❌ 创建分享链接失败!');
      console.error('错误信息:', response.data.show_msg || response.data);
      return null;
    }
  } catch (error) {
    console.error('创建分享链接失败:', error);
    if (error.response) {
      console.error('响应数据:', error.response.data);
      console.error('响应状态:', error.response.status);
    }
    return null;
  }
}

package.json

json
{
  "name": "ceshi",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "axios": "^1.13.2",
    "form-data": "^4.0.5"
  }
}