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.
wiki/server/models/assets.js

239 lines
6.7 KiB

/* global WIKI */
const Model = require('objection').Model
const moment = require('moment')
const path = require('path')
const fs = require('fs-extra')
const _ = require('lodash')
const assetHelper = require('../helpers/asset')
const Promise = require('bluebird')
/**
* Users model
*/
module.exports = class Asset extends Model {
static get tableName() { return 'assets' }
static get jsonSchema () {
return {
type: 'object',
properties: {
id: {type: 'integer'},
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: require('./users'),
join: {
from: 'assets.authorId',
to: 'users.id'
}
},
folder: {
relation: Model.BelongsToOneRelation,
modelClass: require('./assetFolders'),
join: {
from: 'assets.folderId',
to: 'assetFolders.id'
}
}
}
}
async $beforeUpdate(opt, context) {
await super.$beforeUpdate(opt, context)
this.updatedAt = moment.utc().toISOString()
}
async $beforeInsert(context) {
await super.$beforeInsert(context)
this.createdAt = moment.utc().toISOString()
this.updatedAt = moment.utc().toISOString()
}
async getAssetPath() {
let hierarchy = []
if (this.folderId) {
hierarchy = await WIKI.models.assetFolders.getHierarchy(this.folderId)
}
return (this.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${this.filename}` : this.filename
}
async deleteAssetCache() {
await fs.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${this.hash}.dat`))
}
static async upload(opts) {
const fileInfo = path.parse(opts.originalname)
const fileHash = assetHelper.generateHash(opts.assetPath)
// Check for existing asset
let asset = await WIKI.models.assets.query().where({
hash: fileHash,
folderId: opts.folderId
}).first()
// Build Object
let assetRow = {
filename: opts.originalname,
hash: fileHash,
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 fs.readFile(opts.path)
if (asset) {
// Patch existing asset
if (opts.mode === 'upload') {
assetRow.authorId = opts.user.id
}
await WIKI.models.assets.query().patch(assetRow).findById(asset.id)
await WIKI.models.knex('assetData').where({
id: asset.id
}).update({
data: fileBuffer
})
} else {
// Create asset entry
assetRow.authorId = opts.user.id
asset = await WIKI.models.assets.query().insert(assetRow)
await WIKI.models.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.models.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 getAsset(assetPath, res) {
try {
const fileInfo = assetHelper.getPathInfo(assetPath)
const fileHash = assetHelper.generateHash(assetPath)
const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`)
// Force unsafe extensions to download
if (WIKI.config.uploads.forceDownload && !['.png', '.apng', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg'].includes(fileInfo.ext)) {
res.set('Content-disposition', 'attachment; filename=' + encodeURIComponent(fileInfo.base))
}
if (await WIKI.models.assets.getAssetFromCache(assetPath, cachePath, res)) {
return
}
if (await WIKI.models.assets.getAssetFromStorage(assetPath, res)) {
return
}
await WIKI.models.assets.getAssetFromDb(assetPath, fileHash, cachePath, res)
} catch (err) {
if (err.code === `ECONNABORTED` || err.code === `EPIPE`) {
return
}
WIKI.logger.error(err)
res.sendStatus(500)
}
}
static async getAssetFromCache(assetPath, cachePath, res) {
try {
await fs.access(cachePath, fs.constants.R_OK)
} catch (err) {
return false
}
const sendFile = Promise.promisify(res.sendFile, {context: res})
res.type(path.extname(assetPath))
await sendFile(cachePath, { dotfiles: 'deny' })
return true
}
static async getAssetFromStorage(assetPath, res) {
const localLocations = await WIKI.models.storage.getLocalLocations({
asset: {
path: assetPath
}
})
for (let location of _.filter(localLocations, location => Boolean(location.path))) {
const assetExists = await WIKI.models.assets.getAssetFromCache(assetPath, location.path, res)
if (assetExists) {
return true
}
}
return false
}
static async getAssetFromDb(assetPath, fileHash, cachePath, res) {
const asset = await WIKI.models.assets.query().where('hash', fileHash).first()
if (asset) {
const assetData = await WIKI.models.knex('assetData').where('id', asset.id).first()
res.type(asset.ext)
res.send(assetData.data)
await fs.outputFile(cachePath, assetData.data)
} else {
res.sendStatus(404)
}
}
static async flushTempUploads() {
return fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads`))
}
}