|
| 1 | +const async = require('async'); |
| 2 | +const { errors, versioning } = require('arsenal'); |
| 3 | +const { PassThrough } = require('stream'); |
| 4 | + |
| 5 | +const collectCorsHeaders = require('../utilities/collectCorsHeaders'); |
| 6 | +const createAndStoreObject = require('./apiUtils/object/createAndStoreObject'); |
| 7 | +const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils'); |
| 8 | +const { config } = require('../Config'); |
| 9 | +const { setExpirationHeaders } = require('./apiUtils/object/expirationHeaders'); |
| 10 | +const writeContinue = require('../utilities/writeContinue'); |
| 11 | +const { overheadField } = require('../../constants'); |
| 12 | + |
| 13 | + |
| 14 | +const versionIdUtils = versioning.VersionID; |
| 15 | + |
| 16 | + |
| 17 | +/** |
| 18 | + * POST Object in the requested bucket. Steps include: |
| 19 | + * validating metadata for authorization, bucket and object existence etc. |
| 20 | + * store object data in datastore upon successful authorization |
| 21 | + * store object location returned by datastore and |
| 22 | + * object's (custom) headers in metadata |
| 23 | + * return the result in final callback |
| 24 | + * |
| 25 | + * @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info |
| 26 | + * @param {request} request - request object given by router, |
| 27 | + * includes normalized headers |
| 28 | + * @param {object | undefined } streamingV4Params - if v4 auth, |
| 29 | + * object containing accessKey, signatureFromRequest, region, scopeDate, |
| 30 | + * timestamp, and credentialScope |
| 31 | + * (to be used for streaming v4 auth if applicable) |
| 32 | + * @param {object} log - the log request |
| 33 | + * @param {Function} callback - final callback to call with the result |
| 34 | + * @return {undefined} |
| 35 | + */ |
| 36 | +function objectPost(authInfo, request, streamingV4Params, log, callback) { |
| 37 | + const { |
| 38 | + headers, |
| 39 | + method, |
| 40 | + } = request; |
| 41 | + let parsedContentLength = 0; |
| 42 | + const passThroughStream = new PassThrough(); |
| 43 | + const requestType = request.apiMethods || 'objectPost'; |
| 44 | + const valParams = { |
| 45 | + authInfo, |
| 46 | + bucketName: request.formData.bucket, |
| 47 | + objectKey: request.formData.key, |
| 48 | + requestType, |
| 49 | + request, |
| 50 | + }; |
| 51 | + const canonicalID = authInfo.getCanonicalID(); |
| 52 | + |
| 53 | + |
| 54 | + log.trace('owner canonicalID to send to data', { canonicalID }); |
| 55 | + return standardMetadataValidateBucketAndObj(valParams, request.actionImplicitDenies, log, |
| 56 | + (err, bucket, objMD) => { |
| 57 | + const responseHeaders = collectCorsHeaders(headers.origin, |
| 58 | + method, bucket); |
| 59 | + |
| 60 | + if (err && !err.AccessDenied) { |
| 61 | + log.trace('error processing request', { |
| 62 | + error: err, |
| 63 | + method: 'metadataValidateBucketAndObj', |
| 64 | + }); |
| 65 | + return callback(err, responseHeaders); |
| 66 | + } |
| 67 | + if (bucket.hasDeletedFlag() && canonicalID !== bucket.getOwner()) { |
| 68 | + log.trace('deleted flag on bucket and request ' + |
| 69 | + 'from non-owner account'); |
| 70 | + return callback(errors.NoSuchBucket); |
| 71 | + } |
| 72 | + |
| 73 | + return async.waterfall([ |
| 74 | + function countPOSTFileSize(next) { |
| 75 | + if (!request.fileEventData || !request.fileEventData.file) { |
| 76 | + return next(); |
| 77 | + } |
| 78 | + request.fileEventData.file.on('data', (chunk) => { |
| 79 | + parsedContentLength += chunk.length; |
| 80 | + passThroughStream.write(chunk); |
| 81 | + }); |
| 82 | + |
| 83 | + request.fileEventData.file.on('end', () => { |
| 84 | + // Here totalBytes will have the total size of the file |
| 85 | + passThroughStream.end(); |
| 86 | + // Setting the file in the request avoids the need to make changes to createAndStoreObject's |
| 87 | + // parameters and thus all it's subsequent calls. This is necessary as the stream used to create |
| 88 | + // the object is that of the request directly; something we must work around |
| 89 | + // to use the file data produced from the multipart form data. |
| 90 | + /* eslint-disable no-param-reassign */ |
| 91 | + request.fileEventData.file = passThroughStream; |
| 92 | + /* eslint-disable no-param-reassign */ |
| 93 | + request.parsedContentLength = parsedContentLength; |
| 94 | + return next(); |
| 95 | + }); |
| 96 | + return undefined; |
| 97 | + }, |
| 98 | + function objectCreateAndStore(next) { |
| 99 | + writeContinue(request, request._response); |
| 100 | + return createAndStoreObject(request.bucketName, |
| 101 | + bucket, request.formData.key, objMD, authInfo, canonicalID, null, |
| 102 | + request, false, streamingV4Params, overheadField, log, next); |
| 103 | + }, |
| 104 | + ], (err, storingResult) => { |
| 105 | + if (err) { |
| 106 | + return callback(err, responseHeaders); |
| 107 | + } |
| 108 | + setExpirationHeaders(responseHeaders, { |
| 109 | + lifecycleConfig: bucket.getLifecycleConfiguration(), |
| 110 | + objectParams: { |
| 111 | + key: request.key, |
| 112 | + date: storingResult.lastModified, |
| 113 | + tags: storingResult.tags, |
| 114 | + }, |
| 115 | + }); |
| 116 | + if (storingResult) { |
| 117 | + // ETag's hex should always be enclosed in quotes |
| 118 | + responseHeaders.ETag = `"${storingResult.contentMD5}"`; |
| 119 | + } |
| 120 | + const vcfg = bucket.getVersioningConfiguration(); |
| 121 | + const isVersionedObj = vcfg && vcfg.Status === 'Enabled'; |
| 122 | + if (isVersionedObj) { |
| 123 | + if (storingResult && storingResult.versionId) { |
| 124 | + responseHeaders['x-amz-version-id'] = |
| 125 | + versionIdUtils.encode(storingResult.versionId, |
| 126 | + config.versionIdEncodingType); |
| 127 | + } |
| 128 | + } |
| 129 | + return callback(null, responseHeaders); |
| 130 | + }); |
| 131 | + }); |
| 132 | +} |
| 133 | + |
| 134 | +module.exports = objectPost; |
0 commit comments