效率提升-图床工具

81 阅读5分钟

背景

  写Markdown文档的时候,里面的图片怎么管理也是个比较麻烦的事情,目前来说,图片管理大概会有三种方式。

  • 本地图片,markdown引用本地路径
    • 优点:本地管理方便,图片保存,然后引用下路径就行
    • 缺点:分享和管理很麻烦,需要图片拷贝来拷贝去的,很容易丢失。
  • 图片转成Base64String,markdown应用字符串
    • 优点:图片在任何地方都能显示,不需要任何额外成本
    • 缺点:Base64String需要在线转换,并且字符串很长很大,markdown文档会变的非常臃肿
  • 在线图床,图片传到互联网上,markdown引入url地址
    • 优点:管理很方便,文档也很简洁,图片在任何地方都能显示
    • 缺点:依赖互联网平台,比如Github,Gitee等在线图床,并且图片都是防盗链的,无法在其他平台使用,切换平台的时候很麻烦。

需求分析

  最开始也是准备使用在线图床的,但是发现上传下载比较麻烦,使用还有限制,就放弃了。后来考虑到自己有OSS对象存储,用过OSS提供的客户端上传工具,使用起来还是很麻烦,每次都要手动上传,并且如果是截图需要先保存下来,然后再上传OSS,效率还是很低。后来就琢磨自己做一个图床管理工具,分析了自己的需求:

  1. 统一用OSS做存储,OSS提供黑白名单,跨域,防盗链等功能,安全性是可以保证的;
  2. 图片(包括截图)支持一键上传,只要复制下,按快捷键就可以上传;
  3. 也支持手动批量上传,支持上传历史查看和地址复用;

技术介绍

后台

  后台主要实现OSS秘钥管理,以及OSS存储

上传日志

OSS秘钥管理

秘钥一般云平台都会提供

//秘钥可以按照客户端存储
public OssEntity saveOss(OssEntity ossEntity){
    if(StringUtils.isEmpty(ossEntity.getId())){
        ossEntity.setId(UUID.randomUUID().toString());
        ossEntity.setCreateTime(new Date());
        ossDao.insertOss(ossEntity);
    }
    else{
        ossEntity.setUpdateTime(new Date());
        ossDao.updateOss(ossEntity);
    }
    return ossEntity;
}

OSS存储管理

图片一般都不是太大的,所以不需要分片上传,简单的上传功能就行。

/**
* 根据InputStream文件流上传
* 
* @param prefix 文件上传路径前缀
* @param name 文件名
* @param inputStream 要上传的文件流
*/
public static String upload(String prefix, String name, InputStream inputStream){
    name = prefix + "/" + name;
    OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
    PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, name, inputStream);
    ossClient.putObject(putObjectRequest);
    String url =   String.format("http://%s.%s/%s",BUCKET_NAME,ENDPOINT,name);
    logger.info("OSS文件上传成功!" +  url);
    ossClient.shutdown();
    return url;
}

前端

  前端主要实现几个功能,OSS秘钥编辑,图片上传,剪切板图片上传,快捷方式定义,图片地址自动保存到剪切板,图片上传历史管理等。

OSS秘钥编辑

//定义秘钥对象
const OSS = require('ali-oss');
export default function Client(data) {
    //后端提供数据
    return new OSS({
        region: data.endpoints,  //oss-cn-beijing-internal.aliyuncs.com
        accessKeyId: data.keyId,
        accessKeySecret: data.keySecret,
        bucket: data.bucket,
    })
}

//秘钥存储
import request from '../axios/request'
export function saveOss(data){
    return request({
        url: '/oss/saveOss',
        method: 'post',
        data: data
    })
}

图片上传

上传组件,支持拖拽上传,批量上传

<a-upload-dragger
    class="upload"
    name="file"
    accept="image/*"
    :multiple="true"
    :file-list="fileList"
    :remove="handleRemove"
    :beforeUpload="beforeUpload"
