Node实现github图床向阿里云Oss的自动搬运

javascript/jquery

浏览数:9

2020-5-23

AD:资源代下载服务

原油

最近原油下跌不少,疫情不断,股市惊慌失措。于我而言,管我屁事,没钱。奋(pin)斗(qiong)的我,只有好好写代码。偏题了,是想说缘由来着,最近越来越觉得github慢,各种pull,push卡顿,家里打开github网站也巨慢,博客文章打开,各种图裂;现在不止用vscode写代码,还用来写文章,写笔记。所以努力给自己打造一个舒适的写作工具,非常重要。以前都用segmentfault, github来做自己的图片云,可惜免费的始终是有代价的。一直用github issue写文章的我,终于忍不住了,我要换图床。机智的我,去年有活动时,不止买了个3年229的服务器,还话45元买了一个Oss仓库。

vscode 本地图床插件:Picgo

以前都是去github上传了文件,再把地址拷到vscode的文件里,最近想自己写个插件,实现vscode直接上传图片到Oss。一百度,发现自己确实挺落伍,发现个插件Picgo, 我用的阿里云Oss,贴上我个人的vscode配置:

标红的选项很重要。配置完后,截个屏,cmd + option + u 就可以马上感受一下在vscode插图的快感了。

历史文章的图片处理

无法忍受github图片随时图裂,打开缓慢。既然已经有了Oss,那就全部替换了吧,但那么多文章(40+)和笔记(60+),一篇一篇copy,那cmd键估计都按白了吧。懒惰的我,肯定不会,肯定不会用这么土的办法。所以机智的我,基于NodeJs写了一个小工具。

技术栈(Node):fs + http + Oss-Sdk

原理:

  1. 遍历文章源文件,读取图片地址, 并做标记;
  2. 下载图片,并上传到Oss,获取新的图片地址;
  3. 用新的图片地址更新标记位置;
  4. 更新文件;

代码(涉及到较多正则匹配和骚操作,看不懂的请忽略):

const path = require('path');
const fs = require('fs');
const util = require('util');
const https = require("https");
const http = require("http");
const stream = require('stream');
const Oss = require('./oss');

// NOTE: 方案测试通过;
const isExists = util.promisify(fs.exists);
const readFile = util.promisify(fs.readFile); 
const writeFile = util.promisify(fs.writeFile); 

