Skip to content

Commit 12f8e1f

Browse files
designcodeclaude
andauthored
feat: add objects info and support t3:// paths in all objects subcommands (#59)
* feat: add `objects info` subcommand for object metadata Adds a dedicated `objects info` command that shows object metadata (size, content type, modified date, URL) via `head()`. Accepts two positional args (bucket + key) or a single combined path (t3://, tigris://, or bare bucket/key). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: support t3:// paths in all objects subcommands All objects commands (get, put, delete, set, list) now accept t3://, tigris://, or bare bucket/key paths in the bucket argument, matching the convention used by ls, stat, presign, and other top-level commands. Moved resolveObjectArgs to utils/path.ts as a shared helper. It also strips URI prefixes when bucket and key are given as separate args (e.g. `objects get t3://my-bucket report.pdf`). For list, a path component in the bucket arg is used as the prefix when --prefix is not given. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: inline path resolution in put/delete and add t3:// integration tests Revert extracted helper functions back to inline logic and add integration tests covering all objects subcommands with t3:// paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a0c5966 commit 12f8e1f

10 files changed

Lines changed: 358 additions & 34 deletions

File tree

src/lib/objects/delete.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
printSuccess,
1515
} from '@utils/messages.js';
1616
import { getFormat, getOption } from '@utils/options.js';
17+
import { resolveObjectArgs } from '@utils/path.js';
1718

1819
const context = msg('objects', 'delete');
1920