>
    <p class="ant-upload-drag-icon">
    <a-icon type="inbox"/>
    </p>
    <p class="ant-upload-text">
    点击或者拖拽上传文件
    </p>
    <p class="ant-upload-hint">
    支持图片文件上传
    </p>
</a-upload-dragger>

OSS信息存储管理

因为后续要支持直接用快捷方式上传图片或剪切板,所以这里加载OSS相关信息的时候,都会直接存储到主进程的缓存里。

//后续支持快捷方式直接上传图片,需要在主进程里面操作,所以这里每次获取到OSS信息都会同步到主线程的缓存里
getOss("").then(data => {
    if (data) {
        this.ossObject = data;
    }
    //判断有没有urlType,如果有,则继续
    const urlTypeData = this.$ipcRenderer.sendSync("getLocalData",{
        key: "urlType"
    });
    if(JSON.stringify(urlTypeData)==="{}"){
        this.saveUrlType();
    }
    else{
        this.urlType =urlTypeData;
    }
    //存储到主线程缓存下
    this.saveOssObject();
})

saveOssObject(){
    this.$ipcRenderer.send("saveLocalData",{
    key: "ossObject",
    data: this.ossObject,
    })
},
//store.js
//OSS缓存管理,会直接存储到JSON里,比较方便,也不需要太复杂的场景
const fs = require("fs");
const path = require("path")
const logger = require("./log")
let file_path = ""
let file_upload_path =""
let file_math_path = ""
let file_init_path = ""
let file_shortcut_path = ""
let file_express_path=""
function initPath(__static){
    file_path = path.join(__static, "json/assassin.json")
    file_upload_path = path.join(__static, "json/upload.json")
    file_math_path = path.join(__static, "json/math.json")
    file_init_path = path.join(__static, "json/data.json")
    file_shortcut_path = path.join(__static, "json/shortcut.json")
    file_express_path = path.join(__static, "json/express.json")
}
//...........

//基础配置信息
function updateData(key,data){
    const json = getAssassinJson();
    json[key]= data;
    fs.writeFileSync(file_path, JSON.stringify(json));
}
function getData(key){
    const json = getAssassinJson();
    return json[key];
}

export {
    updateData,
    getData,
    addUploadLog,
    getUploadLog,
    addMathData,
    getMathData,
    initPath,
    updateInitData,
    updateShortCutData,
    getShortCutData,
    updateExrepssData,
    getExpressData,
    deleteExressData
}

图片上传

