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
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
dist/**
# upload fixtures — sample project trees used as test payloads, not real source
test/data/**
72 changes: 13 additions & 59 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ module.exports = {
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018,
// required for the type-aware rules below (no-floating-promises / no-misused-promises)
project: ['./tsconfig.json', './tsconfig.test.json'],
tsconfigRootDir: __dirname,
},
env: {
jest: true,
Expand All @@ -13,77 +16,28 @@ module.exports = {
},
plugins: ['jest'],
rules: {
'array-bracket-newline': ['error', 'consistent'],
strict: ['error', 'safe'],
curly: 'error',
'block-scoped-var': 'error',
complexity: 'warn',
'default-case': 'error',
'dot-notation': 'warn',
// --- real bug protection only; formatting is left to Prettier and correctness to tsc ---
curly: ['error', 'multi-line'],
eqeqeq: 'error',
'guard-for-in': 'warn',
'linebreak-style': ['warn', 'unix'],
'no-alert': 'error',
'default-case': 'error',
'guard-for-in': 'error',
'no-case-declarations': 'error',
'no-console': 'error',
'no-constant-condition': 'error',
'no-div-regex': 'error',
'no-empty': 'warn',
'no-empty-pattern': 'error',
'no-implicit-coercion': 'error',
'prefer-arrow-callback': 'warn',
'no-labels': 'error',
'no-loop-func': 'error',
'no-nested-ternary': 'warn',
'no-script-url': 'error',
'no-warning-comments': 'warn',
'quote-props': ['error', 'as-needed'],
'require-yield': 'error',
'max-nested-callbacks': ['error', 4],
'max-depth': ['error', 4],
'require-await': 'error',
'space-before-function-paren': [
'error',
{
anonymous: 'never',
named: 'never',
asyncArrow: 'always',
},
],
'padding-line-between-statements': [
'error',
{ blankLine: 'always', prev: '*', next: 'if' },
{ blankLine: 'always', prev: '*', next: 'function' },
{ blankLine: 'always', prev: '*', next: 'return' },
],
'no-useless-constructor': 'off',
'no-dupe-class-members': 'off',
'no-unused-expressions': 'off',
curly: ['error', 'multi-line'],
'object-curly-spacing': ['error', 'always'],
'comma-dangle': ['error', 'always-multiline'],
'@typescript-eslint/no-useless-constructor': 'error',
'@typescript-eslint/no-unused-expressions': 'error',
'@typescript-eslint/member-delimiter-style': [
'error',
{
multiline: {
delimiter: 'none',
requireLast: true,
},
singleline: {
delimiter: 'comma',
requireLast: false,
},
},
],
// the headline reason to keep ESLint: tsc cannot catch these
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
},
overrides: [
{
files: ['*.spec.ts'],
// tests legitimately assert on values they have set up
files: ['test/**/*.ts'],
rules: {
// '@typescript-eslint/ban-ts-ignore': 'off',
'max-nested-callbacks': ['error', 10], // allow describe/it nesting
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
],
Expand Down
73 changes: 41 additions & 32 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest": "^27.1.5",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-unused-imports": "^2.0.0",
"husky": "^4.3.6",
"jest": "^29.3.1",
"prettier": "^2.7.1",
Expand All @@ -66,6 +65,7 @@
"dependencies": {
"@ethereumjs/wallet": "^2.0.4",
"@ethersphere/bee-js": "^12.0.0",
"@upcoming/swarm-core": "^0.0.8",
"cafe-utility": "^33.3.1",
"chalk": "^2.4.2",
"cli-progress": "^3.11.2",
Expand Down
5 changes: 3 additions & 2 deletions src/command/access/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ export class History extends AccessCommand implements LeafCommand {
const accessHistory = new AccessHistory(this.commandConfig, this.console)
const events = accessHistory.getEvents(this.listName).sort((a, b) => b.createdAt - a.createdAt)

if (events.length === 0) {
const lastEvent = events[0]

if (!lastEvent) {
this.console.error(errorText(`Grantee list with name '${this.listName}' does not exist!`))

exit(1)
}

const lastEvent = events[0]
this.console.log(createKeyValue('Latest history address', lastEvent.historyAddress))
this.console.log(createKeyValue('Latest grantee list reference', lastEvent.granteeListRef))

Expand Down
4 changes: 4 additions & 0 deletions src/command/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export class Download extends RootCommand implements LeafCommand {

if (this.manifestDownload.access) {
const [publisher, historyAddress] = this.manifestDownload.access.split(':')
if (!publisher || !historyAddress) {
this.console.error('Invalid access format. Expected format: publisher:historyAddress')
process.exit(1)
}
const responseAct = await this.bee.downloadFile(this.address.hash, this.manifestDownload.destination, {
actPublisher: publisher,
actHistoryAddress: historyAddress,
Expand Down
11 changes: 9 additions & 2 deletions src/command/feed/feed-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class FeedCommand extends RootCommand {
this.console.log(createKeyValue('Feed Manifest URL', manifestUrl))

if (this.qr) {
printQRCodeWithLabel(publicUrl(manifestUrl), 'QR for Manifest URL', this.console)
await printQRCodeWithLabel(publicUrl(manifestUrl), 'QR for Manifest URL', this.console)
}

this.console.quiet(manifest.toHex())
Expand Down Expand Up @@ -92,7 +92,14 @@ export class FeedCommand extends RootCommand {
this.identity = await pickIdentity(this.commandConfig, this.console)
}

return identities[this.identity]
const identity = identities[this.identity]

if (!identity) {
this.console.error('The provided identity does not exist.')
exit(1)
}

return identity
}

private async writeFeed(stamp: string, wallet: Wallet, topic: Topic, chunkReference: Reference): Promise<FeedInfo> {
Expand Down
70 changes: 64 additions & 6 deletions src/command/manifest/sync.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { MantarayNode, MerkleTree } from '@ethersphere/bee-js'
import { MantarayNode, RedundancyLevel } from '@ethersphere/bee-js'
import { ChunkSplitter, makeErasureBatch, makeIntermediateChunkHandler } from '@upcoming/swarm-core'
import { Binary, Optional } from 'cafe-utility'
import chalk from 'chalk'
import { readFileSync } from 'fs'
Expand All @@ -7,9 +8,17 @@ import { join } from 'path'
import { pickStamp } from '../../service/stamp'
import { readdirDeepAsync } from '../../utils'
import { BzzAddress } from '../../utils/bzz-address'
import { CommandLineError } from '../../utils/error'
import { stampProperties } from '../../utils/option'
import { RootCommand } from '../root-command'

// Bee applies this level itself whenever a client doesn't ask for a specific one
// (verified against a real node - bee-js's own docs claim OFF is the server default,
// which does not match observed behavior). Assumed here so the local pre-check below
// can replicate the same erasure coding Bee applies, rather than compare against a
// bare, non-redundant hash that will never match a redundant upload.
const DEFAULT_REDUNDANCY_LEVEL = RedundancyLevel.MEDIUM

export class Sync extends RootCommand implements LeafCommand {
public readonly name = 'sync'
public readonly description = 'Sync a local folder to an existing manifest'
Expand All @@ -30,13 +39,57 @@ export class Sync extends RootCommand implements LeafCommand {
})
public remove!: boolean

@Option({
key: 'redundancy',
description: 'Redundancy of the upload (OFF, MEDIUM, STRONG, INSANE, PARANOID)',
})
public redundancy!: string

private determineRedundancyLevel(): RedundancyLevel | undefined {
if (!this.redundancy) {
return undefined
}
switch (this.redundancy.toUpperCase()) {
case 'OFF':
return RedundancyLevel.OFF
case 'MEDIUM':
return RedundancyLevel.MEDIUM
case 'STRONG':
return RedundancyLevel.STRONG
case 'INSANE':
return RedundancyLevel.INSANE
case 'PARANOID':
return RedundancyLevel.PARANOID
default:
throw new CommandLineError(`Invalid redundancy level: ${this.redundancy}`)
}
}

private async expectedReference(data: Uint8Array, level: RedundancyLevel): Promise<Uint8Array> {
const onBatch = makeErasureBatch(level, false, async () => {
// no-op: only the resulting hash is needed here, nothing to persist
})
const splitter = new ChunkSplitter(onBatch, undefined, false, makeIntermediateChunkHandler(level))
await splitter.append(data)
const root = await splitter.finalize()

return root.hash().toUint8Array()
}

public async run(): Promise<void> {
super.init()

if (!this.stamp) {
this.stamp = await pickStamp(this.bee, this.console)
}

// Always sent explicitly, even when --redundancy is omitted: leaving this to the
// node's own implicit default would make the comparison below only as reliable as
// a guess about that node's behavior, which we've seen differ between Bee versions
// (2.6.0 defaults to OFF, 2.8.1+ to MEDIUM). Fixing it here removes the ambiguity.
const effectiveRedundancyLevel = this.determineRedundancyLevel() ?? DEFAULT_REDUNDANCY_LEVEL
const uploadOptions = { headers: { 'swarm-redundancy-level': String(effectiveRedundancyLevel) } }

const address = new BzzAddress(this.bzzUrl)

const node = await MantarayNode.unmarshal(this.bee, address.hash)
Expand All @@ -55,25 +108,30 @@ export class Sync extends RootCommand implements LeafCommand {

if (existing) {
const localData = readFileSync(join(this.folder, file))
const rootChunk = await MerkleTree.root(localData)
const expected = await this.expectedReference(new Uint8Array(localData), effectiveRedundancyLevel)

if (Binary.equals(rootChunk.hash(), existing.targetAddress)) {
if (Binary.equals(expected, existing.targetAddress)) {
this.console.log(chalk.gray(file) + ' ' + chalk.blue('UNCHANGED'))
} else {
const { reference } = await this.bee.uploadData(this.stamp, localData)
const { reference } = await this.bee.uploadData(this.stamp, localData, undefined, uploadOptions)
node.addFork(file, reference)
this.console.log(chalk.gray(file) + ' ' + chalk.yellow('CHANGED'))
}
} else {
const { reference } = await this.bee.uploadData(this.stamp, readFileSync(join(this.folder, file)))
const { reference } = await this.bee.uploadData(
this.stamp,
readFileSync(join(this.folder, file)),
undefined,
uploadOptions,
)
node.addFork(file, reference)
this.console.log(chalk.gray(file) + ' ' + chalk.green('NEW'))
}
}

if (this.remove) {
for (const n of nodes) {
if (!files.includes(node.fullPathString)) {
if (!files.includes(n.fullPathString)) {
node.removeFork(n.fullPathString)
this.console.log(chalk.gray(n.fullPathString) + ' ' + chalk.red('REMOVED'))
}
Expand Down
Loading
Loading