Skip to content

Commit ea286ce

Browse files
committed
Public Folders
1 parent c8a9d41 commit ea286ce

File tree

16 files changed

+496
-98
lines changed

16 files changed

+496
-98
lines changed

app/src/components/constants.js

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

68-
/** Maximum Content Length supported by S3 CopyObjectCommand */
68+
/** Maximum Content Length (file size) supported by S3 */
6969
MAXFILEOBJECTLENGTH: 5 * 1024 * 1024 * 1024 * 1024, // 5 TB
7070

7171
/** Maximum object key length supported by S3 */

app/src/components/utils.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ const utils = {
9898
data.bucket = bucketData.bucket;
9999
data.endpoint = bucketData.endpoint;
100100
data.key = bucketData.key;
101+
// data.public = bucketData.public;
101102
data.secretAccessKey = bucketData.secretAccessKey;
102103
if (bucketData.region) data.region = bucketData.region;
103104
} else if (utils.getConfigBoolean('objectStorage.enabled')) {
@@ -364,6 +365,21 @@ const utils = {
364365
&& pathParts.filter(part => !prefixParts.includes(part)).length === 1;
365366
},
366367

368+
/**
369+
* @function isBelowPrefix
370+
* Predicate function determining if a path is 'below' a prefix
371+
* @param {string} prefix The base "folder"
372+
* @param {string} path The "sub-folder" to check
373+
* @returns {boolean} True if path is below of prefix. False in all other cases.
374+
*/
375+
isBelowPrefix(prefix, path) {
376+
if (typeof prefix !== 'string' || typeof path !== 'string') return false;
377+
else if (path === prefix) return false;
378+
else if (prefix === DELIMITER) return true;
379+
else if (path.startsWith(prefix)) return true;
380+
else return false;
381+
},
382+
367383
/**
368384
* @function isTruthy
369385
* 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,
@@ -336,6 +337,48 @@ const controller = {
336337
}
337338
},
338339

340+
341+
/**
342+
* @function togglePublic
343+
* Sets the public flag of a bucket (or folder)
344+
* @param {object} req Express request object
345+
* @param {object} res Express response object
346+
* @param {function} next The next callback function
347+
* @returns {function} Express middleware function
348+
*/
349+
async togglePublic(req, res, next) {
350+
try {
351+
const bucketId = addDashesToUuid(req.params.bucketId);
352+
const publicFlag = isTruthy(req.query.public) ?? false;
353+
const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER), SYSTEM_USER);
354+
355+
const bucket = await getBucket(bucketId);
356+
const data = {
357+
bucketId: bucketId,
358+
path: bucket.key + '/',
359+
public: publicFlag,
360+
userId: userId
361+
};
362+
// Update S3 Policy
363+
await storageService.updatePublic(data).catch((e) => {
364+
log.warn('Failed to apply permission changes to S3' + e, { function: 'togglePublic', ...data });
365+
});
366+
367+
// update public flag for this bucket and all child buckets and objects!
368+
const response = await bucketService.updatePublic({
369+
...bucket,
370+
bucketId: bucketId,
371+
public: publicFlag,
372+
userId: userId
373+
});
374+
375+
res.status(200).json(response);
376+
} catch (e) {
377+
next(errorToProblem(SERVICE, e));
378+
}
379+
},
380+
381+
339382
/**
340383
* @function updateBucket
341384
* Updates a bucket

app/src/controllers/object.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ const controller = {
277277
}
278278

279279
// Preflight existence check for bucketId
280-
const { key: bucketKey } = await getBucket(bucketId);
280+
const { key: bucketKey, public: bucketPublic } = await getBucket(bucketId);
281281

282282
const objId = uuidv4();
283283
const data = {
@@ -319,7 +319,9 @@ const controller = {
319319
existingObjectId: objectId,
320320
});
321321

322-
} catch (err) {
322+
}
323+
// headObject threw an error because object was not found
324+
catch (err) {
323325
if (err instanceof Problem) throw err; // Rethrow Problem type errors
324326

325327
// Object is soft deleted from the bucket
@@ -377,7 +379,8 @@ const controller = {
377379
const object = await objectService.create({
378380
...data,
379381
userId: userId,
380-
path: joinPath(bucketKey, data.name)
382+
path: joinPath(bucketKey, data.name),
383+
public: bucketPublic // set public status to match that of parent 'folder'
381384
}, trx);
382385

383386
// Create Version
@@ -1109,19 +1112,17 @@ const controller = {
11091112
const data = {
11101113
id: objId,
11111114
bucketId: req.currentObject?.bucketId,
1112-
filePath: req.currentObject?.path,
1115+
path: req.currentObject?.path,
11131116
public: publicFlag,
11141117
userId: userId,
1115-
// TODO: Implement if/when we proceed with version-scoped public permission management
1116-
// s3VersionId: await getS3VersionId(
1117-
// req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId
1118-
// )
11191118
};
11201119

1121-
storageService.putObjectPublic(data).catch(() => {
1122-
// Gracefully continue even when S3 ACL management operation fails
1123-
log.warn('Failed to apply ACL permission changes to S3', { function: 'togglePublic', ...data });
1120+
// Update S3 Policy
1121+
await storageService.updatePublic(data).catch((error) => {
1122+
log.warn('Failed to apply permission changes to S3', { function: 'togglePublic', ...data });
1123+
throw new Error(error);
11241124
});
1125+
// Update object record in COMS database
11251126
const response = await objectService.update(data);
11261127

11271128
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) => {
@@ -142,17 +145,8 @@ const controller = {
142145
dbBuckets = dbBuckets.filter(b => b.bucketId !== dbBucket.bucketId);
143146
})
144147
)
145-
);
146-
// add current user's permissions to all buckets
147-
await Promise.all(
148-
dbBuckets.map(bucket => {
149-
return bucketPermissionService.addPermissions(
150-
bucket.bucketId,
151-
currentUserParentBucketPerms.map(permCode => ({ userId, permCode })),
152-
undefined,
153-
trx
154-
);
155-
})
148+
// TODO: delete COMS S3 Policies for deleted COMS buckets and child objects.
149+
// Also consider when using DEL /Bucket endpoint, should we delete policies?
156150
);
157151

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

198+
async syncBucketPublic(key, bucketId, userId) {
199+
let public = false;
200+
public = await storageService.getPublic({ path: key, bucketId: bucketId });
201+
bucketService.update({
202+
bucketId: bucketId,
203+
updatedBy: userId,
204+
public: public
205+
// TODO: consider changing this to actual lastSyncDate
206+
// lastSyncRequestedDate: now(),
207+
});
208+
},
209+
188210
/**
189211
* @function queueObjectRecords
190212
* 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)