Skip to content

Commit fe5c807

Browse files
committed
Public Folders
1 parent fc0f8e8 commit fe5c807

File tree

16 files changed

+506
-98
lines changed

16 files changed

+506
-98
lines changed

app/src/components/constants.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ module.exports = Object.freeze({
5757
/** Maximum Content Length supported by S3 CopyObjectCommand */
5858
MAXCOPYOBJECTLENGTH: 5 * 1024 * 1024 * 1024, // 5 GB
5959

60-
/** Maximum Content Length supported by S3 CopyObjectCommand */
60+
/** Maximum Content Length (file size) supported by S3 */
6161
MAXFILEOBJECTLENGTH: 5 * 1024 * 1024 * 1024 * 1024, // 5 TB
6262

6363
/** Allowable values for the Metadata Directive parameter */

app/src/components/utils.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ const utils = {
9393
data.bucket = bucketData.bucket;
9494
data.endpoint = bucketData.endpoint;
9595
data.key = bucketData.key;
96+
// data.public = bucketData.public;
9697
data.secretAccessKey = bucketData.secretAccessKey;
9798
if (bucketData.region) data.region = bucketData.region;
9899
} else if (utils.getConfigBoolean('objectStorage.enabled')) {
@@ -357,6 +358,21 @@ const utils = {
357358
&& pathParts.filter(part => !prefixParts.includes(part)).length === 1;
358359
},
359360

361+
/**
362+
* @function isBelowPrefix
363+
* Predicate function determining if a path is 'below' a prefix
364+
* @param {string} prefix The base "folder"
365+
* @param {string} path The "sub-folder" to check
366+
* @returns {boolean} True if path is below of prefix. False in all other cases.
367+
*/
368+
isBelowPrefix(prefix, path) {
369+
if (typeof prefix !== 'string' || typeof path !== 'string') return false;
370+
else if (path === prefix) return false;
371+
else if (prefix === DELIMITER) return true;
372+
else if (path.startsWith(prefix)) return true;
373+
else return false;
374+
},
375+
360376
/**
361377
* @function isTruthy
362378
* Returns true if the element name in the object contains a truthy value

app/src/controllers/bucket.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const log = require('../components/log')(module.filename);
99
const {
1010
addDashesToUuid,
1111
getCurrentIdentity,
12+
getBucket,
1213
isTruthy,
1314
joinPath,
1415
mixedQueryToArray,
@@ -323,6 +324,48 @@ const controller = {
323324
}
324325
},
325326

327+
328+
/**
329+
* @function togglePublic
330+
* Sets the public flag of a bucket (or folder)
331+
* @param {object} req Express request object
332+
* @param {object} res Express response object
333+
* @param {function} next The next callback function
334+
* @returns {function} Express middleware function
335+
*/
336+
async togglePublic(req, res, next) {
337+
try {
338+
const bucketId = addDashesToUuid(req.params.bucketId);
339+
const publicFlag = isTruthy(req.query.public) ?? false;
340+
const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER), SYSTEM_USER);
341+
342+
const bucket = await getBucket(bucketId);
343+
const data = {
344+
bucketId: bucketId,
345+
path: bucket.key + '/',
346+
public: publicFlag,
347+
userId: userId
348+
};
349+
// Update S3 Policy
350+
await storageService.updatePublic(data).catch((e) => {
351+
log.warn('Failed to apply permission changes to S3' + e, { function: 'togglePublic', ...data });
352+
});
353+
354+
// update public flag for this bucket and all child buckets and objects!
355+
const response = await bucketService.updatePublic({
356+
...bucket,
357+
bucketId: bucketId,
358+
public: publicFlag,
359+
userId: userId
360+
});
361+
362+
res.status(200).json(response);
363+
} catch (e) {
364+
next(errorToProblem(SERVICE, e));
365+
}
366+
},
367+
368+
326369
/**
327370
* @function updateBucket
328371
* Updates a bucket

app/src/controllers/object.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ const controller = {
274274
}
275275

276276
// Preflight existence check for bucketId
277-
const { key: bucketKey } = await getBucket(bucketId);
277+
const { key: bucketKey, public: bucketPublic } = await getBucket(bucketId);
278278

279279
const objId = uuidv4();
280280
const data = {
@@ -308,7 +308,9 @@ const controller = {
308308
existingObjectId: objectId,
309309
});
310310

311-
} catch (err) {
311+
}
312+
// headObject threw an error because object was not found
313+
catch (err) {
312314
if (err instanceof Problem) throw err; // Rethrow Problem type errors
313315

314316
// Object is soft deleted from the bucket
@@ -366,7 +368,8 @@ const controller = {
366368
const object = await objectService.create({
367369
...data,
368370
userId: userId,
369-
path: joinPath(bucketKey, data.name)
371+
path: joinPath(bucketKey, data.name),
372+
public: bucketPublic // set public status to match that of parent 'folder'
370373
}, trx);
371374

372375
// Create Version
@@ -1074,19 +1077,17 @@ const controller = {
10741077
const data = {
10751078
id: objId,
10761079
bucketId: req.currentObject?.bucketId,
1077-
filePath: req.currentObject?.path,
1080+
path: req.currentObject?.path,
10781081
public: publicFlag,
10791082
userId: userId,
1080-
// TODO: Implement if/when we proceed with version-scoped public permission management
1081-
// s3VersionId: await getS3VersionId(
1082-
// req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId
1083-
// )
10841083
};
10851084

1086-
storageService.putObjectPublic(data).catch(() => {
1087-
// Gracefully continue even when S3 ACL management operation fails
1088-
log.warn('Failed to apply ACL permission changes to S3', { function: 'togglePublic', ...data });
1085+
// Update S3 Policy
1086+
await storageService.updatePublic(data).catch((error) => {
1087+
log.warn('Failed to apply permission changes to S3', { function: 'togglePublic', ...data });
1088+
throw new Error(error);
10891089
});
1090+
// Update object record in COMS database
10901091
const response = await objectService.update(data);
10911092

10921093
res.status(200).json(response);

app/src/controllers/sync.js

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ const controller = {
5656
/**
5757
* sync (ie create or delete) bucket records in COMS db to match 'folders' (S3 key prefixes) that exist in S3
5858
*/
59-
// parent + child bucket records already in COMS db
59+
// get parent + child bucket records already in COMS db
6060
const dbChildBuckets = await bucketService.searchChildBuckets(parentBucket, false, userId);
6161
let dbBuckets = [parentBucket].concat(dbChildBuckets);
62-
// 'folders' that exist below (and including) the parent 'folder' in S3
62+
6363
const s3Response = await storageService.listAllObjectVersions({ bucketId: bucketId, precisePath: false });
6464
const s3Keys = [...new Set([
6565
...s3Response.DeleteMarkers.map(object => formatS3KeyForCompare(object.Key)),
@@ -68,7 +68,7 @@ const controller = {
6868

6969
// Wrap sync sql operations in a single transaction
7070
const response = await utils.trxWrapper(async (trx) => {
71-
71+
// sync bucket records
7272
const syncedBuckets = await this.syncBucketRecords(
7373
dbBuckets,
7474
s3Keys,
@@ -106,6 +106,9 @@ const controller = {
106106
const bucket = await bucketService.read(bucketId);
107107
const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER), SYSTEM_USER);
108108

109+
// sync bucket.public flag
110+
await this.syncBucketPublic(bucket.key, bucket.bucketId, userId);
111+
109112
const s3Objects = await storageService.listAllObjectVersions({ bucketId: bucketId, filterLatest: true });
110113

111114
const response = await utils.trxWrapper(async (trx) => {
@@ -141,17 +144,8 @@ const controller = {
141144
dbBuckets = dbBuckets.filter(b => b.bucketId !== dbBucket.bucketId);
142145
})
143146
)
144-
);
145-
// add current user's permissions to all buckets
146-
await Promise.all(
147-
dbBuckets.map(bucket => {
148-
return bucketPermissionService.addPermissions(
149-
bucket.bucketId,
150-
currentUserParentBucketPerms.map(permCode => ({ userId, permCode })),
151-
undefined,
152-
trx
153-
);
154-
})
147+
// TODO: delete COMS S3 Policies for deleted COMS buckets and child objects.
148+
// Also consider when using DEL /Bucket endpoint, should we delete policies?
155149
);
156150

157151
// Create buckets only found in S3 in COMS db
@@ -176,6 +170,22 @@ const controller = {
176170
});
177171
})
178172
);
173+
174+
// Update permissions and Sync Public status
175+
await Promise.all(
176+
// for each bucket
177+
dbBuckets.map(async bucket => {
178+
// --- Add current user's permissions that exist on parent bucket if they dont already exist
179+
await bucketPermissionService.addPermissions(
180+
bucket.bucketId,
181+
currentUserParentBucketPerms.map(permCode => ({ userId, permCode })),
182+
undefined,
183+
trx
184+
);
185+
// --- Sync S3 Bucket Policies applied by COMS
186+
await this.syncBucketPublic(bucket.key, bucket.bucketId, userId);
187+
})
188+
);
179189
return dbBuckets;
180190
}
181191
catch (err) {
@@ -184,6 +194,18 @@ const controller = {
184194
}
185195
},
186196

197+
async syncBucketPublic(key, bucketId, userId) {
198+
let public = false;
199+
public = await storageService.getPublic({ path: key, bucketId: bucketId });
200+
bucketService.update({
201+
bucketId: bucketId,
202+
updatedBy: userId,
203+
public: public
204+
// TODO: consider changing this to actual lastSyncDate
205+
// lastSyncRequestedDate: now(),
206+
});
207+
},
208+
187209
/**
188210
* @function queueObjectRecords
189211
* Synchronizes (creates / prunes) COMS db object records with state in S3
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
exports.up = function (knex) {
2+
return Promise.resolve()
3+
// // allow null for object.public
4+
// .then(() => knex.schema.alterTable('object', table => {
5+
// table.boolean('public').nullable().alter();
6+
// }))
7+
// // where object.public is false, set to null
8+
// .then(() => knex('object')
9+
// .where({ 'public': false })
10+
// .update({ 'public': null }))
11+
// .then(() => knex.schema.alterTable('object', table => {
12+
// table.boolean('public').nullable().alter();
13+
// }))
14+
// add public column to bucket table
15+
.then(() => knex.schema.alterTable('bucket', table => {
16+
table.boolean('public').notNullable().defaultTo(false);
17+
}));
18+
};
19+
20+
exports.down = function (knex) {
21+
return Promise.resolve()
22+
// drop public column in bucket table
23+
.then(() => knex.schema.alterTable('bucket', table => {
24+
table.dropColumn('public');
25+
}));
26+
// // where object.public is null, set to false
27+
// .then(() => knex('object')
28+
// .where({ 'public': null })
29+
// .update({ 'public': false }))
30+
31+
// disallow null for object.public
32+
// .then(() => knex.schema.alterTable('object', table => {
33+
// table.boolean('public').notNullable().defaultTo(false).alter();
34+
// }));
35+
};

app/src/db/models/tables/bucket.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ class Bucket extends mixin(Model, [
9999
region: { type: 'string', maxLength: 255 },
100100
active: { type: 'boolean' },
101101
lastSyncRequestedDate: { type: ['string', 'null'], format: 'date-time' },
102+
public: { type: 'boolean' },
102103
...stamps
103104
},
104105
additionalProperties: false

0 commit comments

Comments
 (0)