v1.0.0: a diet s3 client

This commit is contained in:
AJ ONeal 2020-03-12 04:26:31 -06:00
commit 9a332a12d9
10 changed files with 657 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.env
node_modules
.*.sw*

4
.jshintrc Normal file
View File

@ -0,0 +1,4 @@
{ "node": true
, "browser": true
, "esversion": 8
}

8
.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"bracketSpacing": true,
"printWidth": 80,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "none",
"useTabs": false
}

41
LICENSE Normal file
View File

@ -0,0 +1,41 @@
Copyright 2020 AJ ONeal
This is open source software; you can redistribute it and/or modify it under the
terms of either:
a) the "MIT License"
b) the "Apache-2.0 License"
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Apache-2.0 License Summary
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

22
README.md Normal file
View File

@ -0,0 +1,22 @@
# [s3.js](https://git.rootprojects.org/root/s3.js) | a [Root](https://rootprojects.org) project
> Minimalist S3 client
A lightweight alternative to the s3 SDK that uses @root/request and aws4.
* set()
* get()
* head()
* delete()
```js
s3.set({
accessKeyId,
secretAccessKey,
region,
bucket,
prefix,
key,
body
})
```

208
index.js Normal file
View File

@ -0,0 +1,208 @@
'use strict';
var aws4 = require('aws4');
var request = require('@root/request');
var env = process.env;
module.exports = {
// HEAD
head: function({
accessKeyId,
secretAccessKey,
region,
bucket,
prefix,
key
}) {
// TODO support minio
/*
var awsHost = config.awsHost;
if (!awsHost) {
if (awsRegion) {
awsHost = awsHost || 's3.'+awsRegion+'.amazonaws.com';
} else {
// default
awsHost = 's3.amazonaws.com';
}
}
*/
/*
if (env.AWS_ACCESS_KEY) {
accessKeyId = accessKeyId || env.AWS_ACCESS_KEY;
secretAccessKey = secretAccessKey || env.AWS_SECRET_ACCESS_KEY;
bucket = bucket || env.AWS_BUCKET;
prefix = prefix || env.AWS_BUCKET_PREFIX;
region = region || env.AWS_REGION;
endpoint = endpoint || env.AWS_ENDPOINT;
}
*/
prefix = prefix || '';
if (prefix) {
// whatever => whatever/
// whatever/ => whatever/
prefix = prefix.replace(/\/?$/, '/');
}
var signed = aws4.sign(
{
// host: awsHost
service: 's3',
region: region,
path: '/' + bucket + '/' + prefix + key,
method: 'HEAD',
signQuery: true
},
{ accessKeyId: accessKeyId, secretAccessKey: secretAccessKey }
);
var url = 'https://' + signed.hostname + signed.path;
return request({ method: 'HEAD', url }).then(function(resp) {
if (200 === resp.statusCode) {
return resp;
}
var err = new Error(
'expected status 200 but got ' +
resp.statusCode +
'. See err.response for more info.'
);
err.url = url;
err.response = resp;
throw err;
});
},
// GET
get: function({
accessKeyId,
secretAccessKey,
region,
bucket,
prefix,
key,
json
}) {
prefix = prefix || '';
if (prefix) {
prefix = prefix.replace(/\/?$/, '/');
}
var signed = aws4.sign(
{
service: 's3',
region: region,
path: '/' + bucket + '/' + prefix + key,
method: 'GET',
signQuery: true
},
{ accessKeyId: accessKeyId, secretAccessKey: secretAccessKey }
);
var url = 'https://' + signed.hostname + signed.path;
// stay binary by default
var encoding = null;
if (json) {
encoding = undefined;
}
return request({ method: 'GET', url, encoding: null, json: json }).then(
function(resp) {
if (200 === resp.statusCode) {
return resp;
}
var err = new Error(
'expected status 200 but got ' +
resp.statusCode +
'. See err.response for more info.'
);
err.url = url;
err.response = resp;
throw err;
}
);
},
// PUT
set: function({
accessKeyId,
secretAccessKey,
region,
bucket,
prefix,
key,
body,
size
}) {
prefix = prefix || '';
if (prefix) {
prefix = prefix.replace(/\/?$/, '/');
}
var signed = aws4.sign(
{
service: 's3',
region: region,
path: '/' + bucket + '/' + prefix + key,
method: 'PUT',
signQuery: true
},
{ accessKeyId: accessKeyId, secretAccessKey: secretAccessKey }
);
var url = 'https://' + signed.hostname + signed.path;
var headers = {};
if ('undefined' !== typeof size) {
headers['Content-Length'] = size;
}
return request({ method: 'PUT', url, body, headers }).then(function(
resp
) {
if (200 === resp.statusCode) {
return resp;
}
var err = new Error(
'expected status 201 but got ' +
resp.statusCode +
'. See err.response for more info.'
);
err.url = url;
err.response = resp;
throw err;
});
},
// DELETE
del: function({
accessKeyId,
secretAccessKey,
region,
bucket,
prefix,
key
}) {
prefix = prefix || '';
if (prefix) {
prefix = prefix.replace(/\/?$/, '/');
}
var signed = aws4.sign(
{
service: 's3',
region: region,
path: '/' + bucket + '/' + prefix + key,
method: 'DELETE',
signQuery: true
},
{ accessKeyId: accessKeyId, secretAccessKey: secretAccessKey }
);
var url = 'https://' + signed.hostname + signed.path;
return request({ method: 'DELETE', url }).then(function(resp) {
if (204 === resp.statusCode) {
return resp;
}
var err = new Error(
'expected status 204 but got ' +
resp.statusCode +
'. See err.response for more info.'
);
err.url = url;
err.response = resp;
throw err;
});
}
};

