Skip to content

Commit

Permalink
Merge pull request #56 from internxt/feat/network-download-v2
Browse files Browse the repository at this point in the history
[_]: feat/network-download-v2
  • Loading branch information
sg-gs authored Apr 21, 2022
2 parents 381a0af + 145458c commit 145d472
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 39 deletions.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@internxt/inxt-js",
"version": "1.6.0",
"version": "1.6.1",
"description": "",
"main": "build/index.js",
"types": "build/index.d.ts",
Expand Down Expand Up @@ -29,7 +29,7 @@
"@types/commander": "^2.12.2",
"@types/express": "^4.17.13",
"@types/jest": "^26.0.4",
"@types/node": "^16.11.4",
"@types/node": "^17.0.0",
"@types/sinon": "^10.0.2",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "^5.1.0",
Expand All @@ -49,12 +49,13 @@
"sinon": "^11.1.1",
"ts-jest": "^27.0.7",
"ts-mocha": "^7.0.0",
"ts-node": "^10.4.0",
"ts-node": "10.7.0",
"typescript": "^3.9.6",
"uuid": "^8.3.2"
},
"dependencies": {
"@internxt/lib": "^1.1.6",
"@internxt/sdk": "^0.7.7",
"async": "^3.2.0",
"axios": "^0.23.0",
"bip39": "^3.0.2",
Expand Down
2 changes: 1 addition & 1 deletion src/cli/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const uploadFolderZipCommand = buildCommand({

export const downloadFileCommand = buildCommand({
version: '0.0.1',
command: 'download-file <fileId> <path> [downloadStrategy]',
command: 'download-file <fileId> <path>',
description: 'Download a file',
options: [],
}).action((fileId, path) => {
Expand Down
56 changes: 43 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { logger, Logger } from './lib/utils/logger';
import { FileInfo, GetFileInfo } from './api/fileinfo';
import { Bridge, CreateFileTokenResponse, GetDownloadLinksResponse } from './services/api';
import { HashStream } from './lib/utils/streams';
import { downloadFileV2 } from './lib/core/download/downloadV2';
import { FileVersionOneError } from '@internxt/sdk/dist/network/download';

type GetBucketsCallback = (err: Error | null, result: any) => void;

Expand Down Expand Up @@ -235,25 +237,53 @@ export class Environment {
return downloadState;
}

let strategy: DownloadStrategy | null = null;
if (!this.config.bridgeUrl) {
opts.finishedCallback(Error('Missing bridge url'), null);

if (strategyObj.label === 'Dynamic') {
strategy = new DownloadDynamicStrategy(strategyObj.params);
return downloadState;
}

if (!strategy) {
opts.finishedCallback(Error('Unknown strategy'), null);
const [downloadPromise, stream] = downloadFileV2(
fileId,
bucketId,
this.config.encryptionKey,
this.config.bridgeUrl,
{
user: this.config.bridgeUser,
pass: this.config.bridgePass
},
opts.progressCallback,
downloadState,
() => {
opts.finishedCallback(null, stream);
}
);

downloadPromise.catch((err) => {
if (err instanceof FileVersionOneError) {
let strategy: DownloadStrategy | null = null;

if (strategyObj.label === 'Dynamic') {
strategy = new DownloadDynamicStrategy(strategyObj.params);
}

return downloadState;
}
if (!strategy) {
opts.finishedCallback(Error('Unknown strategy'), null);

download(this.config, bucketId, fileId, opts, downloadState, strategy)
.then((res) => {
opts.finishedCallback(null, res);
})
.catch((err) => {
return downloadState;
}

download(this.config, bucketId, fileId, opts, downloadState, strategy)
.then((res) => {
opts.finishedCallback(null, res);
})
.catch((downloadErr) => {
opts.finishedCallback(downloadErr, null);
});
} else {
opts.finishedCallback(err, null);
});
}
});

return downloadState;
};
Expand Down
110 changes: 110 additions & 0 deletions src/lib/core/download/downloadV2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { createDecipheriv, randomBytes } from 'crypto';
import { PassThrough, Readable } from 'stream';
import { pipeline } from 'stream/promises';

import { ALGORITHMS, DecryptFileFunction, DownloadFileFunction, Network } from '@internxt/sdk/dist/network';
import { downloadFile, FileVersionOneError } from '@internxt/sdk/dist/network/download';

import { getStream } from '../../../services/request';
import { GenerateFileKey, sha256 } from '../../utils/crypto';
import { Events as ProgressEvents, HashStream, ProgressNotifier } from '../../utils/streams';
import { DownloadProgressCallback } from '.';
import { ActionState } from '../../../api';
import { Events } from '..';
import Errors from './errors';

export function downloadFileV2(
fileId: string,
bucketId: string,
mnemonic: string,
bridgeUrl: string,
creds: { pass: string, user: string },
notifyProgress: DownloadProgressCallback,
actionState: ActionState,
onV2Confirmed: () => void
): [Promise<void>, PassThrough] {
const abortController = new AbortController();

actionState.once(Events.Download.Abort, () => {
abortController.abort();
});

const outStream = new PassThrough();
const network = Network.client(bridgeUrl, {
clientName: 'inxt-js',
clientVersion: '1.0'
}, {
bridgeUser: creds.user,
userId: sha256(Buffer.from(creds.pass)).toString('hex')
});

const fileEncryptedSlices: {
stream: Readable,
hash: string
}[] = [];

const downloadFileStep: DownloadFileFunction = async (downloadables) => {
onV2Confirmed();

for (const downloadable of downloadables.sort((dA, dB) => dA.index - dB.index)) {
fileEncryptedSlices.push({
hash: downloadable.hash,
stream: await getStream(downloadable.url)
});
}
};

const decryptFileStep: DecryptFileFunction = async (algorithm, key, iv, fileSize) => {
if (algorithm !== ALGORITHMS.AES256CTR.type) {
throw Errors.downloadUnknownAlgorithmError;
}

const decipher = createDecipheriv('aes-256-ctr', key as Buffer, iv as Buffer);
const progress = new ProgressNotifier(fileSize, 2000, { emitClose: false });

progress.on(ProgressEvents.Progress, (progress: number) => {
notifyProgress(progress, null, null);
});

for (const fileEncryptedSlice of fileEncryptedSlices) {
const hasher = new HashStream();

await pipeline(fileEncryptedSlice.stream, hasher, decipher, progress, outStream, {
signal: abortController.signal
});

const calculatedHash = hasher.getHash().toString('hex');
const expectedHash = fileEncryptedSlice.hash;

if (calculatedHash !== expectedHash) {
throw Errors.downloadHashMismatchError;
}
}

await new Promise((res) => progress.end(res));
};

const downloadPromise = downloadFile(
fileId,
bucketId,
mnemonic,
network,
{
algorithm: ALGORITHMS.AES256CTR,
randomBytes,
generateFileKey: (mnemonic, bucketId, index) => {
return GenerateFileKey(mnemonic, bucketId, index as Buffer | string);
}
},
Buffer.from,
downloadFileStep,
decryptFileStep
).catch((err) => {
if (err instanceof FileVersionOneError) {
throw err;
}
outStream.emit('error', err);
});

return [downloadPromise, outStream];
}
31 changes: 31 additions & 0 deletions src/lib/core/download/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* DOWNLOAD ERROR CODES: 1XXX
* UPLOAD ERROR CODES: 2XXX
* CRYPTOGRAPHY ERROR CODES: 1/2 + 1XX
*/

enum ErrorCodes {
DownloadHashMismatch = 1000,
DownloadUnknownAlgorithm = 1100,
}

class CodeError extends Error {
constructor(
protected errorMessage: string,
protected errorCode: number
) {
super(`${errorMessage} (ERRNO: ${errorCode})`);
}
}

class DownloadError extends CodeError {
constructor(protected errorCode: number) {
super('Download failed', errorCode);
}
}

export default {
ErrorCodes,
downloadHashMismatchError: new DownloadError(ErrorCodes.DownloadHashMismatch),
downloadUnknownAlgorithmError: new DownloadError(ErrorCodes.DownloadUnknownAlgorithm)
};
Loading

0 comments on commit 145d472

Please sign in to comment.