diff --git a/packages/core/upload/package.json b/packages/core/upload/package.json index c9b18d1a52..c375f41a82 100644 --- a/packages/core/upload/package.json +++ b/packages/core/upload/package.json @@ -40,7 +40,8 @@ "react-redux": "7.2.3", "react-router": "^5.2.0", "react-router-dom": "5.2.0", - "sharp": "0.30.1" + "sharp": "0.30.1", + "uuid": "8.3.2" }, "engines": { "node": ">=12.22.0 <=16.x.x", diff --git a/packages/core/upload/server/constants.js b/packages/core/upload/server/constants.js new file mode 100644 index 0000000000..55a949e0f2 --- /dev/null +++ b/packages/core/upload/server/constants.js @@ -0,0 +1,14 @@ +'use strict'; + +const ACTIONS = { + read: 'plugin::upload.read', + readSettings: 'plugin::upload.settings.read', + create: 'plugin::upload.assets.create', + update: 'plugin::upload.assets.update', + download: 'plugin::upload.assets.download', + copyLink: 'plugin::upload.assets.copy-link', +}; + +module.exports = { + ACTIONS, +}; diff --git a/packages/core/upload/server/content-types/file/schema.js b/packages/core/upload/server/content-types/file/schema.js index e27c6ab05a..79cddbbd0f 100644 --- a/packages/core/upload/server/content-types/file/schema.js +++ b/packages/core/upload/server/content-types/file/schema.js @@ -89,5 +89,16 @@ module.exports = { relation: 'morphToMany', configurable: false, }, + folder: { + type: 'relation', + relation: 'manyToOne', + target: 'plugin::upload.folder', + inversedBy: 'files', + }, + path: { + type: 'string', + min: 1, + required: true, + }, }, }; diff --git a/packages/core/upload/server/content-types/folder/index.js b/packages/core/upload/server/content-types/folder/index.js new file mode 100644 index 0000000000..fd1640d89b --- /dev/null +++ b/packages/core/upload/server/content-types/folder/index.js @@ -0,0 +1,7 @@ +'use strict'; + +const schema = require('./schema'); + +module.exports = { + schema, +}; diff --git a/packages/core/upload/server/content-types/folder/schema.js b/packages/core/upload/server/content-types/folder/schema.js new file mode 100644 index 0000000000..4d2feff37a --- /dev/null +++ b/packages/core/upload/server/content-types/folder/schema.js @@ -0,0 +1,54 @@ +'use strict'; + +module.exports = { + collectionName: 'folders', + info: { + singularName: 'folder', + pluralName: 'folders', + displayName: 'Folder', + }, + options: {}, + pluginOptions: { + 'content-manager': { + visible: false, + }, + 'content-type-builder': { + visible: false, + }, + }, + attributes: { + name: { + type: 'string', + min: 1, + required: true, + }, + uid: { + type: 'string', + unique: true, + required: true, + }, + parent: { + type: 'relation', + relation: 'manyToOne', + target: 'plugin::upload.folder', + inversedBy: 'children', + }, + children: { + type: 'relation', + relation: 'oneToMany', + target: 'plugin::upload.folder', + mappedBy: 'parent', + }, + files: { + type: 'relation', + relation: 'oneToMany', + target: 'plugin::upload.file', + mappedBy: 'folder', + }, + path: { + type: 'string', + min: 1, + required: true, + }, + }, +}; diff --git a/packages/core/upload/server/content-types/index.js b/packages/core/upload/server/content-types/index.js index 1ebbb67dca..84b915bb09 100644 --- a/packages/core/upload/server/content-types/index.js +++ b/packages/core/upload/server/content-types/index.js @@ -1,7 +1,9 @@ 'use strict'; const file = require('./file'); +const folder = require('./folder'); module.exports = { file, + folder, }; diff --git a/packages/core/upload/server/controllers/admin-api.js b/packages/core/upload/server/controllers/admin-api.js deleted file mode 100644 index 03c69df1f9..0000000000 --- a/packages/core/upload/server/controllers/admin-api.js +++ /dev/null @@ -1,207 +0,0 @@ -'use strict'; - -const _ = require('lodash'); -const { contentTypes: contentTypesUtils } = require('@strapi/utils'); -const { ApplicationError, NotFoundError, ForbiddenError } = require('@strapi/utils').errors; -const { getService } = require('../utils'); -const validateSettings = require('./validation/settings'); -const validateUploadBody = require('./validation/upload'); - -const { CREATED_BY_ATTRIBUTE } = contentTypesUtils.constants; - -const ACTIONS = { - read: 'plugin::upload.read', - readSettings: 'plugin::upload.settings.read', - create: 'plugin::upload.assets.create', - update: 'plugin::upload.assets.update', - download: 'plugin::upload.assets.download', - copyLink: 'plugin::upload.assets.copy-link', -}; - -const fileModel = 'plugin::upload.file'; - -module.exports = { - async find(ctx) { - const { - state: { userAbility }, - } = ctx; - - const pm = strapi.admin.services.permission.createPermissionsManager({ - ability: userAbility, - action: ACTIONS.read, - model: fileModel, - }); - - if (!pm.isAllowed) { - return ctx.forbidden(); - } - - const query = pm.addPermissionsQueryTo(ctx.query); - - const { results, pagination } = await getService('upload').findPage(query); - - const sanitizedResults = await pm.sanitizeOutput(results); - - return { results: sanitizedResults, pagination }; - }, - - async findOne(ctx) { - const { - state: { userAbility }, - params: { id }, - } = ctx; - - const { pm, file } = await findEntityAndCheckPermissions( - userAbility, - ACTIONS.read, - fileModel, - id - ); - - ctx.body = await pm.sanitizeOutput(file); - }, - - async destroy(ctx) { - const { id } = ctx.params; - const { userAbility } = ctx.state; - - const { pm, file } = await findEntityAndCheckPermissions( - userAbility, - ACTIONS.update, - fileModel, - id - ); - - await getService('upload').remove(file); - - ctx.body = await pm.sanitizeOutput(file, { action: ACTIONS.read }); - }, - - async updateSettings(ctx) { - const { - request: { body }, - state: { userAbility }, - } = ctx; - - if (userAbility.cannot(ACTIONS.readSettings, fileModel)) { - return ctx.forbidden(); - } - - const data = await validateSettings(body); - - await getService('upload').setSettings(data); - - ctx.body = { data }; - }, - - async getSettings(ctx) { - const { - state: { userAbility }, - } = ctx; - - if (userAbility.cannot(ACTIONS.readSettings, fileModel)) { - return ctx.forbidden(); - } - - const data = await getService('upload').getSettings(); - - ctx.body = { data }; - }, - - async updateFileInfo(ctx) { - const { - state: { userAbility, user }, - query: { id }, - request: { body }, - } = ctx; - - const uploadService = getService('upload'); - const { pm } = await findEntityAndCheckPermissions(userAbility, ACTIONS.update, fileModel, id); - - const data = await validateUploadBody(body); - const file = await uploadService.updateFileInfo(id, data.fileInfo, { user }); - - ctx.body = await pm.sanitizeOutput(file, { action: ACTIONS.read }); - }, - - async replaceFile(ctx) { - const { - state: { userAbility, user }, - query: { id }, - request: { body, files: { files } = {} }, - } = ctx; - - const uploadService = getService('upload'); - const { pm } = await findEntityAndCheckPermissions(userAbility, ACTIONS.update, fileModel, id); - - if (Array.isArray(files)) { - throw new ApplicationError('Cannot replace a file with multiple ones'); - } - - const data = await validateUploadBody(body); - const replacedFiles = await uploadService.replace(id, { data, file: files }, { user }); - - ctx.body = await pm.sanitizeOutput(replacedFiles, { action: ACTIONS.read }); - }, - - async uploadFiles(ctx) { - const { - state: { userAbility, user }, - request: { body, files: { files } = {} }, - } = ctx; - - const uploadService = getService('upload'); - const pm = strapi.admin.services.permission.createPermissionsManager({ - ability: userAbility, - action: ACTIONS.create, - model: fileModel, - }); - - if (!pm.isAllowed) { - return ctx.forbidden(); - } - - const data = await validateUploadBody(body); - const uploadedFiles = await uploadService.upload({ data, files }, { user }); - - ctx.body = await pm.sanitizeOutput(uploadedFiles, { action: ACTIONS.read }); - }, - - async upload(ctx) { - const { - query: { id }, - request: { files: { files } = {} }, - } = ctx; - - if (id && (_.isEmpty(files) || files.size === 0)) { - return this.updateFileInfo(ctx); - } - - if (_.isEmpty(files) || files.size === 0) { - throw new ApplicationError('Files are empty'); - } - - await (id ? this.replaceFile : this.uploadFiles)(ctx); - }, -}; - -const findEntityAndCheckPermissions = async (ability, action, model, id) => { - const file = await getService('upload').findOne(id, [CREATED_BY_ATTRIBUTE]); - - if (_.isNil(file)) { - throw new NotFoundError(); - } - - const pm = strapi.admin.services.permission.createPermissionsManager({ ability, action, model }); - - const creatorId = _.get(file, [CREATED_BY_ATTRIBUTE, 'id']); - const author = creatorId ? await strapi.admin.services.user.findOne(creatorId, ['roles']) : null; - - const fileWithRoles = _.set(_.cloneDeep(file), 'createdBy', author); - - if (pm.ability.cannot(pm.action, pm.toSubject(fileWithRoles))) { - throw new ForbiddenError(); - } - - return { pm, file }; -}; diff --git a/packages/core/upload/server/controllers/admin-file.js b/packages/core/upload/server/controllers/admin-file.js new file mode 100644 index 0000000000..cfa5b3505a --- /dev/null +++ b/packages/core/upload/server/controllers/admin-file.js @@ -0,0 +1,65 @@ +'use strict'; + +const { getService } = require('../utils'); +const { ACTIONS } = require('../constants'); +const findEntityAndCheckPermissions = require('./utils/find-entity-and-check-permissions'); + +const fileModel = 'plugin::upload.file'; + +module.exports = { + async find(ctx) { + const { + state: { userAbility }, + } = ctx; + + const pm = strapi.admin.services.permission.createPermissionsManager({ + ability: userAbility, + action: ACTIONS.read, + model: fileModel, + }); + + if (!pm.isAllowed) { + return ctx.forbidden(); + } + + const query = pm.addPermissionsQueryTo(ctx.query); + + const { results, pagination } = await getService('upload').findPage(query); + + const sanitizedResults = await pm.sanitizeOutput(results); + + return { results: sanitizedResults, pagination }; + }, + + async findOne(ctx) { + const { + state: { userAbility }, + params: { id }, + } = ctx; + + const { pm, file } = await findEntityAndCheckPermissions( + userAbility, + ACTIONS.read, + fileModel, + id + ); + + ctx.body = await pm.sanitizeOutput(file); + }, + + async destroy(ctx) { + const { id } = ctx.params; + const { userAbility } = ctx.state; + + const { pm, file } = await findEntityAndCheckPermissions( + userAbility, + ACTIONS.update, + fileModel, + id + ); + + await getService('upload').remove(file); + + ctx.body = await pm.sanitizeOutput(file, { action: ACTIONS.read }); + }, +}; diff --git a/packages/core/upload/server/controllers/admin-folder.js b/packages/core/upload/server/controllers/admin-folder.js new file mode 100644 index 0000000000..29ccc3ce0b --- /dev/null +++ b/packages/core/upload/server/controllers/admin-folder.js @@ -0,0 +1,74 @@ +'use strict'; + +const { setCreatorFields, pipeAsync } = require('@strapi/utils'); +const { ApplicationError } = require('@strapi/utils').errors; +const { getService } = require('../utils'); +const { validateCreateFolder } = require('./validation/folder'); + +const folderModel = 'plugin::upload.folder'; + +module.exports = { + async find(ctx) { + const permissionsManager = strapi.admin.services.permission.createPermissionsManager({ + ability: ctx.state.userAbility, + model: folderModel, + }); + + const { results, pagination } = await strapi.entityService.findWithRelationCounts(folderModel, { + ...ctx.query, + populate: { + children: { + count: true, + }, + files: { + count: true, + }, + parent: true, + createdBy: true, + updatedBy: true, + }, + }); + + ctx.body = { + results: await permissionsManager.sanitizeOutput(results), + pagination, + }; + }, + async create(ctx) { + const { user } = ctx.state; + const { body, query } = ctx.request; + + await validateCreateFolder(body); + + const existingFolders = await strapi.entityService.findMany(folderModel, { + filters: { + parent: body.parent, + name: body.name, + }, + }); + + if (existingFolders.length > 0) { + throw new ApplicationError('name already taken'); + } + + const { setPathAndUID } = getService('folder'); + + // TODO: wrap with a transaction + const enrichFolder = pipeAsync(setPathAndUID, setCreatorFields({ user })); + const enrichedFolder = await enrichFolder(body); + + const folder = await strapi.entityService.create(folderModel, { + ...query, + data: enrichedFolder, + }); + + const permissionsManager = strapi.admin.services.permission.createPermissionsManager({ + ability: ctx.state.userAbility, + model: folderModel, + }); + + ctx.body = { + data: await permissionsManager.sanitizeOutput(folder), + }; + }, +}; diff --git a/packages/core/upload/server/controllers/admin-settings.js b/packages/core/upload/server/controllers/admin-settings.js new file mode 100644 index 0000000000..0494e9975a --- /dev/null +++ b/packages/core/upload/server/controllers/admin-settings.js @@ -0,0 +1,40 @@ +'use strict'; + +const { getService } = require('../utils'); +const { ACTIONS } = require('../constants'); +const validateSettings = require('./validation/settings'); + +const fileModel = 'plugin::upload.file'; + +module.exports = { + async updateSettings(ctx) { + const { + request: { body }, + state: { userAbility }, + } = ctx; + + if (userAbility.cannot(ACTIONS.readSettings, fileModel)) { + return ctx.forbidden(); + } + + const data = await validateSettings(body); + + await getService('upload').setSettings(data); + + ctx.body = { data }; + }, + + async getSettings(ctx) { + const { + state: { userAbility }, + } = ctx; + + if (userAbility.cannot(ACTIONS.readSettings, fileModel)) { + return ctx.forbidden(); + } + + const data = await getService('upload').getSettings(); + + ctx.body = { data }; + }, +}; diff --git a/packages/core/upload/server/controllers/admin-upload.js b/packages/core/upload/server/controllers/admin-upload.js new file mode 100644 index 0000000000..2be79b2b31 --- /dev/null +++ b/packages/core/upload/server/controllers/admin-upload.js @@ -0,0 +1,88 @@ +'use strict'; + +const _ = require('lodash'); +const { ApplicationError } = require('@strapi/utils').errors; +const { getService } = require('../utils'); +const { ACTIONS } = require('../constants'); +const validateUploadBody = require('./validation/upload'); +const findEntityAndCheckPermissions = require('./utils/find-entity-and-check-permissions'); + +const fileModel = 'plugin::upload.file'; + +module.exports = { + async updateFileInfo(ctx) { + const { + state: { userAbility, user }, + query: { id }, + request: { body }, + } = ctx; + + const uploadService = getService('upload'); + const { pm } = await findEntityAndCheckPermissions(userAbility, ACTIONS.update, fileModel, id); + + const data = await validateUploadBody(body); + const file = await uploadService.updateFileInfo(id, data.fileInfo, { user }); + + ctx.body = await pm.sanitizeOutput(file, { action: ACTIONS.read }); + }, + + async replaceFile(ctx) { + const { + state: { userAbility, user }, + query: { id }, + request: { body, files: { files } = {} }, + } = ctx; + + const uploadService = getService('upload'); + const { pm } = await findEntityAndCheckPermissions(userAbility, ACTIONS.update, fileModel, id); + + if (Array.isArray(files)) { + throw new ApplicationError('Cannot replace a file with multiple ones'); + } + + const data = await validateUploadBody(body); + const replacedFiles = await uploadService.replace(id, { data, file: files }, { user }); + + ctx.body = await pm.sanitizeOutput(replacedFiles, { action: ACTIONS.read }); + }, + + async uploadFiles(ctx) { + const { + state: { userAbility, user }, + request: { body, files: { files } = {} }, + } = ctx; + + const uploadService = getService('upload'); + const pm = strapi.admin.services.permission.createPermissionsManager({ + ability: userAbility, + action: ACTIONS.create, + model: fileModel, + }); + + if (!pm.isAllowed) { + return ctx.forbidden(); + } + + const data = await validateUploadBody(body); + const uploadedFiles = await uploadService.upload({ data, files }, { user }); + + ctx.body = await pm.sanitizeOutput(uploadedFiles, { action: ACTIONS.read }); + }, + + async upload(ctx) { + const { + query: { id }, + request: { files: { files } = {} }, + } = ctx; + + if (id && (_.isEmpty(files) || files.size === 0)) { + return this.updateFileInfo(ctx); + } + + if (_.isEmpty(files) || files.size === 0) { + throw new ApplicationError('Files are empty'); + } + + await (id ? this.replaceFile : this.uploadFiles)(ctx); + }, +}; diff --git a/packages/core/upload/server/controllers/content-api.js b/packages/core/upload/server/controllers/content-api.js index 3bb647d090..6b4dc25d0d 100644 --- a/packages/core/upload/server/controllers/content-api.js +++ b/packages/core/upload/server/controllers/content-api.js @@ -3,7 +3,6 @@ const _ = require('lodash'); const utils = require('@strapi/utils'); const { getService } = require('../utils'); -const validateSettings = require('./validation/settings'); const validateUploadBody = require('./validation/upload'); const { sanitize } = utils; @@ -57,24 +56,6 @@ module.exports = { ctx.body = await sanitizeOutput(file, ctx); }, - async updateSettings(ctx) { - const { - request: { body }, - } = ctx; - - const data = await validateSettings(body); - - await getService('upload').setSettings(data); - - ctx.body = { data }; - }, - - async getSettings(ctx) { - const data = await getService('upload').getSettings(); - - ctx.body = { data }; - }, - async updateFileInfo(ctx) { const { query: { id }, @@ -135,15 +116,4 @@ module.exports = { await (id ? this.replaceFile : this.uploadFiles)(ctx); }, - - async search(ctx) { - const { id } = ctx.params; - const entries = await strapi.query('plugin::upload.file').findMany({ - where: { - $or: [{ hash: { $contains: id } }, { name: { $contains: id } }], - }, - }); - - ctx.body = await sanitizeOutput(entries, ctx); - }, }; diff --git a/packages/core/upload/server/controllers/index.js b/packages/core/upload/server/controllers/index.js index a778bce4ea..eab355c4e3 100644 --- a/packages/core/upload/server/controllers/index.js +++ b/packages/core/upload/server/controllers/index.js @@ -1,9 +1,15 @@ 'use strict'; -const adminApi = require('./admin-api'); +const adminFile = require('./admin-file'); +const adminFolder = require('./admin-folder'); +const adminSettings = require('./admin-settings'); +const adminUpload = require('./admin-upload'); const contentApi = require('./content-api'); module.exports = { - 'admin-api': adminApi, + 'admin-file': adminFile, + 'admin-folder': adminFolder, + 'admin-settings': adminSettings, + 'admin-upload': adminUpload, 'content-api': contentApi, }; diff --git a/packages/core/upload/server/controllers/utils/find-entity-and-check-permissions.js b/packages/core/upload/server/controllers/utils/find-entity-and-check-permissions.js new file mode 100644 index 0000000000..30158284f0 --- /dev/null +++ b/packages/core/upload/server/controllers/utils/find-entity-and-check-permissions.js @@ -0,0 +1,33 @@ +'use strict'; + +const _ = require('lodash'); +const { contentTypes: contentTypesUtils } = require('@strapi/utils'); +const { NotFoundError, ForbiddenError } = require('@strapi/utils').errors; +const { getService } = require('../../utils'); + +const { CREATED_BY_ATTRIBUTE } = contentTypesUtils.constants; + +const findEntityAndCheckPermissions = async (ability, action, model, id) => { + const file = await getService('upload').findOne(id, [CREATED_BY_ATTRIBUTE]); + + if (_.isNil(file)) { + throw new NotFoundError(); + } + + const pm = strapi.admin.services.permission.createPermissionsManager({ ability, action, model }); + + const creatorId = _.get(file, [CREATED_BY_ATTRIBUTE, 'id']); + const author = creatorId ? await strapi.admin.services.user.findOne(creatorId, ['roles']) : null; + + const fileWithRoles = _.set(_.cloneDeep(file), 'createdBy', author); + + if (pm.ability.cannot(pm.action, pm.toSubject(fileWithRoles))) { + throw new ForbiddenError(); + } + + return { pm, file }; +}; + +module.exports = { + findEntityAndCheckPermissions, +}; diff --git a/packages/core/upload/server/controllers/validation/folder.js b/packages/core/upload/server/controllers/validation/folder.js new file mode 100644 index 0000000000..12be56702a --- /dev/null +++ b/packages/core/upload/server/controllers/validation/folder.js @@ -0,0 +1,22 @@ +'use strict'; + +const { yup, validateYupSchema } = require('@strapi/utils'); + +const NO_SLASH_REGEX = /^[^/]+$/; + +const validateCreateFolderSchema = yup + .object() + .shape({ + name: yup + .string() + .min(1) + .matches(NO_SLASH_REGEX, 'name cannot contain slashes') + .required(), + parent: yup.strapiID().nullable(), + }) + .noUnknown() + .required(); + +module.exports = { + validateCreateFolder: validateYupSchema(validateCreateFolderSchema), +}; diff --git a/packages/core/upload/server/routes/admin.js b/packages/core/upload/server/routes/admin.js index 35b7f1c712..0845b2b39d 100644 --- a/packages/core/upload/server/routes/admin.js +++ b/packages/core/upload/server/routes/admin.js @@ -6,7 +6,7 @@ module.exports = { { method: 'GET', path: '/settings', - handler: 'admin-api.getSettings', + handler: 'admin-settings.getSettings', config: { policies: [ 'admin::isAuthenticatedAdmin', @@ -22,7 +22,7 @@ module.exports = { { method: 'PUT', path: '/settings', - handler: 'admin-api.updateSettings', + handler: 'admin-settings.updateSettings', config: { policies: [ 'admin::isAuthenticatedAdmin', @@ -38,7 +38,7 @@ module.exports = { { method: 'POST', path: '/', - handler: 'admin-api.upload', + handler: 'admin-upload.upload', config: { policies: ['admin::isAuthenticatedAdmin'], }, @@ -46,7 +46,7 @@ module.exports = { { method: 'GET', path: '/files', - handler: 'admin-api.find', + handler: 'admin-file.find', config: { policies: [ 'admin::isAuthenticatedAdmin', @@ -62,7 +62,7 @@ module.exports = { { method: 'GET', path: '/files/:id', - handler: 'admin-api.findOne', + handler: 'admin-file.findOne', config: { policies: [ 'admin::isAuthenticatedAdmin', @@ -78,7 +78,7 @@ module.exports = { { method: 'DELETE', path: '/files/:id', - handler: 'admin-api.destroy', + handler: 'admin-file.destroy', config: { policies: [ 'admin::isAuthenticatedAdmin', @@ -91,5 +91,37 @@ module.exports = { ], }, }, + { + method: 'GET', + path: '/folders', + handler: 'admin-folder.find', + config: { + policies: [ + 'admin::isAuthenticatedAdmin', + { + name: 'admin::hasPermissions', + config: { + actions: ['plugin::upload.read'], + }, + }, + ], + }, + }, + { + method: 'POST', + path: '/folders', + handler: 'admin-folder.create', + config: { + policies: [ + 'admin::isAuthenticatedAdmin', + { + name: 'admin::hasPermissions', + config: { + actions: ['plugin::upload.read'], + }, + }, + ], + }, + }, ], }; diff --git a/packages/core/upload/server/services/folder.js b/packages/core/upload/server/services/folder.js new file mode 100644 index 0000000000..428eadaa05 --- /dev/null +++ b/packages/core/upload/server/services/folder.js @@ -0,0 +1,42 @@ +'use strict'; + +const uuid = require('uuid/v4'); +const { trimChars, trimCharsEnd, trimCharsStart } = require('lodash/fp'); + +// TODO: to use once https://github.com/strapi/strapi/pull/12534 is merged +// const { joinBy } = require('@strapi/utils'); + +const folderModel = 'plugin::upload.folder'; + +const joinBy = (joint, ...args) => { + const trim = trimChars(joint); + const trimEnd = trimCharsEnd(joint); + const trimStart = trimCharsStart(joint); + + return args.reduce((url, path, index) => { + if (args.length === 1) return path; + if (index === 0) return trimEnd(path); + if (index === args.length - 1) return url + joint + trimStart(path); + return url + joint + trim(path); + }, ''); +}; + +const generateUID = () => uuid(); + +const setPathAndUID = async folder => { + let parentPath = '/'; + if (folder.parent) { + const parentFolder = await strapi.entityService.findOne(folderModel, folder.parent); + parentPath = parentFolder.path; + } + + return Object.assign(folder, { + uid: generateUID(), + path: joinBy('/', parentPath, folder.name), + }); +}; + +module.exports = { + generateUID, + setPathAndUID, +}; diff --git a/packages/core/upload/server/services/index.js b/packages/core/upload/server/services/index.js index 9219731d81..b648c62df7 100644 --- a/packages/core/upload/server/services/index.js +++ b/packages/core/upload/server/services/index.js @@ -1,11 +1,13 @@ 'use strict'; -const providerService = require('./provider'); -const uploadService = require('./upload'); +const provider = require('./provider'); +const upload = require('./upload'); const imageManipulation = require('./image-manipulation'); +const folder = require('./folder'); module.exports = { - provider: providerService, - upload: uploadService, + provider, + upload, + folder, 'image-manipulation': imageManipulation, }; diff --git a/packages/core/upload/tests/admin/folder.test.e2e.js b/packages/core/upload/tests/admin/folder.test.e2e.js new file mode 100644 index 0000000000..f0754dbf79 --- /dev/null +++ b/packages/core/upload/tests/admin/folder.test.e2e.js @@ -0,0 +1,181 @@ +'use strict'; + +// Test a simple default API with no relations + +const { omit } = require('lodash/fp'); + +const { createTestBuilder } = require('../../../../../test/helpers/builder'); +const { createStrapiInstance } = require('../../../../../test/helpers/strapi'); +const { createAuthRequest } = require('../../../../../test/helpers/request'); + +let strapi; +let rq; +let data = { + folders: [], +}; + +describe('Folder', () => { + const builder = createTestBuilder(); + + beforeAll(async () => { + strapi = await createStrapiInstance(); + rq = await createAuthRequest({ strapi }); + }); + + afterAll(async () => { + await strapi.destroy(); + await builder.cleanup(); + }); + + describe('create', () => { + test('Can create a folder at root level', async () => { + const res = await rq({ + method: 'POST', + url: '/upload/folders?populate=parent', + body: { + name: 'folder-1', + parent: null, + }, + }); + + expect(res.body.data).toMatchObject({ + id: expect.anything(), + name: 'folder-1', + uid: expect.anything(), + path: '/folder-1', + createdAt: expect.anything(), + updatedAt: expect.anything(), + parent: null, + }); + + data.folders.push(omit('parent', res.body.data)); + }); + + test('Cannot create a folder with duplicated name at root level', async () => { + const res = await rq({ + method: 'POST', + url: '/upload/folders?populate=parent', + body: { + name: 'folder-1', + parent: null, + }, + }); + + expect(res.status).toBe(400); + expect(res.body.error.message).toBe('name already taken'); + }); + + test('Cannot create a folder with path containing a slash', async () => { + const res = await rq({ + method: 'POST', + url: '/upload/folders?populate=parent', + body: { + name: 'folder-1/2', + parent: null, + }, + }); + + expect(res.status).toBe(400); + expect(res.body.error.message).toBe('name cannot contain slashes'); + }); + + test('Can create a folder inside another folder', async () => { + const res = await rq({ + method: 'POST', + url: '/upload/folders?populate=parent', + body: { + name: 'folder-2', + parent: data.folders[0].id, + }, + }); + + expect(res.body.data).toMatchObject({ + id: expect.anything(), + name: 'folder-2', + uid: expect.anything(), + path: '/folder-1/folder-2', + createdAt: expect.anything(), + updatedAt: expect.anything(), + parent: data.folders[0], + }); + + data.folders.push(omit('parent', res.body.data)); + }); + }); + + describe('read', () => { + test('Can read folders', async () => { + const res = await rq({ + method: 'GET', + url: '/upload/folders', + }); + + expect(res.body.pagination).toMatchObject({ + page: 1, + pageCount: 1, + pageSize: 10, + total: 2, + }); + expect(res.body.results).toHaveLength(2); + expect(res.body.results).toEqual( + expect.arrayContaining([ + { + children: { count: 1 }, + createdAt: expect.anything(), + createdBy: { + firstname: 'admin', + id: expect.anything(), + lastname: 'admin', + username: null, + }, + files: { count: 0 }, + id: expect.anything(), + name: 'folder-1', + parent: null, + path: '/folder-1', + uid: expect.anything(), + updatedAt: expect.anything(), + updatedBy: { + firstname: 'admin', + id: expect.anything(), + lastname: 'admin', + username: null, + }, + }, + { + children: { count: 0 }, + createdAt: expect.anything(), + createdBy: { + firstname: 'admin', + id: expect.anything(), + lastname: 'admin', + username: null, + }, + files: { + count: 0, + }, + id: expect.anything(), + name: 'folder-2', + parent: { + createdAt: expect.anything(), + id: expect.anything(), + name: 'folder-1', + path: '/folder-1', + uid: expect.anything(), + updatedAt: expect.anything(), + }, + path: '/folder-1/folder-2', + uid: expect.anything(), + updatedAt: expect.anything(), + updatedBy: { + firstname: 'admin', + id: expect.anything(), + lastname: 'admin', + username: null, + }, + }, + ]) + ); + }); + }); +}); diff --git a/packages/core/upload/tests/graphql-upload.test.e2e.js b/packages/core/upload/tests/content-api/graphql-upload.test.e2e.js similarity index 98% rename from packages/core/upload/tests/graphql-upload.test.e2e.js rename to packages/core/upload/tests/content-api/graphql-upload.test.e2e.js index 6158ec2861..84f13ec1cb 100644 --- a/packages/core/upload/tests/graphql-upload.test.e2e.js +++ b/packages/core/upload/tests/content-api/graphql-upload.test.e2e.js @@ -3,8 +3,8 @@ const fs = require('fs'); const path = require('path'); -const { createStrapiInstance } = require('../../../../test/helpers/strapi'); -const { createAuthRequest } = require('../../../../test/helpers/request'); +const { createStrapiInstance } = require('../../../../../test/helpers/strapi'); +const { createAuthRequest } = require('../../../../../test/helpers/request'); let strapi; let rq; diff --git a/packages/core/upload/tests/rec.jpg b/packages/core/upload/tests/content-api/rec.jpg similarity index 100% rename from packages/core/upload/tests/rec.jpg rename to packages/core/upload/tests/content-api/rec.jpg diff --git a/packages/core/upload/tests/rec.pdf b/packages/core/upload/tests/content-api/rec.pdf similarity index 100% rename from packages/core/upload/tests/rec.pdf rename to packages/core/upload/tests/content-api/rec.pdf diff --git a/packages/core/upload/tests/thumbnail_target.png b/packages/core/upload/tests/content-api/thumbnail_target.png similarity index 100% rename from packages/core/upload/tests/thumbnail_target.png rename to packages/core/upload/tests/content-api/thumbnail_target.png diff --git a/packages/core/upload/tests/upload.test.e2e.js b/packages/core/upload/tests/content-api/upload.test.e2e.js similarity index 96% rename from packages/core/upload/tests/upload.test.e2e.js rename to packages/core/upload/tests/content-api/upload.test.e2e.js index 6692b16cbc..38e9f10c1c 100644 --- a/packages/core/upload/tests/upload.test.e2e.js +++ b/packages/core/upload/tests/content-api/upload.test.e2e.js @@ -4,9 +4,9 @@ const fs = require('fs'); const path = require('path'); // Helpers. -const { createTestBuilder } = require('../../../../test/helpers/builder'); -const { createStrapiInstance } = require('../../../../test/helpers/strapi'); -const { createAuthRequest } = require('../../../../test/helpers/request'); +const { createTestBuilder } = require('../../../../../test/helpers/builder'); +const { createStrapiInstance } = require('../../../../../test/helpers/strapi'); +const { createAuthRequest } = require('../../../../../test/helpers/request'); const builder = createTestBuilder(); let strapi;