diff --git a/lib/server/routes/buckets.js b/lib/server/routes/buckets.js index cab89e57..1be506f7 100644 --- a/lib/server/routes/buckets.js +++ b/lib/server/routes/buckets.js @@ -180,6 +180,25 @@ BucketsRouter.prototype.getBucketById = function (req, res, next) { }); }; +BucketsRouter.prototype.getBucketId = function (req, res, next) { + const Bucket = this.storage.models.Bucket; + + Bucket.findOne({ + userId: req.user.uuid, + name: req.params.name + }, '_id', { lean: true }, function (err, bucket) { + if (err) { + return next(new errors.InternalError(err.message)); + } + + if (!bucket) { + return next(new errors.NotFoundError('Bucket not found')); + } + + res.status(200).send({ id: bucket._id }); + }); +}; + /** * Creates a new bucket for the user * @param {http.IncomingMessage} req @@ -434,6 +453,84 @@ BucketsRouter.prototype.createBucketToken = function (req, res, next) { }); }; + +/** + * Creates a bucket entry from the given frame object + * @param {http.IncomingMessage} req + * @param {http.ServerResponse} res + * @param {Function} next + */ +BucketsRouter.prototype.createEntryFromFrame = function (req, res, next) { + const Frame = this.storage.models.Frame; + const Bucket = this.storage.models.Bucket; + const BucketEntry = this.storage.models.BucketEntry; + + if (req.body.filename && + req.body.filename.length > constants.MAX_BUCKETENTRYNAME) { + return next(new errors.BadRequestError('Maximum bucket entry name')); + } + + analytics.track(req.headers.dnt, { + userId: req.user.uuid, + event: 'File Upload Complete' + }); + + Bucket.findOne({ + userId: req.user.uuid, + _id: req.params.id + }, function (err, bucket) { + if (err) { + return next(new errors.InternalError(err.message)); + } + + if (!bucket) { + return next(new errors.NotFoundError('Bucket not found')); + } + + Frame.findOne({ + _id: req.body.frame, + user: req.user.email + }, function (err, frame) { + if (err) { + return next(new errors.InternalError(err.message)); + } + + if (!frame) { + return next(new errors.NotFoundError('Frame not found')); + } + + if (frame.locked) { + return next(new errors.BadRequestError('Frame is already locked')); + } + + BucketEntry.create({ + bucket: bucket._id, + frame: frame._id, + mimetype: req.body.mimetype, + name: req.body.filename, + hmac: req.body.hmac, + erasure: req.body.erasure, + index: req.body.index + }, function (err, entry) { + if (err) { + return next(new errors.InternalError(err.message)); + } + + frame.lock(function (err) { + if (err) { + return next(new errors.InternalError(err.message)); + } + + frame.bucketEntry = entry.id; + frame.save(); + + res.send(merge(entry.toObject(), { size: frame.size })); + }); + }); + }); + }); +}; + /** * Returns the bucket by ID * @param {String|ObjectId} bucketId - The unique _id for the bucket @@ -1003,6 +1100,53 @@ BucketsRouter.prototype.getFiles = async function (req, res, next) { } }; +/** + * Lists the file pointers stored in the given bucket + * @param {http.IncomingMessage} req + * @param {http.ServerResponse} res + * @param {Function} next + */ +BucketsRouter.prototype.listFilesInBucket = function (req, res, next) { + const { Bucket, BucketEntry } = this.storage.models; + + Bucket.findOne({ + _id: req.params.id, + userId: req.user.uuid + }, (err, bucket) => { + if (err) { + return next(new errors.InternalError(err.message)); + } + + if (!bucket) { + return next(new errors.NotFoundError('Bucket not found')); + } + + const startDate = utils.parseTimestamp(req.query.startDate); + const findQuery = { bucket: req.params.id }; + if (startDate) { + findQuery.created = { $gt: startDate }; + } + + const query = BucketEntry.find(findQuery).sort({ created: 1 }).limit(constants.DEFAULT_MAX_ENTRIES); + const stream = query.cursor(); + + stream.pipe(utils.createArrayFormatter(function (entry) { + return { + bucket: entry.bucket, + mimetype: entry.mimetype, + filename: entry.filename, + frame: entry.frame.id, + size: entry.frame.size, + id: entry._id, + created: entry.created, + hmac: entry.hmac, + erasure: entry.erasure, + index: entry.index + }; + })).pipe(res); + }); +}; + /** * Removes the file pointer from the bucket * @param {http.IncomingMessage} req @@ -1103,6 +1247,86 @@ BucketsRouter.prototype.deletePointers = async function (pointers) { } }; +BucketsRouter.prototype.renameFile = function (req, res, next) { + const { Bucket, BucketEntry } = this.storage.models; + + Bucket.findOne({ + _id: req.params.id, + userId: req.user.uuid + }, function (err, bucket) { + if (err) { + return next(new errors.InternalError(err.message)); + } + + if (!bucket) { + return next(new errors.NotFoundError('Bucket not found')); + } + + BucketEntry.findOne({ + bucket: bucket._id, + _id: req.params.file + }, function (err, entry) { + if (err) { + return next(err); + } + + if (!entry) { + return next(new errors.NotFoundError('File not found')); + } + + if (!entry.name) { + return next(new errors.BadRequestError('Invalid name')); + } + + entry.name = req.body.name; + + entry.save(function (err) { + if (err) { + return next(new errors.InternalError(err.message)); + } + + res.status(201).end(); + }); + + }); + + }); +}; + +BucketsRouter.prototype.getFileId = function (req, res, next) { + const Bucket = this.storage.models.Bucket; + const BucketEntry = this.storage.models.BucketEntry; + + Bucket.findOne({ + _id: req.params.id, + userId: req.user.uuid + }, '_id', { lean: true }, function (err, bucket) { + if (err) { + return next(new errors.InternalError(err.message)); + } + + if (!bucket) { + return next(new errors.NotFoundError('Bucket not found')); + } + + BucketEntry.findOne({ + bucket: bucket._id, + name: req.params.name + }, '_id', { lean: true }, function (err, entry) { + if (err) { + return next(new errors.InternalError(err.message)); + } + + if (!entry) { + return next(new errors.NotFoundError('File not found')); + } + res.status(200).send({ id: entry._id }); + }); + }); + +}; + + BucketsRouter.prototype.getFileInfo = function (req, res, next) { this._getBucketUnregistered(req, res, (err, bucket) => { if (err) { @@ -1332,15 +1556,21 @@ BucketsRouter.prototype._definitions = function () { /* jshint maxlen: 140 */ return [ ['GET', '/buckets', this.getLimiter(limiter(1000)), this._verify, this.getBuckets], + ['GET', '/buckets/:id', this.getLimiter(limiter(1000)), this._validate, this._verify, this.getBucketById], + ['GET', '/bucket-ids/:name', this.getLimiter(limiter(1000)), this._validate, this._verify, this.getBucketId], ['POST', '/buckets', this.getLimiter(limiter(1000)), this._verify, this.createBucket], ['DELETE', '/buckets/:id', this.getLimiter(limiter(1000)), this._validate, this._verify, this.destroyBucketById], ['PATCH', '/buckets/:id', this.getLimiter(limiter(1000)), this._validate, this._verify, this.updateBucketById], ['POST', '/buckets/:id/tokens', this.getLimiter(limiter(1000)), this._validate, this._verify, this.createBucketToken], + ['GET', '/buckets/:id/files', this.getLimiter(limiter(1000)), this._validate, this._verify, this.listFilesInBucket], + ['GET', '/buckets/:id/file-ids/:name', this.getLimiter(limiter(1000)), this._validate, this._verify, this.getFileId], ['GET', '/buckets/:id/files/:file', this.getLimiter(limiter(1000)), this._validate, this._usetokenOrVerify, this.getFile], ['GET', '/buckets/:id/bulk-files', this.getLimiter(limiter(1000)), this._validate, this._usetokenOrVerify, this.getFiles], ['DELETE', '/buckets/:id/files/:file', this.getLimiter(limiter(1000)), this._validate, this._verify, this.removeFile], ['GET', '/buckets/:id/files/:file/info', this.getLimiter(limiter(1000)), this._validate, this._usetokenOrVerify, this.getFileInfo], + ['POST', '/buckets/:id/files', this.getLimiter(limiter(1000)), this._validate, this._verify, this.createEntryFromFrame], ['GET', '/buckets/:id/files/:file/mirrors', this.getLimiter(limiter(1000)), this._validate, this._verify, this.listMirrorsForFile], + ['PATCH', '/buckets/:id/files/:file', this.getLimiter(limiter(1000)), this._validate, this._verify, this.renameFile], ['POST', '/v2/buckets/:id/files/start', this.getLimiter(limiter(1000)), this._validate, this._verify, this.startUpload], ['POST', '/v2/buckets/:id/files/finish', this.getLimiter(limiter(1000)), this._validate, this._verify, this.finishUpload], ['GET', '/v2/buckets/:id/files/:file/mirrors', this.getLimiter(limiter(1000)), this._validate, this._verify, this.getDownloadLinks], diff --git a/lib/server/routes/frames.js b/lib/server/routes/frames.js index 7c1cf6b1..2bebac2d 100644 --- a/lib/server/routes/frames.js +++ b/lib/server/routes/frames.js @@ -26,6 +26,52 @@ function FramesRouter(options) { inherits(FramesRouter, Router); +/** + * Destroys the file staging frame if it is not in use by a bucket entry + * @param {http.IncomingMessage} req + * @param {http.ServerResponse} res + * @param {Function} next + */ +FramesRouter.prototype.destroyFrameById = function (req, res, next) { + const { BucketEntry, Frame } = this.storage.models; + + BucketEntry.findOne({ + user: { $in: [req.user.email, req.user._id] }, + frame: req.params.frame + }, function (err, entry) { + if (err) { + return next(new errors.InternalError(err.message)); + } + + if (entry) { + return next(new errors.BadRequestError( + 'Refusing to destroy frame that is referenced by a bucket entry' + )); + } + + Frame.findOne({ + user: { $in: [req.user.email, req.user._id] }, + _id: req.params.frame + }, function (err, frame) { + if (err) { + return next(new errors.InternalError(err.message)); + } + + if (!frame) { + return next(new errors.NotFoundError('Frame not found')); + } + + frame.remove(function (err) { + if (err) { + return next(new errors.InternalError(err.message)); + } + + res.status(204).end(); + }); + }); + }); +}; + /** * Returns the caller's file staging frames * @param {http.IncomingMessage} req @@ -108,6 +154,7 @@ FramesRouter.prototype.getStorageLimit = function (req, res) { FramesRouter.prototype._definitions = function () { /* jshint maxlen: 140 */ return [ + ['DELETE', '/frames/:frame', this.getLimiter(limiter(1000)), this._verify, this.destroyFrameById], ['GET', '/frames', this.getLimiter(limiter(1000)), this._verify, this.getFrames], ['GET', '/frames/:frame', this.getLimiter(limiter(1000)), this._verify, this.getFrameById], ['GET', '/limit', this.getLimiter(limiter(1000)), this._verify, this.getStorageLimit] diff --git a/lib/server/routes/users.js b/lib/server/routes/users.js index 1f34f2b7..6a1d4c85 100644 --- a/lib/server/routes/users.js +++ b/lib/server/routes/users.js @@ -1,5 +1,6 @@ 'use strict'; +const assert = require('assert'); const Router = require('./index'); const middleware = require('storj-service-middleware'); const rawbody = middleware.rawbody; @@ -478,6 +479,34 @@ UsersRouter.prototype.confirmDestroyUser = function (req, res, next) { }); }; +/** + * Get user activation info + * @param {http.IncomingMessage} req + * @param {http.ServerResponse} res + * @param {Function} next + */ +UsersRouter.prototype.isActivated = function (req, res, next) { + const User = this.storage.models.User; + + log.debug('Getting user activation info for %s', req.headers['email']); + + User.findOne({ email: req.headers['email'] }, function (err, user) { + if (err) { + return next(new errors.InternalError(err.message)); + } + + if (!user) { + return next(new errors.BadRequestError('User not found')); + } + + // Send activated info + res.status(200).send({ + activated: user.activated, + uuid: user.uuid, + }); + }); +}; + UsersRouter.prototype.confirmDestroyUserStripe = function (req, res, next) { const self = this; const stripeUtils = require('../stripeService'); diff --git a/lib/utils.js b/lib/utils.js index b01c39b0..08ff7b11 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,7 +4,10 @@ 'use strict'; +const async = require('async'); const through = require('through'); +const crypto = require('crypto'); +const assert = require('assert'); /** * Returns a transform stream that wraps objects written to it @@ -33,6 +36,18 @@ module.exports.createArrayFormatter = function (transformer) { }); }; +/** + * Sort by reputation, to be used with Array.prototype.sort + * @param {Object} - a + * @param {Object} - b + */ +module.exports.sortByReputation = function (a, b) { + const a1 = a.contact.reputation >= 0 ? a.contact.reputation : 0; + const b1 = b.contact.reputation >= 0 ? b.contact.reputation : 0; + + return (a1 === b1) ? 0 : (a1 > b1) ? -1 : 1; +}; + /** * Will get a timestamp integer from a string or number * argument, including ISO formatted strings. @@ -86,3 +101,36 @@ module.exports.recursiveExpandJSON = function (value) { return value; }; + +module.exports.distinct = function (value, index, self) { + return self.indexOf(value) === index; +}; + +/** + * Will iterate an Aggregation Cursor + */ +module.exports.AggregationCursor = function (model, query, manageData, callback) { + const cursor = model.aggregate(query).allowDiskUse(true).cursor().exec(); + const untilFunction = (next) => cursor.next().then(data => { + if (!data) { + return next(null, data); + } + manageData(data, (err) => next(err, data)); + }).catch(next); + const untilCondition = (data, next) => next(null, !data); + async.doUntil(untilFunction, untilCondition, () => cursor.close(callback)); + + return cursor; +}; + +module.exports.randomTime = (max, min) => { + const range = max - min; + + assert(Number.isSafeInteger(range)); + assert(range > 0, 'maxInterval is expected to be greater than minInterval'); + + const entropy = crypto.randomBytes(8).toString('hex'); + const offset = Math.round(parseInt('0x' + entropy) / Math.pow(2, 64) * range); + + return min + offset; +};