//上传前文件校验
beforeUpload(file) {
    const tempFile = this.fileList.filter(p => p.name === file.name);
    if (tempFile.length === 0) {
    this.fileList = [...this.fileList, file];
    } else {
    this.$message.error(`当前文件${file.name}已添加`)
    }
    return false;
},
//图片上传
handleUpload(tempFileList) {
    let fileList;
    if (tempFileList && tempFileList.length > 0) {
        fileList = tempFileList;
    } else {
        fileList = this.fileList;
    }
    const formData = new FormData();
    fileList.forEach(file => {
        formData.append('files[]', file);
    });
    const client = new Client(this.ossObject);
    this.spinning = true;
    const promiseList = [];
    const that = this;
    for (let i = 0; i < fileList.length; i++) {
        let promise = new Promise((resolve, reject) => {
            that.uploadOss(client, fileList[i].name, fileList[i], resolve, reject)
        });
        promiseList.push(promise);
    }
    const urlList = []
    //图片批量上传,这里采用Promise.all 并发多个请求的方式,能提升效率
    Promise.all(promiseList).then(resList => {
        const tempUrl = [];
        for (let i = 0; i < resList.length; i++) {
            if (resList[i].status === 200) {
                let url = resList[i].requestUrls[0];
                tempUrl.push(url);
                // url = url.substring(0, url.lastIndexOf("?"));
                urlList.push(this.formatUrl(url))
            }
        }
        if (urlList.length > 0) {
            const newUrlList = tempUrl.map(p=>{
                return {
                    id:  uuidv4(),
                    name: tempFileList && tempFileList.length > 0?"截图": decodeURIComponent(p.substring(p.lastIndexOf("/")+1)),
                    oUrl: p,
                    url: this.formatUrl(p),
                    time: moment(Date.now()).format('YYYY-MM-DD HH:mm:ss')
                }
            })
            //图片地址复制到剪切板-->右下角动态提醒-->上传历史记录缓存
            this.$ipcRenderer.send("ossUrl", newUrlList);
        }
        this.fileList = [];
        this.spinning = false;
    }).catch((() => {
        this.spinning = false;
    }))

},
uploadOss(client, name, file, resolve, reject) {
    //这里直接用initMultipartUpload上传
    if (file.path) {
    client.put(this.ossObject.path + "/" + name, file.path).then(({res}) => {
        resolve(res)
    }).catch(error => {
        reject(error);
    });
    } else {
    client.put(this.ossObject.path + "/" + uuidv4() + ".png", file).then(({res}) => {
        resolve(res)
    }).catch(error => {
        reject(error);
    });
    }
}
//支持三种图片格式,markdown,image,以及纯粹的url格式
formatUrl(url) {
    switch (this.urlType) {
    case "markdown":
        return `![](${url})`;
    case "html":
        return `<img src="${url}"  alt=""/>`
    default:
        return url;
    }
},
//主线程事件
ipcMain.on("ossUrl", (evt,data) => {
    writeOssUrlToBoard(data);
})
function writeOssUrlToBoard(data){
    log.info("开始写入剪切板了");
    //写入剪切板,批量上传的话,只写入最后一个最新的记录
    clipboard.writeText(data[data.length-1].url)
    log.info("写入剪切板成功");
    //动态提醒
    notify.show({
        title: "OSS上传地址",
        message: data[data.length-1].url
    })
    //存储所有历史记录
    db.addUploadLog(data)
    log.info("通知成功")
}

剪切板图片上传

vue页面模式下

uploadCut(isShortCut) {
    const value = this.$ipcRenderer.sendSync("getCopyBoard");
    if (value === "NO") {
    if (isShortCut) {
        this.$ipcRenderer.send("notify", {
        title: "图片上传",
        message: "剪切板没有图片!"
        })
    } else {
        this.$message.error("当前剪切板没有图片!");
    }
    } else {
    let blob = this.dataURLtoBlob(value);
    const reader = new FileReader();
    reader.readAsArrayBuffer(blob);
    reader.onload = event => {
        const buffer = this.toBuffer(event.target.result);
        this.handleUpload([buffer]);
    };
    }
},
//剪切板图片转换成buffer上传到后台
dataURLtoBlob(dataurl) {
    let arr = dataurl.split(","),
        mime = arr[0].match(/:(.*?);/)[1],
        bstr = atob(arr[1]),
        n = bstr.length,
        u8arr = new Uint8Array(n);
    while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
    }
    return new Blob([u8arr], {type: mime});
},
toBuffer(ab) {
    const buf = new Buffer(ab.byteLength);
    const view = new Uint8Array(ab);
    for (let i = 0; i < buf.length; ++i) {
    buf[i] = view[i];
    }
    return buf;
},


//通过主线程获取剪切板信息,主线程本身也要通过快捷方式获取,处理方式保持一致
ipcMain.on("getCopyBoard", (evt, data) =>{
    evt.returnValue = getCopyText();
})
function getCopyText(){
    let img = clipboard.readImage();
    let dataUrl = img.toDataURL();
    clipboard.clear()
    if(dataUrl==="data:image/png;base64,"){
        return "NO";
    }
    else{
        return dataUrl
    }
}

Nodejs模式下

注册快捷方式


globalShortcut.register(shortcut, function () {
    log.info("开始记录了")
    pasterToOss();
})
log.info("注册上传图床快捷方式成功!")


