Skip to content

Commit

Permalink
Add put-object and more config commands
Browse files Browse the repository at this point in the history
Add ability to remove configs / creds / list creds.
Add ability to dump outbound request verbosely.
  • Loading branch information
vlovich committed Oct 29, 2022
1 parent a64c43b commit cb7be6c
Show file tree
Hide file tree
Showing 3 changed files with 348 additions and 24 deletions.
111 changes: 108 additions & 3 deletions config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ class CandidatePaths {
try {
await new Promise<void>((resolve, reject) => fs.mkdir(parentDir, (err) => err ? reject(err) : resolve()))
} catch (e) {
if (Object.prototype.hasOwnProperty.call(e, 'code') && (e as NodeJS.ErrnoException).code !== 'EEXIST') {
if (Object.prototype.hasOwnProperty.call(e, 'code') && (e as NodeJS.ErrnoException).code === 'EEXIST') {
} else {
console.warn('Trouble creating path', parentDir, e)
continue
}
Expand Down Expand Up @@ -263,6 +264,85 @@ async function retrieveCreds(config: { account_id: string; access_key_id: string
return Ok(secret_access_key)
}

async function removeCred(config: { account_id: string; access_key_id: string }): Promise<Result<void, Error>> {
const endpoint = `https://${config.account_id}.r2.cloudflarestorage.com`

console.log(
`Removing R2 token secret with id ${config.access_key_id} for ${endpoint} from your OS encrypted password storage.`,
)

const keytar = await loadKeytar()
if (keytar.err) {
return keytar
}

if (await keytar.val.deletePassword(endpoint, config.access_key_id)) {
return Ok.EMPTY
}

return Err(new Error('Unknown problem removing token secret'))
}

async function listCreds(account: string): Promise<Result<string[], Error>> {
const keytar = await loadKeytar()
if (keytar.err) {
return keytar
}

const endpoint = `https://${account}.r2.cloudflarestorage.com`

const creds = await keytar.val.findCredentials(endpoint)
return Ok(creds.map(({ account }) => account))
}

export async function listCredsCommand(argv: ArgumentsCamelCase<{ account: string }>): Promise<void> {
const keytar = await loadKeytar()
if (keytar.err) {
process.exitCode = 1
return
}

const endpoint = `https://${argv.account}.r2.cloudflarestorage.com`

for (const cred of await keytar.val.findCredentials(endpoint)) {
console.info(`Found token id ${cred.account}`)
}
}

export async function removeCredCommand(
argv: ArgumentsCamelCase<{ account: string; 'access-key-id'?: string }>,
): Promise<void> {
let access_key_id = argv['access-key-id']
if (access_key_id === undefined) {
const choices = await listCreds(argv.account)
if (choices.err) {
process.exitCode = 1
return
}

if (choices.val.length === 0) {
console.info('No credentials found')
return
}

const prompt = inquirer.createPromptModule()
const answer = await prompt({
name: 'id',
message: `Which access key would you like to remove for account ${argv.account}`,
type: 'list',
choices: choices.val,
})

access_key_id = answer.id as string
}

const result = await removeCred({ account_id: argv.account, access_key_id })
if (result.err) {
process.exitCode = 1
return
}
}

export async function importConfig(argv: ArgumentsCamelCase): Promise<void> {
const r2ConfigPaths = new CandidatePaths('cloudflare', 'r2.toml')
const configFilePath = (await r2ConfigPaths.createInitialConfig()).unwrap()
Expand Down Expand Up @@ -334,7 +414,7 @@ export async function importConfig(argv: ArgumentsCamelCase): Promise<void> {
console.info(`Imported ${numConfigurationsImported} ${importSource} configurations into ${configFilePath}`)
}

export async function initConfig(argv: ArgumentsCamelCase): Promise<void> {
export async function initConfigCommand(argv: ArgumentsCamelCase): Promise<void> {
// TODO: It would be nice to just navigate you through available accounts like wrangler does.
// TODO: Use wrangler creds from ~/.wrangler/config/default.toml to communicate with the API.
const name = argv['name'] as string
Expand All @@ -361,7 +441,7 @@ export async function initConfig(argv: ArgumentsCamelCase): Promise<void> {
console.info(`Added configuration ${name} to ${configFilePath}`)
}

export async function listConfigs(): Promise<void> {
export async function listConfigsCommand(): Promise<void> {
const r2ConfigPaths = new CandidatePaths('cloudflare', 'r2.toml')
const configFilePath = (await r2ConfigPaths.createInitialConfig()).unwrap()
console.log(await readTextFile(configFilePath))
Expand Down Expand Up @@ -411,6 +491,31 @@ export async function retrieveOnlyConfig(): Promise<Result<Config, Error>> {
})
}

export async function removeConfigCommand(argv: ArgumentsCamelCase<{ name: string }>): Promise<void> {
const config = await retrieveConfig(argv.name)
if (config.err) {
process.exitCode = 1
return
}

const r2ConfigPaths = new CandidatePaths('cloudflare', 'r2.toml')
const configFilePath = (await r2ConfigPaths.createInitialConfig()).unwrap()
const existingConfig = TOML.parse(await readTextFile(configFilePath)) as Record<
string,
{ account: string; access_key_id: string }
>
delete existingConfig[config.val.profile]

const removal = await removeCred(config.val)
if (removal.err) {
process.exitCode = 1
console.error(`Failed to remove creds for token ${config.val.access_key_id}`)
return
}

await writeTextFile(configFilePath, TOML.stringify(existingConfig))
}

export async function retrieveConfig(accountOrProfile: string): Promise<Result<Config, Error>> {
const r2ConfigPaths = new CandidatePaths('cloudflare', 'r2.toml')
const configFilePath = (await r2ConfigPaths.createInitialConfig()).unwrap()
Expand Down
51 changes: 42 additions & 9 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import cliProgress from 'cli-progress'
import process from 'node:process'
import { ArgumentsCamelCase } from 'yargs'
import yargs from 'yargs/yargs'
import { importConfig, initConfig, listConfigs } from './config'
import { importConfig, initConfigCommand, listConfigsCommand, listCredsCommand as listCredsCommand, removeConfigCommand, removeCredCommand } from './config'
import { buildS3Commands, GenericCmdArgs, handleS3Cmd } from './s3'

interface ProgressBarOptions {
Expand All @@ -20,18 +20,50 @@ const argv =
.command('import', 'Import your configuration from another tool', (yargs) => {
yargs.option('r', { alias: 'rclone' })
}, importConfig)
.command(['add', 'init'], 'Add an R2 account profile', (yargs) => {
.command(['add <name> <account>', 'init'], 'Add an R2 account profile', (yargs) => {
yargs
.option('name', { describe: 'The name of the profile', requiresArg: true, type: 'string' })
.option('account', {
alias: 'a',
describe: 'The Cloudflare account ID with an R2 subscription',
.positional('name', {
describe: 'The name of the profile',
requiresArg: true,
type: 'string',
demandOption: true,
})
.positional('account', {
describe: 'The Cloudflare account ID with an R2 subscription',
type: 'string',
demandOption: true,
})
.demandOption(['name', 'account'])
}, initConfig)
.command(['list', 'ls'], 'List R2 accounts that are configured', () => {}, listConfigs)
}, initConfigCommand)
.command('rm <name|account>', 'Remove by profile name or account', (yargs) => {
yargs.positional('name', {
type: 'string',
description:
'The name of the profile or the account id. If multiple profiles match the account id you will be prompted which one to remove.',
demandOption: true,
})
}, removeConfigCommand)
.command(['list', 'ls'], 'List R2 accounts that are configured', () => {}, listConfigsCommand)
.command('list-creds <account>', 'List all R2 credentials saved', (yargs) => {
yargs.positional('account', {
type: 'string',
description: 'The Cloudflare account ID to list saved R2 tokens for',
demandOption: true,
})
}, listCredsCommand)
.command('rm-cred <account> [access-key-id]', 'List all R2 credentials saved', (yargs) => {
yargs
.positional('account', {
type: 'string',
description: 'The Cloudflare account ID to list saved R2 tokens for',
demandOption: true,
})
.positional('access-key-id', {
type: 'string',
description:
'The token ID to remove. If not specified you will be prompted to confirm which one to remove.',
})
}, removeCredCommand)
.demandCommand(1, 1)
.help('h')
.alias('h', 'help')
Expand All @@ -48,7 +80,8 @@ const argv =
} | {percentage}% | {value}/{total} | {eta_formatted} | {speed}`,
}, cliProgress.Presets.shades_classic)
return bar
}, moreHeaders), yargs)
}, moreHeaders)
.then(() => process.exit()), yargs)
})
.demandCommand(1, 1)
.strict()
Expand Down
Loading

0 comments on commit cb7be6c

Please sign in to comment.