Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
812 changes: 251 additions & 561 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
"@aws-sdk/credential-providers": "^3.1038.0",
"@smithy/shared-ini-file-loader": "^4.4.9",
"@tigrisdata/iam": "^2.1.1",
"@tigrisdata/storage": "^3.6.0",
"@tigrisdata/storage": "^3.12.0",
"commander": "^14.0.3",
"enquirer": "^2.4.1",
"jose": "^6.2.3",
Expand Down
15 changes: 14 additions & 1 deletion src/cli-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ export function formatArgumentHelp(arg: Argument): string {
description += ' [required]';
}

if (arg.deprecated) {
description += arg.replaced_by
? ` [deprecated: use ${arg.replaced_by}]`
: ' [deprecated]';
}

if (arg['required-when']) {
description += ` [required when: ${arg['required-when']}]`;
}
Expand Down Expand Up @@ -373,7 +379,14 @@ export function addArgumentsToCommand(
new Option(optionString, arg.description ?? '').hideHelp()
);
} else {
cmd.option(optionString, arg.description ?? '', arg.default);
let description = arg.description ?? '';
if (arg.deprecated) {
const hint = arg.replaced_by
? ` Use ${arg.replaced_by} instead.`
: '';
description = `(deprecated) ${description}${hint}`;
}
cmd.option(optionString, description, arg.default);
}
}
});
Expand Down
63 changes: 26 additions & 37 deletions src/lib/buckets/list.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getStorageConfig } from '@auth/provider.js';
import { getBucketInfo, listBuckets } from '@tigrisdata/storage';
import { listBuckets, listForks } from '@tigrisdata/storage';
import { failWithError } from '@utils/exit.js';
import { formatOutput, formatPaginatedOutput } from '@utils/format.js';
import {
Expand All @@ -18,6 +18,7 @@ export default async function list(options: Record<string, unknown>) {

const format = getFormat(options);
const forksOf = getOption<string>(options, ['forks-of', 'forksOf']);
const deleted = getOption<boolean>(options, ['deleted']);
const { limit, pageToken, isPaginated } = getPaginationOptions(options);
const config = await getStorageConfig();

Expand All @@ -27,57 +28,29 @@ export default async function list(options: Record<string, unknown>) {
);
}

const { data, error } = await listBuckets({
...(forksOf
? {}
: {
...(limit !== undefined ? { limit } : {}),
...(pageToken ? { paginationToken: pageToken } : {}),
}),
config,
});

if (error) {
failWithError(context, error);
}

if (!data.buckets || data.buckets.length === 0) {
printEmpty(context);
return;
if (forksOf && deleted) {
console.warn(
'⚠ --deleted is ignored when --forks-of is used; use --deleted on its own to list soft-deleted buckets'
);
}

if (forksOf) {
// Filter for forks of the named source bucket
const { data: bucketInfo, error: infoError } = await getBucketInfo(
forksOf,
{ config }
);
const { data, error: infoError } = await listForks(forksOf, { config });

if (infoError) {
failWithError(context, infoError);
}

if (!bucketInfo.forkInfo?.hasChildren) {
if (!data.forks || data.forks.length === 0) {
printEmpty(context);
return;
}

const forks: Array<{ name: string; created: Date }> = [];

for (const bucket of data.buckets) {
if (bucket.name === forksOf) continue;
const { data: info } = await getBucketInfo(bucket.name, { config });
const isChildOf = info?.forkInfo?.parents?.some(
(p) => p.bucketName === forksOf
);
if (isChildOf) {
forks.push({ name: bucket.name, created: bucket.creationDate });
}
}

if (forks.length === 0) {
printEmpty(context);
return;
for (const bucket of data.forks) {
forks.push({ name: bucket.name, created: bucket.creationDate });
}

const output = formatOutput(forks, format!, 'forks', 'fork', [
Expand All @@ -90,6 +63,22 @@ export default async function list(options: Record<string, unknown>) {
return;
}

const { data, error } = await listBuckets({
...(limit !== undefined ? { limit } : {}),
...(pageToken ? { paginationToken: pageToken } : {}),
...(deleted ? { deleted } : {}),
config,
});

if (error) {
failWithError(context, error);
}

if (!data.buckets || data.buckets.length === 0) {
printEmpty(context);
return;
}

const buckets = data.buckets.map((bucket) => ({
name: bucket.name,
created: bucket.creationDate,
Expand Down
56 changes: 56 additions & 0 deletions src/lib/buckets/restore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { getStorageConfig } from '@auth/provider.js';
import { restoreBucket } from '@tigrisdata/storage';
import {
exitWithError,
failWithError,
getSuccessNextActions,
printNextActions,
} from '@utils/exit.js';
import {
msg,
printFailure,
printStart,
printSuccess,
} from '@utils/messages.js';
import { getFormat, getOption } from '@utils/options.js';

const context = msg('buckets', 'restore');

export default async function restore(options: Record<string, unknown>) {
printStart(context);

const format = getFormat(options);
const name = getOption<string>(options, ['name']);

if (!name) {
failWithError(context, 'Bucket name is required');
}

const { data, error } = await restoreBucket(name, {
config: await getStorageConfig(),
});

if (error) {
printFailure(context, error.message, { name });
exitWithError(error, context);
}

if (!data.restored) {
const message = 'Bucket could not be restored';
printFailure(context, message, { name });
exitWithError(message, context);
}

if (format === 'json') {
const nextActions = getSuccessNextActions(context, { name });
const output: Record<string, unknown> = {
action: 'restored',
name,
};
if (nextActions.length > 0) output.nextActions = nextActions;
console.log(JSON.stringify(output));
}

printSuccess(context, { name });
printNextActions(context, { name });
}
37 changes: 37 additions & 0 deletions src/lib/buckets/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export default async function set(options: Record<string, unknown>) {
'enable-delete-protection',
'enableDeleteProtection',
]);
const softDelete = getOption<string>(options, ['soft-delete', 'softDelete']);
const retentionDays = getOption<string | number>(options, [
'retention-days',
'retentionDays',
]);
const enableAdditionalHeaders = getOption<string | boolean>(options, [
'enable-additional-headers',
'enableAdditionalHeaders',
Expand All @@ -53,6 +58,7 @@ export default async function set(options: Record<string, unknown>) {
cacheControl === undefined &&
customDomain === undefined &&
enableDeleteProtection === undefined &&
softDelete === undefined &&
Comment thread
designcode marked this conversation as resolved.
enableAdditionalHeaders === undefined
) {
failWithError(context, 'At least one setting is required');
Expand Down Expand Up @@ -91,6 +97,37 @@ export default async function set(options: Record<string, unknown>) {
updateOptions.enableDeleteProtection = parseBoolean(enableDeleteProtection);
}

if (
softDelete !== undefined &&
softDelete !== 'enable' &&
softDelete !== 'disable'
) {
failWithError(context, '--soft-delete must be "enable" or "disable"');
}

if (retentionDays !== undefined && softDelete !== 'enable') {
failWithError(
context,
'--retention-days can only be used with --soft-delete enable'
);
}

if (softDelete === 'enable') {
if (retentionDays === undefined) {
failWithError(
context,
'--retention-days is required when enabling soft delete'
);
}
const days = Number(retentionDays);
if (!Number.isInteger(days) || days <= 0) {
failWithError(context, '--retention-days must be a positive integer');
}
updateOptions.softDelete = { enabled: true, retentionDays: days };
} else if (softDelete === 'disable') {
updateOptions.softDelete = { enabled: false };
}

if (enableAdditionalHeaders !== undefined) {
updateOptions.enableAdditionalHeaders = parseBoolean(
enableAdditionalHeaders
Expand Down
32 changes: 32 additions & 0 deletions src/specs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@ commands:
- "tigris buckets list"
- "tigris buckets list --format json"
- "tigris buckets list --forks-of my-bucket"
- "tigris buckets list --deleted"
messages:
onStart: 'Listing buckets...'
onSuccess: 'Found {{count}} bucket(s)'
Expand All @@ -679,6 +680,9 @@ commands:
default: table
- name: forks-of
description: Only list buckets that are forks of the named source bucket
- name: deleted
description: Only list soft-deleted buckets
type: flag
- name: limit
description: Maximum number of items to return per page
- name: page-token
Expand Down Expand Up @@ -793,6 +797,25 @@ commands:
- name: force
type: flag
description: Skip confirmation prompts (alias for --yes)
# restore
- name: restore
description: Restore a soft-deleted bucket within its retention window. List recoverable buckets with "tigris buckets list --deleted"
examples:
- "tigris buckets restore my-bucket"
messages:
onStart: 'Restoring bucket...'
onSuccess: "Bucket '{{name}}' restored successfully"
onFailure: "Failed to restore bucket '{{name}}'"
nextActions:
- command: 'tigris buckets get {{name}}'
description: 'Inspect the restored bucket'
arguments:
- name: name
description: Name of the soft-deleted bucket to restore
type: positional
required: true
examples:
- my-bucket
# set
- name: set
description: Update settings on an existing bucket such as access level, location, caching, or custom domain
Expand All @@ -801,6 +824,8 @@ commands:
- "tigris buckets set my-bucket --access public"
- "tigris buckets set my-bucket --locations iad,fra --cache-control 'max-age=3600'"
- "tigris buckets set my-bucket --custom-domain assets.example.com"
- "tigris buckets set my-bucket --soft-delete enable --retention-days 30"
- "tigris buckets set my-bucket --soft-delete disable"
messages:
onStart: 'Updating bucket...'
onSuccess: 'Bucket {{name}} updated successfully'
Expand Down Expand Up @@ -835,6 +860,13 @@ commands:
- name: enable-delete-protection
description: Enable delete protection
type: boolean
deprecated: true
replaced_by: --soft-delete
- name: soft-delete
description: Enable or disable soft delete (recoverable deletes). Requires --retention-days when enabling
options: [enable, disable]
- name: retention-days
description: Number of days to retain soft-deleted objects (required when enabling --soft-delete)
- name: enable-additional-headers
description: Enable additional HTTP headers (X-Content-Type-Options nosniff)
type: boolean
Expand Down
4 changes: 3 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export interface Argument {
examples?: string[];
/** Hard-removed: providing the flag exits with a redirect message. */
removed?: boolean;
/** Replacement to suggest when a removed argument or command is used. */
/** Soft-deprecated: still works, but flagged in help and superseded by `replaced_by`. */
deprecated?: boolean;
/** Replacement to suggest when a removed or deprecated argument or command is used. */
replaced_by?: string;
}

Expand Down
19 changes: 19 additions & 0 deletions src/utils/bucket-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
} from '@tigrisdata/storage';

import { formatSize } from './format.js';
import { formatLocations } from './locations.js';

/**
* Human-readable description of a rule's transition, or undefined if
Expand Down Expand Up @@ -78,14 +79,23 @@ export function buildBucketInfo(data: BucketInfoResponse) {
value: data.sizeInfo.numberOfObjectsAllVersions?.toString() ?? 'N/A',
},
{ label: 'Default Tier', value: data.settings.defaultTier },
{ label: 'Locations', value: formatLocations(data.locations) },
{
label: 'Snapshots Enabled',
value: data.isSnapshotEnabled ? 'Yes' : 'No',
},
{
label: 'Delete Protection',
// deleteProtection is deprecated in favor of softDelete (shown below),
// but kept here so existing output isn't dropped.
value: data.settings.deleteProtection ? 'Yes' : 'No',
},
{
label: 'Soft Delete',
value: data.settings.softDelete.enabled
? `Enabled (${data.settings.softDelete.retentionDays} day retention)`
: 'Disabled',
},
{
label: 'Allow Object ACL',
value: data.settings.allowObjectAcl ? 'Yes' : 'No',
Expand Down Expand Up @@ -120,6 +130,15 @@ export function buildBucketInfo(data: BucketInfoResponse) {
});
}

if (data.settings.additionalHeaders) {
info.push({
label: 'Additional Headers',
value: Object.entries(data.settings.additionalHeaders)
.map(([key, val]) => `${key}: ${val}`)
.join(', '),
});
}

if (data.settings.corsRules.length) {
info.push({
label: 'CORS Rules',
Expand Down
Loading