assets.js 6.71 KB
Newer Older
1 2 3 4 5 6 7 8
/* 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')
9
const Promise = require('bluebird')
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68

/**
 * 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()
  }

Nick's avatar
Nick committed
69 70 71 72 73 74 75 76 77
  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() {
78
    await fs.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${this.hash}.dat`))
Nick's avatar
Nick committed
79 80
  }

81 82
  static async upload(opts) {
    const fileInfo = path.parse(opts.originalname)
Nick's avatar
Nick committed
83
    const fileHash = assetHelper.generateHash(opts.assetPath)
84

85 86 87 88 89 90 91 92
    // Check for existing asset
    let asset = await WIKI.models.assets.query().where({
      hash: fileHash,
      folderId: opts.folderId
    }).first()

    // Build Object
    let assetRow = {
93 94 95 96 97 98
      filename: opts.originalname,
      hash: fileHash,
      ext: fileInfo.ext,
      kind: _.startsWith(opts.mimetype, 'image/') ? 'image' : 'binary',
      mime: opts.mimetype,
      fileSize: opts.size,
99
      folderId: opts.folderId
100
    }
101

NGPixel's avatar
NGPixel committed
102
    // Sanitize SVG contents
103 104 105 106
    if (
      WIKI.config.uploads.scanSVG &&
      (
        opts.mimetype.toLowerCase().startsWith('image/svg') ||
107
        fileInfo.ext.toLowerCase() === '.svg'
108 109
      )
    ) {
NGPixel's avatar
NGPixel committed
110 111 112 113 114 115 116 117
      const svgSanitizeJob = await WIKI.scheduler.registerJob({
        name: 'sanitize-svg',
        immediate: true,
        worker: true
      }, opts.path)
      await svgSanitizeJob.finished
    }

118 119 120
    // Save asset data
    try {
      const fileBuffer = await fs.readFile(opts.path)
121 122 123

      if (asset) {
        // Patch existing asset
124 125 126
        if (opts.mode === 'upload') {
          assetRow.authorId = opts.user.id
        }
127 128 129 130 131 132 133 134
        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
135
        assetRow.authorId = opts.user.id
136 137 138 139 140 141
        asset = await WIKI.models.assets.query().insert(assetRow)
        await WIKI.models.knex('assetData').insert({
          id: asset.id,
          data: fileBuffer
        })
      }
142 143

      // Move temp upload to cache
144
      if (opts.mode === 'upload') {
145
        await fs.move(opts.path, path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`), { overwrite: true })
146
      } else {
147
        await fs.copy(opts.path, path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`), { overwrite: true })
148
      }
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163

      // 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
          }
        })
      }
164 165 166 167
    } catch (err) {
      WIKI.logger.warn(err)
    }
  }
168 169

  static async getAsset(assetPath, res) {
170
    try {
171
      const fileInfo = assetHelper.getPathInfo(assetPath)
172 173
      const fileHash = assetHelper.generateHash(assetPath)
      const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`)
174 175 176 177 178 179

      // 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=' + fileInfo.base)
      }

180 181 182 183 184 185 186 187 188 189 190 191 192
      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)
193 194 195
    }
  }

196 197 198 199 200 201 202 203 204 205
  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
206
  }
207

208 209 210
  static async getAssetFromStorage(assetPath, res) {
    const localLocations = await WIKI.models.storage.getLocalLocations({
      asset: {
211
        path: assetPath
212 213 214 215 216 217 218 219 220 221
      }
    })
    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
  }
222

223
  static async getAssetFromDb(assetPath, fileHash, cachePath, res) {
224 225 226 227 228 229 230 231 232 233
    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)
    }
  }
Nick's avatar
Nick committed
234 235

  static async flushTempUploads() {
236
    return fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads`))
Nick's avatar
Nick committed
237
  }
238
}