背景
写Markdown文档的时候,里面的图片怎么管理也是个比较麻烦的事情,目前来说,图片管理大概会有三种方式。
- 本地图片,markdown引用本地路径
- 优点:本地管理方便,图片保存,然后引用下路径就行
- 缺点:分享和管理很麻烦,需要图片拷贝来拷贝去的,很容易丢失。
- 图片转成Base64String,markdown应用字符串
- 优点:图片在任何地方都能显示,不需要任何额外成本
- 缺点:Base64String需要在线转换,并且字符串很长很大,markdown文档会变的非常臃肿
- 在线图床,图片传到互联网上,markdown引入url地址
- 优点:管理很方便,文档也很简洁,图片在任何地方都能显示
- 缺点:依赖互联网平台,比如Github,Gitee等在线图床,并且图片都是防盗链的,无法在其他平台使用,切换平台的时候很麻烦。
需求分析
最开始也是准备使用在线图床的,但是发现上传下载比较麻烦,使用还有限制,就放弃了。后来考虑到自己有OSS对象存储,用过OSS提供的客户端上传工具,使用起来还是很麻烦,每次都要手动上传,并且如果是截图需要先保存下来,然后再上传OSS,效率还是很低。后来就琢磨自己做一个图床管理工具,分析了自己的需求:
- 统一用OSS做存储,OSS提供黑白名单,跨域,防盗链等功能,安全性是可以保证的;
- 图片(包括截图)支持一键上传,只要复制下,按快捷键就可以上传;
- 也支持手动批量上传,支持上传历史查看和地址复用;
技术介绍
后台
后台主要实现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 ``;
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 ``;
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的,我也轻度使用过,感觉效果还是可以的。没有特殊需求,可以直接使用互联网上的产品的。