diff --git a/packages/server/src/api/controllers/row/internal.js b/packages/server/src/api/controllers/row/internal.js index 70cacda349..75caaf2fda 100644 --- a/packages/server/src/api/controllers/row/internal.js +++ b/packages/server/src/api/controllers/row/internal.js @@ -26,6 +26,7 @@ const { getFromDesignDoc, getFromMemoryDoc, } = require("../view/utils") +const { cloneDeep } = require("lodash/fp") const CALCULATION_TYPES = { SUM: "sum", @@ -110,14 +111,14 @@ exports.patch = async ctx => { const inputs = ctx.request.body const tableId = inputs.tableId const isUserTable = tableId === InternalTables.USER_METADATA - let dbRow + let oldRow try { - dbRow = await db.get(inputs._id) + oldRow = await db.get(inputs._id) } catch (err) { if (isUserTable) { // don't include the rev, it'll be the global rev // this time - dbRow = { + oldRow = { _id: inputs._id, } } else { @@ -126,13 +127,14 @@ exports.patch = async ctx => { } let dbTable = await db.get(tableId) // need to build up full patch fields before coerce + let combinedRow = cloneDeep(oldRow) for (let key of Object.keys(inputs)) { if (!dbTable.schema[key]) continue - dbRow[key] = inputs[key] + combinedRow[key] = inputs[key] } // this returns the table and row incase they have been updated - let { table, row } = inputProcessing(ctx.user, dbTable, dbRow) + let { table, row } = inputProcessing(ctx.user, dbTable, combinedRow) const validateResult = await validate({ row, table, @@ -150,6 +152,8 @@ exports.patch = async ctx => { tableId: row.tableId, table, }) + // check if any attachments removed + await cleanupAttachments(appId, table, { oldRow, row }) if (isUserTable) { // the row has been updated, need to put it into the ctx diff --git a/packages/server/src/utilities/rowProcessor/index.js b/packages/server/src/utilities/rowProcessor/index.js index 6e4e127f73..9229a061e4 100644 --- a/packages/server/src/utilities/rowProcessor/index.js +++ b/packages/server/src/utilities/rowProcessor/index.js @@ -5,6 +5,8 @@ const { attachmentsRelativeURL } = require("../index") const { processFormulas } = require("./utils") const { deleteFiles } = require("../../utilities/fileSystem/utilities") const { ObjectStoreBuckets } = require("../../constants") +const { isProdAppID, getDeployedAppID, dbExists } = require("@budibase/auth/db") +const CouchDB = require("../../db") const BASE_AUTO_ID = 1 @@ -97,6 +99,23 @@ const TYPE_TRANSFORM_MAP = { }, } +/** + * Given the old state of the row and the new one after an update, this will + * find the keys that have been removed in the updated row. + */ +function getRemovedAttachmentKeys(oldRow, row, attachmentKey) { + if (!oldRow[attachmentKey]) { + return [] + } + const oldKeys = oldRow[attachmentKey].map(attachment => attachment.key) + // no attachments in new row, all removed + if (!row[attachmentKey]) { + return oldKeys + } + const newKeys = row[attachmentKey].map(attachment => attachment.key) + return oldKeys.filter(key => newKeys.indexOf(key) === -1) +} + /** * This will update any auto columns that are found on the row/table with the correct information based on * time now and the current logged in user making the request. @@ -281,10 +300,18 @@ exports.outputProcessing = async ( * @param {object} table The table from which a row is being removed. * @param {any} row optional - the row being removed. * @param {any} rows optional - if multiple rows being deleted can do this in bulk. + * @param {any} oldRow optional - if updating a row this will determine the difference. * @return {Promise} When all attachments have been removed this will return. */ -exports.cleanupAttachments = async (appId, table, {row, rows}) => { - // TODO: check app ID version +exports.cleanupAttachments = async (appId, table, { row, rows, oldRow }) => { + if (!isProdAppID(appId)) { + const prodAppId = getDeployedAppID(appId) + // if prod exists, then don't allow deleting + const exists = await dbExists(CouchDB, prodAppId) + if (exists) { + return + } + } let files = [] function addFiles(row, key) { if (row[key]) { @@ -295,11 +322,16 @@ exports.cleanupAttachments = async (appId, table, {row, rows}) => { if (schema.type !== FieldTypes.ATTACHMENT) { continue } - if (row) { + // if updating, need to manage the differences + if (oldRow && row) { + files = files.concat(getRemovedAttachmentKeys(oldRow, row, key)) + } else if (row) { addFiles(row, key) } else if (rows) { rows.forEach(row => addFiles(row, key)) } } - return deleteFiles(ObjectStoreBuckets.APPS, files) + if (files.length > 0) { + return deleteFiles(ObjectStoreBuckets.APPS, files) + } }