mirror of https://github.com/requarks/wiki
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
241 lines
6.7 KiB
241 lines
6.7 KiB
import { Model } from 'objection'
|
|
import path from 'node:path'
|
|
import fse from 'fs-extra'
|
|
import { startsWith } from 'lodash-es'
|
|
import { generateHash } from '../helpers/common.mjs'
|
|
|
|
import { User } from './users.mjs'
|
|
|
|
/**
|
|
* Users model
|
|
*/
|
|
export class Asset extends Model {
|
|
static get tableName() { return 'assets' }
|
|
|
|
static get jsonSchema () {
|
|
return {
|
|
type: 'object',
|
|
|
|
properties: {
|
|
id: {type: 'string'},
|
|
filename: {type: 'string'},
|
|
hash: {type: 'string'},
|
|
ext: {type: 'string'},
|
|
kind: {type: 'string'},
|
|
mime: {type: 'string'},
|
|
fileSize: {type: 'integer'},
|
|
metadata: {type: 'object'},
|
|
createdAt: {type: 'string'},
|
|
updatedAt: {type: 'string'}
|
|
}
|
|
}
|
|
}
|
|
|
|
static get relationMappings() {
|
|
return {
|
|
author: {
|
|
relation: Model.BelongsToOneRelation,
|
|
modelClass: User,
|
|
join: {
|
|
from: 'assets.authorId',
|
|
to: 'users.id'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async $beforeUpdate(opt, context) {
|
|
await super.$beforeUpdate(opt, context)
|
|
|
|
this.updatedAt = new Date().toISOString()
|
|
}
|
|
async $beforeInsert(context) {
|
|
await super.$beforeInsert(context)
|
|
|
|
this.createdAt = new Date().toISOString()
|
|
this.updatedAt = new Date().toISOString()
|
|
}
|
|
|
|
async getAssetPath() {
|
|
let hierarchy = []
|
|
if (this.folderId) {
|
|
hierarchy = await WIKI.db.assetFolders.getHierarchy(this.folderId)
|
|
}
|
|
return (this.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${this.filename}` : this.filename
|
|
}
|
|
|
|
async deleteAssetCache() {
|
|
await fse.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${this.hash}.dat`))
|
|
}
|
|
|
|
static async upload(opts) {
|
|
const fileInfo = path.parse(opts.originalname)
|
|
|
|
// Check for existing asset
|
|
let asset = await WIKI.db.assets.query().where({
|
|
// hash: fileHash,
|
|
folderId: opts.folderId
|
|
}).first()
|
|
|
|
// Build Object
|
|
let assetRow = {
|
|
filename: opts.originalname,
|
|
ext: fileInfo.ext,
|
|
kind: startsWith(opts.mimetype, 'image/') ? 'image' : 'binary',
|
|
mime: opts.mimetype,
|
|
fileSize: opts.size,
|
|
folderId: opts.folderId
|
|
}
|
|
|
|
// Sanitize SVG contents
|
|
if (
|
|
WIKI.config.uploads.scanSVG &&
|
|
(
|
|
opts.mimetype.toLowerCase().startsWith('image/svg') ||
|
|
fileInfo.ext.toLowerCase() === '.svg'
|
|
)
|
|
) {
|
|
const svgSanitizeJob = await WIKI.scheduler.registerJob({
|
|
name: 'sanitize-svg',
|
|
immediate: true,
|
|
worker: true
|
|
}, opts.path)
|
|
await svgSanitizeJob.finished
|
|
}
|
|
|
|
// Save asset data
|
|
try {
|
|
const fileBuffer = await fse.readFile(opts.path)
|
|
|
|
if (asset) {
|
|
// Patch existing asset
|
|
if (opts.mode === 'upload') {
|
|
assetRow.authorId = opts.user.id
|
|
}
|
|
await WIKI.db.assets.query().patch(assetRow).findById(asset.id)
|
|
await WIKI.db.knex('assetData').where({
|
|
id: asset.id
|
|
}).update({
|
|
data: fileBuffer
|
|
})
|
|
} else {
|
|
// Create asset entry
|
|
assetRow.authorId = opts.user.id
|
|
asset = await WIKI.db.assets.query().insert(assetRow)
|
|
await WIKI.db.knex('assetData').insert({
|
|
id: asset.id,
|
|
data: fileBuffer
|
|
})
|
|
}
|
|
|
|
// Move temp upload to cache
|
|
// if (opts.mode === 'upload') {
|
|
// await fs.move(opts.path, path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`), { overwrite: true })
|
|
// } else {
|
|
// await fs.copy(opts.path, path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`), { overwrite: true })
|
|
// }
|
|
|
|
// Add to Storage
|
|
if (!opts.skipStorage) {
|
|
await WIKI.db.storage.assetEvent({
|
|
event: 'uploaded',
|
|
asset: {
|
|
...asset,
|
|
path: await asset.getAssetPath(),
|
|
data: fileBuffer,
|
|
authorId: opts.user.id,
|
|
authorName: opts.user.name,
|
|
authorEmail: opts.user.email
|
|
}
|
|
})
|
|
}
|
|
} catch (err) {
|
|
WIKI.logger.warn(err)
|
|
}
|
|
}
|
|
|
|
static async getThumbnail ({ id, path, locale, siteId }) {
|
|
return WIKI.db.tree.query()
|
|
.select('tree.*', 'assets.preview', 'assets.previewState')
|
|
.innerJoin('assets', 'tree.id', 'assets.id')
|
|
.where(id ? { 'tree.id': id } : {
|
|
'tree.hash': generateHash(path),
|
|
'tree.locale': locale,
|
|
'tree.siteId': siteId
|
|
})
|
|
.first()
|
|
}
|
|
|
|
static async getAsset({ pathArgs, siteId }, res) {
|
|
try {
|
|
const fileInfo = path.parse(pathArgs.path.toLowerCase())
|
|
const fileHash = generateHash(pathArgs.path)
|
|
const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${siteId}/${fileHash}.dat`)
|
|
|
|
// Force unsafe extensions to download
|
|
if (WIKI.config.security.forceAssetDownload && !['.png', '.apng', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg'].includes(fileInfo.ext)) {
|
|
res.set('Content-disposition', 'attachment; filename=' + encodeURIComponent(fileInfo.base))
|
|
}
|
|
|
|
if (await WIKI.db.assets.getAssetFromCache({ cachePath, extName: fileInfo.ext }, res)) {
|
|
return
|
|
}
|
|
// if (await WIKI.db.assets.getAssetFromStorage(assetPath, res)) {
|
|
// return
|
|
// }
|
|
await WIKI.db.assets.getAssetFromDb({ pathArgs, fileHash, cachePath, siteId }, res)
|
|
} catch (err) {
|
|
if (err.code === `ECONNABORTED` || err.code === `EPIPE`) {
|
|
return
|
|
}
|
|
WIKI.logger.error(err)
|
|
res.sendStatus(500)
|
|
}
|
|
}
|
|
|
|
static async getAssetFromCache({ cachePath, extName }, res) {
|
|
try {
|
|
await fse.access(cachePath, fse.constants.R_OK)
|
|
} catch (err) {
|
|
return false
|
|
}
|
|
res.type(extName)
|
|
await new Promise(resolve => res.sendFile(cachePath, { dotfiles: 'deny' }, resolve))
|
|
return true
|
|
}
|
|
|
|
static async getAssetFromStorage(assetPath, res) {
|
|
const localLocations = await WIKI.db.storage.getLocalLocations({
|
|
asset: {
|
|
path: assetPath
|
|
}
|
|
})
|
|
for (let location of localLocations.filter(location => Boolean(location.path))) {
|
|
const assetExists = await WIKI.db.assets.getAssetFromCache(assetPath, location.path, res)
|
|
if (assetExists) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
static async getAssetFromDb({ pathArgs, fileHash, cachePath, siteId }, res) {
|
|
const asset = await WIKI.db.knex('tree').where({
|
|
siteId,
|
|
hash: fileHash
|
|
}).first()
|
|
if (asset) {
|
|
const assetData = await WIKI.db.knex('assets').where('id', asset.id).first()
|
|
res.type(assetData.fileExt)
|
|
res.send(assetData.data)
|
|
await fse.outputFile(cachePath, assetData.data)
|
|
} else {
|
|
res.sendStatus(404)
|
|
}
|
|
}
|
|
|
|
static async flushTempUploads() {
|
|
return fse.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads`))
|
|
}
|
|
}
|