function pasterToOss(){
    const value = getCopyText();
    if (value === "NO") {
        notify.show({
            title: "图片上传",
            message: "剪切板没有图片!"
        });
    } else {
        let urlType = db.getData("urlType");
        if(!urlType){
            urlType = "markdown";
        }
        let ossObject = db.getData("ossObject")
        //如果还没有配置,就给个默认值
        if(!ossObject){
            ossObject = {
                id: "",
                keyId: "xxxx",
                keySecret: "xxxxx",
                user: this.$configInfo.user,
                bucket: "xxxxx",
                endpoints: "oss-cn-beijing",
                path: "xxxxxx"
            }
        }
        const base64Data = value.replace(/^data:image\/\w+;base64,/, "");
        const dataBuffer = new Buffer(base64Data, 'base64'); // 解码图片
        oss.uploadFile(urlType,ossObject,[dataBuffer],function (data){
            const formatData = data.map(p=>{
                return {
                    id:  UuidV4(), //普通的UUID,区分每一次上传
                    name: "截图",
                    oUrl: p,
                    url: oss.formatUrl(urlType,p),
                    time: moment(Date.now()).format('YYYY-MM-DD HH:mm:ss')
                }
            })
            writeOssUrlToBoard(formatData);
        });
    }
}

OSS工具类

本来这个和Vue的上传到OSS逻辑可以保持一致的,后来考虑一些细节和管理的问题,把他分开了。

import {v4 as uuidv4} from "uuid";
const OSS = require('ali-oss');
const uploadFile =(urlType, ossObject, fileList, callBack)=>{
    const client = new OSS({
        region: ossObject.endpoints,  //oss-cn-beijing-internal.aliyuncs.com
        accessKeyId: ossObject.keyId,
        accessKeySecret: ossObject.keySecret,
        bucket: ossObject.bucket,
    });
    const promiseList = [];
    for (let i = 0; i < fileList.length; i++) {
        let promise = new Promise((resolve, reject) => {
            uploadOss(ossObject,client, fileList[i].name, fileList[i], resolve, reject)
        });
        promiseList.push(promise);
    }
    const urlList = []
    Promise.all(promiseList).then(resList => {
        for (let i = 0; i < resList.length; i++) {
            if (resList[i].status === 200) {
                let url = resList[i].requestUrls[0];
                urlList.push(url)
            }
        }
        callBack(urlList);
    }).catch((error => {
        console.log(error)
    }))
}

const formatUrl =(urlType,url)=> {
    switch (urlType) {
        case "markdown":
            return `![](${url})`;
        case "html":
            return `<img src="${url}"  alt=""/>`
        default:
            return url;
    }
}
const dataURLtoBlob =(dataUrl)=> {
    let arr = dataUrl.split(","),
        mime = arr[0].match(/:(.*?);/)[1],
        bstr = Buffer.from(arr[1]).toString('base64'),
        n = bstr.length,
        u8arr = new Uint8Array(n);
    while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
    }
    return new Blob([u8arr], {type: mime});
}
// 将blob转换为file
// noinspection JSUnusedLocalSymbols
const blobToFile =(theBlob, fileName)=> {
    theBlob.lastModifiedDate = new Date();
    theBlob.name = fileName;
    return theBlob;
}
const toBuffer =(ab)=> {
    const buf = new Buffer(ab.byteLength);
    const view = new Uint8Array(ab);
    for (let i = 0; i < buf.length; ++i) {
        buf[i] = view[i];
    }
    return buf;
}
const uploadOss =(ossObject,client, name, file, resolve, reject)=> {
    //这里直接用initMultipartUpload上传
    if (file.path) {
        client.put(ossObject.path + "/" + name, file.path).then(({res}) => {
            resolve(res)
        }).catch(error => {
            reject(error);
        });
    } else {
        client.put(ossObject.path + "/" + uuidv4() + ".png", file).then(({res}) => {
            resolve(res)
        }).catch(error => {
            reject(error);
        });
    }
}

export {
    uploadFile,
    dataURLtoBlob,
    toBuffer,
    formatUrl
}

总结

  图床工具确实是个好工具,节省了我大量的时间。后来互联网上查了下类似的工具很多,,比如PicGo,它支持很多云平台的OSS的,我也轻度使用过,感觉效果还是可以的。没有特殊需求,可以直接使用互联网上的产品的。