node-swiftclient
Version:
A Node.js client library for interacting with OpenStack Swift Object Storage
1,009 lines (987 loc) • 35 kB
JavaScript
'use strict';
var stream = require('stream');
/* eslint-disable @typescript-eslint/no-unused-vars */
function extractUTCOffset(dateString) {
try {
if (dateString.endsWith('Z')) {
return 0;
}
const offsetRegex = /([+-])(\d{2}):?(\d{2})?/;
const match = dateString.match(offsetRegex);
if (match) {
const sign = match[1] === '+' ? 1 : -1;
const hours = parseInt(match[2], 10);
const minutes = match[3] ? parseInt(match[3], 10) : 0;
return sign * (hours * 60 + minutes);
}
if (dateString.includes('GMT')) {
const gmtRegex = /GMT([+-]\d{2}):?(\d{2})?/;
const gmtMatch = dateString.match(gmtRegex);
if (gmtMatch) {
const sign = gmtMatch[1][0] === '+' ? 1 : -1;
const hours = parseInt(gmtMatch[1].slice(1), 10);
const minutes = gmtMatch[2] ? parseInt(gmtMatch[2], 10) : 0;
return sign * (hours * 60 + minutes);
}
}
} catch (error) {
/** noop */
}
return 0;
}
function getServerDateTimeOffset(response) {
var _a;
try {
const serverDateTimeStr = (_a = response.headers.get('Date') || response.headers.get('Last-Modified')) !== null && _a !== void 0 ? _a : null;
const timezoneOffsetMinutes = serverDateTimeStr ? extractUTCOffset(serverDateTimeStr) : new Date().getTimezoneOffset();
const sign = timezoneOffsetMinutes <= 0 ? '+' : '-';
const absOffset = Math.abs(timezoneOffsetMinutes);
const hours = String(Math.floor(absOffset / 60)).padStart(2, '0');
const minutes = String(absOffset % 60).padStart(2, '0');
const timezoneOffset = `${sign}${hours}:${minutes}`; // e.g., "+02:00"
return timezoneOffset;
} catch (error) {
return '+00:00';
}
}
function parseDateWithServerTimezone(dateString, serverTimezoneOffset) {
try {
if (hasTimezone(dateString)) {
return new Date(dateString);
}
const dateWithSameTZ = `${dateString}${serverTimezoneOffset}`;
return new Date(dateWithSameTZ);
} catch (error) {
return new Date();
}
}
function hasTimezone(dateString) {
const timezoneRegex = /(Z|[+-]\d{2}:\d{2}|UTC|GMT)/i;
return timezoneRegex.test(dateString);
}
/**
* Fetch with a timeout using native fetch and AbortController, with retry support.
* @param url - URL to fetch
* @param options - Fetch options
* @param timeout - Timeout in milliseconds (default: 15000ms)
* @param retries - Number of retry attempts (default: 3)
* @returns A Promise that resolves with the Response or rejects on timeout/error
*/
async function fetchWithTimeout(url, options = {}, timeout = 30000, retries = 3) {
for (let attempt = 0; attempt <= retries; attempt++) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, Object.assign(Object.assign({}, options), {
signal: controller.signal
}));
clearTimeout(id);
return response;
} catch (error) {
clearTimeout(id);
const isLastAttempt = attempt === retries;
const isAbortError = error.name === 'AbortError';
if (isLastAttempt) {
if (isAbortError) {
throw new Error(`Request to ${typeof url === 'string' ? url : url.url} timed out after ${timeout} ms (after ${retries + 1} attempts)`);
}
throw error;
}
// Optionally wait before retrying
await new Promise(resolve => setTimeout(resolve, 100));
}
}
// Should never reach here
throw new Error('Unexpected error in fetchWithTimeout');
}
const wait = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
async function tryAuthentication(authenticator) {
let success = false;
let retries = 0;
let lastError = '';
while (!success && retries < 3) {
try {
const auth = await authenticator.authenticate();
return auth;
} catch (error) {
success = false;
lastError = error;
}
await wait(500);
retries++;
}
Promise.reject(lastError);
}
class SwiftEntity {
constructor(childName, urlSuffix, authenticator) {
this.childName = childName;
this.urlSuffix = urlSuffix ? `/${urlSuffix}` : '';
this.authenticator = authenticator;
}
async list(query, extraHeaders) {
const listQuery = Object.assign({
format: 'json'
}, query ? query : {});
const querystring = '?' + new URLSearchParams(listQuery).toString();
const auth = await tryAuthentication(this.authenticator);
const response = await fetchWithTimeout(auth.url + this.urlSuffix + querystring, {
headers: this.getHeaders(null, extraHeaders, auth.token)
});
if (!response.ok) throw new Error(`Error fetching list: ${response.statusText}`);
const objects = await response.json();
const serverTimezoneOffset = getServerDateTimeOffset(response);
for (const object of objects) {
if (typeof object.last_modified === 'string') {
object.last_modified = parseDateWithServerTimezone(object.last_modified, serverTimezoneOffset);
}
}
return objects;
}
async update(name, meta, extraHeaders) {
const auth = await tryAuthentication(this.authenticator);
const response = await fetchWithTimeout(`${auth.url + this.urlSuffix}/${name}`, {
method: 'POST',
headers: this.getHeaders(meta, extraHeaders, auth.token)
});
if (!response.ok) throw new Error(`Error updating ${name}: ${response.statusText}`);
return;
}
async getMeta(name) {
const auth = await tryAuthentication(this.authenticator);
const response = await fetchWithTimeout(`${auth.url + this.urlSuffix}/${name}`, {
method: 'HEAD',
headers: this.getHeaders(null, null, auth.token)
});
if (!response.ok) throw new Error(`Error fetching metadata for ${name}: ${response.statusText}`);
const meta = {};
const headers = response.headers;
const regex = new RegExp(`^X-${this.childName}-Meta-(.*)$`, 'i');
headers.forEach((value, key) => {
const match = key.match(regex);
if (match) meta[match[1]] = value;
});
return meta;
}
async delete(name) {
const auth = await tryAuthentication(this.authenticator);
const response = await fetch(`${auth.url + this.urlSuffix}/${name}`, {
method: 'DELETE',
headers: this.getHeaders(null, null, auth.token)
});
if (!response.ok) throw new Error(`Error deleting ${name}: ${response.statusText}`);
}
getHeaders(meta, extraHeaders, token) {
const headers = Object.assign({
'x-auth-token': token,
accept: 'application/json'
}, extraHeaders);
if (meta != null) {
for (const k in meta) {
if (Object.prototype.hasOwnProperty.call(meta, k)) {
headers[`X-${this.childName}-Meta-${k}`] = meta[k];
}
}
}
return headers;
}
}
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __await(v) {
return this instanceof __await ? (this.v = v, this) : new __await(v);
}
function __asyncGenerator(thisArg, _arguments, generator) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var g = generator.apply(thisArg, _arguments || []), i, q = [];
return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
function fulfill(value) { resume("next", value); }
function reject(value) { resume("throw", value); }
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
const invalidListConfigErr = 'Invalid filter configuration: The "delimiter" option cannot be used without specifying a "prefix". ' + 'If you intend to query object folders, please use the "listObjectFolders" function instead.';
class SwiftCommonContainer extends SwiftEntity {
constructor(containerName, authenticator) {
super('Object', containerName, authenticator);
}
async listObjects(options, additionalQueryParams, extraHeaders) {
if (typeof (options === null || options === void 0 ? void 0 : options.delimiter) === 'string' && !options.prefix) {
throw new Error(invalidListConfigErr);
}
let queryParams = {};
if (additionalQueryParams) {
queryParams = Object.assign({}, additionalQueryParams);
}
if (options) {
if (options.marker) {
queryParams.marker = options.marker;
}
if (options.end_marker) {
queryParams.end_marker = options.end_marker;
}
if (typeof options.reverse === 'boolean') {
queryParams.reverse = `${options.reverse}`;
}
if (typeof options.limit === 'number') {
queryParams.limit = `${Math.round(options.limit)}`;
}
if (options.delimiter) {
queryParams.delimiter = options.delimiter;
}
if (this.hasPrefix(options)) {
if (options.delimiter) {
queryParams.delimiter = options.delimiter;
} else {
queryParams.delimiter = '/';
}
if (options.prefix) {
queryParams.prefix = this.ensureTrailingDelimiter(options.prefix, queryParams.delimiter);
}
}
}
return this.list(queryParams, extraHeaders);
}
async listObjectFolders(options, additionalQueryParams, extraHeaders) {
var _a;
const queryParams = additionalQueryParams ? Object.assign({}, additionalQueryParams) : {};
queryParams.delimiter = (options === null || options === void 0 ? void 0 : options.delimiter) || '/';
if (options) {
if (options.marker) {
queryParams.marker = this.ensureTrailingDelimiter(options.marker, queryParams.delimiter);
}
if (options.end_marker) {
queryParams.end_marker = options.end_marker;
}
if (typeof options.reverse === 'boolean') {
queryParams.reverse = `${options.reverse}`;
}
if (typeof options.limit === 'number') {
queryParams.limit = `${Math.round(options.limit)}`;
}
}
const result = await this.list(queryParams, extraHeaders);
if (result.length > 0 && (!((_a = result[0]) === null || _a === void 0 ? void 0 : _a.subdir) || typeof result[0].subdir !== 'string')) {
throw new Error('Result of query is not in swift subdir format. Please try another delimiter!');
}
return result;
}
async getObjectMeta(objectName) {
return this.getMeta(objectName);
}
iterateObjects(options, additionalQueryParams, extraHeaders) {
return __asyncGenerator(this, arguments, function* iterateObjects_1() {
var _a;
if (typeof (options === null || options === void 0 ? void 0 : options.delimiter) === 'string' && !options.prefix) {
throw new Error(invalidListConfigErr);
}
const batchSize = (_a = options === null || options === void 0 ? void 0 : options.batchSize) !== null && _a !== void 0 ? _a : 10000;
let marker = undefined;
let isCompleted = false;
let lastKey = '';
while (!isCompleted) {
const objects = yield __await(this.listObjects({
limit: batchSize,
marker: marker,
prefix: options === null || options === void 0 ? void 0 : options.prefix,
delimiter: options === null || options === void 0 ? void 0 : options.delimiter
}, additionalQueryParams, extraHeaders));
if (objects.length === 0) {
isCompleted = true;
break;
}
const keys = this.getTopObjectKeys(objects);
if (keys === lastKey) {
isCompleted = true;
break;
}
lastKey = keys;
for (const object of objects) {
yield yield __await(object);
}
isCompleted = objects.length < batchSize;
marker = objects[objects.length - 1].name;
}
});
}
iterateObjectFolders(options, additionalQueryParams, extraHeaders) {
return __asyncGenerator(this, arguments, function* iterateObjectFolders_1() {
var _a;
const batchSize = (_a = options === null || options === void 0 ? void 0 : options.batchSize) !== null && _a !== void 0 ? _a : 10000;
let marker = undefined;
let isCompleted = false;
let lastKey = '';
while (!isCompleted) {
const subDir = yield __await(this.listObjectFolders({
limit: batchSize,
marker: marker,
delimiter: options === null || options === void 0 ? void 0 : options.delimiter
}, additionalQueryParams, extraHeaders));
if (subDir.length === 0) {
isCompleted = true;
break;
}
const keys = this.getTopObjectKeysSubdir(subDir);
if (keys === lastKey) {
isCompleted = true;
break;
}
lastKey = keys;
for (const object of subDir) {
yield yield __await(object);
}
isCompleted = subDir.length < batchSize;
marker = subDir[subDir.length - 1].subdir;
}
});
}
async patchObjectMeta(name, meta, extraHeaders) {
await this.update(name, meta !== null && meta !== void 0 ? meta : null, extraHeaders !== null && extraHeaders !== void 0 ? extraHeaders : null);
}
async putObject(objectName, streamOrBuffer, meta, extraHeaders) {
let stream;
if (Buffer.isBuffer(streamOrBuffer)) {
stream = this.bufferToStream(streamOrBuffer);
} else {
stream = streamOrBuffer;
}
const auth = await tryAuthentication(this.authenticator);
const headers = this.getHeaders(meta !== null && meta !== void 0 ? meta : null, extraHeaders !== null && extraHeaders !== void 0 ? extraHeaders : null, auth.token);
const url = `${auth.url + this.urlSuffix}/${objectName}`;
const duplex = {
duplex: 'half'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
};
// Using Fetch Streaming API
const req = new Request(url, Object.assign({
method: 'PUT',
headers: headers,
body: stream
}, duplex));
const response = await fetch(req);
if (response.status < 200 || response.status >= 300) {
throw new Error(`HTTP ${response.status}`);
}
}
async deleteObject(objectName, when) {
if (when) {
const headers = {};
if (when instanceof Date) {
headers['X-Delete-At'] = Math.floor(when.getTime() / 1000).toString();
} else if (typeof when === 'number') {
headers['X-Delete-After'] = when.toString();
} else {
throw new Error('Expected `when` to be a number of seconds or a Date object');
}
const auth = await tryAuthentication(this.authenticator);
const response = await fetch(`${auth.url + this.urlSuffix}/${objectName}`, {
method: 'POST',
headers: this.getHeaders(null, headers, auth.token)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
} else {
await super.delete(objectName);
}
}
async getObject(objectName) {
const auth = await tryAuthentication(this.authenticator);
const response = await fetch(`${auth.url + this.urlSuffix}/${objectName}`, {
method: 'GET',
headers: {
'x-auth-token': auth.token
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
if (response.body) {
return response.body.getReader();
} else {
throw new Error('Response does not have a body');
}
}
async getObjectAsBuffer(objectName) {
const auth = await tryAuthentication(this.authenticator);
const response = await fetch(`${auth.url + this.urlSuffix}/${objectName}`, {
method: 'GET',
headers: {
'x-auth-token': auth.token
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
if (response.body) {
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
return buffer;
} else {
throw new Error('Response does not have a body');
}
}
async getObjectInfo(objectName) {
const auth = await tryAuthentication(this.authenticator);
const response = await fetchWithTimeout(`${auth.url + this.urlSuffix}/${objectName}`, {
method: 'HEAD',
headers: {
'x-auth-token': auth.token
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
let byteLength = -1;
try {
byteLength = parseInt(response.headers.get('Content-Length') || '0');
} catch (error) {
/** noop */
}
const serverTimezoneOffset = getServerDateTimeOffset(response);
return {
bytes: byteLength,
last_modified: response.headers.get('Last-Modified') ? parseDateWithServerTimezone(response.headers.get('Last-Modified'), serverTimezoneOffset) : new Date(),
name: objectName,
content_type: response.headers.get('Content-Type'),
hash: response.headers.get('Etag')
};
}
getTopObjectKeys(objects) {
let keys = '';
const len = Math.min(objects.length, 10);
for (let index = 0; index < len; index++) {
keys += objects[index].name + '_';
}
return keys;
}
getTopObjectKeysSubdir(objects) {
let keys = '';
const len = Math.min(objects.length, 10);
for (let index = 0; index < len; index++) {
keys += objects[index].subdir + '_';
}
return keys;
}
bufferToStream(buffer) {
return new stream.Readable({
read() {
this.push(buffer);
this.push(null);
}
});
}
ensureTrailingDelimiter(inputStr, delimiter) {
const str = inputStr.trim();
if (str.charAt(str.length - 1) !== delimiter) {
return str + delimiter;
}
return str;
}
hasPrefix(options) {
const opt = options;
return typeof opt.prefix === 'string' && opt.prefix.trim().length > 0;
}
}
const AUTH_STATUS = {
UNAUTHENTICATED: 0,
AUTHENTICATED: 1,
FAILED: 2
};
class SwiftAuthenticatorV1 {
constructor(authUrl, username, password, tenant) {
this.authUrl = authUrl;
this.username = username;
this.password = password;
this.tenant = tenant;
// Authentication process flags
this.authStatus = AUTH_STATUS.UNAUTHENTICATED;
this.authError = null;
}
async runAuth() {
try {
const res = await fetchWithTimeout(this.authUrl, {
headers: {
'x-auth-user': this.tenant === null ? this.username : this.tenant + ':' + this.username,
'x-auth-key': this.password
}
});
this.url = res.headers.get('x-storage-url').replace(new RegExp('//', 'g'), '/');
this.token = res.headers.get('x-auth-token');
this.authStatus = AUTH_STATUS.AUTHENTICATED;
} catch (error) {
this.authStatus = AUTH_STATUS.FAILED;
this.authError = error;
this.url = '';
this.token = '';
}
}
async _authenticate() {
switch (this.authStatus) {
case AUTH_STATUS.UNAUTHENTICATED:
await this.runAuth();
return this._authenticate();
case AUTH_STATUS.AUTHENTICATED:
return {
url: this.url,
token: this.token
};
case AUTH_STATUS.FAILED:
await this.runAuth();
{
return {
url: this.url,
token: this.token
};
}
}
return Promise.reject(this.authError);
}
async authenticate() {
return this._authenticate();
}
}
class V2Auth {
constructor(useApiKey) {
this.auth = null;
this.region = '';
this.useApiKey = useApiKey;
this.useApiKeyOk = false;
this.notFirst = false;
}
async request(connection) {
this.region = connection.region;
if (this.notFirst && !this.useApiKeyOk) {
this.useApiKey = !this.useApiKey;
}
this.notFirst = true;
let body;
if (!this.useApiKey) {
body = {
auth: {
passwordCredentials: {
username: connection.userName,
password: connection.apiKey
},
tenantName: connection.tenant || undefined,
tenantId: connection.tenantId || undefined
}
};
} else {
body = {
auth: {
'RAX-KSKEY:apiKeyCredentials': {
username: connection.userName,
apiKey: connection.apiKey
},
tenantName: connection.tenant || undefined,
tenantId: connection.tenantId || undefined
}
};
}
let url = connection.authUrl;
if (!url.endsWith('/')) url += '/';
url += 'tokens';
const response = await fetchWithTimeout(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': connection.userAgent
},
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(`Authentication request failed with status: ${response.status}`);
}
return response;
}
async response(response) {
try {
const jsonResponse = await response.json();
this.auth = jsonResponse;
this.useApiKeyOk = true;
} catch (err) {
throw new Error('Failed to parse authentication response: ' + err.message);
}
}
endpointUrl(type, endpointType) {
var _a, _b;
const catalog = ((_b = (_a = this.auth) === null || _a === void 0 ? void 0 : _a.access) === null || _b === void 0 ? void 0 : _b.serviceCatalog) || [];
for (const service of catalog) {
if (service.type === type) {
for (const endpoint of service.endpoints) {
if (this.region == undefined || this.region === null || this.region === endpoint.region) {
switch (endpointType) {
case 'internal':
return endpoint.internalURL || endpoint.internalUrl;
case 'public':
return endpoint.publicURL || endpoint.publicUrl;
case 'admin':
return endpoint.adminURL || endpoint.adminUrl;
default:
return '';
}
}
}
}
}
return '';
}
storageUrl(isInternal = false) {
const endpointType = isInternal ? 'internal' : 'public';
const url = this.endpointUrl('object-store', endpointType);
return url.replace(new RegExp('//', 'g'), '/');
}
token() {
var _a, _b, _c;
return ((_c = (_b = (_a = this.auth) === null || _a === void 0 ? void 0 : _a.access) === null || _b === void 0 ? void 0 : _b.token) === null || _c === void 0 ? void 0 : _c.id) || '';
}
expires() {
var _a, _b, _c;
const expires = (_c = (_b = (_a = this.auth) === null || _a === void 0 ? void 0 : _a.access) === null || _b === void 0 ? void 0 : _b.token) === null || _c === void 0 ? void 0 : _c.expires;
if (!expires) return null;
return new Date(expires);
}
cdnUrl() {
return this.endpointUrl('rax:object-cdn', 'public');
}
}
class SwiftAuthenticatorV2 {
constructor(connection) {
this.connection = connection;
this.auth = new V2Auth(
// Guess as to whether using API key or
// password it will try both eventually so
// this is just an optimization.
connection.apiKey.length >= 32);
}
async authenticate() {
const response = await this.auth.request(this.connection);
await this.auth.response(response);
return {
url: this.auth.storageUrl(process.env.SWIFT_INTERNAL === 'true'),
token: this.auth.token()
};
}
}
const V3_AUTH_METHODS = {
TOKEN: 'token',
PASSWORD: 'password',
APPLICATION_CREDENTIAL: 'application_credential'
};
var EndpointType;
(function (EndpointType) {
EndpointType["Public"] = "public";
EndpointType["Internal"] = "internal";
EndpointType["Admin"] = "admin";
})(EndpointType || (EndpointType = {}));
class SwiftAuthenticatorV3 {
constructor(config) {
this.config = config;
this.region = config.region;
}
async authenticate() {
const authRequest = this.buildAuthRequest();
try {
const response = await fetchWithTimeout(this.buildAuthUrl(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': this.config.userAgent || 'NodeJS OpenStack Client'
},
body: JSON.stringify(authRequest)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
this.responseHeaders = response.headers;
this.authResponse = await response.json();
return {
token: this.getToken(),
url: this.getStorageUrl(process.env.SWIFT_INTERNAL === 'true')
};
} catch (error) {
throw new Error(`Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
buildAuthUrl() {
let url = this.config.authUrl;
if (!url.endsWith('/')) {
url += '/';
}
return `${url}auth/tokens`;
}
buildAuthRequest() {
const authRequest = {
auth: {
identity: {
methods: []
}
}
};
if ((this.config.applicationCredentialId || this.config.applicationCredentialName) && this.config.applicationCredentialSecret) {
let user;
if (this.config.applicationCredentialId) {
user = {};
}
if (!user && this.config.userId) {
user = {
id: this.config.userId
};
}
if (!user && !this.config.userName) {
throw new Error('UserID or Name should be provided');
}
if (!user && this.config.domainId) {
user = {
name: this.config.userName,
domain: {
id: this.config.domainId
}
};
}
if (!user && this.config.domain) {
user = {
name: this.config.userName,
domain: {
name: this.config.domain
}
};
}
if (!user) {
throw new Error('DomainID or Domain should be provided');
}
authRequest.auth.identity.methods = [V3_AUTH_METHODS.APPLICATION_CREDENTIAL];
authRequest.auth.identity.applicationCredential = {
id: this.config.applicationCredentialId,
name: this.config.applicationCredentialName,
secret: this.config.applicationCredentialSecret,
user
};
} else if (!this.config.userName && !this.config.userId) {
authRequest.auth.identity.methods = [V3_AUTH_METHODS.TOKEN];
authRequest.auth.identity.token = {
id: this.config.apiKey
};
} else {
authRequest.auth.identity.methods = [V3_AUTH_METHODS.PASSWORD];
const user = {
name: this.config.userName,
id: this.config.userId,
password: this.config.apiKey
};
let domain;
if (this.config.domain) {
domain = {
name: this.config.domain
};
} else if (this.config.domainId) {
domain = {
id: this.config.domainId
};
}
user.domain = domain;
authRequest.auth.identity.password = {
user
};
}
if (authRequest.auth.identity.methods[0] !== V3_AUTH_METHODS.APPLICATION_CREDENTIAL) {
if (this.config.trustId) {
authRequest.auth.scope = {
trust: {
id: this.config.trustId
}
};
} else if (this.config.tenantId || this.config.tenant) {
authRequest.auth.scope = {
project: {}
};
if (this.config.tenantId) {
authRequest.auth.scope.project.id = this.config.tenantId;
} else if (this.config.tenant) {
authRequest.auth.scope.project.name = this.config.tenant;
if (this.config.tenantDomain) {
authRequest.auth.scope.project.domain = {
name: this.config.tenantDomain
};
} else if (this.config.tenantDomainId) {
authRequest.auth.scope.project.domain = {
id: this.config.tenantDomainId
};
} else if (this.config.domain) {
authRequest.auth.scope.project.domain = {
name: this.config.domain
};
} else if (this.config.domainId) {
authRequest.auth.scope.project.domain = {
id: this.config.domainId
};
} else {
authRequest.auth.scope.project.domain = {
name: 'Default'
};
}
}
}
}
return authRequest;
}
getStorageUrl(internal = false) {
const endpointType = internal ? EndpointType.Internal : EndpointType.Public;
return this.getStorageUrlForEndpoint(endpointType);
}
getStorageUrlForEndpoint(endpointType) {
if (!this.authResponse) {
throw new Error('Authentication not performed');
}
const objectStoreCatalog = this.authResponse.token.catalog.find(catalog => catalog.type === 'object-store');
if (!objectStoreCatalog) {
return '';
}
const endpoint = objectStoreCatalog.endpoints.find(ep => ep.interface === endpointType && (!this.region || ep.region === this.region));
//replace double slashes
const endpoint2 = (endpoint ? endpoint.url : '').replace(new RegExp('//', 'g'), '/');
return endpoint2;
}
getToken() {
if (!this.responseHeaders) {
throw new Error('Authentication not performed');
}
return this.responseHeaders.get('x-subject-token') || '';
}
getExpires() {
if (!this.authResponse) {
throw new Error('Authentication not performed');
}
return new Date(this.authResponse.token.expires_at);
}
getCdnUrl() {
return '';
}
}
class UnsupportedAuthenticator {
constructor(authVersion) {
this.authVersion = authVersion;
}
authenticate() {
throw new Error(`Auth version ${this.authVersion} not supported`);
}
}
function getAuthenticatorForVersion(config) {
var _a;
switch (config.authVersion) {
case 1:
return new SwiftAuthenticatorV1(config.authUrl, config.userName, config.password, (_a = config.tenant) !== null && _a !== void 0 ? _a : null);
case 2:
return new SwiftAuthenticatorV2(config);
case 3:
return new SwiftAuthenticatorV3(config);
default:
return new UnsupportedAuthenticator(config.authVersion);
}
}
/**
* Represents the main client for interacting with a Swift storage service.
* Provides methods to manage containers and objects within those containers.
*/
class SwiftClient {
constructor(config) {
this.sw = new SwiftEntity('Container', null, getAuthenticatorForVersion(config));
}
/**
* Creates a new container in the Swift storage.
* @param containerName - The name of the container to create.
* @param publicRead - Whether the container should be publicly readable.
* @param meta - Optional metadata to associate with the container.
* @param extraHeaders - Optional extra headers to include in the request.
* @returns A promise that resolves when the container is successfully created.
*/
async createContainer(containerName, publicRead, meta, extraHeaders) {
if (typeof publicRead === 'undefined') {
publicRead = false;
}
if (publicRead) {
if (!extraHeaders) extraHeaders = {};
extraHeaders['x-container-read'] = '.r:*';
}
const auth = await tryAuthentication(this.sw.authenticator);
const req = new Request(`${auth.url}/${containerName}`, {
method: 'PUT',
headers: this.sw.getHeaders(meta !== null && meta !== void 0 ? meta : null, extraHeaders !== null && extraHeaders !== void 0 ? extraHeaders : null, auth.token)
});
const response = await fetchWithTimeout(req);
if (response.status < 200 || response.status >= 300) {
throw new Error(`HTTP ${response.status}`);
}
}
/**
* Retrieves information about the client configuration or state.
* @returns A promise resolving with client information.
*/
async getClientInfo() {
const auth = await tryAuthentication(this.sw.authenticator);
const infoUrl = new URL(auth.url).origin + '/info';
const response = await fetchWithTimeout(infoUrl, {
method: 'GET',
headers: {
'x-auth-token': auth.token
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
/**
* Fetches metadata for a specified container.
* @param containerName - The name of the container.
* @returns A promise resolving with the container's metadata as a key-value object.
*/
getContainerMeta(containerName) {
return this.sw.getMeta(containerName);
}
/**
* Deletes a specified container.
* @param containerName - The name of the container to delete.
* @returns A promise that resolves when the container is successfully deleted.
*/
deleteContainer(containerName) {
return this.sw.delete(containerName);
}
/**
* Lists all containers accessible to the client.
* @param query - Optional query parameters as a string or key-value pairs.
* @param extraHeaders - Optional extra headers to include in the request.
* @returns A promise resolving with an array of container data.
*/
async listAllContainers(query, extraHeaders) {
const containers = await this.sw.list(query, extraHeaders);
return containers;
}
/**
* Retrieves an interface to interact with a specific container.
* @param containerName - The name of the container.
* @returns A SwiftContainer instance for interacting with the container.
*/
getContainer(containerName) {
return new SwiftCommonContainer(containerName, this.sw.authenticator);
}
}
exports.SwiftClient = SwiftClient;