@@ -22,14 +23,18 @@ export default async function deleteObject(options: Record<string, unknown>) {
2223

2324
const format = getFormat(options);
2425

25-
const bucket = getOption<string>(options, ['bucket']);
26-
const keys = getOption<string | string[]>(options, ['key']);
26+
const bucketArg = getOption<string>(options, ['bucket']);
27+
const keysArg = getOption<string | string[]>(options, ['key']);
2728
const force = getOption<boolean>(options, ['yes', 'y', 'force']);
2829

29-
if (!bucket) {
30-
failWithError(context, 'Bucket name is required');
30+
if (!bucketArg) {
31+
failWithError(context, 'Bucket name or path is required');
3132
}
3233

34+
const resolved = resolveObjectArgs(bucketArg);
35+
const bucket = resolved.bucket;
36+
const keys = keysArg || resolved.key || undefined;
37+
3338
if (!keys) {
3439
failWithError(context, 'Object key is required');
3540
}

src/lib/objects/get.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { get } from '@tigrisdata/storage';
33
import { failWithError } from '@utils/exit.js';
44
import { msg, printStart, printSuccess } from '@utils/messages.js';
55
import { getFormat, getOption } from '@utils/options.js';
6+
import { resolveObjectArgs } from '@utils/path.js';
67
import { createWriteStream, writeFileSync } from 'fs';
78
import { extname } from 'path';
89
import { Readable } from 'stream';
@@ -106,8 +107,8 @@ export default async function getObject(options: Record<string, unknown>) {
106107

107108
const outputFormat = getFormat(options);
108109

109-
const bucket = getOption<string>(options, ['bucket']);
110-
const key = getOption<string>(options, ['key']);
110+
const bucketArg = getOption<string>(options, ['bucket']);
111+
const keyArg = getOption<string>(options, ['key']);
111112
const output = getOption<string>(options, ['output', 'o', 'O']);
112113
const modeOption = getOption<string>(options, ['mode', 'm', 'M']);
113114
const snapshotVersion = getOption<string>(options, [
@@ -116,10 +117,12 @@ export default async function getObject(options: Record<string, unknown>) {
116117
'snapshot',
117118
]);
118119

119-
if (!bucket) {
120-
failWithError(context, 'Bucket name is required');
120+
if (!bucketArg) {
121+
failWithError(context, 'Bucket name or path is required');
121122
}
122123

124+
const { bucket, key } = resolveObjectArgs(bucketArg, keyArg);
125+
123126
if (!key) {
124127
failWithError(context, 'Object key is required');
125128
}

src/lib/objects/info.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { getStorageConfig } from '@auth/provider.js';
2+
import { head } from '@tigrisdata/storage';
3+
import { failWithError } from '@utils/exit.js';
4+
import { formatOutput, formatSize } from '@utils/format.js';
5+
import { msg, printStart, printSuccess } from '@utils/messages.js';
6+
import { getFormat, getOption } from '@utils/options.js';
7+
import { resolveObjectArgs } from '@utils/path.js';
8+
9+
const context = msg('objects', 'info');
10+
11+
export default async function objectInfo(options: Record<string, unknown>) {
12+
printStart(context);
13+
14+
const format = getFormat(options);
15+
const bucketArg = getOption<string>(options, ['bucket']);
16+
const keyArg = getOption<string>(options, ['key']);
17+
const snapshotVersion = getOption<string>(options, [
18+
'snapshot-version',
19+
'snapshotVersion',
20+
'snapshot',
21+
]);
22+
23+
if (!bucketArg) {
24+
failWithError(context, 'Bucket name or path is required');
25+
}
26+
27+
const { bucket, key } = resolveObjectArgs(bucketArg, keyArg);
28+
29+
if (!key) {
30+
failWithError(context, 'Object key is required');
31+
}
32+
33+
const config = await getStorageConfig();
34+
35+
const { data, error } = await head(key, {
36+
...(snapshotVersion ? { snapshotVersion } : {}),
37+
config: {
38+
...config,
39+
bucket,
40+
},
41+
});
42+
43+
if (error) {
44+
failWithError(context, error);
45+
}
46+
47+
if (!data) {
48+
failWithError(context, 'Object not found');
49+
}
50+
51+
const info = [
52+
{ metric: 'Path', value: data.path },
53+
{ metric: 'Size', value: formatSize(data.size) },
54+
{ metric: 'Content-Type', value: data.contentType || 'N/A' },
55+
{ metric: 'Content-Disposition', value: data.contentDisposition || 'N/A' },
56+
{ metric: 'Modified', value: data.modified.toISOString() },
57+
{ metric: 'URL', value: data.url },
58+
];
59+
60+
const output = formatOutput(info, format!, 'object-info', 'info', [
61+
{ key: 'metric', header: 'Metric' },
62+
{ key: 'value', header: 'Value' },
63+
]);
64+
65+
console.log(output);
66+
printSuccess(context, { bucket, key });
67+
}

src/lib/objects/list.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,34 @@ import { failWithError } from '@utils/exit.js';
44
import { formatOutput, formatSize } from '@utils/format.js';
55
import { msg, printEmpty, printStart, printSuccess } from '@utils/messages.js';
66
import { getFormat, getOption } from '@utils/options.js';
7+
import { parseAnyPath } from '@utils/path.js';
78

89
const context = msg('objects', 'list');
910

1011
export default async function listObjects(options: Record<string, unknown>) {
1112
printStart(context);
1213

13-
const bucket = getOption<string>(options, ['bucket']);
14-
const prefix = getOption<string>(options, ['prefix', 'p', 'P']);
14+
const bucketArg = getOption<string>(options, ['bucket']);
15+
const prefixFlag = getOption<string>(options, ['prefix', 'p', 'P']);
1516
const format = getFormat(options);
1617
const snapshotVersion = getOption<string>(options, [
1718
'snapshot-version',
1819
'snapshotVersion',
1920
'snapshot',
2021
]);
2122

22-
if (!bucket) {
23+
if (!bucketArg) {
2324
failWithError(context, 'Bucket name is required');
2425
}
2526

27+
const parsed = parseAnyPath(bucketArg);
28+
const bucket = parsed.bucket;
29+
const prefix = prefixFlag || parsed.path || undefined;
30+
2631
const config = await getStorageConfig();
2732

2833
const { data, error } = await list({
29-
prefix: prefix || undefined,
34+
prefix,
3035
...(snapshotVersion ? { snapshotVersion } : {}),
3136
config: {
3237
...config,

src/lib/objects/put.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { failWithError, printNextActions } from '@utils/exit.js';
44
import { formatOutput, formatSize } from '@utils/format.js';
55
import { msg, printStart, printSuccess } from '@utils/messages.js';
66
import { getFormat, getOption } from '@utils/options.js';
7+
import { resolveObjectArgs } from '@utils/path.js';
78
import { calculateUploadParams } from '@utils/upload.js';
89
import { createReadStream, statSync } from 'fs';
910
import { Readable } from 'stream';
@@ -13,9 +14,9 @@ const context = msg('objects', 'put');
1314
export default async function putObject(options: Record<string, unknown>) {
1415
printStart(context);
1516

16-
const bucket = getOption<string>(options, ['bucket']);
17-
const key = getOption<string>(options, ['key']);
18-
const file = getOption<string>(options, ['file']);
17+
const bucketArg = getOption<string>(options, ['bucket']);
18+
const keyArg = getOption<string>(options, ['key']);
19+
const fileArg = getOption<string>(options, ['file']);
1920
const access = getOption<string>(options, ['access', 'a', 'A'], 'private');
2021
const contentType = getOption<string>(options, [
2122
'content-type',
@@ -25,10 +26,15 @@ export default async function putObject(options: Record<string, unknown>) {
2526
]);
2627
const format = getFormat(options);
2728

28-
if (!bucket) {
29-
failWithError(context, 'Bucket name is required');
29+
if (!bucketArg) {
30+
failWithError(context, 'Bucket name or path is required');
3031
}
3132

33+
const combined = resolveObjectArgs(bucketArg);
34+
const bucket = combined.bucket;
35+
const key = combined.key || keyArg;
36+
const file = combined.key ? keyArg || fileArg : fileArg;
37+
3238
if (!key) {
3339
failWithError(context, 'Object key is required');
3440
}

src/lib/objects/set.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { updateObject } from '@tigrisdata/storage';
33
import { failWithError } from '@utils/exit.js';
44
import { msg, printStart, printSuccess } from '@utils/messages.js';
55
import { getFormat, getOption } from '@utils/options.js';
6+
import { resolveObjectArgs } from '@utils/path.js';
67

78
const context = msg('objects', 'set');
89

@@ -11,15 +12,17 @@ export default async function setObject(options: Record<string, unknown>) {
1112

1213
const format = getFormat(options);
1314

14-
const bucket = getOption<string>(options, ['bucket']);
15-
const key = getOption<string>(options, ['key']);
15+
const bucketArg = getOption<string>(options, ['bucket']);
16+
const keyArg = getOption<string>(options, ['key']);
1617
const access = getOption<string>(options, ['access', 'a', 'A']);
1718
const newKey = getOption<string>(options, ['new-key', 'n', 'newKey']);
1819

19-
if (!bucket) {
20-
failWithError(context, 'Bucket name is required');
20+
if (!bucketArg) {
21+
failWithError(context, 'Bucket name or path is required');
2122
}
2223

24+
const { bucket, key } = resolveObjectArgs(bucketArg, keyArg);
25+
2326
if (!key) {
2427
failWithError(context, 'Object key is required');
2528
}

0 commit comments

Comments
 (0)