diff --git a/src/main/constant.ts b/src/main/constant.ts index 58920c25..25da0614 100644 --- a/src/main/constant.ts +++ b/src/main/constant.ts @@ -23,6 +23,8 @@ export const USER_DATA = path.join(USER_DIR, './data') export const BIN_DIR = convertAppPath(path.join(__dirname, '../../bin')) export const RESOURCES_DIR = convertAppPath(path.join(__dirname, 'resources')) +export const CACHE_DIR = path.join(os.tmpdir(), APP_NAME) + export const BUILD_IN_STYLES = ['github.css'] export const PANDOC_REFERENCE_FILE = 'pandoc-reference.docx' diff --git a/src/main/server/plantuml.ts b/src/main/server/plantuml.ts index 1d387459..d5cf25be 100644 --- a/src/main/server/plantuml.ts +++ b/src/main/server/plantuml.ts @@ -1,11 +1,12 @@ import request from 'request' -import fs from 'fs' +import * as crypto from 'crypto' +import fs from 'fs-extra' import path from 'path' import pako from 'pako' import { PlantUmlPipe } from 'plantuml-pipe' import commandExists from 'command-exists' import config from '../config' -import { ASSETS_DIR, BIN_DIR } from '../constant' +import { ASSETS_DIR, BIN_DIR, CACHE_DIR } from '../constant' import { getAction } from '../action' function plantumlBase64 (base64: string) { @@ -14,9 +15,57 @@ function plantumlBase64 (base64: string) { return base64.split('').map(x => map[x] || '').join('') } -export default async function (data: string): Promise<{ content: any, type: string }> { - const code = pako.inflateRaw(Buffer.from(data, 'base64')) +function getCacheKey (api: string, type: string, data: string) { + return crypto.createHash('sha256').update(api + type + data).digest('hex') +} +async function gcCache (cacheDir: string) { + const files = await fs.readdir(cacheDir) + if (files.length < 4000) { + return + } + + const stats = await Promise.all(files.map(file => fs.stat(path.join(cacheDir, file)))) + stats.sort((a, b) => a.atimeMs - b.atimeMs) + + for (let i = 0; i < stats.length / 2; i++) { + await fs.remove(path.join(cacheDir, files[i])) + } +} + +async function getCacheData (key: string, gen: () => Promise) { + const cacheDir = path.join(CACHE_DIR, 'plantuml') + await fs.ensureDir(cacheDir) + + gcCache(cacheDir) + + const cacheFile = path.join(cacheDir, key) + if (await fs.pathExists(cacheFile)) { + const stat = await fs.stat(cacheFile) + if (stat.size) { + return fs.createReadStream(cacheFile) + } + } + + const data = await gen() + if (!data) { + throw new Error('No data') + } + + if (typeof data.pipe === 'function') { + await new Promise((resolve, reject) => { + data.on('end', resolve) + data.on('error', reject) + data.pipe(fs.createWriteStream(cacheFile)) + }) + return fs.createReadStream(cacheFile) + } else { + await fs.writeFile(cacheFile, data) + return fs.createReadStream(cacheFile) + } +} + +export default async function (data: string): Promise<{ content: any, type: string }> { const api: string = config.get('plantuml-api', 'local') if (api.startsWith('local')) { @@ -28,30 +77,44 @@ export default async function (data: string): Promise<{ content: any, type: stri const format = api.split('-')[1] || 'png' const type = format === 'png' ? 'image/png' : 'image/svg+xml' - const jarPath = path.join(BIN_DIR, 'plantuml.jar') - const puml = new PlantUmlPipe({ - split: format === 'svg', - outputFormat: format as 'png' | 'svg', - plantUmlArgs: ['-charset', 'UTF-8'], - jarPath, + const cacheKey = getCacheKey(api, type, data) + const content = await getCacheData(cacheKey, async () => { + const jarPath = path.join(BIN_DIR, 'plantuml.jar') + + const puml = new PlantUmlPipe({ + split: format === 'svg', + outputFormat: format as 'png' | 'svg', + plantUmlArgs: ['-charset', 'UTF-8'], + jarPath, + }) + + puml.in.write(pako.inflateRaw(Buffer.from(data, 'base64'))) + puml.in.end() + + return puml.out }) - puml.in.write(code) - puml.in.end() - - return { content: puml.out, type } + return { content, type } } else { const url = api.replace('{data}', plantumlBase64(data)) const agent = await getAction('get-proxy-agent')(url) - return new Promise((resolve, reject) => { - request({ agent, url, encoding: null }, function (err: any, res: any) { - if (err) { - reject(err) - } else { - resolve({ content: res.body, type: res.headers['content-type'] }) - } + let type = '' + + const cacheKey = getCacheKey(api, type, data) + const content = await getCacheData(cacheKey, async () => { + return new Promise((resolve, reject) => { + request({ agent, url, encoding: null }, function (err: any, res: any) { + if (err) { + reject(err) + } else { + type = res.headers['content-type'] + resolve(res.body) + } + }) }) }) + + return { content, type } } }