This repository has been archived on 2022-11-26. You can view files and clone it, but cannot push or open issues or pull requests.
IcyNet.eu/server/api/image.js

177 lines
4.3 KiB
JavaScript

import gm from 'gm'
import { URL } from 'url'
import path from 'path'
import crypto from 'crypto'
import { v4 as uuid } from 'uuid'
import { downloadURL } from '../../scripts/http'
const fs = require('fs-extra')
const gravatar = 'https://www.gravatar.com/avatar/'
const uploads = path.join(__dirname, '../../', 'usercontent')
const images = path.join(uploads, 'images')
const maxFileSize = 1000000
export const imageTypes = {
'image/png': '.png',
'image/jpg': '.jpg',
'image/jpeg': '.jpeg'
}
function decodeBase64Image (dataString) {
const matches = dataString.match(/^data:([A-Za-z-+/]+);base64,(.+)$/)
const response = {}
if (matches.length !== 3) {
return null
}
response.type = matches[1]
response.data = Buffer.from(matches[2], 'base64')
return response
}
function saneFields (fields) {
const out = {}
for (const i in fields) {
const entry = fields[i]
if (typeof entry === 'object' && entry.length === 1 && !isNaN(parseInt(entry[0]))) {
out[i] = parseInt(entry[0])
}
}
return out
}
async function bailOut (file, error) {
await fs.unlink(file)
throw new Error(error)
}
export async function imageBase64 (baseObj) {
if (!baseObj) return null
const imgData = decodeBase64Image(baseObj)
if (!imgData) return null
if (!imageTypes[imgData.type]) return null
let imageName = 'base64-' + uuid()
const ext = imageTypes[imgData.type] || '.png'
imageName += ext
const fpath = path.join(images, imageName)
try {
await fs.writeFile(fpath, imgData.data)
} catch (e) {
console.error(e)
return null
}
return { file: fpath }
}
export function gravatarURL (email) {
const sum = crypto.createHash('md5').update(email).digest('hex')
return gravatar + sum + '.jpg'
}
export async function downloadImage (imgUrl, designation) {
if (!imgUrl) return null
if (!designation) designation = 'download'
let imageName = designation + '-' + uuid()
const uridata = new URL(imgUrl)
const pathdata = path.parse(uridata.href)
imageName += pathdata.ext || '.png'
try {
await downloadURL(imgUrl, path.join(images, imageName))
} catch (e) {
return null
}
return imageName
}
export async function uploadImage (identifier, fields, files) {
if (!files.image) throw new Error('No image file')
let file = files.image[0]
if (file.size > maxFileSize) return bailOut(file.path, 'Image is too large! 1 MB max')
fields = saneFields(fields)
// Get file info, generate a file name
const fileHash = uuid()
const contentType = file.headers['content-type']
if (!contentType) return bailOut(file.path, 'Invalid or missing content-type header')
file = file.path
// Make sure content type is allowed
let match = false
for (const i in imageTypes) {
if (i === contentType) {
match = true
break
}
}
if (!match) return bailOut(file, 'Invalid image type. Only PNG, JPG and JPEG files are allowed.')
const extension = imageTypes[contentType]
const fileName = identifier + '-' + fileHash + extension
// Check for cropping
if (fields.x == null || fields.y == null || fields.width == null || fields.height == null) {
return bailOut(file, 'Images can only be cropped on the server side due to security reasons.')
}
if (fields.x < 0 || fields.y < 0 || fields.x > fields.width + fields.x || fields.y > fields.height + fields.y) {
return bailOut(file, 'Impossible crop.')
}
// Check 1 : 1 aspect ratio
if (Math.floor(fields.width / fields.height) !== 1) {
return bailOut(file, 'Avatars can only have an aspect ratio of 1:1')
}
// Upscaling is not allowed
if ((fields.scaleX != null && fields.scaleX > 1) || (fields.scaleY != null && fields.scaleY > 1)) {
return bailOut(file, 'Image upscaling is not allowed.')
}
if (fields.scaleX != null) {
fields.x *= fields.scaleX
fields.width *= fields.scaleX
}
if (fields.scaleY != null) {
fields.y *= fields.scaleY
fields.height *= fields.scaleY
}
// Crop
try {
await new Promise(function (resolve, reject) {
gm(file)
.crop(fields.width, fields.height, fields.x, fields.y)
.write(path.join(images, fileName), (err) => {
if (err) return reject(err)
resolve(fileName)
})
})
await fs.unlink(file)
} catch (e) {
console.error(e)
return bailOut(file, 'An error occured while cropping.')
}
return { file: fileName }
}