diff --git a/backend/public/uploads/.gitkeep b/backend/public/uploads/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/resolvers/fileUpload/index.js b/backend/src/resolvers/fileUpload/index.js new file mode 100644 index 000000000..65a48a34d --- /dev/null +++ b/backend/src/resolvers/fileUpload/index.js @@ -0,0 +1,27 @@ +import { createWriteStream } from 'fs' +import path from 'path' +import slug from 'slug' + +const storeUpload = ({ createReadStream, fileLocation}) => + new Promise((resolve, reject) => + createReadStream() + .pipe(createWriteStream(`public${fileLocation}`)) + .on("finish", resolve) + .on("error", reject) + ); + +export default async function fileUpload(params, { file, url}, uploadCallback = storeUpload) { + const upload = params[file] + + if (upload) { + const { createReadStream, filename } = await upload; + const { name } = path.parse(filename) + const fileLocation = `/uploads/${Date.now()}-${slug(name)}` + await uploadCallback({ createReadStream, fileLocation }); + delete params[file] + + params[url] = fileLocation + } + + return params +} diff --git a/backend/src/resolvers/fileUpload/spec.js b/backend/src/resolvers/fileUpload/spec.js new file mode 100644 index 000000000..93e98adfc --- /dev/null +++ b/backend/src/resolvers/fileUpload/spec.js @@ -0,0 +1,56 @@ +import fileUpload from '.' + +describe('fileUpload', () => { + let params + let uploadCallback + + beforeEach(() => { + params = { + uploadAttribute: { + filename: 'avatar.jpg', + mimetype: 'image/jpeg', + encoding: '7bit', + createReadStream: jest.fn() + } + } + uploadCallback = jest.fn() + }) + + it('calls uploadCallback', async () => { + await fileUpload(params, { file: 'uploadAttribute', url: 'attribute'}, uploadCallback) + expect(uploadCallback).toHaveBeenCalled() + }) + + describe('file name', () => { + it('saves the upload url in params[url]', async () => { + await fileUpload(params, { file: 'uploadAttribute', url: 'attribute'}, uploadCallback) + expect(params.attribute).toMatch(/^\/uploads\/\d+-avatar$/) + }) + + it('uses the name without file ending', async () => { + params.uploadAttribute.filename = 'somePng.png' + await fileUpload(params, { file: 'uploadAttribute', url: 'attribute'}, uploadCallback) + expect(params.attribute).toMatch(/^\/uploads\/\d+-somePng/) + }) + + it('creates a url safe name', async () => { + params.uploadAttribute.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg?foo- bar' + await fileUpload(params, { file: 'uploadAttribute', url: 'attribute'}, uploadCallback) + expect(params.attribute).toMatch(/^\/uploads\/\d+-foo-bar-avatar$/) + }) + + describe('in case of duplicates', () => { + it('creates unique names to avoid overwriting existing files', async () => { + const { attribute: first } = await fileUpload({ + ...params + }, { file: 'uploadAttribute', url: 'attribute'}, uploadCallback) + + await new Promise(resolve => setTimeout(resolve, 1000)); + const { attribute: second } = await fileUpload({ + ...params + }, { file: 'uploadAttribute', url: 'attribute'}, uploadCallback) + expect(first).not.toEqual(second) + }) + }) + }) +}) diff --git a/backend/src/resolvers/users.js b/backend/src/resolvers/users.js index 33ba8c36b..838f45138 100644 --- a/backend/src/resolvers/users.js +++ b/backend/src/resolvers/users.js @@ -1,29 +1,15 @@ import { neo4jgraphql } from 'neo4j-graphql-js' -import { createWriteStream } from 'fs' - - -const storeUpload = ({ stream, fileLocation}) => - new Promise((resolve, reject) => - stream - .pipe(createWriteStream(`public${fileLocation}`)) - .on("finish", () => resolve()) - .on("error", reject) - ); +import fileUpload from './fileUpload' export default { Mutation: { UpdateUser: async (object, params, context, resolveInfo) => { - const { avatarUpload } = params - - if (avatarUpload) { - const { stream, filename } = await avatarUpload ; - const fileLocation = `/uploads/${filename}` - await storeUpload({ stream, fileLocation }); - delete params.avatarUpload - - params.avatar = fileLocation - } + params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar'}) + return await neo4jgraphql(object, params, context, resolveInfo, false) + }, + CreateUser: async (object, params, context, resolveInfo) => { + params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar'}) return await neo4jgraphql(object, params, context, resolveInfo, false) } - }, + } };