diff --git a/README.md b/README.md index 7de51c6a..6ec5ea98 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ the [releases tab](https://github.com/ethersphere/swarm-cli/releases). - [Uploading a File](#uploading-a-file) - [Creating an Identity](#creating-an-identity) - [Uploading to a Feed](#uploading-to-a-feed) + - [Upload file with ACT](#upload-file-with-act) + - [Download file with ACT](#download-file-with-act) + - [Create grantees list](#create-grantees-list) + - [Get grantees list](#get-grantees-list) + - [Patch grantees list](#patch-grantees-list) - [Description](#description) - [Installation](#installation) - [From npm](#from-npm) @@ -68,6 +73,64 @@ the [releases tab](https://github.com/ethersphere/swarm-cli/releases). ![Swarm CLI Feed Upload Command](./docs/feed-upload.gif) +## Upload a file with ACT + +```sh +swarm-cli upload --act --stamp +``` + +## Download a file with ACT + +```sh +swarm-cli download --act --act-history-address --act-publisher +``` + +## Create a grantees list + +```sh +swarm-cli grantee create grantees.json --stamp +``` + +`grantees.json`: + +```json +{ "grantees": [ + "02ceff1422a7026ba54ad89967d81f2805a55eb3d05f64eb5c49ea6024212b12e8", + "02ceff1422a7026ba54ad89967d81f2805a55eb3d05f64eb5c49ea6024212b12e9", + "02ceff1422a7026ba54ad89967d81f2805a55eb3d05f64eb5c49ea6024212b12ee" +] +} +``` + +## Get a grantees list + +```sh +swarm-cli grantee get +``` + +## Patch a grantees list + +```sh +swarm-cli grantee patch grantees-patch.json \ + --reference \ + --history \ + --stamp +``` + +`grantees-patch.json`: + +```json +{ + "add": [ + "02ceff1422a7026ba54ad89967d81f2805a55eb3d05f64eb5c49ea6024212b12e7" + ], + "revoke": [ + "02ceff1422a7026ba54ad89967d81f2805a55eb3d05f64eb5c49ea6024212b12e9", + "02ceff1422a7026ba54ad89967d81f2805a55eb3d05f64eb5c49ea6024212b12ee" + ] +} +``` + # Description > Manage your Bee node and interact with the Swarm network via the CLI diff --git a/src/command/download.ts b/src/command/download.ts index ba0693eb..80a8cf6e 100644 --- a/src/command/download.ts +++ b/src/command/download.ts @@ -14,11 +14,22 @@ export class Download extends RootCommand implements LeafCommand { public manifestDownload!: ManifestDownload private address!: BzzAddress + private actReqHeaders: Record = {} public async run(): Promise { await super.init() + const { act, actTimestamp, actHistoryAddress, actPublisher } = this.manifestDownload - this.address = await makeBzzAddress(this.bee, this.manifestDownload.bzzUrl) + if (act) { + this.actReqHeaders = { + 'Swarm-Act': 'true', + 'Swarm-Act-Timestamp': actTimestamp, + 'Swarm-Act-History-Address': actHistoryAddress, + 'Swarm-Act-Publisher': actPublisher, + } + } + + this.address = await makeBzzAddress(this.bee, this.manifestDownload.bzzUrl, this.actReqHeaders) if (await this.isManifest()) { this.manifestDownload.address = this.address @@ -29,7 +40,12 @@ export class Download extends RootCommand implements LeafCommand { } private async downloadFile(): Promise { - const response = await this.bee.downloadFile(this.address.hash) + const response = await (this.manifestDownload.act + ? this.bee.downloadFile(this.address.hash, '', { + headers: this.actReqHeaders, + }) + : this.bee.downloadFile(this.address.hash)) + const { name, data } = response if (this.manifestDownload.stdout) { diff --git a/src/command/feed/feed-command.ts b/src/command/feed/feed-command.ts index 1a429762..a4c90f9b 100644 --- a/src/command/feed/feed-command.ts +++ b/src/command/feed/feed-command.ts @@ -93,7 +93,7 @@ export class FeedCommand extends RootCommand { ) const { reference } = await writer.upload(stamp, chunkReference as Reference) - return { reference, manifest } + return { reference: reference.toString(), manifest } } finally { spinner.stop() } diff --git a/src/command/grantee/create.ts b/src/command/grantee/create.ts new file mode 100644 index 00000000..408df187 --- /dev/null +++ b/src/command/grantee/create.ts @@ -0,0 +1,40 @@ +import { Argument, LeafCommand, Option } from 'furious-commander' +import { GranteeCommand } from './grantee-command' +import { stampProperties } from '../../utils/option' +import { createKeyValue } from '../../utils/text' +import fs from 'fs' + +export class Create extends GranteeCommand implements LeafCommand { + public readonly name = 'create' + public readonly description = 'Create grantee list' + private actReqHeaders: Record = {} + + @Argument({ + key: 'path', + description: 'Path to the file with grantee list', + required: true, + autocompletePath: true, + conflicts: 'stdin', + }) + public path!: string + + @Option({ key: 'stdin', type: 'boolean', description: 'Take data from standard input', conflicts: 'path' }) + public stdin!: boolean + + @Option(stampProperties) + public stamp!: string + + public async run(): Promise { + await super.init() + this.actReqHeaders = { + 'Swarm-Act': 'true', + } + const granteesFile = fs.readFileSync(this.path, 'utf8') + const createGrantees = JSON.parse(granteesFile) + const grantees = createGrantees.grantees + + const response = await this.bee.createGrantees(this.stamp, grantees) + this.console.log(createKeyValue('Grantee reference', response.ref)) + this.console.log(createKeyValue('Grantee history reference', response.historyref)) + } +} diff --git a/src/command/grantee/get.ts b/src/command/grantee/get.ts new file mode 100644 index 00000000..a3eb2a1d --- /dev/null +++ b/src/command/grantee/get.ts @@ -0,0 +1,22 @@ +import { Argument, LeafCommand } from 'furious-commander' +import { GranteeCommand } from './grantee-command' +import { createKeyValue } from '../../utils/text' + +export class Get extends GranteeCommand implements LeafCommand { + public readonly name = 'get' + public readonly description = 'Get grantee list' + + @Argument({ + key: 'reference', + description: 'Grantee list reference', + required: true, + conflicts: 'stdin', + }) + public reference!: string + + public async run(): Promise { + await super.init() + const response = await this.bee.getGrantees(this.reference) + this.console.log(createKeyValue('Grantee public keys', response.data.join('\n'))) + } +} diff --git a/src/command/grantee/grantee-command.ts b/src/command/grantee/grantee-command.ts new file mode 100644 index 00000000..9ff05600 --- /dev/null +++ b/src/command/grantee/grantee-command.ts @@ -0,0 +1,3 @@ +import { RootCommand } from '../root-command' + +export class GranteeCommand extends RootCommand {} diff --git a/src/command/grantee/index.ts b/src/command/grantee/index.ts new file mode 100644 index 00000000..336e8a2a --- /dev/null +++ b/src/command/grantee/index.ts @@ -0,0 +1,12 @@ +import { GroupCommand } from 'furious-commander' +import { Create } from './create' +import { Get } from './get' +import { Patch } from './patch' + +export class Grantee implements GroupCommand { + public readonly name = 'grantee' + + public readonly description = 'Create, Get, Patch grantee list' + + public subCommandClasses = [Create, Get, Patch] +} diff --git a/src/command/grantee/patch.ts b/src/command/grantee/patch.ts new file mode 100644 index 00000000..ae714327 --- /dev/null +++ b/src/command/grantee/patch.ts @@ -0,0 +1,58 @@ +import { Argument, LeafCommand, Option } from 'furious-commander' +import { GranteeCommand } from './grantee-command' +import { stampProperties } from '../../utils/option' +import { createKeyValue } from '../../utils/text' +import fs from 'fs' + +export class Patch extends GranteeCommand implements LeafCommand { + public readonly name = 'patch' + public readonly description = 'Patch grantee list' + private actReqHeaders: Record = {} + + @Argument({ + key: 'path', + description: 'Path to the JSON file with grantee patch (add, revoke)', + required: true, + autocompletePath: true, + conflicts: 'stdin', + }) + public path!: string + + @Option({ key: 'stdin', type: 'boolean', description: 'Take data from standard input', conflicts: 'path' }) + public stdin!: boolean + + @Option(stampProperties) + public stamp!: string + + @Option({ + key: 'reference', + type: 'string', + description: 'Encrypted grantee list reference with 128 characters length', + length: 128, + required: true, + }) + public eref!: string + + @Option({ + key: 'history', + type: 'string', + description: 'Swarm address reference to the ACT history entry', + length: 64, + required: true, + }) + public history!: string + + public async run(): Promise { + await super.init() + this.actReqHeaders = { + 'Swarm-Act': 'true', + 'Swarm-Act-Timestamp': Date.now().toString(), + } + const patchContent = fs.readFileSync(this.path, 'utf8') + const patch = JSON.parse(patchContent) + + const response = await this.bee.patchGrantees(this.stamp, this.eref, this.history, patch, this.actReqHeaders) + this.console.log(createKeyValue('Grantee reference', response.ref)) + this.console.log(createKeyValue('Grantee history reference', response.historyref)) + } +} diff --git a/src/command/manifest/download.ts b/src/command/manifest/download.ts index ca9e6a1f..26a299cc 100644 --- a/src/command/manifest/download.ts +++ b/src/command/manifest/download.ts @@ -24,6 +24,20 @@ export class Download extends ManifestCommand implements LeafCommand { @Option({ key: 'stdout', type: 'boolean', description: 'Print to stdout (single files only)' }) public stdout!: boolean + @Option({ key: 'act', type: 'boolean', description: 'Download with ACT', default: false }) + public act!: boolean + + @Option({ key: 'act-timestamp', type: 'string', description: 'ACT history timestamp', default: '1' }) + public actTimestamp!: string + + // required if act is true + @Option({ key: 'act-history-address', type: 'string', description: 'ACT history address', required: { when: 'act' } }) + public actHistoryAddress!: string + + // required if act is true + @Option({ key: 'act-publisher', type: 'string', description: 'ACT publisher', required: { when: 'act' } }) + public actPublisher!: string + public async run(): Promise { await super.init() diff --git a/src/command/upload.ts b/src/command/upload.ts index b9a47f67..934364be 100644 --- a/src/command/upload.ts +++ b/src/command/upload.ts @@ -48,6 +48,18 @@ export class Upload extends RootCommand implements LeafCommand { @Option({ key: 'deferred', type: 'boolean', description: 'Do not wait for network sync', default: true }) public deferred!: boolean + @Option({ + key: 'act', + type: 'boolean', + description: 'Upload with ACT', + default: false, + required: { when: 'act-history-address' }, + }) + public act!: boolean + + @Option({ key: 'act-history-address', type: 'string', description: 'ACT history address' }) + public optHistoryAddress!: string + @Option({ key: 'sync', type: 'boolean', @@ -113,6 +125,7 @@ export class Upload extends RootCommand implements LeafCommand { // CLASS FIELDS public hash!: string + public historyAddress!: string public stdinData!: Buffer @@ -160,6 +173,10 @@ export class Upload extends RootCommand implements LeafCommand { this.console.dim('Data has been sent to the Bee node successfully!') this.console.log(createKeyValue('Swarm hash', this.hash)) + + if (this.act) { + this.console.log(createKeyValue('Swarm history address', this.historyAddress)) + } this.console.dim('Waiting for file chunks to be synced on Swarm network...') if (this.sync && tag) { @@ -208,29 +225,57 @@ export class Upload extends RootCommand implements LeafCommand { } } + private actHeaders(): Record { + if (this.act && this.optHistoryAddress) { + return { 'swarm-act-history-address': this.optHistoryAddress } + } + + return {} + } + private async uploadStdin(tag?: Tag): Promise { if (this.fileName) { const contentType = this.contentType || getMime(this.fileName) || undefined - const { reference } = await this.bee.uploadFile(this.stamp, this.stdinData, this.fileName, { - tag: tag && tag.uid, - pin: this.pin, - encrypt: this.encrypt, - contentType, - deferred: this.deferred, - redundancyLevel: this.determineRedundancyLevel(), - }) + const { reference, historyAddress } = await this.bee.uploadFile( + this.stamp, + this.stdinData, + this.fileName, + { + tag: tag && tag.uid, + pin: this.pin, + encrypt: this.encrypt, + contentType, + deferred: this.deferred, + redundancyLevel: this.determineRedundancyLevel(), + act: this.act, + }, + { headers: this.actHeaders() }, + ) this.hash = reference + if (this.act && historyAddress !== undefined) { + this.historyAddress = historyAddress + } + return `${this.bee.url}/bzz/${this.hash}/` } else { - const { reference } = await this.bee.uploadData(this.stamp, this.stdinData, { - tag: tag?.uid, - deferred: this.deferred, - encrypt: this.encrypt, - redundancyLevel: this.determineRedundancyLevel(), - }) + const { reference, historyAddress } = await this.bee.uploadData( + this.stamp, + this.stdinData, + { + tag: tag?.uid, + deferred: this.deferred, + encrypt: this.encrypt, + redundancyLevel: this.determineRedundancyLevel(), + }, + { headers: this.actHeaders() }, + ) this.hash = reference + if (this.act && historyAddress !== undefined) { + this.historyAddress = historyAddress + } + return `${this.bee.url}/bytes/${this.hash}` } } @@ -241,17 +286,27 @@ export class Upload extends RootCommand implements LeafCommand { folder: true, type: 'buffer', }) - const { reference } = await this.bee.uploadFilesFromDirectory(this.stamp, this.path, { - indexDocument: this.indexDocument, - errorDocument: this.errorDocument, - tag: tag && tag.uid, - pin: this.pin, - encrypt: this.encrypt, - deferred: this.deferred, - redundancyLevel: this.determineRedundancyLevel(), - }) + const { reference, historyAddress } = await this.bee.uploadFilesFromDirectory( + this.stamp, + this.path, + { + indexDocument: this.indexDocument, + errorDocument: this.errorDocument, + tag: tag && tag.uid, + pin: this.pin, + encrypt: this.encrypt, + deferred: this.deferred, + redundancyLevel: this.determineRedundancyLevel(), + act: this.act, + }, + { headers: this.actHeaders() }, + ) this.hash = reference + if (this.act && historyAddress !== undefined) { + this.historyAddress = historyAddress + } + return `${this.bee.url}/bzz/${this.hash}/` } @@ -264,16 +319,27 @@ export class Upload extends RootCommand implements LeafCommand { }) const readable = FS.createReadStream(this.path) const parsedPath = parse(this.path) - const { reference } = await this.bee.uploadFile(this.stamp, readable, this.determineFileName(parsedPath.base), { - tag: tag && tag.uid, - pin: this.pin, - encrypt: this.encrypt, - contentType, - deferred: this.deferred, - redundancyLevel: this.determineRedundancyLevel(), - }) + const { reference, historyAddress } = await this.bee.uploadFile( + this.stamp, + readable, + this.determineFileName(parsedPath.base), + { + tag: tag && tag.uid, + pin: this.pin, + encrypt: this.encrypt, + contentType, + deferred: this.deferred, + redundancyLevel: this.determineRedundancyLevel(), + act: this.act, + }, + { headers: this.actHeaders() }, + ) this.hash = reference + if (this.act && historyAddress !== undefined) { + this.historyAddress = historyAddress + } + return `${this.bee.url}/bzz/${this.hash}/` } @@ -379,6 +445,13 @@ export class Upload extends RootCommand implements LeafCommand { return false } + if (this.act) { + this.console.error('You are trying to upload to the gateway which does not support ACT.') + this.console.error('Please try again without the --act option.') + + return true + } + if (this.pin) { this.console.error('You are trying to upload to the gateway which does not support pinning.') this.console.error('Please try again without the --pin option.') diff --git a/src/config.ts b/src/config.ts index f629b109..002963c0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -14,6 +14,7 @@ import { Stamp } from './command/stamp' import { Status } from './command/status' import { Upload } from './command/upload' import { Utility } from './command/utility' +import { Grantee } from './command/grantee' export const beeApiUrl: IOption = { key: 'bee-api-url', @@ -119,4 +120,5 @@ export const rootCommandClasses = [ Manifest, Stake, Utility, + Grantee, ] diff --git a/src/utils/bzz-address.ts b/src/utils/bzz-address.ts index 579b922f..a5c3db7b 100644 --- a/src/utils/bzz-address.ts +++ b/src/utils/bzz-address.ts @@ -31,10 +31,9 @@ export class BzzAddress { } } -export async function makeBzzAddress(bee: Bee, url: string): Promise { +export async function makeBzzAddress(bee: Bee, url: string, headers?: Record): Promise { const address = new BzzAddress(url) - - const feedReference = await resolveFeedManifest(bee, address.hash) + const feedReference = await resolveFeedManifest(bee, address.hash, headers) if (feedReference) { address.hash = feedReference @@ -43,8 +42,8 @@ export async function makeBzzAddress(bee: Bee, url: string): Promise return address } -async function resolveFeedManifest(bee: Bee, hash: string): Promise { - const metadata = await getRootSlashMetadata(bee, hash) +async function resolveFeedManifest(bee: Bee, hash: string, headers?: Record): Promise { + const metadata = await getRootSlashMetadata(bee, hash, headers) if (!metadata) { return null @@ -63,8 +62,14 @@ async function resolveFeedManifest(bee: Bee, hash: string): Promise { - const data = await bee.downloadData(hash) +async function getRootSlashMetadata( + bee: Bee, + hash: string, + reqHeaders?: Record, +): Promise { + const data = await bee.downloadData(hash, { + headers: reqHeaders, + }) const node = new MantarayNode() node.deserialize(data) diff --git a/test/command/upload.spec.ts b/test/command/upload.spec.ts index 0557b4a6..c28bb7dc 100644 --- a/test/command/upload.spec.ts +++ b/test/command/upload.spec.ts @@ -36,6 +36,13 @@ describeCommand('Test Upload command', ({ consoleMessages, hasMessageContaining expect(uploadCommand.hash).toHaveLength(128) }) + it('should upload file with act', async () => { + const commandBuilder = await invokeTestCli(['upload', 'README.md', '--act', ...getStampOption()]) + const uploadCommand = commandBuilder.runnable as Upload + expect(uploadCommand.hash).toHaveLength(64) + expect(uploadCommand.historyAddress).toHaveLength(64) + }) + it('should upload folder and encrypt', async () => { const commandBuilder = await invokeTestCli(['upload', 'test/testpage', '--encrypt', ...getStampOption()]) const uploadCommand = commandBuilder.runnable as Upload