24
package-lock.json generated Normal file
View File

@ -0,0 +1,24 @@
{
"name": "@root/s3",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@root/request": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.5.0.tgz",
"integrity": "sha512-J9RUIwVU99/cOVuDVYlNpr4G0A1/3ZxhCXIRiTZzu8RntOnb0lmDBMckhaus5ry9x/dBqJKDplFIgwHbLi6rLA=="
},
"aws4": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug=="
},
"dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
"dev": true
}
}
}

33
package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "@root/s3",
"version": "1.0.0",
"description": "A simple, lightweight s3 client with only 2 dependencies",
"main": "index.js",
"files": [
"lib"
],
"directories": {
"example": "examples"
},
"scripts": {
"test": "node test.js"
},
"repository": {
"type": "git",
"url": "https://git.rootprojects.org/root/s3.js.git"
},
"keywords": [
"s3",
"lightweight",
"alternative"
],
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "(MIT OR Apache-2.0)",
"dependencies": {
"@root/request": "^1.5.0",
"aws4": "^1.9.1"
},
"devDependencies": {
"dotenv": "^8.2.0"
}
}

BIN
test.bin Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

314
test.js Normal file
View File

@ -0,0 +1,314 @@
'use strict';
require('dotenv').config();
var env = process.env;
var s3 = require('./index.js');
var accessKeyId = env.AWS_ACCESS_KEY;
var secretAccessKey = env.AWS_SECRET_ACCESS_KEY;
var region = env.AWS_REGION;
var bucket = env.AWS_BUCKET;
var prefix = env.AWS_BUCKET_PREFIX;
var key = 'test-file';
var fs = require('fs');
async function run() {
// UPLOAD
//var testFile = __filename;
var testFile = 'test.bin';
var stat = fs.statSync(testFile);
var size = stat.size;
var stream = fs.createReadStream(testFile);
var file = fs.readFileSync(testFile);
await s3
.set({
accessKeyId,
secretAccessKey,
region,
bucket,
prefix,
key,
body: stream,
size
})
.then(function(resp) {
console.info('PASS: stream uploaded file');
return null;
})
.catch(function(err) {
console.error('Error:');
console.error('PUT Response:');
if (err.response) {
console.error(err.response.statusCode);
console.error(err.response.headers);
console.error(
(err.response.body && err.response.body) ||
JSON.stringify(err.response.body)
);
} else {
console.error(err);
}
process.exit(1);
});
// CHECK DOES EXIST
await s3
.head({
accessKeyId,
secretAccessKey,
region,
bucket,
prefix,
key
})
.then(function(resp) {
console.info('PASS: streamed file exists');
return null;
})
.catch(function(err) {
console.error('HEAD Response:');
if (err.response) {
console.error(err.response.statusCode);
console.error(err.response.headers);
} else {
console.error(err);
}
process.exit(1);
});
// GET STREAMED FILE
await s3
.get({
accessKeyId,
secretAccessKey,
region,
bucket,
prefix,
key
})
.then(function(resp) {
if (file.toString('binary') === resp.body.toString('binary')) {
console.info(
'PASS: streamed file downloaded with same contents'
);
return null;
}
throw new Error("file contents don't match");
})
.catch(function(err) {
console.error('Error:');
console.error('GET Response:');
if (err.response) {
console.error(err.response.statusCode);
console.error(err.response.headers);
} else {
console.error(err);
}
process.exit(1);
});
// DELETE TEST FILE
await s3
.del({
accessKeyId,
secretAccessKey,
region,
bucket,
prefix,
key
})
.then(function(resp) {
console.info('PASS: delete file');
return null;
})
.catch(function(err) {
console.error('Error:');
console.error('DELETE Response:');
if (err.response) {
console.error(err.response.statusCode);
console.error(err.response.headers);
} else {
console.error(err);
}
process.exit(1);
});
// SHOULD NOT EXIST
await s3
.head({
accessKeyId,
secretAccessKey,
region,
bucket,
prefix,
key
})
.then(function(resp) {
var err = new Error('file should not exist');
err.response = resp;
throw err;
})
.catch(function(err) {
if (err.response && 404 === err.response.statusCode) {
console.info('PASS: streamed file deleted');
return null;
}
console.error('Error:');
console.error('HEAD Response:');
if (err.response) {
console.error(err.response.statusCode);
console.error(err.response.headers);
} else {
console.error(err);
}
process.exit(1);
});
// CREATE WITHOUT STREAM
await s3
.set({
accessKeyId,
secretAccessKey,
region,
bucket,
prefix,
key,
body: file
})
.then(function(resp) {
console.info('PASS: one-shot upload');
return null;
})
.catch(function(err) {
console.error('Error:');
console.error('PUT Response:');
if (err.response) {
console.error(err.response.statusCode);
console.error(err.response.headers);
console.error(
(err.response.body && err.response.body) ||
JSON.stringify(err.response.body)
);
} else {
console.error(err);
}
process.exit(1);
});
// CHECK DOES EXIST
await s3
.head({
accessKeyId,
secretAccessKey,
region,
bucket,
prefix,
key
})
.then(function(resp) {
console.info('PASS: one-shot upload exists');
return null;
})
.catch(function(err) {
console.error('Error:');
console.error('HEAD Response:');
if (err.response) {
console.error(err.response.statusCode);
console.error(err.response.headers);
} else {
console.error(err);
}
process.exit(1);
});
// GET ONE-SHOT FILE
await s3
.get({
accessKeyId,
secretAccessKey,
region,
bucket,
prefix,
key
})
.then(function(resp) {
if (file.toString('binary') === resp.body.toString('binary')) {
console.info(
'PASS: one-shot file downloaded with same contents'
);
return null;
}
throw new Error("file contents don't match");
})
.catch(function(err) {
console.error('Error:');
console.error('GET Response:');
if (err.response) {
console.error(err.response.statusCode);
console.error(err.response.headers);
} else {
console.error(err);
}
process.exit(1);
});
// DELETE FILE
await s3
.del({
accessKeyId,
secretAccessKey,
region,
bucket,
prefix,
key
})
.then(function(resp) {
console.info('PASS: DELETE');
return null;
})
.catch(function(err) {
console.error('Error:');
console.error('DELETE Response:');
if (err.response) {
console.error(err.response.statusCode);
console.error(err.response.headers);
} else {
console.error(err);
}
process.exit(1);
});
// SHOULD NOT EXIST
await s3
.head({
accessKeyId,
secretAccessKey,
region,
bucket,
prefix,
key
})
.then(function(resp) {
var err = new Error('file should not exist');
err.response = resp;
throw err;
})
.catch(function(err) {
if (err.response && 404 === err.response.statusCode) {
console.info('PASS: streamed file deleted');
return null;
}
console.error('Error:');
console.error('HEAD Response:');
if (err.response) {
console.error(err.response.statusCode);
console.error(err.response.headers);
} else {
console.error(err);
}
process.exit(1);
});
}
run();