Compare commits
63 Commits
developmen
...
feature/AR
Author | SHA1 | Date |
---|---|---|
Vianney Rancurel | c55c4abe6c | |
Taylor McKinnon | 13f33a81a6 | |
bbuchanan9 | 08c1a2046d | |
bbuchanan9 | ab239ffa54 | |
bbuchanan9 | ad1ee70c4e | |
bbuchanan9 | 61bb75b276 | |
Dora Korpar | 52bbe85463 | |
Rahul Padigela | 8921a0d9c7 | |
Electra Chong | 10a99a5f44 | |
Electra Chong | d053da3f6c | |
Electra Chong | 2ec6808e91 | |
Vinh Tao | ad6faf8a5a | |
Electra Chong | a486fdd457 | |
Guillaume Gimenez | ed2e4d6e9d | |
Bennett Buchanan | 21486bc9fd | |
Bennett Buchanan | f56df90adb | |
David Pineau | a91fa0ecda | |
Michael Zapata | dd63d98ac9 | |
alexandre-merle | ab5dc5c444 | |
alexandre.merle | dae83707f3 | |
Antonin Coulibaly | 26902a0b83 | |
Lauren Spiegel | 4409b43622 | |
Rahul Padigela | 21ca9104c1 | |
Rahul Padigela | f38c39bdc2 | |
Lauren Spiegel | c2b401d7c3 | |
Antonin Coulibaly | 70a36337ec | |
Michael Zapata | f73ad7cc3b | |
claire | d3f7d01162 | |
Lauren Spiegel | a8b1d06818 | |
Rahul Padigela | a8a0af06b1 | |
Antonin Coulibaly | d9100dd72b | |
Michael Zapata | 426c4628af | |
Lauren Spiegel | f83f2fab27 | |
Michael Zapata | 527ecdd00a | |
Lauren Spiegel | 8dad5d526b | |
Lauren Spiegel | b7ecaa93ec | |
Lauren Spiegel | d12a21249d | |
Lauren Spiegel | 6bede12bba | |
Lauren Spiegel | c4cb173841 | |
Rached Ben Mustapha | aa94555b02 | |
Lauren Spiegel | 879ac76ebc | |
Lauren Spiegel | 5a91754821 | |
Lauren Spiegel | c41adcc6ad | |
Michael Zapata | 6274b7b8c4 | |
Michael Zapata | a60594a86d | |
Rahul Padigela | 3a1e2938ae | |
Michael Zapata | fd5a3563c7 | |
Adrien Vergé | 1f5b23f72f | |
Michael Zapata | e8925f3e31 | |
Rahul Padigela | 0ca1c1f050 | |
Michael Zapata | f9f2fbb0e2 | |
Michael Zapata | e9277ce008 | |
Michael Zapata | 5f6d904bd5 | |
Michael Zapata | 9a644a0559 | |
Michael Zapata | 812f1c7492 | |
Michael Zapata | 1bd47c7b35 | |
Michael Zapata | d96a2b07cc | |
Michael Zapata | 179aaa4501 | |
Lauren Spiegel | 3cc563d02f | |
Lauren Spiegel | 339c7c2459 | |
Lauren Spiegel | addd7b5fe3 | |
Michael Zapata | 7f020f3e3c | |
Michael Zapata | 7d332764be |
7
index.js
7
index.js
|
@ -106,6 +106,13 @@ module.exports = {
|
||||||
require('./lib/storage/metadata/file/MetadataFileClient'),
|
require('./lib/storage/metadata/file/MetadataFileClient'),
|
||||||
LogConsumer:
|
LogConsumer:
|
||||||
require('./lib/storage/metadata/bucketclient/LogConsumer'),
|
require('./lib/storage/metadata/bucketclient/LogConsumer'),
|
||||||
|
inMemory: {
|
||||||
|
metastore:
|
||||||
|
require('./lib/storage/metadata/in_memory/metastore'),
|
||||||
|
metadata: require('./lib/storage/metadata/in_memory/metadata'),
|
||||||
|
bucketUtilities:
|
||||||
|
require('./lib/storage/metadata/in_memory/bucket_utilities'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
file: {
|
file: {
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
module.exports = {
|
||||||
|
Basic: require('./basic').List,
|
||||||
|
Delimiter: require('./delimiter').Delimiter,
|
||||||
|
DelimiterVersions: require('./delimiterVersions')
|
||||||
|
.DelimiterVersions,
|
||||||
|
DelimiterMaster: require('./delimiterMaster')
|
||||||
|
.DelimiterMaster,
|
||||||
|
MPU: require('./MPU').MultipartUploads,
|
||||||
|
};
|
|
@ -3,8 +3,6 @@ const errors = require('../../errors');
|
||||||
const BucketInfo = require('../../models/BucketInfo');
|
const BucketInfo = require('../../models/BucketInfo');
|
||||||
|
|
||||||
const BucketClientInterface = require('./bucketclient/BucketClientInterface');
|
const BucketClientInterface = require('./bucketclient/BucketClientInterface');
|
||||||
const BucketFileInterface = require('./file/BucketFileInterface');
|
|
||||||
const MongoClientInterface = require('./mongoclient/MongoClientInterface');
|
|
||||||
const metastore = require('./in_memory/metastore');
|
const metastore = require('./in_memory/metastore');
|
||||||
|
|
||||||
let CdmiMetadata;
|
let CdmiMetadata;
|
||||||
|
@ -71,25 +69,10 @@ class MetadataWrapper {
|
||||||
if (clientName === 'mem') {
|
if (clientName === 'mem') {
|
||||||
this.client = metastore;
|
this.client = metastore;
|
||||||
this.implName = 'memorybucket';
|
this.implName = 'memorybucket';
|
||||||
} else if (clientName === 'file') {
|
|
||||||
this.client = new BucketFileInterface(params, logger);
|
|
||||||
this.implName = 'bucketfile';
|
|
||||||
} else if (clientName === 'scality') {
|
} else if (clientName === 'scality') {
|
||||||
this.client = new BucketClientInterface(params, bucketclient,
|
this.client = new BucketClientInterface(params, bucketclient,
|
||||||
logger);
|
logger);
|
||||||
this.implName = 'bucketclient';
|
this.implName = 'bucketclient';
|
||||||
} else if (clientName === 'mongodb') {
|
|
||||||
this.client = new MongoClientInterface({
|
|
||||||
replicaSetHosts: params.mongodb.replicaSetHosts,
|
|
||||||
writeConcern: params.mongodb.writeConcern,
|
|
||||||
replicaSet: params.mongodb.replicaSet,
|
|
||||||
readPreference: params.mongodb.readPreference,
|
|
||||||
database: params.mongodb.database,
|
|
||||||
replicationGroupId: params.replicationGroupId,
|
|
||||||
path: params.mongodb.path,
|
|
||||||
logger,
|
|
||||||
});
|
|
||||||
this.implName = 'mongoclient';
|
|
||||||
} else if (clientName === 'cdmi') {
|
} else if (clientName === 'cdmi') {
|
||||||
if (!CdmiMetadata) {
|
if (!CdmiMetadata) {
|
||||||
throw new Error('Unauthorized backend');
|
throw new Error('Unauthorized backend');
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
const ListResult = require('./ListResult');
|
||||||
|
|
||||||
|
class ListMultipartUploadsResult extends ListResult {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.Uploads = [];
|
||||||
|
this.NextKeyMarker = undefined;
|
||||||
|
this.NextUploadIdMarker = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
addUpload(uploadInfo) {
|
||||||
|
this.Uploads.push({
|
||||||
|
key: decodeURIComponent(uploadInfo.key),
|
||||||
|
value: {
|
||||||
|
UploadId: uploadInfo.uploadId,
|
||||||
|
Initiator: {
|
||||||
|
ID: uploadInfo.initiatorID,
|
||||||
|
DisplayName: uploadInfo.initiatorDisplayName,
|
||||||
|
},
|
||||||
|
Owner: {
|
||||||
|
ID: uploadInfo.ownerID,
|
||||||
|
DisplayName: uploadInfo.ownerDisplayName,
|
||||||
|
},
|
||||||
|
StorageClass: uploadInfo.storageClass,
|
||||||
|
Initiated: uploadInfo.initiated,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.MaxKeys += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ListMultipartUploadsResult,
|
||||||
|
};
|
|
@ -0,0 +1,27 @@
|
||||||
|
class ListResult {
|
||||||
|
constructor() {
|
||||||
|
this.IsTruncated = false;
|
||||||
|
this.NextMarker = undefined;
|
||||||
|
this.CommonPrefixes = [];
|
||||||
|
/*
|
||||||
|
Note: this.MaxKeys will get incremented as
|
||||||
|
keys are added so that when response is returned,
|
||||||
|
this.MaxKeys will equal total keys in response
|
||||||
|
(with each CommonPrefix counting as 1 key)
|
||||||
|
*/
|
||||||
|
this.MaxKeys = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
addCommonPrefix(prefix) {
|
||||||
|
if (!this.hasCommonPrefix(prefix)) {
|
||||||
|
this.CommonPrefixes.push(prefix);
|
||||||
|
this.MaxKeys += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCommonPrefix(prefix) {
|
||||||
|
return (this.CommonPrefixes.indexOf(prefix) !== -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ListResult;
|
|
@ -0,0 +1,62 @@
|
||||||
|
# bucket_mem design
|
||||||
|
|
||||||
|
## RATIONALE
|
||||||
|
|
||||||
|
The bucket API will be used for managing buckets behind the S3 interface.
|
||||||
|
|
||||||
|
We plan to have only 2 backends using this interface:
|
||||||
|
|
||||||
|
* One production backend
|
||||||
|
* One debug backend purely in memory
|
||||||
|
|
||||||
|
One important remark here is that we don't want an abstraction but a
|
||||||
|
duck-typing style interface (different classes MemoryBucket and Bucket having
|
||||||
|
the same methods putObjectMD(), getObjectMD(), etc).
|
||||||
|
|
||||||
|
Notes about the memory backend: The backend is currently a simple key/value
|
||||||
|
store in memory. The functions actually use nextTick() to emulate the future
|
||||||
|
asynchronous behavior of the production backend.
|
||||||
|
|
||||||
|
## BUCKET API
|
||||||
|
|
||||||
|
The bucket API is a very simple API with 5 functions:
|
||||||
|
|
||||||
|
- putObjectMD(): put metadata for an object in the bucket
|
||||||
|
- getObjectMD(): get metadata from the bucket
|
||||||
|
- deleteObjectMD(): delete metadata for an object from the bucket
|
||||||
|
- deleteBucketMD(): delete a bucket
|
||||||
|
- getBucketListObjects(): perform the complex bucket listing AWS search
|
||||||
|
function with various flavors. This function returns a response in a
|
||||||
|
ListBucketResult object.
|
||||||
|
|
||||||
|
getBucketListObjects(prefix, marker, delimiter, maxKeys, callback) behavior is
|
||||||
|
the following:
|
||||||
|
|
||||||
|
prefix (not required): Limits the response to keys that begin with the
|
||||||
|
specified prefix. You can use prefixes to separate a bucket into different
|
||||||
|
groupings of keys. (You can think of using prefix to make groups in the same
|
||||||
|
way you'd use a folder in a file system.)
|
||||||
|
|
||||||
|
marker (not required): Specifies the key to start with when listing objects in
|
||||||
|
a bucket. Amazon S3 returns object keys in alphabetical order, starting with
|
||||||
|
key after the marker in order.
|
||||||
|
|
||||||
|
delimiter (not required): A delimiter is a character you use to group keys.
|
||||||
|
All keys that contain the same string between the prefix, if specified, and the
|
||||||
|
first occurrence of the delimiter after the prefix are grouped under a single
|
||||||
|
result element, CommonPrefixes. If you don't specify the prefix parameter, then
|
||||||
|
the substring starts at the beginning of the key. The keys that are grouped
|
||||||
|
under CommonPrefixes are not returned elsewhere in the response.
|
||||||
|
|
||||||
|
maxKeys: Sets the maximum number of keys returned in the response body. You can
|
||||||
|
add this to your request if you want to retrieve fewer than the default 1000
|
||||||
|
keys. The response might contain fewer keys but will never contain more. If
|
||||||
|
there are additional keys that satisfy the search criteria but were not
|
||||||
|
returned because maxKeys was exceeded, the response contains an attribute of
|
||||||
|
IsTruncated set to true and a NextMarker. To return the additional keys, call
|
||||||
|
the function again using NextMarker as your marker argument in the function.
|
||||||
|
|
||||||
|
Any key that does not contain the delimiter will be returned individually in
|
||||||
|
Contents rather than in CommonPrefixes.
|
||||||
|
|
||||||
|
If there is an error, the error subfield is returned in the response.
|
|
@ -0,0 +1,51 @@
|
||||||
|
function markerFilterMPU(allMarkers, array) {
|
||||||
|
const { keyMarker, uploadIdMarker } = allMarkers;
|
||||||
|
for (let i = 0; i < array.length; i++) {
|
||||||
|
// If the keyMarker is the same as the key,
|
||||||
|
// check the uploadIdMarker. If uploadIdMarker is the same
|
||||||
|
// as or alphabetically after the uploadId of the item,
|
||||||
|
// eliminate the item.
|
||||||
|
if (uploadIdMarker && keyMarker === array[i].key) {
|
||||||
|
const laterId =
|
||||||
|
[uploadIdMarker, array[i].uploadId].sort()[1];
|
||||||
|
if (array[i].uploadId === laterId) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
array.shift();
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the keyMarker is alphabetically after the key
|
||||||
|
// of the item in the array, eliminate the item from the array.
|
||||||
|
const laterItem =
|
||||||
|
[keyMarker, array[i].key].sort()[1];
|
||||||
|
if (keyMarker === array[i].key || keyMarker === laterItem) {
|
||||||
|
array.shift();
|
||||||
|
i--;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefixFilter(prefix, array) {
|
||||||
|
for (let i = 0; i < array.length; i++) {
|
||||||
|
if (array[i].indexOf(prefix) !== 0) {
|
||||||
|
array.splice(i, 1);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isKeyInContents(responseObject, key) {
|
||||||
|
return responseObject.Contents.some(val => val.key === key);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
markerFilterMPU,
|
||||||
|
prefixFilter,
|
||||||
|
isKeyInContents,
|
||||||
|
};
|
|
@ -0,0 +1,148 @@
|
||||||
|
const errors = require('../../../errors');
|
||||||
|
|
||||||
|
const { markerFilterMPU, prefixFilter } = require('./bucket_utilities');
|
||||||
|
const { ListMultipartUploadsResult } = require('./ListMultipartUploadsResult');
|
||||||
|
const { metadata } = require('./metadata');
|
||||||
|
|
||||||
|
const defaultMaxKeys = 1000;
|
||||||
|
function getMultipartUploadListing(bucket, params, callback) {
|
||||||
|
const { delimiter, keyMarker,
|
||||||
|
uploadIdMarker, prefix, queryPrefixLength, splitter } = params;
|
||||||
|
const splitterLen = splitter.length;
|
||||||
|
const maxKeys = params.maxKeys !== undefined ?
|
||||||
|
Number.parseInt(params.maxKeys, 10) : defaultMaxKeys;
|
||||||
|
const response = new ListMultipartUploadsResult();
|
||||||
|
const keyMap = metadata.keyMaps.get(bucket.getName());
|
||||||
|
if (prefix) {
|
||||||
|
response.Prefix = prefix;
|
||||||
|
if (typeof prefix !== 'string') {
|
||||||
|
return callback(errors.InvalidArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyMarker) {
|
||||||
|
response.KeyMarker = keyMarker;
|
||||||
|
if (typeof keyMarker !== 'string') {
|
||||||
|
return callback(errors.InvalidArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadIdMarker) {
|
||||||
|
response.UploadIdMarker = uploadIdMarker;
|
||||||
|
if (typeof uploadIdMarker !== 'string') {
|
||||||
|
return callback(errors.InvalidArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delimiter) {
|
||||||
|
response.Delimiter = delimiter;
|
||||||
|
if (typeof delimiter !== 'string') {
|
||||||
|
return callback(errors.InvalidArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxKeys && typeof maxKeys !== 'number') {
|
||||||
|
return callback(errors.InvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort uploads alphatebetically by objectKey and if same objectKey,
|
||||||
|
// then sort in ascending order by time initiated
|
||||||
|
let uploads = [];
|
||||||
|
keyMap.forEach((val, key) => {
|
||||||
|
uploads.push(key);
|
||||||
|
});
|
||||||
|
uploads.sort((a, b) => {
|
||||||
|
const aIndex = a.indexOf(splitter);
|
||||||
|
const bIndex = b.indexOf(splitter);
|
||||||
|
const aObjectKey = a.substring(aIndex + splitterLen);
|
||||||
|
const bObjectKey = b.substring(bIndex + splitterLen);
|
||||||
|
const aInitiated = keyMap.get(a).initiated;
|
||||||
|
const bInitiated = keyMap.get(b).initiated;
|
||||||
|
if (aObjectKey === bObjectKey) {
|
||||||
|
if (Date.parse(aInitiated) >= Date.parse(bInitiated)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (Date.parse(aInitiated) < Date.parse(bInitiated)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (aObjectKey < bObjectKey) ? -1 : 1;
|
||||||
|
});
|
||||||
|
// Edit the uploads array so it only
|
||||||
|
// contains keys that contain the prefix
|
||||||
|
uploads = prefixFilter(prefix, uploads);
|
||||||
|
uploads = uploads.map(stringKey => {
|
||||||
|
const index = stringKey.indexOf(splitter);
|
||||||
|
const index2 = stringKey.indexOf(splitter, index + splitterLen);
|
||||||
|
const storedMD = keyMap.get(stringKey);
|
||||||
|
return {
|
||||||
|
key: stringKey.substring(index + splitterLen, index2),
|
||||||
|
uploadId: stringKey.substring(index2 + splitterLen),
|
||||||
|
bucket: storedMD.eventualStorageBucket,
|
||||||
|
initiatorID: storedMD.initiator.ID,
|
||||||
|
initiatorDisplayName: storedMD.initiator.DisplayName,
|
||||||
|
ownerID: storedMD['owner-id'],
|
||||||
|
ownerDisplayName: storedMD['owner-display-name'],
|
||||||
|
storageClass: storedMD['x-amz-storage-class'],
|
||||||
|
initiated: storedMD.initiated,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// If keyMarker specified, edit the uploads array so it
|
||||||
|
// only contains keys that occur alphabetically after the marker.
|
||||||
|
// If there is also an uploadIdMarker specified, filter to eliminate
|
||||||
|
// any uploads that share the keyMarker and have an uploadId before
|
||||||
|
// the uploadIdMarker.
|
||||||
|
if (keyMarker) {
|
||||||
|
const allMarkers = {
|
||||||
|
keyMarker,
|
||||||
|
uploadIdMarker,
|
||||||
|
};
|
||||||
|
uploads = markerFilterMPU(allMarkers, uploads);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through uploads and filter uploads
|
||||||
|
// with keys containing delimiter
|
||||||
|
// into response.CommonPrefixes and filter remaining uploads
|
||||||
|
// into response.Uploads
|
||||||
|
for (let i = 0; i < uploads.length; i++) {
|
||||||
|
const currentUpload = uploads[i];
|
||||||
|
// If hit maxKeys, stop adding keys to response
|
||||||
|
if (response.MaxKeys >= maxKeys) {
|
||||||
|
response.IsTruncated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// If a delimiter is specified, find its
|
||||||
|
// index in the current key AFTER THE OCCURRENCE OF THE PREFIX
|
||||||
|
// THAT WAS SENT IN THE QUERY (not the prefix including the splitter
|
||||||
|
// and other elements)
|
||||||
|
let delimiterIndexAfterPrefix = -1;
|
||||||
|
const currentKeyWithoutPrefix =
|
||||||
|
currentUpload.key.slice(queryPrefixLength);
|
||||||
|
let sliceEnd;
|
||||||
|
if (delimiter) {
|
||||||
|
delimiterIndexAfterPrefix = currentKeyWithoutPrefix
|
||||||
|
.indexOf(delimiter);
|
||||||
|
sliceEnd = delimiterIndexAfterPrefix + queryPrefixLength;
|
||||||
|
}
|
||||||
|
// If delimiter occurs in current key, add key to
|
||||||
|
// response.CommonPrefixes.
|
||||||
|
// Otherwise add upload to response.Uploads
|
||||||
|
if (delimiterIndexAfterPrefix > -1) {
|
||||||
|
const keySubstring = currentUpload.key.slice(0, sliceEnd + 1);
|
||||||
|
response.addCommonPrefix(keySubstring);
|
||||||
|
} else {
|
||||||
|
response.NextKeyMarker = currentUpload.key;
|
||||||
|
response.NextUploadIdMarker = currentUpload.uploadId;
|
||||||
|
response.addUpload(currentUpload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// `response.MaxKeys` should be the value from the original `MaxUploads`
|
||||||
|
// parameter specified by the user (or else the default 1000). Redefine it
|
||||||
|
// here, so it does not equal the value of `uploads.length`.
|
||||||
|
response.MaxKeys = maxKeys;
|
||||||
|
// If `response.MaxKeys` is 0, `response.IsTruncated` should be `false`.
|
||||||
|
response.IsTruncated = maxKeys === 0 ? false : response.IsTruncated;
|
||||||
|
return callback(null, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getMultipartUploadListing;
|
|
@ -0,0 +1,8 @@
|
||||||
|
const metadata = {
|
||||||
|
buckets: new Map,
|
||||||
|
keyMaps: new Map,
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
metadata,
|
||||||
|
};
|
|
@ -0,0 +1,334 @@
|
||||||
|
const errors = require('../../../errors');
|
||||||
|
const list = require('../../../algos/list/exportAlgos');
|
||||||
|
const genVID =
|
||||||
|
require('../../../versioning/VersionID').generateVersionId;
|
||||||
|
const getMultipartUploadListing = require('./getMultipartUploadListing');
|
||||||
|
const { metadata } = require('./metadata');
|
||||||
|
|
||||||
|
const defaultMaxKeys = 1000;
|
||||||
|
let uidCounter = 0;
|
||||||
|
|
||||||
|
function generateVersionId() {
|
||||||
|
return genVID(uidCounter++, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatVersionKey(key, versionId) {
|
||||||
|
return `${key}\0${versionId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inc(str) {
|
||||||
|
return str ? (str.slice(0, str.length - 1) +
|
||||||
|
String.fromCharCode(str.charCodeAt(str.length - 1) + 1)) : str;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metastore = {
|
||||||
|
createBucket: (bucketName, bucketMD, log, cb) => {
|
||||||
|
process.nextTick(() => {
|
||||||
|
metastore.getBucketAttributes(bucketName, log, (err, bucket) => {
|
||||||
|
// TODO Check whether user already owns the bucket,
|
||||||
|
// if so return "BucketAlreadyOwnedByYou"
|
||||||
|
// If not owned by user, return "BucketAlreadyExists"
|
||||||
|
if (bucket) {
|
||||||
|
return cb(errors.BucketAlreadyExists);
|
||||||
|
}
|
||||||
|
metadata.buckets.set(bucketName, bucketMD);
|
||||||
|
metadata.keyMaps.set(bucketName, new Map);
|
||||||
|
return cb();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
putBucketAttributes: (bucketName, bucketMD, log, cb) => {
|
||||||
|
process.nextTick(() => {
|
||||||
|
metastore.getBucketAttributes(bucketName, log, err => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
metadata.buckets.set(bucketName, bucketMD);
|
||||||
|
return cb();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getBucketAttributes: (bucketName, log, cb) => {
|
||||||
|
process.nextTick(() => {
|
||||||
|
if (!metadata.buckets.has(bucketName)) {
|
||||||
|
return cb(errors.NoSuchBucket);
|
||||||
|
}
|
||||||
|
return cb(null, metadata.buckets.get(bucketName));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteBucket: (bucketName, log, cb) => {
|
||||||
|
process.nextTick(() => {
|
||||||
|
metastore.getBucketAttributes(bucketName, log, err => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
if (metadata.keyMaps.has(bucketName)
|
||||||
|
&& metadata.keyMaps.get(bucketName).length > 0) {
|
||||||
|
return cb(errors.BucketNotEmpty);
|
||||||
|
}
|
||||||
|
metadata.buckets.delete(bucketName);
|
||||||
|
metadata.keyMaps.delete(bucketName);
|
||||||
|
return cb(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
putObject: (bucketName, objName, objVal, params, log, cb) => {
|
||||||
|
process.nextTick(() => {
|
||||||
|
// Ignore the PUT done by AbortMPU
|
||||||
|
if (params && params.isAbort) {
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
return metastore.getBucketAttributes(bucketName, log, err => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
valid combinations of versioning options:
|
||||||
|
- !versioning && !versionId: normal non-versioning put
|
||||||
|
- versioning && !versionId: create a new version
|
||||||
|
- versionId: update (PUT/DELETE) an existing version,
|
||||||
|
and also update master version in case the put
|
||||||
|
version is newer or same version than master.
|
||||||
|
if versionId === '' update master version
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (params && params.versionId) {
|
||||||
|
objVal.versionId = params.versionId; // eslint-disable-line
|
||||||
|
const mst = metadata.keyMaps.get(bucketName).get(objName);
|
||||||
|
if (mst && mst.versionId === params.versionId || !mst) {
|
||||||
|
metadata.keyMaps.get(bucketName).set(objName, objVal);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
objName = formatVersionKey(objName, params.versionId);
|
||||||
|
metadata.keyMaps.get(bucketName).set(objName, objVal);
|
||||||
|
return cb(null, `{"versionId":"${objVal.versionId}"}`);
|
||||||
|
}
|
||||||
|
if (params && params.versioning) {
|
||||||
|
const versionId = generateVersionId();
|
||||||
|
objVal.versionId = versionId; // eslint-disable-line
|
||||||
|
metadata.keyMaps.get(bucketName).set(objName, objVal);
|
||||||
|
// eslint-disable-next-line
|
||||||
|
objName = formatVersionKey(objName, versionId);
|
||||||
|
metadata.keyMaps.get(bucketName).set(objName, objVal);
|
||||||
|
return cb(null, `{"versionId":"${versionId}"}`);
|
||||||
|
}
|
||||||
|
if (params && params.versionId === '') {
|
||||||
|
const versionId = generateVersionId();
|
||||||
|
objVal.versionId = versionId; // eslint-disable-line
|
||||||
|
metadata.keyMaps.get(bucketName).set(objName, objVal);
|
||||||
|
return cb(null, `{"versionId":"${objVal.versionId}"}`);
|
||||||
|
}
|
||||||
|
metadata.keyMaps.get(bucketName).set(objName, objVal);
|
||||||
|
return cb(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getBucketAndObject: (bucketName, objName, params, log, cb) => {
|
||||||
|
process.nextTick(() => {
|
||||||
|
metastore.getBucketAttributes(bucketName, log, (err, bucket) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err, { bucket });
|
||||||
|
}
|
||||||
|
if (params && params.versionId) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
objName = formatVersionKey(objName, params.versionId);
|
||||||
|
}
|
||||||
|
if (!metadata.keyMaps.has(bucketName)
|
||||||
|
|| !metadata.keyMaps.get(bucketName).has(objName)) {
|
||||||
|
return cb(null, { bucket: bucket.serialize() });
|
||||||
|
}
|
||||||
|
return cb(null, {
|
||||||
|
bucket: bucket.serialize(),
|
||||||
|
obj: JSON.stringify(
|
||||||
|
metadata.keyMaps.get(bucketName).get(objName)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getObject: (bucketName, objName, params, log, cb) => {
|
||||||
|
process.nextTick(() => {
|
||||||
|
metastore.getBucketAttributes(bucketName, log, err => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
if (params && params.versionId) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
objName = formatVersionKey(objName, params.versionId);
|
||||||
|
}
|
||||||
|
if (!metadata.keyMaps.has(bucketName)
|
||||||
|
|| !metadata.keyMaps.get(bucketName).has(objName)) {
|
||||||
|
return cb(errors.NoSuchKey);
|
||||||
|
}
|
||||||
|
return cb(null, metadata.keyMaps.get(bucketName).get(objName));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteObject: (bucketName, objName, params, log, cb) => {
|
||||||
|
process.nextTick(() => {
|
||||||
|
metastore.getBucketAttributes(bucketName, log, err => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
if (!metadata.keyMaps.get(bucketName).has(objName)) {
|
||||||
|
return cb(errors.NoSuchKey);
|
||||||
|
}
|
||||||
|
if (params && params.versionId) {
|
||||||
|
const baseKey = inc(formatVersionKey(objName, ''));
|
||||||
|
const vobjName = formatVersionKey(objName,
|
||||||
|
params.versionId);
|
||||||
|
metadata.keyMaps.get(bucketName).delete(vobjName);
|
||||||
|
const mst = metadata.keyMaps.get(bucketName).get(objName);
|
||||||
|
if (mst.versionId === params.versionId) {
|
||||||
|
const keys = [];
|
||||||
|
metadata.keyMaps.get(bucketName).forEach((val, key) => {
|
||||||
|
if (key < baseKey && key > vobjName) {
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (keys.length === 0) {
|
||||||
|
metadata.keyMaps.get(bucketName).delete(objName);
|
||||||
|
return cb();
|
||||||
|
}
|
||||||
|
const key = keys.sort()[0];
|
||||||
|
const value = metadata.keyMaps.get(bucketName).get(key);
|
||||||
|
metadata.keyMaps.get(bucketName).set(objName, value);
|
||||||
|
}
|
||||||
|
return cb();
|
||||||
|
}
|
||||||
|
metadata.keyMaps.get(bucketName).delete(objName);
|
||||||
|
return cb();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_hasDeleteMarker(key, keyMap) {
|
||||||
|
const objectMD = keyMap.get(key);
|
||||||
|
if (objectMD['x-amz-delete-marker'] !== undefined) {
|
||||||
|
return (objectMD['x-amz-delete-marker'] === true);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
listObject(bucketName, params, log, cb) {
|
||||||
|
process.nextTick(() => {
|
||||||
|
const {
|
||||||
|
prefix,
|
||||||
|
marker,
|
||||||
|
delimiter,
|
||||||
|
maxKeys,
|
||||||
|
continuationToken,
|
||||||
|
startAfter,
|
||||||
|
} = params;
|
||||||
|
if (prefix && typeof prefix !== 'string') {
|
||||||
|
return cb(errors.InvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (marker && typeof marker !== 'string') {
|
||||||
|
return cb(errors.InvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delimiter && typeof delimiter !== 'string') {
|
||||||
|
return cb(errors.InvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxKeys && typeof maxKeys !== 'number') {
|
||||||
|
return cb(errors.InvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (continuationToken && typeof continuationToken !== 'string') {
|
||||||
|
return cb(errors.InvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startAfter && typeof startAfter !== 'string') {
|
||||||
|
return cb(errors.InvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If paramMaxKeys is undefined, the default parameter will set it.
|
||||||
|
// However, if it is null, the default parameter will not set it.
|
||||||
|
let numKeys = maxKeys;
|
||||||
|
if (numKeys === null) {
|
||||||
|
numKeys = defaultMaxKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!metadata.keyMaps.has(bucketName)) {
|
||||||
|
return cb(errors.NoSuchBucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If marker specified, edit the keys array so it
|
||||||
|
// only contains keys that occur alphabetically after the marker
|
||||||
|
const listingType = params.listingType;
|
||||||
|
const extension = new list[listingType](params, log);
|
||||||
|
const listingParams = extension.genMDParams();
|
||||||
|
|
||||||
|
const keys = [];
|
||||||
|
metadata.keyMaps.get(bucketName).forEach((val, key) => {
|
||||||
|
if (listingParams.gt && listingParams.gt >= key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (listingParams.gte && listingParams.gte > key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (listingParams.lt && key >= listingParams.lt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (listingParams.lte && key > listingParams.lte) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return keys.push(key);
|
||||||
|
});
|
||||||
|
keys.sort();
|
||||||
|
|
||||||
|
// Iterate through keys array and filter keys containing
|
||||||
|
// delimiter into response.CommonPrefixes and filter remaining
|
||||||
|
// keys into response.Contents
|
||||||
|
for (let i = 0; i < keys.length; ++i) {
|
||||||
|
const currentKey = keys[i];
|
||||||
|
// Do not list object with delete markers
|
||||||
|
if (this._hasDeleteMarker(currentKey,
|
||||||
|
metadata.keyMaps.get(bucketName))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const objMD = metadata.keyMaps.get(bucketName).get(currentKey);
|
||||||
|
const value = JSON.stringify(objMD);
|
||||||
|
const obj = {
|
||||||
|
key: currentKey,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
// calling Ext.filter(obj) adds the obj to the Ext result if
|
||||||
|
// not filtered.
|
||||||
|
// Also, Ext.filter returns false when hit max keys.
|
||||||
|
// What a nifty function!
|
||||||
|
if (extension.filter(obj) < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cb(null, extension.result());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
listMultipartUploads(bucketName, listingParams, log, cb) {
|
||||||
|
process.nextTick(() => {
|
||||||
|
metastore.getBucketAttributes(bucketName, log, (err, bucket) => {
|
||||||
|
if (bucket === undefined) {
|
||||||
|
// no on going multipart uploads, return empty listing
|
||||||
|
return cb(null, {
|
||||||
|
IsTruncated: false,
|
||||||
|
NextMarker: undefined,
|
||||||
|
MaxKeys: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return getMultipartUploadListing(bucket, listingParams, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = metastore;
|
|
@ -3,7 +3,7 @@
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
},
|
},
|
||||||
"version": "7.10.13",
|
"version": "7.10.14",
|
||||||
"description": "Common utilities for the S3 project components",
|
"description": "Common utilities for the S3 project components",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"acl": {
|
||||||
|
"Canned": "private",
|
||||||
|
"FULL_CONTROL": [],
|
||||||
|
"WRITE": [],
|
||||||
|
"WRITE_ACP": [],
|
||||||
|
"READ": [],
|
||||||
|
"READ_ACP": []
|
||||||
|
},
|
||||||
|
"name": "BucketName",
|
||||||
|
"owner": "9d8fe19a78974c56dceb2ea4a8f01ed0f5fecb9d29f80e9e3b84104e4a3ea520",
|
||||||
|
"ownerDisplayName": "anonymousCoward",
|
||||||
|
"creationDate": "2018-06-04T17:45:42.592Z",
|
||||||
|
"mdBucketModelVersion": 8,
|
||||||
|
"transient": false,
|
||||||
|
"deleted": false,
|
||||||
|
"serverSideEncryption": null,
|
||||||
|
"versioningConfiguration": null,
|
||||||
|
"websiteConfiguration": null,
|
||||||
|
"locationConstraint": "us-east-1",
|
||||||
|
"readLocationConstraint": "us-east-1",
|
||||||
|
"cors": null,
|
||||||
|
"replicationConfiguration": null,
|
||||||
|
"lifecycleConfiguration": null,
|
||||||
|
"uid": "fea97818-6a9a-11e8-9777-e311618cc5d4"
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
const async = require('async');
|
||||||
|
const assert = require('assert');
|
||||||
|
const werelogs = require('werelogs');
|
||||||
|
const MetadataWrapper = require('../../../../../lib/storage/metadata/MetadataWrapper');
|
||||||
|
const fakeBucketInfo = require('./FakeBucketInfo.json');
|
||||||
|
|
||||||
|
describe('InMemory', () => {
|
||||||
|
const fakeBucket = 'fake';
|
||||||
|
const logger = new werelogs.Logger('Injector');
|
||||||
|
const memBackend = new MetadataWrapper(
|
||||||
|
'mem', {}, null, logger);
|
||||||
|
|
||||||
|
before(done => {
|
||||||
|
memBackend.createBucket(fakeBucket, fakeBucketInfo, logger, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(done => {
|
||||||
|
memBackend.deleteBucket(fakeBucket, logger, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('basic', done => {
|
||||||
|
async.waterfall([
|
||||||
|
next => {
|
||||||
|
memBackend.putObjectMD(fakeBucket, 'foo', 'bar', {}, logger, err => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
next => {
|
||||||
|
memBackend.getObjectMD(fakeBucket, 'foo', {}, logger, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
assert.deepEqual(data, 'bar');
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
next => {
|
||||||
|
memBackend.deleteObjectMD(fakeBucket, 'foo', {}, logger, err => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
next => {
|
||||||
|
memBackend.getObjectMD(fakeBucket, 'foo', {}, logger, err => {
|
||||||
|
if (err) {
|
||||||
|
assert.deepEqual(err.message, 'NoSuchKey');
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
return next(new Error('unexpected success'));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
], done);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue