radosgw-admin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +182 -0
- package/README.md +411 -0
- package/dist/index.cjs +1903 -0
- package/dist/index.d.cts +1599 -0
- package/dist/index.d.ts +1599 -0
- package/dist/index.js +1869 -0
- package/package.json +105 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1903 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
BaseClient: () => BaseClient,
|
|
24
|
+
RGWAuthError: () => RGWAuthError,
|
|
25
|
+
RGWConflictError: () => RGWConflictError,
|
|
26
|
+
RGWError: () => RGWError,
|
|
27
|
+
RGWNotFoundError: () => RGWNotFoundError,
|
|
28
|
+
RGWValidationError: () => RGWValidationError,
|
|
29
|
+
RadosGWAdminClient: () => RadosGWAdminClient,
|
|
30
|
+
signRequest: () => signRequest
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/signer.ts
|
|
35
|
+
var import_node_crypto = require("crypto");
|
|
36
|
+
var ALGORITHM = "AWS4-HMAC-SHA256";
|
|
37
|
+
var SERVICE = "s3";
|
|
38
|
+
var UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
|
|
39
|
+
function sha256(data) {
|
|
40
|
+
return (0, import_node_crypto.createHash)("sha256").update(data).digest("hex");
|
|
41
|
+
}
|
|
42
|
+
function hmacSha256(key, data) {
|
|
43
|
+
return (0, import_node_crypto.createHmac)("sha256", key).update(data).digest();
|
|
44
|
+
}
|
|
45
|
+
function hmacSha256Hex(key, data) {
|
|
46
|
+
return (0, import_node_crypto.createHmac)("sha256", key).update(data).digest("hex");
|
|
47
|
+
}
|
|
48
|
+
function getDateStamp(date) {
|
|
49
|
+
return date.toISOString().replace(/[-:]/g, "").slice(0, 8);
|
|
50
|
+
}
|
|
51
|
+
function getAmzDate(date) {
|
|
52
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
|
|
53
|
+
}
|
|
54
|
+
function getSigningKey(secretKey, dateStamp, region) {
|
|
55
|
+
const kDate = hmacSha256(`AWS4${secretKey}`, dateStamp);
|
|
56
|
+
const kRegion = hmacSha256(kDate, region);
|
|
57
|
+
const kService = hmacSha256(kRegion, SERVICE);
|
|
58
|
+
return hmacSha256(kService, "aws4_request");
|
|
59
|
+
}
|
|
60
|
+
function getCanonicalQueryString(url) {
|
|
61
|
+
const params = Array.from(url.searchParams.entries());
|
|
62
|
+
params.sort((a, b) => {
|
|
63
|
+
if (a[0] === b[0]) return a[1].localeCompare(b[1]);
|
|
64
|
+
return a[0].localeCompare(b[0]);
|
|
65
|
+
});
|
|
66
|
+
return params.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
67
|
+
}
|
|
68
|
+
function signRequest(request, date) {
|
|
69
|
+
const now = date ?? /* @__PURE__ */ new Date();
|
|
70
|
+
const dateStamp = getDateStamp(now);
|
|
71
|
+
const amzDate = getAmzDate(now);
|
|
72
|
+
const { method, url, accessKey, secretKey, region } = request;
|
|
73
|
+
const headers = {};
|
|
74
|
+
for (const [k, v] of Object.entries(request.headers)) {
|
|
75
|
+
headers[k.toLowerCase()] = v;
|
|
76
|
+
}
|
|
77
|
+
headers["host"] = url.host;
|
|
78
|
+
headers["x-amz-date"] = amzDate;
|
|
79
|
+
headers["x-amz-content-sha256"] = UNSIGNED_PAYLOAD;
|
|
80
|
+
const sortedHeaderKeys = Object.keys(headers).sort((a, b) => a.localeCompare(b));
|
|
81
|
+
const canonicalHeaders = sortedHeaderKeys.map((k) => `${k}:${headers[k].trim()}`).join("\n");
|
|
82
|
+
const signedHeaders = sortedHeaderKeys.join(";");
|
|
83
|
+
const canonicalRequest = [
|
|
84
|
+
method,
|
|
85
|
+
url.pathname,
|
|
86
|
+
getCanonicalQueryString(url),
|
|
87
|
+
canonicalHeaders + "\n",
|
|
88
|
+
signedHeaders,
|
|
89
|
+
UNSIGNED_PAYLOAD
|
|
90
|
+
].join("\n");
|
|
91
|
+
const credentialScope = `${dateStamp}/${region}/${SERVICE}/aws4_request`;
|
|
92
|
+
const stringToSign = [ALGORITHM, amzDate, credentialScope, sha256(canonicalRequest)].join("\n");
|
|
93
|
+
const signingKey = getSigningKey(secretKey, dateStamp, region);
|
|
94
|
+
const signature = hmacSha256Hex(signingKey, stringToSign);
|
|
95
|
+
const authorization = `${ALGORITHM} Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
96
|
+
return {
|
|
97
|
+
"x-amz-date": amzDate,
|
|
98
|
+
"x-amz-content-sha256": UNSIGNED_PAYLOAD,
|
|
99
|
+
Authorization: authorization
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/errors.ts
|
|
104
|
+
var RGWError = class extends Error {
|
|
105
|
+
constructor(message, statusCode, code, uid) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.statusCode = statusCode;
|
|
108
|
+
this.code = code;
|
|
109
|
+
this.uid = uid;
|
|
110
|
+
this.name = "RGWError";
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var RGWNotFoundError = class extends RGWError {
|
|
114
|
+
constructor(resource, id) {
|
|
115
|
+
super(`${resource} not found: "${id}"`, 404, "NoSuchResource");
|
|
116
|
+
this.name = "RGWNotFoundError";
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var RGWValidationError = class extends RGWError {
|
|
120
|
+
constructor(message) {
|
|
121
|
+
super(message, void 0, "ValidationError");
|
|
122
|
+
this.name = "RGWValidationError";
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var RGWAuthError = class extends RGWError {
|
|
126
|
+
constructor(message) {
|
|
127
|
+
super(message, 403, "AccessDenied");
|
|
128
|
+
this.name = "RGWAuthError";
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
var RGWConflictError = class extends RGWError {
|
|
132
|
+
constructor(resource, id) {
|
|
133
|
+
super(`${resource} already exists: "${id}"`, 409, "AlreadyExists");
|
|
134
|
+
this.name = "RGWConflictError";
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// src/client.ts
|
|
139
|
+
var DEFAULT_ADMIN_PATH = "/admin";
|
|
140
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
141
|
+
var DEFAULT_REGION = "us-east-1";
|
|
142
|
+
function validateConfig(config) {
|
|
143
|
+
if (!config.host || typeof config.host !== "string") {
|
|
144
|
+
throw new RGWValidationError("host is required and must be a non-empty string");
|
|
145
|
+
}
|
|
146
|
+
if (!config.accessKey || typeof config.accessKey !== "string") {
|
|
147
|
+
throw new RGWValidationError("accessKey is required and must be a non-empty string");
|
|
148
|
+
}
|
|
149
|
+
if (!config.secretKey || typeof config.secretKey !== "string") {
|
|
150
|
+
throw new RGWValidationError("secretKey is required and must be a non-empty string");
|
|
151
|
+
}
|
|
152
|
+
if (config.port !== void 0 && (config.port < 1 || config.port > 65535 || !Number.isInteger(config.port))) {
|
|
153
|
+
throw new RGWValidationError("port must be an integer between 1 and 65535");
|
|
154
|
+
}
|
|
155
|
+
if (config.timeout !== void 0 && (config.timeout < 0 || !Number.isFinite(config.timeout))) {
|
|
156
|
+
throw new RGWValidationError("timeout must be a non-negative finite number");
|
|
157
|
+
}
|
|
158
|
+
if (config.maxRetries !== void 0 && (config.maxRetries < 0 || !Number.isInteger(config.maxRetries))) {
|
|
159
|
+
throw new RGWValidationError("maxRetries must be a non-negative integer");
|
|
160
|
+
}
|
|
161
|
+
if (config.retryDelay !== void 0 && (config.retryDelay < 0 || !Number.isFinite(config.retryDelay))) {
|
|
162
|
+
throw new RGWValidationError("retryDelay must be a non-negative finite number");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function toCamelCase(obj) {
|
|
166
|
+
if (Array.isArray(obj)) return obj.map(toCamelCase);
|
|
167
|
+
if (obj !== null && typeof obj === "object") {
|
|
168
|
+
return Object.fromEntries(
|
|
169
|
+
Object.entries(obj).map(([k, v]) => [
|
|
170
|
+
k.replaceAll(/[-_.]([a-z])/g, (_, c) => c.toUpperCase()),
|
|
171
|
+
toCamelCase(v)
|
|
172
|
+
])
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return obj;
|
|
176
|
+
}
|
|
177
|
+
function toKebabCase(key) {
|
|
178
|
+
return key.replaceAll(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
179
|
+
}
|
|
180
|
+
function mapHttpError(status, body, code) {
|
|
181
|
+
switch (status) {
|
|
182
|
+
case 404:
|
|
183
|
+
return new RGWNotFoundError("Resource", code ?? "unknown");
|
|
184
|
+
case 403:
|
|
185
|
+
return new RGWAuthError(
|
|
186
|
+
body || "Access denied. Check your admin credentials and user capabilities."
|
|
187
|
+
);
|
|
188
|
+
case 409:
|
|
189
|
+
return new RGWConflictError("Resource", code ?? "unknown");
|
|
190
|
+
case 400:
|
|
191
|
+
return new RGWValidationError(body || "Invalid request parameters");
|
|
192
|
+
default:
|
|
193
|
+
return new RGWError(body || `HTTP ${status}`, status, code);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
var BaseClient = class {
|
|
197
|
+
host;
|
|
198
|
+
port;
|
|
199
|
+
accessKey;
|
|
200
|
+
secretKey;
|
|
201
|
+
adminPath;
|
|
202
|
+
timeout;
|
|
203
|
+
region;
|
|
204
|
+
debug;
|
|
205
|
+
maxRetries;
|
|
206
|
+
retryDelay;
|
|
207
|
+
insecure;
|
|
208
|
+
constructor(config) {
|
|
209
|
+
validateConfig(config);
|
|
210
|
+
let host = config.host;
|
|
211
|
+
while (host.endsWith("/")) {
|
|
212
|
+
host = host.slice(0, -1);
|
|
213
|
+
}
|
|
214
|
+
this.host = host;
|
|
215
|
+
this.port = config.port;
|
|
216
|
+
this.accessKey = config.accessKey;
|
|
217
|
+
this.secretKey = config.secretKey;
|
|
218
|
+
this.adminPath = config.adminPath ?? DEFAULT_ADMIN_PATH;
|
|
219
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
220
|
+
this.region = config.region ?? DEFAULT_REGION;
|
|
221
|
+
this.debug = config.debug ?? false;
|
|
222
|
+
this.maxRetries = config.maxRetries ?? 0;
|
|
223
|
+
this.retryDelay = config.retryDelay ?? 200;
|
|
224
|
+
this.insecure = config.insecure ?? false;
|
|
225
|
+
if (this.insecure) {
|
|
226
|
+
this.log("WARNING: TLS certificate verification is disabled");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Logs a debug message with optional structured data.
|
|
231
|
+
*/
|
|
232
|
+
log(message, data) {
|
|
233
|
+
if (!this.debug) return;
|
|
234
|
+
const prefix = "[radosgw-admin]";
|
|
235
|
+
if (data) {
|
|
236
|
+
console.debug(prefix, message, JSON.stringify(data, null, 2));
|
|
237
|
+
} else {
|
|
238
|
+
console.debug(prefix, message);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Determines whether an error is retryable (5xx, timeouts, network errors).
|
|
243
|
+
*/
|
|
244
|
+
isRetryable(error) {
|
|
245
|
+
if (error instanceof RGWError) {
|
|
246
|
+
if (error.statusCode !== void 0) {
|
|
247
|
+
return error.statusCode >= 500;
|
|
248
|
+
}
|
|
249
|
+
if (error.code === "NetworkError" || error.code === "Timeout") {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (error instanceof Error) {
|
|
254
|
+
return error.name === "AbortError" || this.hasNetworkErrorPattern(error);
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Checks an error and its cause chain for network error patterns.
|
|
260
|
+
*/
|
|
261
|
+
hasNetworkErrorPattern(error) {
|
|
262
|
+
const patterns = [
|
|
263
|
+
"ECONNRESET",
|
|
264
|
+
"ECONNREFUSED",
|
|
265
|
+
"ETIMEDOUT",
|
|
266
|
+
"ENOTFOUND",
|
|
267
|
+
"EHOSTUNREACH",
|
|
268
|
+
"ENETUNREACH",
|
|
269
|
+
"ECONNABORTED",
|
|
270
|
+
"fetch failed"
|
|
271
|
+
];
|
|
272
|
+
let current = error;
|
|
273
|
+
while (current) {
|
|
274
|
+
if (patterns.some((p) => current.message.includes(p))) {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
current = current.cause instanceof Error ? current.cause : void 0;
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Returns a promise that resolves after the given number of milliseconds.
|
|
283
|
+
*/
|
|
284
|
+
async delay(ms) {
|
|
285
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Makes a signed HTTP request to the RGW Admin API with retry support.
|
|
289
|
+
*
|
|
290
|
+
* @param options - Request method, path, query params, and optional body
|
|
291
|
+
* @returns Parsed and camelCase-transformed JSON response, or void for empty responses
|
|
292
|
+
*/
|
|
293
|
+
async request(options) {
|
|
294
|
+
let lastError;
|
|
295
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
296
|
+
if (attempt > 0) {
|
|
297
|
+
const backoff = this.retryDelay * Math.pow(2, attempt - 1);
|
|
298
|
+
this.log(`retry ${attempt}/${this.maxRetries} after ${backoff}ms`);
|
|
299
|
+
await this.delay(backoff);
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
return await this.executeRequest(options);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
305
|
+
this.log("error", { error: lastError.message });
|
|
306
|
+
if (attempt < this.maxRetries && this.isRetryable(error)) {
|
|
307
|
+
this.log("retryable error", { error: lastError.message, attempt });
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
throw lastError ?? new RGWError("Request failed after retries", void 0, "RetryExhausted");
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Builds the full request URL with query parameters.
|
|
317
|
+
*/
|
|
318
|
+
buildUrl(path, query) {
|
|
319
|
+
const baseUrl = this.port ? `${this.host}:${this.port}` : this.host;
|
|
320
|
+
const fullPath = `${this.adminPath}${path}`;
|
|
321
|
+
const url = new URL(fullPath, baseUrl);
|
|
322
|
+
if (query) {
|
|
323
|
+
for (const [key, value] of Object.entries(query)) {
|
|
324
|
+
if (value !== void 0) {
|
|
325
|
+
url.searchParams.set(toKebabCase(key), String(value));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
url.searchParams.set("format", "json");
|
|
330
|
+
return url;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Temporarily disables TLS certificate verification for insecure mode.
|
|
334
|
+
* Returns the previous value so it can be restored.
|
|
335
|
+
*/
|
|
336
|
+
disableTlsVerification() {
|
|
337
|
+
const prev = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
|
338
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
339
|
+
return prev;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Restores the TLS verification setting to its previous value.
|
|
343
|
+
*/
|
|
344
|
+
restoreTlsVerification(prev) {
|
|
345
|
+
if (prev === void 0) {
|
|
346
|
+
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
|
347
|
+
} else {
|
|
348
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = prev;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Parses an error response body to extract the RGW error code.
|
|
353
|
+
*/
|
|
354
|
+
parseErrorCode(text) {
|
|
355
|
+
try {
|
|
356
|
+
const errorBody = JSON.parse(text);
|
|
357
|
+
return errorBody.Code ?? void 0;
|
|
358
|
+
} catch {
|
|
359
|
+
return void 0;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Wraps a non-RGW error into the appropriate RGW error type.
|
|
364
|
+
*/
|
|
365
|
+
wrapFetchError(error) {
|
|
366
|
+
if (error instanceof RGWError) return error;
|
|
367
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
368
|
+
return new RGWError(`Request timed out after ${this.timeout}ms`, void 0, "Timeout");
|
|
369
|
+
}
|
|
370
|
+
return new RGWError(
|
|
371
|
+
error instanceof Error ? error.message : "Unknown error occurred",
|
|
372
|
+
void 0,
|
|
373
|
+
"NetworkError"
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Executes a single signed HTTP request to the RGW Admin API.
|
|
378
|
+
*/
|
|
379
|
+
async executeRequest(options) {
|
|
380
|
+
const { method, path, query, body } = options;
|
|
381
|
+
const url = this.buildUrl(path, query);
|
|
382
|
+
const headers = {
|
|
383
|
+
"Content-Type": "application/json"
|
|
384
|
+
};
|
|
385
|
+
const signedHeaders = signRequest({
|
|
386
|
+
method,
|
|
387
|
+
url,
|
|
388
|
+
headers,
|
|
389
|
+
accessKey: this.accessKey,
|
|
390
|
+
secretKey: this.secretKey,
|
|
391
|
+
region: this.region
|
|
392
|
+
});
|
|
393
|
+
const controller = new AbortController();
|
|
394
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
395
|
+
const prevTls = this.insecure ? this.disableTlsVerification() : void 0;
|
|
396
|
+
try {
|
|
397
|
+
const fetchOptions = {
|
|
398
|
+
method,
|
|
399
|
+
headers: { ...headers, ...signedHeaders },
|
|
400
|
+
signal: controller.signal
|
|
401
|
+
};
|
|
402
|
+
if (body) {
|
|
403
|
+
fetchOptions.body = JSON.stringify(body);
|
|
404
|
+
}
|
|
405
|
+
const safeUrl = url.toString().replaceAll(/([?&]secret-key=)[^&]*/gi, "$1[REDACTED]");
|
|
406
|
+
this.log("request", { method, url: safeUrl });
|
|
407
|
+
const response = await fetch(url.toString(), fetchOptions);
|
|
408
|
+
const text = await response.text();
|
|
409
|
+
if (!response.ok) {
|
|
410
|
+
throw mapHttpError(response.status, text, this.parseErrorCode(text));
|
|
411
|
+
}
|
|
412
|
+
this.log("response", { status: response.status, body: text.slice(0, 500) });
|
|
413
|
+
if (!text) {
|
|
414
|
+
return void 0;
|
|
415
|
+
}
|
|
416
|
+
const parsed = JSON.parse(text);
|
|
417
|
+
return toCamelCase(parsed);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
throw this.wrapFetchError(error);
|
|
420
|
+
} finally {
|
|
421
|
+
clearTimeout(timeoutId);
|
|
422
|
+
if (this.insecure) {
|
|
423
|
+
this.restoreTlsVerification(prevTls);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// src/validators.ts
|
|
430
|
+
function validateUid(uid) {
|
|
431
|
+
if (!uid || typeof uid !== "string" || uid.trim() !== uid || uid.trim().length === 0) {
|
|
432
|
+
throw new RGWValidationError(
|
|
433
|
+
"uid is required and must be a non-empty string without leading/trailing whitespace"
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function validateUidNoColon(uid) {
|
|
438
|
+
validateUid(uid);
|
|
439
|
+
if (uid.includes(":")) {
|
|
440
|
+
throw new RGWValidationError(
|
|
441
|
+
'uid must not contain colons \u2014 colons are reserved for subuser notation (e.g. "uid:subuser")'
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/modules/users.ts
|
|
447
|
+
function validateEmail(email) {
|
|
448
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
449
|
+
throw new RGWValidationError(
|
|
450
|
+
`Invalid email address: "${email}". Expected format: "user@domain.tld"`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function validateUserCaps(caps) {
|
|
455
|
+
if (!caps.trim()) {
|
|
456
|
+
throw new RGWValidationError("userCaps must not be empty if provided");
|
|
457
|
+
}
|
|
458
|
+
const capSegment = /^[a-z][a-z-]*=(?:\*|read,\s*write|read|write)$/i;
|
|
459
|
+
const valid = caps.split(";").map((s) => s.trim()).every((seg) => capSegment.test(seg));
|
|
460
|
+
if (!valid) {
|
|
461
|
+
throw new RGWValidationError(
|
|
462
|
+
`Invalid userCaps format: "${caps}". Expected "type=perm" or "type1=perm;type2=perm". Valid perms: *, read, write, "read, write". Example: "users=*;buckets=read"`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
var UsersModule = class {
|
|
467
|
+
constructor(client) {
|
|
468
|
+
this.client = client;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Create a new RGW user.
|
|
472
|
+
*
|
|
473
|
+
* @param input - User creation parameters. `uid` and `displayName` are required.
|
|
474
|
+
* @returns The newly created user object with keys, caps, and quotas.
|
|
475
|
+
* @throws {RGWValidationError} If `uid` contains colons, `displayName` is blank,
|
|
476
|
+
* `email` is malformed, or `userCaps` is an invalid capability string.
|
|
477
|
+
* @throws {RGWConflictError} If a user with the given `uid` already exists.
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* ```typescript
|
|
481
|
+
* const user = await client.users.create({
|
|
482
|
+
* uid: 'alice',
|
|
483
|
+
* displayName: 'Alice Example',
|
|
484
|
+
* email: 'alice@example.com',
|
|
485
|
+
* maxBuckets: 100,
|
|
486
|
+
* userCaps: 'users=read;buckets=*',
|
|
487
|
+
* });
|
|
488
|
+
* console.log(user.keys[0].accessKey);
|
|
489
|
+
* ```
|
|
490
|
+
*/
|
|
491
|
+
async create(input) {
|
|
492
|
+
validateUidNoColon(input.uid);
|
|
493
|
+
if (!input.displayName || typeof input.displayName !== "string" || input.displayName.trim().length === 0) {
|
|
494
|
+
throw new RGWValidationError("displayName is required and must be a non-empty string");
|
|
495
|
+
}
|
|
496
|
+
if (input.displayName !== input.displayName.trim()) {
|
|
497
|
+
throw new RGWValidationError("displayName must not have leading or trailing whitespace");
|
|
498
|
+
}
|
|
499
|
+
if (input.email !== void 0) {
|
|
500
|
+
validateEmail(input.email);
|
|
501
|
+
}
|
|
502
|
+
if (input.userCaps !== void 0) {
|
|
503
|
+
validateUserCaps(input.userCaps);
|
|
504
|
+
}
|
|
505
|
+
return this.client.request({
|
|
506
|
+
method: "PUT",
|
|
507
|
+
path: "/user",
|
|
508
|
+
query: {
|
|
509
|
+
uid: input.uid,
|
|
510
|
+
displayName: input.displayName,
|
|
511
|
+
email: input.email,
|
|
512
|
+
keyType: input.keyType,
|
|
513
|
+
accessKey: input.accessKey,
|
|
514
|
+
secretKey: input.secretKey,
|
|
515
|
+
userCaps: input.userCaps,
|
|
516
|
+
generateKey: input.generateKey,
|
|
517
|
+
maxBuckets: input.maxBuckets,
|
|
518
|
+
suspended: input.suspended,
|
|
519
|
+
tenant: input.tenant,
|
|
520
|
+
opMask: input.opMask
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Get full user information including keys, caps, and quotas.
|
|
526
|
+
*
|
|
527
|
+
* For multi-tenant setups pass the `tenant` parameter instead of embedding
|
|
528
|
+
* it in the uid string. Both `get('alice', 'acme')` and `get('acme$alice')`
|
|
529
|
+
* resolve to the same user; the former is preferred for clarity.
|
|
530
|
+
*
|
|
531
|
+
* @param uid - The user ID (without tenant prefix).
|
|
532
|
+
* @param tenant - Optional tenant name. When provided, resolves to `tenant$uid`.
|
|
533
|
+
* @returns Full user object.
|
|
534
|
+
* @throws {RGWValidationError} If `uid` is empty.
|
|
535
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* ```typescript
|
|
539
|
+
* // Standard lookup
|
|
540
|
+
* const user = await client.users.get('alice');
|
|
541
|
+
*
|
|
542
|
+
* // Multi-tenant lookup
|
|
543
|
+
* const tenantUser = await client.users.get('alice', 'acme');
|
|
544
|
+
* ```
|
|
545
|
+
*/
|
|
546
|
+
async get(uid, tenant) {
|
|
547
|
+
validateUid(uid);
|
|
548
|
+
const effectiveUid = tenant ? `${tenant}$${uid}` : uid;
|
|
549
|
+
return this.client.request({
|
|
550
|
+
method: "GET",
|
|
551
|
+
path: "/user",
|
|
552
|
+
query: { uid: effectiveUid }
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Look up a user by their S3 access key.
|
|
557
|
+
*
|
|
558
|
+
* Useful for mapping an incoming S3 request's access key to the owning user
|
|
559
|
+
* without knowing the uid in advance.
|
|
560
|
+
*
|
|
561
|
+
* @param accessKey - The S3 access key to look up.
|
|
562
|
+
* @returns The user that owns this access key.
|
|
563
|
+
* @throws {RGWValidationError} If `accessKey` is empty.
|
|
564
|
+
* @throws {RGWNotFoundError} If no user owns this access key.
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
567
|
+
* ```typescript
|
|
568
|
+
* const user = await client.users.getByAccessKey('AKIAIOSFODNN7EXAMPLE');
|
|
569
|
+
* console.log('Key belongs to:', user.userId);
|
|
570
|
+
* ```
|
|
571
|
+
*/
|
|
572
|
+
async getByAccessKey(accessKey) {
|
|
573
|
+
if (!accessKey || typeof accessKey !== "string" || accessKey.trim().length === 0) {
|
|
574
|
+
throw new RGWValidationError("accessKey is required and must be a non-empty string");
|
|
575
|
+
}
|
|
576
|
+
return this.client.request({
|
|
577
|
+
method: "GET",
|
|
578
|
+
path: "/user",
|
|
579
|
+
query: { accessKey }
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Modify an existing user's properties.
|
|
584
|
+
*
|
|
585
|
+
* Only the provided fields are updated — omitted fields retain their current values.
|
|
586
|
+
*
|
|
587
|
+
* @param input - Properties to update. `uid` is required; all other fields are optional.
|
|
588
|
+
* @returns The updated user object.
|
|
589
|
+
* @throws {RGWValidationError} If `uid` is empty, `email` is malformed,
|
|
590
|
+
* or `userCaps` is an invalid capability string.
|
|
591
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* ```typescript
|
|
595
|
+
* const updated = await client.users.modify({
|
|
596
|
+
* uid: 'alice',
|
|
597
|
+
* displayName: 'Alice Updated',
|
|
598
|
+
* maxBuckets: 200,
|
|
599
|
+
* opMask: 'read, write',
|
|
600
|
+
* });
|
|
601
|
+
* ```
|
|
602
|
+
*/
|
|
603
|
+
async modify(input) {
|
|
604
|
+
validateUid(input.uid);
|
|
605
|
+
if (input.email !== void 0) {
|
|
606
|
+
validateEmail(input.email);
|
|
607
|
+
}
|
|
608
|
+
if (input.userCaps !== void 0) {
|
|
609
|
+
validateUserCaps(input.userCaps);
|
|
610
|
+
}
|
|
611
|
+
return this.client.request({
|
|
612
|
+
method: "POST",
|
|
613
|
+
path: "/user",
|
|
614
|
+
query: {
|
|
615
|
+
uid: input.uid,
|
|
616
|
+
displayName: input.displayName,
|
|
617
|
+
email: input.email,
|
|
618
|
+
maxBuckets: input.maxBuckets,
|
|
619
|
+
suspended: input.suspended,
|
|
620
|
+
userCaps: input.userCaps,
|
|
621
|
+
opMask: input.opMask
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Delete a user. Optionally purge all user data (buckets and objects).
|
|
627
|
+
*
|
|
628
|
+
* @param input - `uid` is required. Set `purgeData: true` to delete all objects.
|
|
629
|
+
* @throws {RGWValidationError} If `uid` is empty.
|
|
630
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
631
|
+
*
|
|
632
|
+
* @example
|
|
633
|
+
* ```typescript
|
|
634
|
+
* // Safe delete — fails if user owns buckets
|
|
635
|
+
* await client.users.delete({ uid: 'alice' });
|
|
636
|
+
*
|
|
637
|
+
* // ⚠️ Force delete with full data purge
|
|
638
|
+
* await client.users.delete({ uid: 'alice', purgeData: true });
|
|
639
|
+
* ```
|
|
640
|
+
*/
|
|
641
|
+
async delete(input) {
|
|
642
|
+
validateUid(input.uid);
|
|
643
|
+
if (input.purgeData) {
|
|
644
|
+
console.warn(
|
|
645
|
+
`[radosgw-admin] WARNING: purgeData=true will permanently delete all objects for user "${input.uid}"`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
return this.client.request({
|
|
649
|
+
method: "DELETE",
|
|
650
|
+
path: "/user",
|
|
651
|
+
query: {
|
|
652
|
+
uid: input.uid,
|
|
653
|
+
purgeData: input.purgeData
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* List all user IDs in the cluster (or tenant).
|
|
659
|
+
*
|
|
660
|
+
* Returns the full list in a single call. The RGW `/metadata/user` endpoint
|
|
661
|
+
* supports `marker` and `max-entries` for server-side pagination, but has a
|
|
662
|
+
* default limit of 1000 entries. This method requests up to 100,000 entries
|
|
663
|
+
* to avoid silent truncation on large clusters.
|
|
664
|
+
*
|
|
665
|
+
* For clusters with more than 100k users, use the planned `paginate()` method
|
|
666
|
+
* (see v1.6 roadmap).
|
|
667
|
+
*
|
|
668
|
+
* @returns Array of user ID strings.
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* ```typescript
|
|
672
|
+
* const uids = await client.users.list();
|
|
673
|
+
* console.log('Total users:', uids.length);
|
|
674
|
+
* ```
|
|
675
|
+
*/
|
|
676
|
+
async list() {
|
|
677
|
+
const result = await this.client.request({
|
|
678
|
+
method: "GET",
|
|
679
|
+
path: "/metadata/user",
|
|
680
|
+
query: { maxEntries: 1e5 }
|
|
681
|
+
});
|
|
682
|
+
if (Array.isArray(result)) {
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
685
|
+
return result.keys;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Suspend a user account. The user's data is preserved but all API access is denied.
|
|
689
|
+
*
|
|
690
|
+
* @param uid - The user ID to suspend.
|
|
691
|
+
* @returns The updated user object with `suspended: 1`.
|
|
692
|
+
* @throws {RGWValidationError} If `uid` is empty.
|
|
693
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
694
|
+
*
|
|
695
|
+
* @example
|
|
696
|
+
* ```typescript
|
|
697
|
+
* const suspended = await client.users.suspend('alice');
|
|
698
|
+
* console.log(suspended.suspended); // 1
|
|
699
|
+
* ```
|
|
700
|
+
*/
|
|
701
|
+
async suspend(uid) {
|
|
702
|
+
validateUid(uid);
|
|
703
|
+
return this.client.request({
|
|
704
|
+
method: "POST",
|
|
705
|
+
path: "/user",
|
|
706
|
+
query: { uid, suspended: true }
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Re-enable a suspended user account.
|
|
711
|
+
*
|
|
712
|
+
* @param uid - The user ID to enable.
|
|
713
|
+
* @returns The updated user object with `suspended: 0`.
|
|
714
|
+
* @throws {RGWValidationError} If `uid` is empty.
|
|
715
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
716
|
+
*
|
|
717
|
+
* @example
|
|
718
|
+
* ```typescript
|
|
719
|
+
* const enabled = await client.users.enable('alice');
|
|
720
|
+
* console.log(enabled.suspended); // 0
|
|
721
|
+
* ```
|
|
722
|
+
*/
|
|
723
|
+
async enable(uid) {
|
|
724
|
+
validateUid(uid);
|
|
725
|
+
return this.client.request({
|
|
726
|
+
method: "POST",
|
|
727
|
+
path: "/user",
|
|
728
|
+
query: { uid, suspended: false }
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Get storage usage statistics for a user.
|
|
733
|
+
*
|
|
734
|
+
* Returns the full user object with an additional `stats` field containing:
|
|
735
|
+
* - `size` / `sizeKb` — logical bytes/KB used
|
|
736
|
+
* - `sizeActual` / `sizeKbActual` — bytes/KB on disk (accounting for alignment)
|
|
737
|
+
* - `sizeUtilized` / `sizeKbUtilized` — bytes/KB utilized (used + overhead)
|
|
738
|
+
* - `numObjects` — total number of objects stored
|
|
739
|
+
*
|
|
740
|
+
* Pass `sync: true` to force RGW to recalculate stats from the backing store
|
|
741
|
+
* before returning (slower but accurate).
|
|
742
|
+
*
|
|
743
|
+
* @param uid - The user ID.
|
|
744
|
+
* @param sync - If true, forces a stats sync before returning. Default: false.
|
|
745
|
+
* @returns User object with embedded {@link RGWUserStatData} `stats` field.
|
|
746
|
+
* @throws {RGWValidationError} If `uid` is empty.
|
|
747
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
748
|
+
*
|
|
749
|
+
* @example
|
|
750
|
+
* ```typescript
|
|
751
|
+
* // Fast read (may be slightly stale)
|
|
752
|
+
* const result = await client.users.getStats('alice');
|
|
753
|
+
*
|
|
754
|
+
* // Force sync — accurate but slower
|
|
755
|
+
* const synced = await client.users.getStats('alice', true);
|
|
756
|
+
*
|
|
757
|
+
* console.log('Objects:', result.stats.numObjects);
|
|
758
|
+
* console.log('Size (KB):', result.stats.sizeKb);
|
|
759
|
+
* console.log('Actual disk (KB):', result.stats.sizeKbActual);
|
|
760
|
+
* ```
|
|
761
|
+
*/
|
|
762
|
+
async getStats(uid, sync) {
|
|
763
|
+
validateUid(uid);
|
|
764
|
+
return this.client.request({
|
|
765
|
+
method: "GET",
|
|
766
|
+
path: "/user",
|
|
767
|
+
query: {
|
|
768
|
+
uid,
|
|
769
|
+
stats: true,
|
|
770
|
+
sync
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
// src/modules/keys.ts
|
|
777
|
+
var KeysModule = class {
|
|
778
|
+
constructor(client) {
|
|
779
|
+
this.client = client;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Generate a new S3 or Swift key for a user.
|
|
783
|
+
*
|
|
784
|
+
* Returns the user's **entire** key list after the operation, not just the
|
|
785
|
+
* newly created key. To identify the new key, compare with the key list
|
|
786
|
+
* before generation or look for the last entry.
|
|
787
|
+
*
|
|
788
|
+
* @param input - Key generation parameters. `uid` is required.
|
|
789
|
+
* @returns Array of **all** keys belonging to the user after generation.
|
|
790
|
+
* @throws {RGWValidationError} If `uid` is missing or invalid.
|
|
791
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
792
|
+
*
|
|
793
|
+
* @example
|
|
794
|
+
* ```typescript
|
|
795
|
+
* // Auto-generate a new S3 key
|
|
796
|
+
* const allKeys = await client.keys.generate({ uid: 'alice' });
|
|
797
|
+
* const newKey = allKeys[allKeys.length - 1]; // newest key is last
|
|
798
|
+
* console.log('New key:', newKey.accessKey);
|
|
799
|
+
*
|
|
800
|
+
* // Supply specific credentials (disable auto-generation)
|
|
801
|
+
* const allKeys = await client.keys.generate({
|
|
802
|
+
* uid: 'alice',
|
|
803
|
+
* accessKey: 'MY_ACCESS_KEY',
|
|
804
|
+
* secretKey: 'MY_SECRET_KEY',
|
|
805
|
+
* generateKey: false,
|
|
806
|
+
* });
|
|
807
|
+
* ```
|
|
808
|
+
*/
|
|
809
|
+
async generate(input) {
|
|
810
|
+
validateUid(input.uid);
|
|
811
|
+
return this.client.request({
|
|
812
|
+
method: "PUT",
|
|
813
|
+
path: "/user",
|
|
814
|
+
query: {
|
|
815
|
+
key: "",
|
|
816
|
+
uid: input.uid,
|
|
817
|
+
keyType: input.keyType,
|
|
818
|
+
accessKey: input.accessKey,
|
|
819
|
+
secretKey: input.secretKey,
|
|
820
|
+
generateKey: input.generateKey
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Revoke an S3 or Swift key.
|
|
826
|
+
*
|
|
827
|
+
* @param input - `accessKey` is required. `uid` is required for Swift keys.
|
|
828
|
+
* @returns Resolves when the key has been revoked.
|
|
829
|
+
* @throws {RGWValidationError} If `accessKey` is missing.
|
|
830
|
+
* @throws {RGWNotFoundError} If the key does not exist.
|
|
831
|
+
*
|
|
832
|
+
* @example
|
|
833
|
+
* ```typescript
|
|
834
|
+
* // Revoke an S3 key by access key ID
|
|
835
|
+
* await client.keys.revoke({ accessKey: 'OLDKEY123' });
|
|
836
|
+
*
|
|
837
|
+
* // Revoke a Swift key (uid required)
|
|
838
|
+
* await client.keys.revoke({
|
|
839
|
+
* accessKey: 'SWIFTKEY',
|
|
840
|
+
* uid: 'alice',
|
|
841
|
+
* keyType: 'swift',
|
|
842
|
+
* });
|
|
843
|
+
* ```
|
|
844
|
+
*/
|
|
845
|
+
async revoke(input) {
|
|
846
|
+
if (!input.accessKey || typeof input.accessKey !== "string" || input.accessKey.trim().length === 0) {
|
|
847
|
+
throw new RGWValidationError("accessKey is required and must be a non-empty string");
|
|
848
|
+
}
|
|
849
|
+
if (input.keyType === "swift" && !input.uid) {
|
|
850
|
+
throw new RGWValidationError('uid is required when revoking a Swift key (keyType: "swift")');
|
|
851
|
+
}
|
|
852
|
+
return this.client.request({
|
|
853
|
+
method: "DELETE",
|
|
854
|
+
path: "/user",
|
|
855
|
+
query: {
|
|
856
|
+
key: "",
|
|
857
|
+
accessKey: input.accessKey,
|
|
858
|
+
uid: input.uid,
|
|
859
|
+
keyType: input.keyType
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
// src/modules/subusers.ts
|
|
866
|
+
function validateSubuser(subuser) {
|
|
867
|
+
if (!subuser || typeof subuser !== "string" || subuser.trim().length === 0) {
|
|
868
|
+
throw new RGWValidationError("subuser is required and must be a non-empty string");
|
|
869
|
+
}
|
|
870
|
+
if (!subuser.includes(":")) {
|
|
871
|
+
throw new RGWValidationError('subuser must be in "uid:name" format (e.g. "alice:swift")');
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
var SubusersModule = class {
|
|
875
|
+
constructor(client) {
|
|
876
|
+
this.client = client;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Create a new subuser for a user.
|
|
880
|
+
*
|
|
881
|
+
* Returns the user's **entire** subuser list after the operation, not just the
|
|
882
|
+
* newly created subuser. To identify the new entry, compare with the list
|
|
883
|
+
* before creation or look for the matching `id`.
|
|
884
|
+
*
|
|
885
|
+
* @param input - Subuser creation parameters. `uid` and `subuser` are required.
|
|
886
|
+
* `subuser` must be in `uid:name` format (e.g. `"alice:swift"`).
|
|
887
|
+
* @returns Array of **all** subusers belonging to the user after creation.
|
|
888
|
+
* @throws {RGWValidationError} If `uid` or `subuser` is missing, invalid, or not in `uid:name` format.
|
|
889
|
+
* @throws {RGWNotFoundError} If the parent user does not exist.
|
|
890
|
+
*
|
|
891
|
+
* @example
|
|
892
|
+
* ```typescript
|
|
893
|
+
* const subusers = await client.subusers.create({
|
|
894
|
+
* uid: 'alice',
|
|
895
|
+
* subuser: 'alice:swift',
|
|
896
|
+
* access: 'readwrite',
|
|
897
|
+
* keyType: 'swift',
|
|
898
|
+
* generateSecret: true,
|
|
899
|
+
* });
|
|
900
|
+
* console.log(subusers[0].id, subusers[0].permissions);
|
|
901
|
+
* ```
|
|
902
|
+
*/
|
|
903
|
+
async create(input) {
|
|
904
|
+
validateUid(input.uid);
|
|
905
|
+
validateSubuser(input.subuser);
|
|
906
|
+
return this.client.request({
|
|
907
|
+
method: "PUT",
|
|
908
|
+
path: "/user",
|
|
909
|
+
query: {
|
|
910
|
+
subuser: input.subuser,
|
|
911
|
+
uid: input.uid,
|
|
912
|
+
secretKey: input.secretKey,
|
|
913
|
+
keyType: input.keyType,
|
|
914
|
+
access: input.access,
|
|
915
|
+
generateSecret: input.generateSecret
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Modify an existing subuser's properties.
|
|
921
|
+
*
|
|
922
|
+
* @param input - Properties to update. `uid` and `subuser` are required.
|
|
923
|
+
* @returns Array of subusers belonging to the user after modification.
|
|
924
|
+
* @throws {RGWValidationError} If `uid` or `subuser` is missing or invalid.
|
|
925
|
+
* @throws {RGWNotFoundError} If the parent user or subuser does not exist.
|
|
926
|
+
*
|
|
927
|
+
* @example
|
|
928
|
+
* ```typescript
|
|
929
|
+
* const subusers = await client.subusers.modify({
|
|
930
|
+
* uid: 'alice',
|
|
931
|
+
* subuser: 'alice:swift',
|
|
932
|
+
* access: 'full',
|
|
933
|
+
* });
|
|
934
|
+
* ```
|
|
935
|
+
*/
|
|
936
|
+
async modify(input) {
|
|
937
|
+
validateUid(input.uid);
|
|
938
|
+
validateSubuser(input.subuser);
|
|
939
|
+
return this.client.request({
|
|
940
|
+
method: "POST",
|
|
941
|
+
path: "/user",
|
|
942
|
+
query: {
|
|
943
|
+
subuser: input.subuser,
|
|
944
|
+
uid: input.uid,
|
|
945
|
+
secretKey: input.secretKey,
|
|
946
|
+
keyType: input.keyType,
|
|
947
|
+
access: input.access,
|
|
948
|
+
generateSecret: input.generateSecret
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Remove a subuser from a user. Optionally purge the subuser's keys.
|
|
954
|
+
*
|
|
955
|
+
* @param input - `uid` and `subuser` are required. `purgeKeys` defaults to true.
|
|
956
|
+
* @returns Resolves when the subuser has been removed.
|
|
957
|
+
* @throws {RGWValidationError} If `uid` or `subuser` is missing or invalid.
|
|
958
|
+
* @throws {RGWNotFoundError} If the parent user or subuser does not exist.
|
|
959
|
+
*
|
|
960
|
+
* @example
|
|
961
|
+
* ```typescript
|
|
962
|
+
* // Remove subuser and purge keys
|
|
963
|
+
* await client.subusers.remove({
|
|
964
|
+
* uid: 'alice',
|
|
965
|
+
* subuser: 'alice:swift',
|
|
966
|
+
* });
|
|
967
|
+
*
|
|
968
|
+
* // Remove subuser but keep keys
|
|
969
|
+
* await client.subusers.remove({
|
|
970
|
+
* uid: 'alice',
|
|
971
|
+
* subuser: 'alice:swift',
|
|
972
|
+
* purgeKeys: false,
|
|
973
|
+
* });
|
|
974
|
+
* ```
|
|
975
|
+
*/
|
|
976
|
+
async remove(input) {
|
|
977
|
+
validateUid(input.uid);
|
|
978
|
+
validateSubuser(input.subuser);
|
|
979
|
+
if (input.purgeKeys === true) {
|
|
980
|
+
console.warn(
|
|
981
|
+
`[radosgw-admin] WARNING: purgeKeys=true will permanently delete all keys for subuser "${input.subuser}"`
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
return this.client.request({
|
|
985
|
+
method: "DELETE",
|
|
986
|
+
path: "/user",
|
|
987
|
+
query: {
|
|
988
|
+
subuser: input.subuser,
|
|
989
|
+
uid: input.uid,
|
|
990
|
+
purgeKeys: input.purgeKeys
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
// src/modules/buckets.ts
|
|
997
|
+
function validateBucket(bucket) {
|
|
998
|
+
if (!bucket || typeof bucket !== "string" || bucket.trim().length === 0) {
|
|
999
|
+
throw new RGWValidationError("bucket is required and must be a non-empty string");
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
var BucketsModule = class {
|
|
1003
|
+
constructor(client) {
|
|
1004
|
+
this.client = client;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* List all buckets in the cluster.
|
|
1008
|
+
*
|
|
1009
|
+
* The RGW `/bucket` endpoint has a default limit of 1000 entries.
|
|
1010
|
+
* This method requests up to 100,000 entries to avoid silent truncation
|
|
1011
|
+
* on large clusters.
|
|
1012
|
+
*
|
|
1013
|
+
* For clusters with more than 100k buckets, use the planned `paginate()`
|
|
1014
|
+
* method (see v1.6 roadmap).
|
|
1015
|
+
*
|
|
1016
|
+
* @returns Array of bucket name strings.
|
|
1017
|
+
*
|
|
1018
|
+
* @example
|
|
1019
|
+
* ```typescript
|
|
1020
|
+
* const all = await client.buckets.list();
|
|
1021
|
+
* console.log(`Cluster has ${all.length} buckets`);
|
|
1022
|
+
* ```
|
|
1023
|
+
*/
|
|
1024
|
+
async list() {
|
|
1025
|
+
const result = await this.client.request({
|
|
1026
|
+
method: "GET",
|
|
1027
|
+
path: "/bucket",
|
|
1028
|
+
query: { maxEntries: 1e5 }
|
|
1029
|
+
});
|
|
1030
|
+
if (Array.isArray(result)) {
|
|
1031
|
+
return result;
|
|
1032
|
+
}
|
|
1033
|
+
return result.buckets;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* List buckets owned by a specific user.
|
|
1037
|
+
*
|
|
1038
|
+
* @param uid - User ID to filter buckets by. Required.
|
|
1039
|
+
* @returns Array of bucket name strings owned by the user.
|
|
1040
|
+
* @throws {RGWValidationError} If `uid` is missing or invalid.
|
|
1041
|
+
*
|
|
1042
|
+
* @example
|
|
1043
|
+
* ```typescript
|
|
1044
|
+
* const userBuckets = await client.buckets.listByUser('alice');
|
|
1045
|
+
* console.log(`alice has ${userBuckets.length} buckets`);
|
|
1046
|
+
* ```
|
|
1047
|
+
*/
|
|
1048
|
+
async listByUser(uid) {
|
|
1049
|
+
validateUid(uid);
|
|
1050
|
+
return this.client.request({
|
|
1051
|
+
method: "GET",
|
|
1052
|
+
path: "/bucket",
|
|
1053
|
+
query: { uid }
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Get detailed metadata about a bucket.
|
|
1058
|
+
*
|
|
1059
|
+
* @param bucket - The bucket name to inspect.
|
|
1060
|
+
* @returns Full bucket object with owner, usage, quota, and placement info.
|
|
1061
|
+
* @throws {RGWValidationError} If `bucket` is empty.
|
|
1062
|
+
* @throws {RGWNotFoundError} If the bucket does not exist.
|
|
1063
|
+
*
|
|
1064
|
+
* @example
|
|
1065
|
+
* ```typescript
|
|
1066
|
+
* const info = await client.buckets.getInfo('my-bucket');
|
|
1067
|
+
* console.log(`Owner: ${info.owner}`);
|
|
1068
|
+
* console.log(`Objects: ${info.usage.rgwMain.numObjects}`);
|
|
1069
|
+
* console.log(`Size: ${(info.usage.rgwMain.sizeKb / 1024).toFixed(2)} MB`);
|
|
1070
|
+
* ```
|
|
1071
|
+
*/
|
|
1072
|
+
async getInfo(bucket) {
|
|
1073
|
+
validateBucket(bucket);
|
|
1074
|
+
return this.client.request({
|
|
1075
|
+
method: "GET",
|
|
1076
|
+
path: "/bucket",
|
|
1077
|
+
query: { bucket }
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Delete a bucket. Optionally purge all objects inside it.
|
|
1082
|
+
*
|
|
1083
|
+
* @param input - `bucket` is required. Set `purgeObjects: true` to delete all objects.
|
|
1084
|
+
* @returns Resolves when the bucket has been deleted.
|
|
1085
|
+
* @throws {RGWValidationError} If `bucket` is empty.
|
|
1086
|
+
* @throws {RGWNotFoundError} If the bucket does not exist.
|
|
1087
|
+
*
|
|
1088
|
+
* @example
|
|
1089
|
+
* ```typescript
|
|
1090
|
+
* // Safe delete (fails if bucket has objects)
|
|
1091
|
+
* await client.buckets.delete({ bucket: 'my-bucket' });
|
|
1092
|
+
*
|
|
1093
|
+
* // Force delete with object purge
|
|
1094
|
+
* await client.buckets.delete({ bucket: 'my-bucket', purgeObjects: true });
|
|
1095
|
+
* ```
|
|
1096
|
+
*/
|
|
1097
|
+
async delete(input) {
|
|
1098
|
+
validateBucket(input.bucket);
|
|
1099
|
+
if (input.purgeObjects) {
|
|
1100
|
+
console.warn(
|
|
1101
|
+
`[radosgw-admin] WARNING: purgeObjects=true will permanently delete all objects in bucket "${input.bucket}"`
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
return this.client.request({
|
|
1105
|
+
method: "DELETE",
|
|
1106
|
+
path: "/bucket",
|
|
1107
|
+
query: {
|
|
1108
|
+
bucket: input.bucket,
|
|
1109
|
+
purgeObjects: input.purgeObjects
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Transfer ownership of a bucket to a different user.
|
|
1115
|
+
*
|
|
1116
|
+
* @param input - `bucket`, `bucketId`, and `uid` are all required.
|
|
1117
|
+
* @returns Resolves when ownership has been transferred.
|
|
1118
|
+
* @throws {RGWValidationError} If any required field is missing.
|
|
1119
|
+
* @throws {RGWNotFoundError} If the bucket or user does not exist.
|
|
1120
|
+
*
|
|
1121
|
+
* @example
|
|
1122
|
+
* ```typescript
|
|
1123
|
+
* const info = await client.buckets.getInfo('my-bucket');
|
|
1124
|
+
* await client.buckets.transferOwnership({
|
|
1125
|
+
* bucket: 'my-bucket',
|
|
1126
|
+
* bucketId: info.id,
|
|
1127
|
+
* uid: 'bob',
|
|
1128
|
+
* });
|
|
1129
|
+
* ```
|
|
1130
|
+
*/
|
|
1131
|
+
async transferOwnership(input) {
|
|
1132
|
+
validateBucket(input.bucket);
|
|
1133
|
+
validateUid(input.uid);
|
|
1134
|
+
if (!input.bucketId || typeof input.bucketId !== "string" || input.bucketId.trim().length === 0) {
|
|
1135
|
+
throw new RGWValidationError("bucketId is required and must be a non-empty string");
|
|
1136
|
+
}
|
|
1137
|
+
return this.client.request({
|
|
1138
|
+
method: "PUT",
|
|
1139
|
+
path: "/bucket",
|
|
1140
|
+
query: {
|
|
1141
|
+
bucket: input.bucket,
|
|
1142
|
+
bucketId: input.bucketId,
|
|
1143
|
+
uid: input.uid
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Remove ownership of a bucket from a user without deleting the bucket.
|
|
1149
|
+
*
|
|
1150
|
+
* **Warning:** This leaves the bucket in an orphaned state with no owner.
|
|
1151
|
+
* The bucket and its objects remain intact but cannot be managed via the
|
|
1152
|
+
* S3 API until ownership is reassigned with {@link transferOwnership}.
|
|
1153
|
+
*
|
|
1154
|
+
* @param input - `bucket` and `uid` are required.
|
|
1155
|
+
* @returns Resolves when ownership has been removed.
|
|
1156
|
+
* @throws {RGWValidationError} If any required field is missing.
|
|
1157
|
+
* @throws {RGWNotFoundError} If the bucket or user does not exist.
|
|
1158
|
+
*
|
|
1159
|
+
* @example
|
|
1160
|
+
* ```typescript
|
|
1161
|
+
* await client.buckets.removeOwnership({
|
|
1162
|
+
* bucket: 'my-bucket',
|
|
1163
|
+
* uid: 'alice',
|
|
1164
|
+
* });
|
|
1165
|
+
* ```
|
|
1166
|
+
*/
|
|
1167
|
+
async removeOwnership(input) {
|
|
1168
|
+
validateBucket(input.bucket);
|
|
1169
|
+
validateUid(input.uid);
|
|
1170
|
+
return this.client.request({
|
|
1171
|
+
method: "POST",
|
|
1172
|
+
path: "/bucket",
|
|
1173
|
+
query: {
|
|
1174
|
+
bucket: input.bucket,
|
|
1175
|
+
uid: input.uid
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Verify and optionally repair a bucket's index.
|
|
1181
|
+
*
|
|
1182
|
+
* With `fix: false` (default), this is a safe read-only operation that reports
|
|
1183
|
+
* any inconsistencies. Set `fix: true` to actually repair detected issues.
|
|
1184
|
+
*
|
|
1185
|
+
* @param input - `bucket` is required. `checkObjects` and `fix` are optional.
|
|
1186
|
+
* @returns Index check result with invalid entries and header comparison.
|
|
1187
|
+
* @throws {RGWValidationError} If `bucket` is empty.
|
|
1188
|
+
* @throws {RGWNotFoundError} If the bucket does not exist.
|
|
1189
|
+
*
|
|
1190
|
+
* @example
|
|
1191
|
+
* ```typescript
|
|
1192
|
+
* // Dry run — check only
|
|
1193
|
+
* const result = await client.buckets.verifyIndex({
|
|
1194
|
+
* bucket: 'my-bucket',
|
|
1195
|
+
* checkObjects: true,
|
|
1196
|
+
* fix: false,
|
|
1197
|
+
* });
|
|
1198
|
+
* console.log('Invalid entries:', result.invalidMultipartEntries);
|
|
1199
|
+
*
|
|
1200
|
+
* // Fix detected issues
|
|
1201
|
+
* await client.buckets.verifyIndex({
|
|
1202
|
+
* bucket: 'my-bucket',
|
|
1203
|
+
* fix: true,
|
|
1204
|
+
* });
|
|
1205
|
+
* ```
|
|
1206
|
+
*/
|
|
1207
|
+
async verifyIndex(input) {
|
|
1208
|
+
validateBucket(input.bucket);
|
|
1209
|
+
if (input.fix === true) {
|
|
1210
|
+
console.warn(
|
|
1211
|
+
`[radosgw-admin] WARNING: fix=true will repair the bucket index for "${input.bucket}" \u2014 this mutates index data`
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
return this.client.request({
|
|
1215
|
+
method: "GET",
|
|
1216
|
+
path: "/bucket",
|
|
1217
|
+
query: {
|
|
1218
|
+
index: "",
|
|
1219
|
+
bucket: input.bucket,
|
|
1220
|
+
checkObjects: input.checkObjects,
|
|
1221
|
+
fix: input.fix
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
// src/modules/quota.ts
|
|
1228
|
+
function validateQuotaValue(field, value) {
|
|
1229
|
+
if (value !== void 0 && typeof value === "number" && value < -1) {
|
|
1230
|
+
throw new RGWValidationError(`${field} must be -1 (unlimited) or >= 0, got ${value}`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
function parseSizeString(size) {
|
|
1234
|
+
if (typeof size === "number") return size;
|
|
1235
|
+
const units = {
|
|
1236
|
+
K: 1024,
|
|
1237
|
+
M: 1024 ** 2,
|
|
1238
|
+
G: 1024 ** 3,
|
|
1239
|
+
T: 1024 ** 4
|
|
1240
|
+
};
|
|
1241
|
+
const match = size.toUpperCase().trim().match(/^(\d+(?:\.\d+)?)\s*([KMGT]?)B?$/);
|
|
1242
|
+
if (!match) {
|
|
1243
|
+
throw new RGWValidationError(
|
|
1244
|
+
`Invalid size string: "${size}". Use format like "10G", "500M", "1T", or a number in bytes`
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
const value = parseFloat(match[1]);
|
|
1248
|
+
const unit = match[2];
|
|
1249
|
+
return Math.floor(value * (units[unit] ?? 1));
|
|
1250
|
+
}
|
|
1251
|
+
var QuotaModule = class {
|
|
1252
|
+
constructor(client) {
|
|
1253
|
+
this.client = client;
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Get the user-level quota for a user.
|
|
1257
|
+
*
|
|
1258
|
+
* @param uid - The user ID.
|
|
1259
|
+
* @returns The user's quota configuration.
|
|
1260
|
+
* @throws {RGWValidationError} If `uid` is empty.
|
|
1261
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
1262
|
+
*
|
|
1263
|
+
* @example
|
|
1264
|
+
* ```typescript
|
|
1265
|
+
* const quota = await client.quota.getUserQuota('alice');
|
|
1266
|
+
* console.log('Enabled:', quota.enabled);
|
|
1267
|
+
* console.log('Max size:', quota.maxSize, 'bytes');
|
|
1268
|
+
* console.log('Max objects:', quota.maxObjects);
|
|
1269
|
+
* ```
|
|
1270
|
+
*/
|
|
1271
|
+
async getUserQuota(uid) {
|
|
1272
|
+
validateUid(uid);
|
|
1273
|
+
return this.client.request({
|
|
1274
|
+
method: "GET",
|
|
1275
|
+
path: "/user",
|
|
1276
|
+
query: { uid, quota: "", quotaType: "user" }
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Set the user-level quota for a user.
|
|
1281
|
+
*
|
|
1282
|
+
* The `maxSize` field accepts either a number (bytes) or a human-readable string
|
|
1283
|
+
* like `"10G"`, `"500M"`, `"1T"`.
|
|
1284
|
+
*
|
|
1285
|
+
* @param input - Quota settings. `uid` is required.
|
|
1286
|
+
* @throws {RGWValidationError} If `uid` is empty, `maxSize` format is invalid, or `maxSize`/`maxObjects` is a negative number other than -1.
|
|
1287
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
1288
|
+
*
|
|
1289
|
+
* @example
|
|
1290
|
+
* ```typescript
|
|
1291
|
+
* await client.quota.setUserQuota({
|
|
1292
|
+
* uid: 'alice',
|
|
1293
|
+
* maxSize: '10G',
|
|
1294
|
+
* maxObjects: 50000,
|
|
1295
|
+
* enabled: true,
|
|
1296
|
+
* });
|
|
1297
|
+
* ```
|
|
1298
|
+
*/
|
|
1299
|
+
async setUserQuota(input) {
|
|
1300
|
+
validateUid(input.uid);
|
|
1301
|
+
validateQuotaValue("maxObjects", input.maxObjects);
|
|
1302
|
+
const maxSize = input.maxSize !== void 0 ? parseSizeString(input.maxSize) : void 0;
|
|
1303
|
+
validateQuotaValue("maxSize", maxSize);
|
|
1304
|
+
return this.client.request({
|
|
1305
|
+
method: "PUT",
|
|
1306
|
+
path: "/user",
|
|
1307
|
+
query: {
|
|
1308
|
+
uid: input.uid,
|
|
1309
|
+
quota: "",
|
|
1310
|
+
quotaType: "user",
|
|
1311
|
+
maxSize,
|
|
1312
|
+
maxObjects: input.maxObjects,
|
|
1313
|
+
enabled: input.enabled ?? true
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Enable the user-level quota for a user without changing quota values.
|
|
1319
|
+
*
|
|
1320
|
+
* @param uid - The user ID.
|
|
1321
|
+
* @throws {RGWValidationError} If `uid` is empty.
|
|
1322
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
1323
|
+
*
|
|
1324
|
+
* @example
|
|
1325
|
+
* ```typescript
|
|
1326
|
+
* await client.quota.enableUserQuota('alice');
|
|
1327
|
+
* ```
|
|
1328
|
+
*/
|
|
1329
|
+
async enableUserQuota(uid) {
|
|
1330
|
+
validateUid(uid);
|
|
1331
|
+
return this.client.request({
|
|
1332
|
+
method: "PUT",
|
|
1333
|
+
path: "/user",
|
|
1334
|
+
query: { uid, quota: "", quotaType: "user", enabled: true }
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Disable the user-level quota for a user without changing quota values.
|
|
1339
|
+
*
|
|
1340
|
+
* @param uid - The user ID.
|
|
1341
|
+
* @throws {RGWValidationError} If `uid` is empty.
|
|
1342
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
1343
|
+
*
|
|
1344
|
+
* @example
|
|
1345
|
+
* ```typescript
|
|
1346
|
+
* await client.quota.disableUserQuota('alice');
|
|
1347
|
+
* ```
|
|
1348
|
+
*/
|
|
1349
|
+
async disableUserQuota(uid) {
|
|
1350
|
+
validateUid(uid);
|
|
1351
|
+
return this.client.request({
|
|
1352
|
+
method: "PUT",
|
|
1353
|
+
path: "/user",
|
|
1354
|
+
query: { uid, quota: "", quotaType: "user", enabled: false }
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Get the bucket-level quota for a user.
|
|
1359
|
+
*
|
|
1360
|
+
* This returns the per-bucket quota applied to all buckets owned by the user,
|
|
1361
|
+
* not a quota for a specific bucket. RGW bucket quotas are configured per-user.
|
|
1362
|
+
*
|
|
1363
|
+
* @param uid - The user ID (bucket owner), not a bucket name.
|
|
1364
|
+
* @throws {RGWValidationError} If `uid` is empty.
|
|
1365
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
1366
|
+
*
|
|
1367
|
+
* @example
|
|
1368
|
+
* ```typescript
|
|
1369
|
+
* const quota = await client.quota.getBucketQuota('alice');
|
|
1370
|
+
* console.log('Bucket quota enabled:', quota.enabled);
|
|
1371
|
+
* ```
|
|
1372
|
+
*/
|
|
1373
|
+
async getBucketQuota(uid) {
|
|
1374
|
+
validateUid(uid);
|
|
1375
|
+
return this.client.request({
|
|
1376
|
+
method: "GET",
|
|
1377
|
+
path: "/user",
|
|
1378
|
+
query: { uid, quota: "", quotaType: "bucket" }
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Set the bucket-level quota for a user's buckets.
|
|
1383
|
+
*
|
|
1384
|
+
* The `maxSize` field accepts either a number (bytes) or a human-readable string
|
|
1385
|
+
* like `"1G"`, `"500M"`.
|
|
1386
|
+
*
|
|
1387
|
+
* @param input - Quota settings. `uid` is required.
|
|
1388
|
+
* @throws {RGWValidationError} If `uid` is empty, `maxSize` format is invalid, or `maxSize`/`maxObjects` is a negative number other than -1.
|
|
1389
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
1390
|
+
*
|
|
1391
|
+
* @example
|
|
1392
|
+
* ```typescript
|
|
1393
|
+
* await client.quota.setBucketQuota({
|
|
1394
|
+
* uid: 'alice',
|
|
1395
|
+
* maxSize: '1G',
|
|
1396
|
+
* maxObjects: 10000,
|
|
1397
|
+
* enabled: true,
|
|
1398
|
+
* });
|
|
1399
|
+
* ```
|
|
1400
|
+
*/
|
|
1401
|
+
async setBucketQuota(input) {
|
|
1402
|
+
validateUid(input.uid);
|
|
1403
|
+
validateQuotaValue("maxObjects", input.maxObjects);
|
|
1404
|
+
const maxSize = input.maxSize !== void 0 ? parseSizeString(input.maxSize) : void 0;
|
|
1405
|
+
validateQuotaValue("maxSize", maxSize);
|
|
1406
|
+
return this.client.request({
|
|
1407
|
+
method: "PUT",
|
|
1408
|
+
path: "/user",
|
|
1409
|
+
query: {
|
|
1410
|
+
uid: input.uid,
|
|
1411
|
+
quota: "",
|
|
1412
|
+
quotaType: "bucket",
|
|
1413
|
+
maxSize,
|
|
1414
|
+
maxObjects: input.maxObjects,
|
|
1415
|
+
enabled: input.enabled ?? true
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Enable the bucket-level quota for a user without changing quota values.
|
|
1421
|
+
*
|
|
1422
|
+
* @param uid - The user ID.
|
|
1423
|
+
* @throws {RGWValidationError} If `uid` is empty.
|
|
1424
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
1425
|
+
*
|
|
1426
|
+
* @example
|
|
1427
|
+
* ```typescript
|
|
1428
|
+
* await client.quota.enableBucketQuota('alice');
|
|
1429
|
+
* ```
|
|
1430
|
+
*/
|
|
1431
|
+
async enableBucketQuota(uid) {
|
|
1432
|
+
validateUid(uid);
|
|
1433
|
+
return this.client.request({
|
|
1434
|
+
method: "PUT",
|
|
1435
|
+
path: "/user",
|
|
1436
|
+
query: { uid, quota: "", quotaType: "bucket", enabled: true }
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Disable the bucket-level quota for a user without changing quota values.
|
|
1441
|
+
*
|
|
1442
|
+
* @param uid - The user ID.
|
|
1443
|
+
* @throws {RGWValidationError} If `uid` is empty.
|
|
1444
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
1445
|
+
*
|
|
1446
|
+
* @example
|
|
1447
|
+
* ```typescript
|
|
1448
|
+
* await client.quota.disableBucketQuota('alice');
|
|
1449
|
+
* ```
|
|
1450
|
+
*/
|
|
1451
|
+
async disableBucketQuota(uid) {
|
|
1452
|
+
validateUid(uid);
|
|
1453
|
+
return this.client.request({
|
|
1454
|
+
method: "PUT",
|
|
1455
|
+
path: "/user",
|
|
1456
|
+
query: { uid, quota: "", quotaType: "bucket", enabled: false }
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1461
|
+
// src/modules/ratelimit.ts
|
|
1462
|
+
function validateBucket2(bucket) {
|
|
1463
|
+
if (!bucket || typeof bucket !== "string" || bucket.trim().length === 0) {
|
|
1464
|
+
throw new RGWValidationError("bucket is required and must be a non-empty string");
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
function validateRateLimitValue(field, value) {
|
|
1468
|
+
if (value !== void 0 && typeof value === "number" && value < 0) {
|
|
1469
|
+
throw new RGWValidationError(`${field} must be >= 0 (0 = unlimited), got ${value}`);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
function validateRateLimitFields(input) {
|
|
1473
|
+
validateRateLimitValue("maxReadOps", input.maxReadOps);
|
|
1474
|
+
validateRateLimitValue("maxWriteOps", input.maxWriteOps);
|
|
1475
|
+
validateRateLimitValue("maxReadBytes", input.maxReadBytes);
|
|
1476
|
+
validateRateLimitValue("maxWriteBytes", input.maxWriteBytes);
|
|
1477
|
+
}
|
|
1478
|
+
var RateLimitModule = class {
|
|
1479
|
+
constructor(client) {
|
|
1480
|
+
this.client = client;
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Get the rate limit configuration for a user.
|
|
1484
|
+
*
|
|
1485
|
+
* @param uid - The user ID.
|
|
1486
|
+
* @returns The user's rate limit configuration.
|
|
1487
|
+
* @throws {RGWValidationError} If `uid` is empty.
|
|
1488
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
1489
|
+
*
|
|
1490
|
+
* @example
|
|
1491
|
+
* ```typescript
|
|
1492
|
+
* const limit = await client.rateLimit.getUserLimit('alice');
|
|
1493
|
+
* console.log('Read ops/min:', limit.maxReadOps);
|
|
1494
|
+
* console.log('Write ops/min:', limit.maxWriteOps);
|
|
1495
|
+
* ```
|
|
1496
|
+
*/
|
|
1497
|
+
async getUserLimit(uid) {
|
|
1498
|
+
validateUid(uid);
|
|
1499
|
+
return this.client.request({
|
|
1500
|
+
method: "GET",
|
|
1501
|
+
path: "/ratelimit",
|
|
1502
|
+
query: { ratelimit: "", uid, scope: "user" }
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Set the rate limit for a user.
|
|
1507
|
+
*
|
|
1508
|
+
* Values are per RGW instance. Divide by the number of RGW daemons for cluster-wide limits.
|
|
1509
|
+
*
|
|
1510
|
+
* @param input - Rate limit settings. `uid` is required. `enabled` defaults to `true`.
|
|
1511
|
+
* @throws {RGWValidationError} If `uid` is empty or any rate limit value is negative.
|
|
1512
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
1513
|
+
*
|
|
1514
|
+
* @example
|
|
1515
|
+
* ```typescript
|
|
1516
|
+
* await client.rateLimit.setUserLimit({
|
|
1517
|
+
* uid: 'alice',
|
|
1518
|
+
* maxReadOps: 100,
|
|
1519
|
+
* maxWriteOps: 50,
|
|
1520
|
+
* maxWriteBytes: 52428800, // 50MB/min
|
|
1521
|
+
* enabled: true,
|
|
1522
|
+
* });
|
|
1523
|
+
* ```
|
|
1524
|
+
*/
|
|
1525
|
+
async setUserLimit(input) {
|
|
1526
|
+
validateUid(input.uid);
|
|
1527
|
+
validateRateLimitFields(input);
|
|
1528
|
+
return this.client.request({
|
|
1529
|
+
method: "POST",
|
|
1530
|
+
path: "/ratelimit",
|
|
1531
|
+
query: {
|
|
1532
|
+
ratelimit: "",
|
|
1533
|
+
uid: input.uid,
|
|
1534
|
+
scope: "user",
|
|
1535
|
+
maxReadOps: input.maxReadOps,
|
|
1536
|
+
maxWriteOps: input.maxWriteOps,
|
|
1537
|
+
maxReadBytes: input.maxReadBytes,
|
|
1538
|
+
maxWriteBytes: input.maxWriteBytes,
|
|
1539
|
+
enabled: input.enabled ?? true
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Disable the rate limit for a user without changing the configured values.
|
|
1545
|
+
*
|
|
1546
|
+
* @param uid - The user ID.
|
|
1547
|
+
* @throws {RGWValidationError} If `uid` is empty.
|
|
1548
|
+
* @throws {RGWNotFoundError} If the user does not exist.
|
|
1549
|
+
*
|
|
1550
|
+
* @example
|
|
1551
|
+
* ```typescript
|
|
1552
|
+
* await client.rateLimit.disableUserLimit('alice');
|
|
1553
|
+
* ```
|
|
1554
|
+
*/
|
|
1555
|
+
async disableUserLimit(uid) {
|
|
1556
|
+
validateUid(uid);
|
|
1557
|
+
return this.client.request({
|
|
1558
|
+
method: "POST",
|
|
1559
|
+
path: "/ratelimit",
|
|
1560
|
+
query: { ratelimit: "", uid, scope: "user", enabled: false }
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Get the rate limit configuration for a bucket.
|
|
1565
|
+
*
|
|
1566
|
+
* @param bucket - The bucket name.
|
|
1567
|
+
* @returns The bucket's rate limit configuration.
|
|
1568
|
+
* @throws {RGWValidationError} If `bucket` is empty.
|
|
1569
|
+
* @throws {RGWNotFoundError} If the bucket does not exist.
|
|
1570
|
+
*
|
|
1571
|
+
* @example
|
|
1572
|
+
* ```typescript
|
|
1573
|
+
* const limit = await client.rateLimit.getBucketLimit('my-bucket');
|
|
1574
|
+
* console.log('Read ops/min:', limit.maxReadOps);
|
|
1575
|
+
* ```
|
|
1576
|
+
*/
|
|
1577
|
+
async getBucketLimit(bucket) {
|
|
1578
|
+
validateBucket2(bucket);
|
|
1579
|
+
return this.client.request({
|
|
1580
|
+
method: "GET",
|
|
1581
|
+
path: "/ratelimit",
|
|
1582
|
+
query: { ratelimit: "", bucket, scope: "bucket" }
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Set the rate limit for a bucket.
|
|
1587
|
+
*
|
|
1588
|
+
* Values are per RGW instance. Divide by the number of RGW daemons for cluster-wide limits.
|
|
1589
|
+
*
|
|
1590
|
+
* @param input - Rate limit settings. `bucket` is required. `enabled` defaults to `true`.
|
|
1591
|
+
* @throws {RGWValidationError} If `bucket` is empty or any rate limit value is negative.
|
|
1592
|
+
* @throws {RGWNotFoundError} If the bucket does not exist.
|
|
1593
|
+
*
|
|
1594
|
+
* @example
|
|
1595
|
+
* ```typescript
|
|
1596
|
+
* await client.rateLimit.setBucketLimit({
|
|
1597
|
+
* bucket: 'my-bucket',
|
|
1598
|
+
* maxReadOps: 200,
|
|
1599
|
+
* maxWriteOps: 100,
|
|
1600
|
+
* enabled: true,
|
|
1601
|
+
* });
|
|
1602
|
+
* ```
|
|
1603
|
+
*/
|
|
1604
|
+
async setBucketLimit(input) {
|
|
1605
|
+
validateBucket2(input.bucket);
|
|
1606
|
+
validateRateLimitFields(input);
|
|
1607
|
+
return this.client.request({
|
|
1608
|
+
method: "POST",
|
|
1609
|
+
path: "/ratelimit",
|
|
1610
|
+
query: {
|
|
1611
|
+
ratelimit: "",
|
|
1612
|
+
bucket: input.bucket,
|
|
1613
|
+
scope: "bucket",
|
|
1614
|
+
maxReadOps: input.maxReadOps,
|
|
1615
|
+
maxWriteOps: input.maxWriteOps,
|
|
1616
|
+
maxReadBytes: input.maxReadBytes,
|
|
1617
|
+
maxWriteBytes: input.maxWriteBytes,
|
|
1618
|
+
enabled: input.enabled ?? true
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Disable the rate limit for a bucket without changing the configured values.
|
|
1624
|
+
*
|
|
1625
|
+
* @param bucket - The bucket name.
|
|
1626
|
+
* @throws {RGWValidationError} If `bucket` is empty.
|
|
1627
|
+
* @throws {RGWNotFoundError} If the bucket does not exist.
|
|
1628
|
+
*
|
|
1629
|
+
* @example
|
|
1630
|
+
* ```typescript
|
|
1631
|
+
* await client.rateLimit.disableBucketLimit('my-bucket');
|
|
1632
|
+
* ```
|
|
1633
|
+
*/
|
|
1634
|
+
async disableBucketLimit(bucket) {
|
|
1635
|
+
validateBucket2(bucket);
|
|
1636
|
+
return this.client.request({
|
|
1637
|
+
method: "POST",
|
|
1638
|
+
path: "/ratelimit",
|
|
1639
|
+
query: { ratelimit: "", bucket, scope: "bucket", enabled: false }
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Get the global rate limit configuration for all scopes (user, bucket, anonymous).
|
|
1644
|
+
*
|
|
1645
|
+
* @returns Global rate limits for user, bucket, and anonymous scopes.
|
|
1646
|
+
*
|
|
1647
|
+
* @example
|
|
1648
|
+
* ```typescript
|
|
1649
|
+
* const global = await client.rateLimit.getGlobal();
|
|
1650
|
+
* console.log('Anonymous read limit:', global.anonymous.maxReadOps);
|
|
1651
|
+
* ```
|
|
1652
|
+
*/
|
|
1653
|
+
async getGlobal() {
|
|
1654
|
+
return this.client.request({
|
|
1655
|
+
method: "GET",
|
|
1656
|
+
path: "/ratelimit",
|
|
1657
|
+
query: { ratelimit: "", global: "" }
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Set a global rate limit for a specific scope.
|
|
1662
|
+
*
|
|
1663
|
+
* Use `scope: 'anonymous'` to protect public-read buckets from abuse.
|
|
1664
|
+
*
|
|
1665
|
+
* @param input - Rate limit settings. `scope` is required. `enabled` defaults to `true`.
|
|
1666
|
+
* @throws {RGWValidationError} If `scope` is invalid or any rate limit value is negative.
|
|
1667
|
+
*
|
|
1668
|
+
* @example
|
|
1669
|
+
* ```typescript
|
|
1670
|
+
* // Limit anonymous access globally
|
|
1671
|
+
* await client.rateLimit.setGlobal({
|
|
1672
|
+
* scope: 'anonymous',
|
|
1673
|
+
* maxReadOps: 50,
|
|
1674
|
+
* maxWriteOps: 0,
|
|
1675
|
+
* enabled: true,
|
|
1676
|
+
* });
|
|
1677
|
+
* ```
|
|
1678
|
+
*/
|
|
1679
|
+
async setGlobal(input) {
|
|
1680
|
+
const validScopes = ["user", "bucket", "anonymous"];
|
|
1681
|
+
if (!validScopes.includes(input.scope)) {
|
|
1682
|
+
throw new RGWValidationError(
|
|
1683
|
+
`scope must be one of: ${validScopes.join(", ")}. Got: "${input.scope}"`
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
validateRateLimitFields(input);
|
|
1687
|
+
return this.client.request({
|
|
1688
|
+
method: "POST",
|
|
1689
|
+
path: "/ratelimit",
|
|
1690
|
+
query: {
|
|
1691
|
+
ratelimit: "",
|
|
1692
|
+
global: "",
|
|
1693
|
+
scope: input.scope,
|
|
1694
|
+
maxReadOps: input.maxReadOps,
|
|
1695
|
+
maxWriteOps: input.maxWriteOps,
|
|
1696
|
+
maxReadBytes: input.maxReadBytes,
|
|
1697
|
+
maxWriteBytes: input.maxWriteBytes,
|
|
1698
|
+
enabled: input.enabled ?? true
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
|
|
1704
|
+
// src/modules/usage.ts
|
|
1705
|
+
function normalizeDate(value) {
|
|
1706
|
+
if (value instanceof Date) {
|
|
1707
|
+
if (isNaN(value.getTime())) {
|
|
1708
|
+
throw new RGWValidationError("Invalid Date object provided for date range filter");
|
|
1709
|
+
}
|
|
1710
|
+
return value.toISOString().slice(0, 10);
|
|
1711
|
+
}
|
|
1712
|
+
if (!/^\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2}:\d{2})?$/.test(value.trim())) {
|
|
1713
|
+
throw new RGWValidationError(
|
|
1714
|
+
`Invalid date string: "${value}". Use "YYYY-MM-DD" or "YYYY-MM-DD HH:MM:SS"`
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
return value.trim();
|
|
1718
|
+
}
|
|
1719
|
+
function warnIfInvertedRange(start, end) {
|
|
1720
|
+
if (start !== void 0 && end !== void 0 && start > end) {
|
|
1721
|
+
console.warn(
|
|
1722
|
+
`[radosgw-admin] WARNING: start "${start}" is after end "${end}" \u2014 RGW will return empty results`
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
var UsageModule = class {
|
|
1727
|
+
constructor(client) {
|
|
1728
|
+
this.client = client;
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Retrieve a usage report for a user or the entire cluster.
|
|
1732
|
+
*
|
|
1733
|
+
* Omit `uid` to get cluster-wide usage across all users.
|
|
1734
|
+
* Omit `start`/`end` to get all available usage data.
|
|
1735
|
+
*
|
|
1736
|
+
* > **Note:** Usage logging must be enabled in `ceph.conf`:
|
|
1737
|
+
* > `rgw enable usage log = true`
|
|
1738
|
+
*
|
|
1739
|
+
* @param input - Optional filters: `uid`, `start`, `end`, `showEntries`, `showSummary`.
|
|
1740
|
+
* @returns A usage report with `entries` (per-bucket detail) and `summary` (per-user totals).
|
|
1741
|
+
* @throws {RGWValidationError} If `uid` is provided but empty/whitespace.
|
|
1742
|
+
* @throws {RGWValidationError} If a date string is not in a recognised format.
|
|
1743
|
+
*
|
|
1744
|
+
* @example
|
|
1745
|
+
* ```typescript
|
|
1746
|
+
* // Cluster-wide usage, all time
|
|
1747
|
+
* const all = await client.usage.get();
|
|
1748
|
+
*
|
|
1749
|
+
* // Single user, date range
|
|
1750
|
+
* const report = await client.usage.get({
|
|
1751
|
+
* uid: 'alice',
|
|
1752
|
+
* start: '2025-01-01',
|
|
1753
|
+
* end: '2025-01-31',
|
|
1754
|
+
* });
|
|
1755
|
+
*
|
|
1756
|
+
* for (const s of report.summary) {
|
|
1757
|
+
* console.log(s.user, 'sent', s.total.bytesSent, 'bytes');
|
|
1758
|
+
* }
|
|
1759
|
+
* ```
|
|
1760
|
+
*/
|
|
1761
|
+
async get(input = {}) {
|
|
1762
|
+
if (input.uid !== void 0) {
|
|
1763
|
+
validateUid(input.uid);
|
|
1764
|
+
}
|
|
1765
|
+
const start = input.start !== void 0 ? normalizeDate(input.start) : void 0;
|
|
1766
|
+
const end = input.end !== void 0 ? normalizeDate(input.end) : void 0;
|
|
1767
|
+
warnIfInvertedRange(start, end);
|
|
1768
|
+
return this.client.request({
|
|
1769
|
+
method: "GET",
|
|
1770
|
+
path: "/usage",
|
|
1771
|
+
query: {
|
|
1772
|
+
uid: input.uid,
|
|
1773
|
+
start,
|
|
1774
|
+
end,
|
|
1775
|
+
showEntries: input.showEntries,
|
|
1776
|
+
showSummary: input.showSummary
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Delete (trim) usage log entries matching the given filters.
|
|
1782
|
+
*
|
|
1783
|
+
* **Destructive operation** — deleted log entries cannot be recovered.
|
|
1784
|
+
*
|
|
1785
|
+
* When no `uid` is provided the trim applies cluster-wide. In that case
|
|
1786
|
+
* `removeAll: true` is required to prevent accidental full-cluster log wipes.
|
|
1787
|
+
*
|
|
1788
|
+
* @param input - Trim filters. Omit entirely to trim nothing (use `removeAll: true` explicitly).
|
|
1789
|
+
* @throws {RGWValidationError} If `uid` is provided but empty/whitespace.
|
|
1790
|
+
* @throws {RGWValidationError} If `uid` is omitted but `removeAll` is not `true`.
|
|
1791
|
+
* @throws {RGWValidationError} If a date string is not in a recognised format.
|
|
1792
|
+
*
|
|
1793
|
+
* @example
|
|
1794
|
+
* ```typescript
|
|
1795
|
+
* // Trim a specific user's logs up to end of 2024
|
|
1796
|
+
* await client.usage.trim({ uid: 'alice', end: '2024-12-31' });
|
|
1797
|
+
*
|
|
1798
|
+
* // ⚠️ Trim all logs before 2024 across the entire cluster
|
|
1799
|
+
* await client.usage.trim({ end: '2023-12-31', removeAll: true });
|
|
1800
|
+
* ```
|
|
1801
|
+
*/
|
|
1802
|
+
async trim(input = {}) {
|
|
1803
|
+
if (input.uid !== void 0) {
|
|
1804
|
+
validateUid(input.uid);
|
|
1805
|
+
}
|
|
1806
|
+
if (!input.uid && input.removeAll !== true) {
|
|
1807
|
+
throw new RGWValidationError(
|
|
1808
|
+
"trim() without a uid removes logs for ALL users. Set removeAll: true to confirm."
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
if (input.removeAll) {
|
|
1812
|
+
console.warn(
|
|
1813
|
+
"[radosgw-admin] WARNING: usage.trim() with removeAll=true will permanently delete usage logs" + (input.uid ? ` for user "${input.uid}"` : " for ALL users")
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
const start = input.start !== void 0 ? normalizeDate(input.start) : void 0;
|
|
1817
|
+
const end = input.end !== void 0 ? normalizeDate(input.end) : void 0;
|
|
1818
|
+
warnIfInvertedRange(start, end);
|
|
1819
|
+
return this.client.request({
|
|
1820
|
+
method: "DELETE",
|
|
1821
|
+
path: "/usage",
|
|
1822
|
+
query: {
|
|
1823
|
+
uid: input.uid,
|
|
1824
|
+
start,
|
|
1825
|
+
end,
|
|
1826
|
+
removeAll: input.removeAll
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
|
|
1832
|
+
// src/modules/info.ts
|
|
1833
|
+
var InfoModule = class {
|
|
1834
|
+
constructor(client) {
|
|
1835
|
+
this.client = client;
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Get basic cluster information including the Ceph cluster FSID.
|
|
1839
|
+
*
|
|
1840
|
+
* Useful for confirming connectivity and identifying which cluster the
|
|
1841
|
+
* client is connected to when managing multiple environments.
|
|
1842
|
+
*
|
|
1843
|
+
* @returns Cluster info containing the cluster ID (FSID).
|
|
1844
|
+
* @throws {RGWAuthError} If the credentials are invalid or lack permission.
|
|
1845
|
+
*
|
|
1846
|
+
* @example
|
|
1847
|
+
* ```typescript
|
|
1848
|
+
* const info = await client.info.get();
|
|
1849
|
+
* console.log('Cluster FSID:', info.info.storageBackends[0].clusterId);
|
|
1850
|
+
* ```
|
|
1851
|
+
*/
|
|
1852
|
+
async get() {
|
|
1853
|
+
return this.client.request({
|
|
1854
|
+
method: "GET",
|
|
1855
|
+
path: "/info",
|
|
1856
|
+
query: {}
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
};
|
|
1860
|
+
|
|
1861
|
+
// src/index.ts
|
|
1862
|
+
var RadosGWAdminClient = class {
|
|
1863
|
+
/** @internal */
|
|
1864
|
+
_client;
|
|
1865
|
+
/** User management operations. */
|
|
1866
|
+
users;
|
|
1867
|
+
/** S3/Swift key management operations. */
|
|
1868
|
+
keys;
|
|
1869
|
+
/** Subuser management operations. */
|
|
1870
|
+
subusers;
|
|
1871
|
+
/** Bucket management operations. */
|
|
1872
|
+
buckets;
|
|
1873
|
+
/** Quota management operations (user-level and bucket-level). */
|
|
1874
|
+
quota;
|
|
1875
|
+
/** Rate limit management operations (user, bucket, and global). */
|
|
1876
|
+
rateLimit;
|
|
1877
|
+
/** Usage & analytics operations — query and trim RGW usage logs. */
|
|
1878
|
+
usage;
|
|
1879
|
+
/** Cluster info operations. */
|
|
1880
|
+
info;
|
|
1881
|
+
constructor(config) {
|
|
1882
|
+
this._client = new BaseClient(config);
|
|
1883
|
+
this.users = new UsersModule(this._client);
|
|
1884
|
+
this.keys = new KeysModule(this._client);
|
|
1885
|
+
this.subusers = new SubusersModule(this._client);
|
|
1886
|
+
this.buckets = new BucketsModule(this._client);
|
|
1887
|
+
this.quota = new QuotaModule(this._client);
|
|
1888
|
+
this.rateLimit = new RateLimitModule(this._client);
|
|
1889
|
+
this.usage = new UsageModule(this._client);
|
|
1890
|
+
this.info = new InfoModule(this._client);
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1894
|
+
0 && (module.exports = {
|
|
1895
|
+
BaseClient,
|
|
1896
|
+
RGWAuthError,
|
|
1897
|
+
RGWConflictError,
|
|
1898
|
+
RGWError,
|
|
1899
|
+
RGWNotFoundError,
|
|
1900
|
+
RGWValidationError,
|
|
1901
|
+
RadosGWAdminClient,
|
|
1902
|
+
signRequest
|
|
1903
|
+
});
|