- 🚀 Vanilla JS + Only 1 dependency rock-req for HTTP requests.
- 🌎 Provide one or a list of storages credentials: the SDK will switch storage if something goes wrong (Server/DNS not responding, timeout, error 500, too many redirection, authentication error, and more...). As soon as the main storage is available, the SDK returns to the main storage
- ✨ If a request fails due to an authentication token expiration, the SDK fetches a new authentication token and retry the initial request with it (Concerns only Swift Storage).
- 🚩 When initialising the Tiny SDK client, provide only a list of S3 or a list of Swift credentials, switching from one storage system to another is not supported.
- ✅ Production battle-tested against hundreds of GBs of file uploads & downloads
you need a minimum of one object storage container, or you can synchronize Object Storages containers in order to access same objects if a fallback occur:
- Sync 2 containers:
1 <=> 2. They would both need to share the same secret synchronization key. - You can also set up a chain of synced containers if you want more than two. You would point
1 -> 2, then2 -> 3, and finally3 -> 1for three containers. They would all need to share the same secret synchronization key. Learn more on the OpenStack documentation or on the OVHCloud documentation.
Quick tutorial to synchronise 1 container into another with OVHCloud Object Storage (1 -> 2 one way sync)
- Install the
swift-pythonclient, an easy way to access Storages is with the Swift command line client, run on your terminal:
$ pip install python-swiftclient
- Download the OpenStack RC file on the OVH account to change environment variables. Tab
Public Cloud>Users & Roles> Pick the user and “Download OpenStack’s RC file” - Open a terminal, load the contents of the file into the current environment:
$ source openrc.sh- In order for the containers to identify themselves, a key must be created and then configured on each container:
$ sharedKey=$(openssl rand -base64 32)- See which region you are connected to:
env | grep OS_REGION- Retrieve the Account ID
AUTH_xxxxxxxof the destination container in order to configure the source container:
destContainer=$(swift --debug stat containerBHS 2>&1 | grep 'curl -i.*storage' | awk '{ print $4 }') && echo $destContainer- Change to the source region:
OS_REGION_NAME=RegionSource- Upload the key and the destination sync url to the source container:
$ swift post -t ‘//OVH_PUBLIC_CLOUD/RegionDestination/AUTH_xxxxxxxxx/containerNameDestination’ -k "$sharedKey" containerNameSource- You can check that this has been configured by using the following command:
$ swift stat containerName- You can check if the synchronization worked by listing the files in each of the containers:
$ OS_REGION_NAME=RegionSource && swift list containerName
$ OS_REGION_NAME=RegionDestination && swift list containerName$ npm install --save tiny-storage-client
// or
$ yarn add tiny-storage-clientInitialise the SDK with one or multiple storage, if something goes wrong, the next region will take over automatically. If any storage is available, an error message is returned Error: Object Storages are not available.
const storageClient = require('tiny-storage-client');
let storage = storageClient([{
authUrl : 'https://auth.cloud.ovh.net/v3', // REQUIRED
username : 'username-1', // REQUIRED
password : 'password-1', // REQUIRED
region : 'region-1', // REQUIRED
tenantName : 'tenantName-1' // OPTIONAL
},
{
authUrl : 'https://auth.cloud.ovh.net/v3',
username : 'username-2',
password : 'password-2',
region : 'region-2'
}]);
storage.connection((err) => {
if (err) {
// Invalid credentials
}
// Success, connected!
})const path = require('path');
/** SOLUTION 1: The file content can be passed by giving the file absolute path **/
storage.uploadFile('container', 'filename.jpg', path.join(__dirname, './assets/file.txt'), (err, resp) => {
if (err) {
// handle error
}
/**
* Response details:
* - resp.headers
* - resp.statusCode
* - resp.body
**/
});
/** SOLUTION 2: A Buffer can be passed for the file content **/
storage.uploadFile('container', 'filename.jpg', Buffer.from("File content"), (err, resp) => {
if (err) {
// handle error
}
/**
* Response details:
* - resp.headers
* - resp.statusCode
* - resp.body
**/
});
/** SOLUTION 3: Pass a function returning a ReadStream **/
storage.uploadFile('container', 'filename.jpg', () => fs.createReadStream(path.join(__dirname, './assets/file.txt')), (err, resp) => {
if (err) {
// handle error
}
/**
* Response details:
* - resp.headers
* - resp.statusCode
* - resp.body
**/
});
/** SOLUTION 4: the function accepts a optionnal fourth argument `option` including query parameters and headers. List of query parameters and headers: https://docs.openstack.org/api-ref/object-store/?expanded=create-or-replace-object-detail#create-or-replace-object **/
storage.uploadFile('container', 'filename.jpg', Buffer.from("File content"), { queries: { temp_url_expires: '1440619048' }, headers: { 'X-Object-Meta-LocationOrigin': 'Paris/France' }}, (err, resp) => {
if (err) {
// handle error
}
/**
* Response details:
* - resp.headers
* - resp.statusCode
* - resp.body
**/
});/** Solution 1: Download the file as Buffer **/
storage.downloadFile('containerName', 'filename.jpg', (err, resp) => {
if (err) {
// handle error
}
/**
* Response details:
* - resp.headers
* - resp.statusCode
* - resp.body => the content of the file as a Buffer
**/
});
/** Solution 2: Download the file as Stream, set the option `output` with a function returning the output Stream */
function createOutputStream(opts, res) {
const writer = fs.createWriteStream('2023-invoice.pdf')
writer.on('error', (e) => { /* clean up your stuff */ })
return writer
}
storage.downloadFile('containerName', '2023-invoice.pdf', { output: createOutputStream }, (err, resp) => {
if (err) {
return console.log("Error on download: ", err);
}
/**
* Request response:
* - resp.headers
* - resp.statusCode
*
* When the callback is called, the stream is closed and the file created,
* you don't have to pipe yourself!
*/
})storage.deleteFile('templates', 'filename.jpg', (err, resp) => {
if (err) {
// handle error
}
/**
* Response details:
* - resp.headers
* - resp.statusCode
* - resp.body
**/
});/**
* SOLUTION 1
**/
storage.listFiles('templates', function (err, resp) {
if (err) {
// handle error
}
/**
* Response details:
* - resp.headers
* - resp.statusCode
* - resp.body => List of objects as an Array, example:
* [
* {
* "bytes": 47560,
* "last_modified": "2020-12-03T10:14:40.049830",
* "hash": "bd3593e317e71fd9992405f29475afd4",
* "name": "invoice.pdf",
* "content_type": "application/pdf"
* }
* ]
**/
});
/**
* SOLUTION 2
* Possible to pass queries and overwrite request headers, list of options: https://docs.openstack.org/api-ref/object-store/? expanded=show-container-details-and-list-objects-detail#show-container-details-and-list-objects
**/
storage.listFiles('templates', { queries: { prefix: 'prefixName' }, headers: { Accept: 'application/xml' } }, function (err, resp) {
if (err) {
// handle error
}
/**
* Response details:
* - resp.headers
* - resp.statusCode
* - resp.body
**/
});Shows object metadata. Checkout the list of headers.
storage.getFileMetadata('templates', 'filename.jpg', (err, resp) => {
if (err) {
// handle error
}
/**
* Response details:
* - resp.headers
* - resp.statusCode
* - resp.body
**/
/**
* Returned headers `resp.headers`: {
* Content-Length: 14
* Accept-Ranges: bytes
* Last-Modified: Thu, 16 Jan 2014 21:12:31 GMT
* Etag: 451e372e48e0f6b1114fa0724aa79fa1
* X-Timestamp: 1389906751.73463
* X-Object-Meta-Book: GoodbyeColumbus
* Content-Type: application/octet-stream
* X-Trans-Id: tx37ea34dcd1ed48ca9bc7d-0052d84b6f
* X-Openstack-Request-Id: tx37ea34dcd1ed48ca9bc7d-0052d84b6f
* Date: Thu, 16 Jan 2014 21:13:19 GMT
* X-Object-Meta-Custom-Metadata-1: Value
* X-Object-Meta-Custom-Metadata-2: Value
* }
*
* Related documentation: https://docs.openstack.org/api-ref/object-store/?expanded=show-object-metadata-detail#show-object-metadata
*/
});To create or update custom metadata, use the "X-Object-Meta-name" header, where name is the name of the metadata item. The function overwrite all custom metadata applied on the file.
Checkout the list of headers availables.
storage.setFileMetadata('templates', 'filename.jpg', { headers: { 'Content-Type': 'image/jpeg', 'X-Object-Meta-LocationOrigin': 'Paris/France', 'X-Delete-At': 1440619048 }}, (err, resp) => {
if (err) {
// handle error
}
/**
* Response details:
* - resp.headers
* - resp.statusCode
* - resp.body
**/
});Bulk delete files (Maximum 10 000 objects per requests).
/**
* Provide a list of filenames as second argument, it can be:
* - a list of string ["object1.pdf", "object2.docx", "object3.pptx"]
* - a list of objects with `key` as attribute name [{ "key": "object1.pdf"}, { "key": "object2.docx" }, { "key": "object3.pptx" }]
* - Or a list of objects with `name` as attribute for the filename: [{ "name" : "file1.png" }, { "name": "file2.docx" }]
* - Or a list of objects with a custom Key for filenames, you must define `fileNameKey` as option (third argument).
*/
const filesToDelete = [ { name: '1685696359848.jpg' }, { name: 'template-column.docx' }, { name: 'test file |1234.odt' } ]
swift.deleteFiles('', filesToDelete, function(err, resp) {
if (err) {
// handle error
}
/**
* Request response:
* - resp.headers
* - resp.statusCode
* - resp.body => body as Object, example: {"Response Status":"200 OK","Response Body":"","Number Deleted":3,"Number Not Found":0,"Errors":[]}
**/
})Shows container metadata, including the number of objects and the total bytes of all objects stored in the container.
storage.headBucket('templates', (err, resp) => {
if (err) {
// handle error
}
/**
* Request response:
* - resp.headers
* - resp.statusCode
* - resp.body
**/
});Shows details for an account and lists containers, sorted by name, in the account.
storage.listBuckets((err, resp) => {
if (err) {
// handle error
}
/**
* Request response:
* - resp.headers
* - resp.statusCode
* - resp.body > The response body returns a list of containers as an Array, example:
* [
* {
* name: 'container1',
* count: 55,
* bytes: 106522,
* last_modified: '2022-01-12T14:02:33.672010'
* }
* ]
**/
})The request function can be used to request the object storage with custom options.
Prototype to get the data as Buffer:
request(method, path, { headers, queries, body }, (err, resp) => {}).Prototype to get the data as Stream
Set the option output with a function returning the output Stream.
When the callback is called, the stream is closed and the file created.
function createOutputStream(opts, res) {
const writer = fs.createWriteStream('file.pdf')
writer.on('error', (e) => { /* clean up your stuff */ })
return writer
}
request(method, path, { headers, queries, body, output: createOutputStream }, (err) => {})The base URL requests by default the account, passing an empty string will request the account details. For container requests, pass the container name, such as: /{container}. For file requests, pass the container and the file, such as: /{container}/{filename}. Object Storage Swift API specification: https://docs.openstack.org/api-ref/object-store/
The request function automatically reconnects to the Object Storage or switch storage if something goes wrong.
Example of custom request, bulk delete file from a customerDocuments container:
const _headers = {
'Content-Type': 'text/plain',
'Accept' : 'application/json'
}
storage.request('POST', '/customerDocuments?bulk-delete', { headers: _headers, body: 'file1\nfile2\n' }, (err, resp) => {
/**
* Response details:
* - resp.headers
* - resp.statusCode
* - resp.body => body as Javascript Object, example: {"Response Status":"200 OK","Response Body":"","Number Deleted":3,"Number Not Found":0,"Errors":[]}
**/
});By default, logs are printed with to console.log. You can use the setLogFunction to override the default log function. Create a function with two arguments: message as a string, level as a string and the value can be: info/warning/error. Example to use:
storage.setLogFunction((message, level) => {
console.log(`${level} : ${message}`);
})The default request timeout is 5 seconds, change it by calling setTimeout:
storage.setTimeout(30000); // 30 secondsTo simplify requests to custom named containers into different SWIFT providers, it is possible to create aliases by providing a buckets object on credentials. When calling a function, define the bucket alias as first argument, it will request the current active storage automatically.
const storageClient = require('tiny-storage-client')
const swift = storageClient([
/** SWIFT Storage 1 **/
{
username : '',
password : '',
authUrl : '',
region : '',
buckets : {
invoices: "invoices-ovh-gra",
www : "www-ovh-gra"
}
},
/** SWIFT Storage 2 **/
{
username : '',
password : '',
authUrl : '',
region : '',
buckets : {
invoices: "invoices-aws-paris",
www : "www-aws-paris"
}
}
])
/**
* On the following example, "downloadFile" will request the main storage "invoices-ovh-gra"
* or the backup "invoices-aws-paris" if something goes wrong.
*/
swift.downloadFile('invoices', '2023-invoice.pdf', (err, resp) => {
if (err) {
return console.log("Error on download: ", err);
}
/**
* Request response:
* - resp.body => downloaded file as Buffer
* - resp.headers
* - resp.statusCode
*/
})