mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 19:41:39 +08:00

1) There is now a new preference page under images for "Maintaining server paths". You specify a server path (such as /images/) and a local path (such as c:\hugo\static\images), and Marktext will do the right mapping internally. Image previews will come from the images on your computer, while the content stored in the document will represent the corresponding server paths. Full documentation has been added to IMAGES.md. 2) Path variables (like ${filename}) have been expanded to support year, month, and day. This helps prevent filename collisions and is common in many blogging platforms. 3) Pasting images and dragging them into Marktext supports this path mapping as well, moving them to the local path as appropriate 4) Image imports (pasting and or dragging) now retain the source image file name if possible, only using the image hash if there is a conflict. Images with names are easier to manage and show up appropriately in search engines 4) The image edit dialog now has a 'Rename' tab so that you can rename the images to something more specific when they are just pasted from the clipboard. This simplifies the previous workflow that used to require editing the markdown and renaming the file in the filesystem manually. There is also now a 'User Notification Dialog' component to let Marktext communicate with the user about error conditions.
258 lines
7.4 KiB
JavaScript
258 lines
7.4 KiB
JavaScript
import path from 'path'
|
|
import crypto from 'crypto'
|
|
import fs from 'fs-extra'
|
|
import { statSync, constants } from 'fs'
|
|
import cp from 'child_process'
|
|
import { tmpdir } from 'os'
|
|
import dayjs from 'dayjs'
|
|
import { Octokit } from '@octokit/rest'
|
|
import { isImageFile } from 'common/filesystem/paths'
|
|
import { isWindows } from './index'
|
|
|
|
export const create = async (pathname, type) => {
|
|
return type === 'directory'
|
|
? fs.ensureDir(pathname)
|
|
: fs.outputFile(pathname, '')
|
|
}
|
|
|
|
export const paste = async ({ src, dest, type }) => {
|
|
return type === 'cut'
|
|
? fs.move(src, dest)
|
|
: fs.copy(src, dest)
|
|
}
|
|
|
|
export const rename = async (src, dest) => {
|
|
return fs.move(src, dest)
|
|
}
|
|
|
|
export const getHash = (content, encoding, type) => {
|
|
return crypto.createHash(type).update(content, encoding).digest('hex')
|
|
}
|
|
|
|
export const getContentHash = content => {
|
|
return getHash(content, 'utf8', 'sha1')
|
|
}
|
|
|
|
/**
|
|
* Moves an image to a relative position.
|
|
*
|
|
* @param {String} cwd The relative base path (project root or full folder path of opened file).
|
|
* @param {String} relativeName The relative directory name.
|
|
* @param {String} filePath The full path to the opened file in editor.
|
|
* @param {String} imagePath The image to move.
|
|
* @returns {String} The relative path the the image from given `filePath`.
|
|
*/
|
|
export const moveToRelativeFolder = async (cwd, relativeName, filePath, imagePath) => {
|
|
if (!relativeName) {
|
|
// Use fallback name according settings description
|
|
relativeName = 'assets'
|
|
} else if (path.isAbsolute(relativeName)) {
|
|
throw new Error('Invalid relative directory name.')
|
|
}
|
|
|
|
// Path combination:
|
|
// - markdown file directory + relative directory name or
|
|
// - root directory + relative directory name
|
|
const absPath = path.resolve(cwd, relativeName)
|
|
const dstPath = path.resolve(absPath, path.basename(imagePath))
|
|
await fs.ensureDir(absPath)
|
|
await fs.move(imagePath, dstPath, { overwrite: true })
|
|
|
|
// Find relative path between given file and saved image.
|
|
const dstRelPath = path.relative(path.dirname(filePath), dstPath)
|
|
|
|
if (isWindows) {
|
|
// Use forward slashes for better compatibility with websites.
|
|
return dstRelPath.replace(/\\/g, '/')
|
|
}
|
|
return dstRelPath
|
|
}
|
|
|
|
export const moveImageToFolder = async (pathname, image, outputDir) => {
|
|
await fs.ensureDir(outputDir)
|
|
const isPath = typeof image === 'string'
|
|
if (isPath) {
|
|
const dirname = path.dirname(pathname)
|
|
const imagePath = path.resolve(dirname, image)
|
|
const isImage = isImageFile(imagePath)
|
|
if (isImage) {
|
|
const filename = path.basename(imagePath)
|
|
const extname = path.extname(imagePath)
|
|
const noHashPath = path.join(outputDir, filename)
|
|
|
|
// If the file doesn't need to be moved, return it
|
|
if (noHashPath === imagePath) {
|
|
return imagePath
|
|
}
|
|
|
|
let targetPath = noHashPath
|
|
if (fs.existsSync(targetPath)) {
|
|
// If a file already exists in the target location, use the image's content hash to avoid
|
|
// name conflict.
|
|
const hash = getContentHash(imagePath)
|
|
targetPath = path.join(outputDir, `${hash}${extname}`)
|
|
}
|
|
|
|
await fs.copy(imagePath, targetPath)
|
|
return targetPath
|
|
} else {
|
|
return Promise.resolve(image)
|
|
}
|
|
} else {
|
|
const imagePath = path.join(outputDir, `${dayjs().format('YYYY-MM-DD-HH-mm-ss')}-${image.name}`)
|
|
const binaryString = await new Promise((resolve, reject) => {
|
|
const fileReader = new FileReader()
|
|
fileReader.onload = () => {
|
|
resolve(fileReader.result)
|
|
}
|
|
fileReader.readAsBinaryString(image)
|
|
})
|
|
await fs.writeFile(imagePath, binaryString, 'binary')
|
|
return imagePath
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @jocs todo, rewrite it use class
|
|
*/
|
|
export const uploadImage = async (pathname, image, preferences) => {
|
|
const { currentUploader, imageBed, githubToken: auth, cliScript } = preferences
|
|
const { owner, repo, branch } = imageBed.github
|
|
const isPath = typeof image === 'string'
|
|
const MAX_SIZE = 5 * 1024 * 1024
|
|
let re
|
|
let rj
|
|
const promise = new Promise((resolve, reject) => {
|
|
re = resolve
|
|
rj = reject
|
|
})
|
|
|
|
if (currentUploader === 'none') {
|
|
rj('No image uploader provided.')
|
|
}
|
|
|
|
const uploadByGithub = (content, filename) => {
|
|
const octokit = new Octokit({
|
|
auth
|
|
})
|
|
const path = dayjs().format('YYYY/MM') + `/${dayjs().format('DD-HH-mm-ss')}-${filename}`
|
|
const message = `Upload by MarkText at ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`
|
|
const payload = {
|
|
owner,
|
|
repo,
|
|
path,
|
|
branch,
|
|
message,
|
|
content
|
|
}
|
|
if (!branch) {
|
|
delete payload.branch
|
|
}
|
|
octokit.repos.createOrUpdateFileContents(payload)
|
|
.then(result => {
|
|
re(result.data.content.download_url)
|
|
})
|
|
.catch(_ => {
|
|
rj('Upload failed, the image will be copied to the image folder')
|
|
})
|
|
}
|
|
|
|
const uploadByCommand = async (uploader, filepath) => {
|
|
let isPath = true
|
|
if (typeof filepath !== 'string') {
|
|
isPath = false
|
|
const data = new Uint8Array(filepath)
|
|
filepath = path.join(tmpdir(), +new Date())
|
|
await fs.writeFile(filepath, data)
|
|
}
|
|
if (uploader === 'picgo') {
|
|
cp.exec(`picgo u "${filepath}"`, async (err, data) => {
|
|
if (!isPath) {
|
|
await fs.unlink(filepath)
|
|
}
|
|
if (err) {
|
|
return rj(err)
|
|
}
|
|
const parts = data.split('[PicGo SUCCESS]:')
|
|
if (parts.length === 2) {
|
|
re(parts[1].trim())
|
|
} else {
|
|
rj('PicGo upload error')
|
|
}
|
|
})
|
|
} else {
|
|
cp.execFile(cliScript, [filepath], async (err, data) => {
|
|
if (!isPath) {
|
|
await fs.unlink(filepath)
|
|
}
|
|
if (err) {
|
|
return rj(err)
|
|
}
|
|
re(data.trim())
|
|
})
|
|
}
|
|
}
|
|
|
|
const notification = () => {
|
|
rj('Cannot upload more than 5M image, the image will be copied to the image folder')
|
|
}
|
|
|
|
if (isPath) {
|
|
const dirname = path.dirname(pathname)
|
|
const imagePath = path.resolve(dirname, image)
|
|
const isImage = isImageFile(imagePath)
|
|
if (isImage) {
|
|
const { size } = await fs.stat(imagePath)
|
|
if (size > MAX_SIZE) {
|
|
notification()
|
|
} else {
|
|
switch (currentUploader) {
|
|
case 'cliScript':
|
|
case 'picgo':
|
|
uploadByCommand(currentUploader, imagePath)
|
|
break
|
|
case 'github': {
|
|
const imageFile = await fs.readFile(imagePath)
|
|
const base64 = Buffer.from(imageFile).toString('base64')
|
|
uploadByGithub(base64, path.basename(imagePath))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
re(image)
|
|
}
|
|
} else {
|
|
const { size } = image
|
|
if (size > MAX_SIZE) {
|
|
notification()
|
|
} else {
|
|
const reader = new FileReader()
|
|
reader.onload = async () => {
|
|
switch (currentUploader) {
|
|
case 'picgo':
|
|
case 'cliScript':
|
|
uploadByCommand(currentUploader, reader.result)
|
|
break
|
|
default:
|
|
uploadByGithub(reader.result, image.name)
|
|
}
|
|
}
|
|
|
|
const readerFunction = currentUploader !== 'github' ? 'readAsArrayBuffer' : 'readAsDataURL'
|
|
reader[readerFunction](image)
|
|
}
|
|
}
|
|
return promise
|
|
}
|
|
|
|
export const isFileExecutableSync = (filepath) => {
|
|
try {
|
|
const stat = statSync(filepath)
|
|
return stat.isFile() && (stat.mode & (constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH)) !== 0
|
|
} catch (err) {
|
|
// err ignored
|
|
return false
|
|
}
|
|
}
|