const reg = /\!\[[\s\S]{3,20}\]\((http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?\)/g;

const urlReg = /(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/;

const typeReg = /http.*(?=\.[jpg,png,jpeg,gif])/;
const HasReg = /http.*\.(jpg|png|jpeg|gif)$/;

const httpReg = /^http:.+/;
const BUcket_Dir = 'article';

function getRadomName() {
  const randomCode = Math.floor(Math.random() * 26) + 65;
  return `${Date.now()}-${String.fromCharCode(randomCode)}`;
}

const config = {
  accessKeyId: 'your id',
  accessKeySecret: 'your secret'
};

const oss = new Oss()

function httpGet(url, enable, check) {
  return new Promise((resolve,reject) => {
    const method = httpReg.test(url) ? http : https;
    method.get(url,
      (res) => {
      const chunks = [];
      let size = 0;
      enable && console.log('start');

      if (check) {
        if (res.statusCode != 200) {
          resolve('none');
        } else {
          resolve(url);
        }
        return;
      }

      if (res.statusCode == 301) {
        resolve('move');
      }
      res.on('data', (data) => {
        // 收集获取到的文件流
        // console.log('trans', data.length);
        chunks.push(data);
        size += data.length;
      });
      res.on('end', () => {
        // 文件流拼接获取buffer
        enable && console.log('end', size);
        resolve(Buffer.concat(chunks, size));
      });
    })
  }).catch((error) => {
    console.error(error);
  });
}
async function getImage({ url, dir }) {
  // 判断url 是否带有图片类型标识, 然后生成新的图片地址
  let target;
  if (url.indexOf('user-images.githubusercontent.com') > 1) {
    url = url.replace('https://user-images.githubusercontent.com', 'http://github-production-user-asset-6210df.s3.amazonaws.com');
    const arr = url.split('/');
    target = `${dir}${arr[arr.length-1]}`;
    const ossUrl = `https://doddle.oss-cn-beijing.aliyuncs.com/${target}`;
    // 检查文件是否已上传过
    const checkExsit = await httpGet(ossUrl, true, true);
    if (checkExsit === ossUrl) {
      return ossUrl;
    }
  } else {
    target = HasReg.test(url) ?
    url.replace(typeReg, `${dir}${getRadomName()}`) :
    `${dir}${getRadomName()}.png`;
  }
  // 获取buffer
  const buffer = await httpGet(url, true);
  // 发生错误,原地址直接返回,不做替换;
  if (!buffer) {
    console.log('error happen');
    return url;
  }
  // 如果响应301,则原地址直接返回
  if (buffer === 'move') {
    return url;
  }
  // 生成临时文件流, 并将Buffer写入流中
  const fileSream = new stream.PassThrough();
  fileSream.end(buffer);

  // 上传,并获取存储的url
  const result = await oss.uploadStream(target, fileSream);
  return result.url;
}

async function replaceUrl(file, name) {
  const dir = `${BUcket_Dir}/${name ? name : file.replace('.md', '')}/`;
  const filePath = path.resolve(__dirname, '../arcticle/', file);

  const isExist = await isExists(filePath);

  // oss 只初始化一次;
  if (!oss.init) {
    oss.create(config);
  }

  // 判断目标文件是否存在;
  if(!isExist) {
    console.log('file:', file, 'is not exist');
    return;
  }

  // 读取文件内容
  const content = await readFile(filePath,'utf8');

  let count = 0;
  const queue = [];
  // 内容替换;
  const con = content.replace(reg, (str) => {
    // oss 北京的,就不用替换了
    if (str.indexOf('doddle.oss-cn-beijing') > 0) {
      return str;
    }
    const url = str.slice(str.indexOf('(') + 1, str.length - 1);
    // 占位符
    const address = `url-${count}-end`;
    queue.push({ url, dir });
    // console.log('new', newUrl);
    count++;
    // 先用站位符替换目标Url,后面再进行下一步;
    return str.replace(urlReg, address);
  });

  // 根据获取原始url,获取对应的Oss url
  const res = await Promise.all(queue.map((param) => getImage(param)));
  // console.log('count', count, res);

  count = 0;
  // 根据响应的url数组,替换站位符号
  const final = con.replace(/url-[\d]+-end/g, function() {
    return res[count++];
  });

  // 反写文件
  await writeFile(filePath, final, {
    encoding: 'utf-8'
  });
  console.log('finish the file:', file);
}

fs.readdir('./arcticle', (err, files) => {
  // console.log('file', files.length);
  files.forEach((file) => {
    // console.log('file', file);
    replaceUrl(file, 'shim');
  });
})

原理其实很简单,实现也没花多长时间,但还是因为github在国内被墙耽误了不少时间,但我的情况你并不一定遇到,比如:

但机智的我,把https链接换成http,发现图片也能访问,但用Node 发起http请求时,发现响应301,再一看浏览器,发现确实被重定向了。所以就有了下面这段代码:

if (url.indexOf('user-images.githubusercontent.com') > 1) {
    url = url.replace('https://user-images.githubusercontent.com', 'http://github-production-user-asset-6210df.s3.amazonaws.com');
}

上面代码注释给的很详细了,看不懂可以留言。

附Oss封装代码:

const OSS = require('ali-oss');

module.exports =  class MyOss {
  constructor() {
    this.client = null;
    this.upload = this.upload.bind(this);
    this.uploadStream = this.uploadStream.bind(this);
    this.getList = this.getList.bind(this);
    this.init = false;
  }

  // 初始化Client
  create({ accessKeyId, accessKeySecret }) {
    this.client = new OSS({
      bucket: 'doddle',
      region: 'oss-cn-beijing',
      accessKeyId,
      accessKeySecret,
      secure: true, // 设置为true,返回的url才是https的
    });
    this.init = true;
  }

  async upload({ file, key, options = {} }) {
    try {
      const result = await this.client.put(key, file, options);
      return result;
    } catch (e) {
      console.log(e);
      return false;
    }
  }

  async uploadStream(name, file) {
    try {
      const result = await this.client.putStream(name, file);
      return result;
    } catch (e) {
      console.log(e);
      return false;
    }
  }

  async getList(dir = '') {
    try {
      const result = await this.client.list({
        prefix: dir
      });
      return result;
    } catch (e) {
      console.error(e);
      return false;
    }
  }
}

作者:Denzel