Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validate cli tool #9

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions dist/cli.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env node
export {};
//# sourceMappingURL=cli.d.ts.map
1 change: 1 addition & 0 deletions dist/cli.d.ts.map

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

55 changes: 55 additions & 0 deletions dist/cli.js

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

1 change: 1 addition & 0 deletions dist/cli.js.map

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

8 changes: 7 additions & 1 deletion dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -24,8 +24,14 @@ export declare function exportActorProfile({ actorProfile, outbox, followers, fo
/**
* Imports an ActivityPub profile from a .tar archive stream.
* @param tarStream - A ReadableStream containing the .tar archive.
* @param options - Options for the import process.
* @returns A promise that resolves to the parsed profile data.
*/
export declare function importActorProfile(tarStream: Readable): Promise<Record<string, any>>;
export declare function importActorProfile(tarStream: Readable, options?: {
console?: Pick<Console, 'log' | 'warn' | 'error'>;
onError?: (error: Error, context: {
fileName?: string;
}) => void;
}): Promise<Record<string, any>>;
export * from './verify';
//# sourceMappingURL=index.d.ts.map
2 changes: 1 addition & 1 deletion dist/index.d.ts.map

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

34 changes: 18 additions & 16 deletions dist/index.js

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

2 changes: 1 addition & 1 deletion dist/index.js.map
2 changes: 1 addition & 1 deletion dist/verify.d.ts.map
13 changes: 12 additions & 1 deletion dist/verify.js
2 changes: 1 addition & 1 deletion dist/verify.js.map
Binary file modified out/test-export-2024-01-01.tar
Binary file not shown.
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -2,9 +2,12 @@
"name": "@interop/wallet-export-ts",
"description": "A Javascript/Typescript library for exporting Universal Wallet Backup Containers.",
"version": "0.1.6",
"bin": {
"wallet-export": "./dist/cli.js"
},
"scripts": {
"build": "npm run clear && tsc -p tsconfig.json && ./build-dist.sh",
"clear": "rimraf dist/*",
"clear": "npx rimraf dist/*",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"prepare": "npm run build",
@@ -63,7 +66,7 @@
"mocha": "^10.2.0",
"nyc": "^15.1.0",
"prettier": "^3.1.0",
"rimraf": "^5.0.5",
"rimraf": "^5.0.10",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
},
56 changes: 56 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env node

import { parseArgs } from 'node:util'
import fs from 'node:fs'
import { type Readable } from 'node:stream'
import { validateExportStream } from './verify.js'

// Parse command-line arguments
const { values, positionals } = parseArgs({
options: {
output: {
type: 'string',
short: 'o'
}
},
allowPositionals: true
})

const [command, filePath] = positionals

if (command !== 'validate') {
console.error('Usage: wallet-export validate <path-to-export.tsr>')
process.exit(1)
}

// Handle stdin (e.g., `cat file.tar | wallet-export validate /dev/stdin`)
const inputStream: Readable =
filePath === '/dev/stdin' ? process.stdin : fs.createReadStream(filePath)

// Validate the archive
validateExportStream(inputStream)
.then(({ valid, errors }) => {
if (values.output === 'json') {
// Output errors as JSON
console.log(JSON.stringify({ valid, errors }, null, 2))
} else {
// Output errors to stdio
if (valid) {
console.log('✅ Export is valid.')
} else {
console.error('❌ Export is invalid:')
errors.forEach((error) => {
console.error(`- ${error}`)
})
}
}

// Exit with appropriate code
if (!valid) {
process.exit(1)
}
})
.catch((error) => {
console.error('An error occurred:', error)
process.exit(1)
})
40 changes: 22 additions & 18 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -182,31 +182,35 @@ export async function exportActorProfile({
/**
* Imports an ActivityPub profile from a .tar archive stream.
* @param tarStream - A ReadableStream containing the .tar archive.
* @param options - Options for the import process.
* @returns A promise that resolves to the parsed profile data.
*/
export async function importActorProfile(
tarStream: Readable
tarStream: Readable,
options: {
console?: Pick<Console, 'log' | 'warn' | 'error'>
onError?: (error: Error, context: { fileName?: string }) => void
} = {}
): Promise<Record<string, any>> {
console.log('🚀 Starting to process tar stream...')
const { console = undefined } = options
const extract = tar.extract()
const result: Record<string, any> = {}

return await new Promise((resolve, reject) => {
extract.on('entry', (header, stream, next) => {
// Normalize fileName to include only `activitypub/filename`
const originalFileName = header.name
const fileName = `activitypub/${path.basename(originalFileName)}`
const basename = path.basename(originalFileName)
console?.log('🚀 ~ extract.on ~ basename:', basename)
const fileName = `activitypub/${basename}`

// Skip system-generated files
if (
fileName.startsWith('activitypub/._') ||
fileName.endsWith('.DS_Store')
) {
console.warn(`Skipping system-generated file: ${fileName}`)
if (basename.startsWith('.')) {
console?.warn(`Skipping system-generated file: ${fileName}`)
next()
return
}

console.log(`Processing file: ${fileName}`)
console?.log(`Processing file: ${fileName}`)
let content = ''

stream.on('data', (chunk) => {
@@ -217,39 +221,39 @@ export async function importActorProfile(
try {
if (fileName.endsWith('.json')) {
result[fileName] = JSON.parse(content)
console.log('Parsed JSON file successfully:', fileName)
console?.log('Parsed JSON file successfully:', fileName)
} else if (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) {
result[fileName] = YAML.parse(content)
} else if (fileName.endsWith('.csv')) {
result[fileName] = content
} else {
console.warn(`Unsupported file type: ${fileName}, skipping...`)
console?.warn(`Unsupported file type: ${fileName}, skipping...`)
}
} catch (error: any) {
console.error(`Error processing file ${fileName}:`, error.message)
next(error)
} finally {
next() // Always continue
}
})

stream.on('error', (error: any) => {
console.error(`Stream error on file ${fileName}:`, error.message)
next() // Continue even on stream error
console?.error(`Stream error on file ${fileName}:`, error.message)
next(error) // Continue even on stream error
})
})

extract.on('finish', () => {
console.log('All files processed successfully.')
console?.log('All files processed successfully.')
resolve(result)
})

extract.on('error', (error) => {
console.error('Error during tar extraction:', error.message)
console?.error('Error during tar extraction:', error.message)
reject(new Error('Failed to extract tar file.'))
})

tarStream.on('error', (error) => {
console.error('Error in tar stream:', error.message)
console?.error('Error in tar stream:', error.message)
reject(new Error('Failed to process tar stream.'))
})

15 changes: 14 additions & 1 deletion src/verify.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as tar from 'tar-stream'
import { type Readable } from 'stream'
import YAML from 'yaml'
import path from 'path'

/**
* Validates the structure and content of an exported ActivityPub tarball.
@@ -22,7 +23,19 @@ export async function validateExportStream(

return await new Promise((resolve) => {
extract.on('entry', (header, stream, next) => {
const fileName = header.name.toLowerCase() // Normalize file name
const originalFileName = header.name
const basename = path.basename(originalFileName)
const fileName =
basename === 'manifest.yaml'
? 'manifest.yaml'
: `activitypub/${basename}`

// Skip system-generated files
if (basename.startsWith('.')) {
console?.warn(`Skipping system-generated file: ${fileName}`)
next()
return
}
foundFiles.add(fileName)

let content = ''
8 changes: 4 additions & 4 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -26,10 +26,10 @@ describe('exportActorProfile', () => {
packStream.pipe(tarball)

// Ensure the tarball finishes writing
await new Promise((resolve, reject) => {
tarball.on('finish', resolve)
tarball.on('error', reject)
})
// await new Promise((resolve, reject) => {
// // tarball.on('finish', resolve)
// // tarball.on('error', reject)
// })
})
})