使用nodejs将文件上传至七牛云

597 阅读1分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情

事件篇

之前关注的一个B站up主,由于视频总喜欢“开团”,导致视频经常失效,常常是点开B站消息,发现大大的“视频已失效”,因此,我用nodejs写了个定时任务,每半分钟跑一遍他的视频列表接口,一旦发现就用you-get下载下来(顺便存进数据库里):

// 部分代码,这个今天不是重点就不全部展示了
// 查新视频的方法
function getVideoInfo(mid, pn = 1, ps = 30) {
  return new Promise(resolve => {
    connectDB().then(async() => {
      var config = {
        method: 'get',
        url: `https://api.bilibili.com/x/space/arc/search?mid=${mid}&ps=${ps}&tid=0&pn=${pn}&keyword=&order=pubdate`,
        headers: {}
      };
    
      queryDocument(BILIBLI_DATABASE, VIDEO_COLLECTION, {mid: mid}).then(vlist => {
        axios(config)
          .then(function (response) {
            var newVlist = response.data.data.list.vlist.map(item => {
              return {
                mid: mid,
                title: item.title,
                description: item.description,
                pic: item.pic,
                aid: item.aid,
                bvid: item.bvid,
                length: item.length,
                created: new Date(item.created * 1000),
                cache: false, // 新加入的默认没缓存
              }
            });
            if (!newVlist.length) {
              logInfo(`${mid} 妹有视频`)
              closeDB();
              return;
            }
            var videoToWrite = [];//记录需要写入的新纪录
            newVlist.forEach(item => {
              // 根据时间判断记录是否重复
              if (!vlist.find(e => e.created.getTime() == item.created.getTime())) {
                videoToWrite.push(item);
              }
            })
            if (videoToWrite.length) {
              addManyDocument(BILIBLI_DATABASE,VIDEO_COLLECTION,videoToWrite).then(() => {
                closeDB();
                resolve();
              });
            }
            else {
              logInfo(`${mid} 无新视频`)
              closeDB();
            }
          })
          .catch(function (error) {
            logError(error);
          });
      }).catch(err => {
        logError({message:"**数据库又连不上了",err})
      })
    }).catch(err => {
      logError({message:"**数据库又连不上了",err})
    });
  })
}
// 使用you-get下载的方法
function downloadBilibiliVideo(bvid, timeStamp) {
  return new Promise(function (resolve, reject) {
    var cmd = `you-get -o "${DOWNLOAD_PATH}" -O ${bvid}-${timeStamp || new Date().getTime()} "https://www.bilibili.com/video/${bvid}" "--no-caption"`;
    exec(cmd, {
      maxBuffer: 1024 * 8000
    }, function (err, stdout, stderr) {
      if (err) {
        console.log(err);
        reject(err);
      } else if (stderr.length > 0) {
        reject(new Error(stderr.toString()));
      } else {
        resolve();
      }
    });
  });
};

我把这个程序跑在我的ECS上,那个幼小的、硬盘只有60G的ECS终于受不了这庞大的数据,吃不消了(那个up投稿数快500了):

image.png

瘦身势在必行!

解决篇

定期手动上传到百度网盘?不行:

  • 太笨了
  • 一不小心还给你整失效了
  • 太慢了,不仅是下载还有上传(没会员没人权)
  • 不支持外链,我特意建了数据库是为可以随时通过http访问的

既然要支持上面几个条件,那么解决这个问题的选择就挥之欲出了:

OSS对象存储

经过比较,我选了七牛云,支持cdn,也支持自定义域名,那么,接下来就是将缓存的文件自动上传到OSS了,这里用到了七牛云的nodejsAPI,文档在此

由于程序都运行在我自己后台的node服务中,不担心ak泄露的问题,因此我选择了服务器直传的方式(否则就只能采用服务端生成凭证,客户端使用凭证进行上传的流程):

image.png

首先,完成鉴权,使用从控制台中获取的accessKey和secretKey生成鉴权对象mac:

var accessKey = '';
var secretKey = '';
var mac = new qiniu.auth.digest.Mac(accessKey, secretKey);

之后,传入参数,一个最简单的参数就是存储桶名称scope:

var options = {
  scope: "bilibili_video",
};
var putPolicy = new qiniu.rs.PutPolicy(options);
var uploadToken = putPolicy.uploadToken(mac);

继续,传入配置,构建文件上传对象:

var config = new qiniu.conf.Config();
// 空间对应的机房,具体变量名和机房对应可以去文档找
config.zone = qiniu.zone.Zone_z3;
// 是否使用https域名
config.useHttpsDomain = true;
// 上传是否使用cdn加速
config.useCdnDomain = true;

var formUploader = new qiniu.form_up.FormUploader(config);
var putExtra = new qiniu.form_up.PutExtra();// 这个用来设置额外参数,比如mimeType之类的,类似于用form-data方式上传文件

最后,使用构建好的上传对象上传文件即可:

formUploader.putFile(uploadToken, key, filePath, putExtra, function (respErr,
  respBody, respInfo) {
  if (respErr) {
    throw respErr;
  }

  if (respInfo.statusCode == 200) {
    resolve(respBody)
    logInfo(respBody, "OSS")
  } else {
    // console.log(respInfo.statusCode);
    // console.log(respBody);
    reject();
    logError(respInfo.statusCode, "OSS")
    logError(respBody, "OSS")
  }
});

整个代码的全貌如下:

const qiniu = require("qiniu");
const { logInfo, logError } = require("./log");

const accessKey = '';
const secretKey = '';
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);


function uploadFile(filePath, key, bucket = "bilibili-video", rewrite = false) {
  return new Promise((resolve, reject) => {
    var options = {
      scope: `${bucket}${rewrite ? (":" + key) : ""}`,
    };
    var putPolicy = new qiniu.rs.PutPolicy(options);
    var uploadToken = putPolicy.uploadToken(mac);


    var config = new qiniu.conf.Config();
    // 空间对应的机房
    config.zone = qiniu.zone.Zone_z3;
    // 是否使用https域名
    config.useHttpsDomain = true;
    // 上传是否使用cdn加速
    config.useCdnDomain = true;

    var formUploader = new qiniu.form_up.FormUploader(config);
    var putExtra = new qiniu.form_up.PutExtra();
    // 文件上传
    formUploader.putFile(uploadToken, key, filePath, putExtra, function (respErr,
      respBody, respInfo) {
      if (respErr) {
        throw respErr;
      }

      if (respInfo.statusCode == 200) {
        resolve(respBody)
        logInfo(respBody, "OSS")
      } else {
        // console.log(respInfo.statusCode);
        // console.log(respBody);
        reject();
        logError(respInfo.statusCode, "OSS")
        logError(respBody, "OSS")
      }
    });
  });
}

module.exports = { uploadFile }

值得一提的是,要覆盖文件的时候,可以在scope参数中在原来存储桶名的结尾加上冒号:和文件的key,即可实现上传覆盖原文件。