Skip to content

Commit 470d6bd

Browse files
authored
Merge pull request #153 from internxt/feat/improve-progress-reporting
[_]: Feat/improve-progress-reporting
2 parents d5c9727 + a0b0c62 commit 470d6bd

16 files changed

Lines changed: 147 additions & 113 deletions

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
"express-basic-auth": "1.2.1",
5555
"fast-xml-parser": "4.5.1",
5656
"mime-types": "2.1.35",
57-
"node-fetch": "2.7.0",
5857
"openpgp": "5.11.2",
5958
"pm2": "5.4.3",
6059
"range-parser": "^1.2.1",
@@ -75,8 +74,7 @@
7574
"@types/express": "5.0.0",
7675
"@types/mime-types": "2.1.4",
7776
"@types/node": "22.10.6",
78-
"@types/node-fetch": "2.6.12",
79-
"@types/range-parser": "^1.2.7",
77+
"@types/range-parser": "1.2.7",
8078
"@vitest/coverage-istanbul": "2.1.8",
8179
"@vitest/spy": "2.1.8",
8280
"eslint": "9.17.0",

src/commands/download-file.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,13 @@ export default class DownloadFile extends Command {
7979
user.bucket,
8080
user.mnemonic,
8181
driveFile.fileId,
82+
driveFile.size,
8283
StreamUtils.writeStreamToWritableStream(fileWriteStream),
8384
undefined,
8485
{
8586
abortController: new AbortController(),
8687
progressCallback: (progress) => {
87-
progressBar.update(progress);
88+
progressBar.update(progress * 0.99);
8889
},
8990
},
9091
);

src/commands/upload-file.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,22 +74,22 @@ export default class UploadFile extends Command {
7474

7575
CLIUtils.done();
7676

77-
const timer = CLIUtils.timer();
7877
// 2. Upload file to the Network
7978
const fileStream = createReadStream(filePath);
79+
const timer = CLIUtils.timer();
8080
const progressBar = CLIUtils.progress({
8181
format: 'Uploading file [{bar}] {percentage}%',
8282
linewrap: true,
8383
});
84-
progressBar.start(1, 0);
84+
progressBar.start(100, 0);
8585
const [uploadPromise, abortable] = await networkFacade.uploadFromStream(
8686
user.bucket,
8787
user.mnemonic,
8888
stats.size,
8989
fileStream,
9090
{
9191
progressCallback: (progress) => {
92-
progressBar.update(progress);
92+
progressBar.update(progress * 0.99);
9393
},
9494
},
9595
);
@@ -100,7 +100,6 @@ export default class UploadFile extends Command {
100100
});
101101

102102
const uploadResult = await uploadPromise;
103-
progressBar.stop();
104103

105104
// 3. Create the file in Drive
106105
const fileInfo = path.parse(filePath);
@@ -115,6 +114,9 @@ export default class UploadFile extends Command {
115114
name: '',
116115
});
117116

117+
progressBar.update(100);
118+
progressBar.stop();
119+
118120
const uploadTime = timer.stop();
119121
this.log('\n');
120122
// eslint-disable-next-line max-len

src/services/crypto.service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,11 @@ export class CryptoService {
166166
return decryptedStream;
167167
}
168168

169-
public async getEncryptionTransform(key: Buffer, iv: Buffer): Promise<Transform> {
169+
public getEncryptionTransform = (key: Buffer, iv: Buffer): Transform => {
170170
const cipher = createCipheriv('aes-256-ctr', key, iv);
171-
172171
return cipher;
173-
}
172+
};
173+
174174
/**
175175
* Generates the key and the iv by transforming a secret and a salt.
176176
* It will generate the same key and iv if the same secret and salt is used.

src/services/network/download.service.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
import axios from 'axios';
2+
import { DownloadProgressCallback } from '../../types/network.types';
23

34
export class DownloadService {
45
static readonly instance = new DownloadService();
56

67
async downloadFile(
78
url: string,
89
options: {
9-
progressCallback?: (progress: number) => void;
10+
progressCallback?: DownloadProgressCallback;
1011
abortController?: AbortController;
1112
rangeHeader?: string;
1213
},
1314
): Promise<ReadableStream<Uint8Array>> {
1415
const response = await axios.get(url, {
1516
responseType: 'stream',
1617
onDownloadProgress(progressEvent) {
17-
if (options.progressCallback && progressEvent.total) {
18-
const reportedProgress = progressEvent.loaded / progressEvent.total;
19-
20-
options.progressCallback(reportedProgress);
18+
if (options.progressCallback && progressEvent.loaded) {
19+
options.progressCallback(progressEvent.loaded);
2120
}
2221
},
2322
headers: {

src/services/network/network-facade.service.ts

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@ import {
1010
import { Environment } from '@internxt/inxt-js';
1111
import { randomBytes } from 'node:crypto';
1212
import { Readable, Transform } from 'node:stream';
13-
import { DownloadOptions, UploadOptions, UploadProgressCallback } from '../../types/network.types';
13+
import { DownloadOptions, UploadOptions, UploadProgressCallback, DownloadProgressCallback } from '../../types/network.types';
1414
import { CryptoService } from '../crypto.service';
1515
import { UploadService } from './upload.service';
1616
import { DownloadService } from './download.service';
1717
import { ValidationService } from '../validation.service';
1818
import { HashStream } from '../../utils/hash.utils';
19-
import { ProgressTransform } from '../../utils/stream.utils';
2019
import { RangeOptions } from '../../utils/network.utils';
2120

2221
export class NetworkFacade {
@@ -54,6 +53,7 @@ export class NetworkFacade {
5453
bucketId: string,
5554
mnemonic: string,
5655
fileId: string,
56+
size: number,
5757
to: WritableStream,
5858
rangeOptions?: RangeOptions,
5959
options?: DownloadOptions,
@@ -62,13 +62,10 @@ export class NetworkFacade {
6262
let fileStream: ReadableStream<Uint8Array>;
6363
const abortable = options?.abortController ?? new AbortController();
6464

65-
const onProgress: UploadProgressCallback = (progress: number) => {
65+
const onProgress: DownloadProgressCallback = (loadedBytes: number) => {
6666
if (!options?.progressCallback) return;
67-
options.progressCallback(progress);
68-
};
69-
70-
const onDownloadProgress = (progress: number) => {
71-
onProgress(progress);
67+
const reportedProgress = Math.round((loadedBytes / size) * 100);
68+
options.progressCallback(reportedProgress);
7269
};
7370

7471
const decryptFile: DecryptFileFunction = async (_, key, iv) => {
@@ -97,7 +94,7 @@ export class NetworkFacade {
9794
}
9895

9996
const encryptedContentStream = await this.downloadService.downloadFile(downloadable.url, {
100-
progressCallback: onDownloadProgress,
97+
progressCallback: onProgress,
10198
abortController: options?.abortController,
10299
rangeHeader: rangeOptions?.range,
103100
});
@@ -142,38 +139,31 @@ export class NetworkFacade {
142139
const hashStream = new HashStream();
143140
const abortable = options?.abortController ?? new AbortController();
144141
let encryptionTransform: Transform;
145-
const progressTransform = new ProgressTransform({ totalBytes: size }, (progress) => {
146-
if (options?.progressCallback) {
147-
options.progressCallback(progress * 0.95);
148-
}
149-
});
142+
let hash: Buffer;
150143

151-
const onProgress: UploadProgressCallback = (progress: number) => {
144+
const onProgress: UploadProgressCallback = (loadedBytes: number) => {
152145
if (!options?.progressCallback) return;
153-
options.progressCallback(progress);
146+
const reportedProgress = Math.round((loadedBytes / size) * 100);
147+
options.progressCallback(reportedProgress);
154148
};
155149

156150
const encryptFile: EncryptFileFunction = async (_, key, iv) => {
157-
encryptionTransform = from
158-
.pipe(
159-
await this.cryptoService.getEncryptionTransform(
160-
Buffer.from(key as ArrayBuffer),
161-
Buffer.from(iv as ArrayBuffer),
162-
),
163-
)
164-
.pipe(hashStream);
151+
const encryptionCipher = this.cryptoService.getEncryptionTransform(
152+
Buffer.from(key as ArrayBuffer),
153+
Buffer.from(iv as ArrayBuffer),
154+
);
155+
encryptionTransform = from.pipe(encryptionCipher).pipe(hashStream);
165156
};
166157

167158
const uploadFile: UploadFileFunction = async (url) => {
168-
await this.uploadService.uploadFile(url, encryptionTransform.pipe(progressTransform), {
159+
await this.uploadService.uploadFile(url, encryptionTransform, {
169160
abortController: abortable,
170-
progressCallback: () => {
171-
// No progress here, we are using the progressTransform
172-
},
161+
progressCallback: onProgress,
173162
});
174-
175-
return hashStream.getHash().toString('hex');
163+
hash = hashStream.getHash();
164+
return hash.toString('hex');
176165
};
166+
177167
const uploadOperation = async () => {
178168
const uploadResult = await NetworkUpload.uploadFile(
179169
this.network,
@@ -184,12 +174,10 @@ export class NetworkFacade {
184174
encryptFile,
185175
uploadFile,
186176
);
187-
const fileHash: Buffer = Buffer.from('');
188177

189-
onProgress(1);
190178
return {
191179
fileId: uploadResult,
192-
hash: fileHash,
180+
hash: hash,
193181
};
194182
};
195183

src/services/network/upload.service.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import { Readable } from 'node:stream';
2-
import fetch from 'node-fetch';
3-
import { AbortSignal } from 'node-fetch/externals';
2+
import axios from 'axios';
43
import { UploadOptions } from '../../types/network.types';
54

65
export class UploadService {
76
public static readonly instance: UploadService = new UploadService();
87

98
async uploadFile(url: string, from: Readable, options: UploadOptions): Promise<{ etag: string }> {
10-
const response = await fetch(url, {
11-
method: 'PUT',
12-
body: from,
13-
signal: options.abortController?.signal as AbortSignal,
9+
const response = await axios.put(url, from, {
10+
signal: options.abortController?.signal,
11+
onUploadProgress: (progressEvent) => {
12+
if (options.progressCallback && progressEvent.loaded) {
13+
options.progressCallback(progressEvent.loaded);
14+
}
15+
},
1416
});
1517

16-
const etag = response.headers.get('etag');
17-
options.progressCallback(1);
18+
const etag = response.headers['etag'];
1819
if (!etag) {
1920
throw new Error('Missing Etag in response when uploading file');
2021
}

src/types/network.types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ export interface NetworkCredentials {
33
pass: string;
44
}
55

6-
export type UploadProgressCallback = (progress: number) => void;
6+
export type DownloadProgressCallback = (downloadedBytes: number) => void;
7+
export type UploadProgressCallback = (uploadedBytes: number) => void;
78
export interface NetworkOperationBaseOptions {
89
progressCallback: UploadProgressCallback;
910
abortController?: AbortController;

src/webdav/handlers/GET.handler.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,6 @@ export class GETRequestHandler implements WebDavMethodHandler {
4343
const { user } = await authService.getAuthDetails();
4444
webdavLogger.info(`[GET] [${driveFile.uuid}] Network ready for download`);
4545

46-
const writable = new WritableStream({
47-
write(chunk) {
48-
res.write(chunk);
49-
},
50-
close() {
51-
res.end();
52-
},
53-
});
54-
5546
const range = req.headers['range'];
5647
const rangeOptions = NetworkUtils.parseRangeHeader({
5748
range,
@@ -66,10 +57,20 @@ export class GETRequestHandler implements WebDavMethodHandler {
6657
res.header('Content-Type', 'application/octet-stream');
6758
res.header('Content-length', contentLength.toString());
6859

60+
const writable = new WritableStream({
61+
write(chunk) {
62+
res.write(chunk);
63+
},
64+
close() {
65+
res.end();
66+
},
67+
});
68+
6969
const [executeDownload] = await networkFacade.downloadToStream(
7070
driveFile.bucket,
7171
user.mnemonic,
7272
driveFile.fileId,
73+
contentLength,
7374
writable,
7475
rangeOptions,
7576
);

src/webdav/handlers/MKCOL.handler.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { webdavLogger } from '../../utils/logger.utils';
77
import { XMLUtils } from '../../utils/xml.utils';
88
import { AsyncUtils } from '../../utils/async.utils';
99
import { DriveFolderItem } from '../../types/drive.types';
10-
import { MethodNotAllowed } from '../../utils/errors.utils';
1110

1211
export class MKCOLRequestHandler implements WebDavMethodHandler {
1312
constructor(
@@ -21,8 +20,6 @@ export class MKCOLRequestHandler implements WebDavMethodHandler {
2120
const { driveDatabaseManager, driveFolderService } = this.dependencies;
2221
const resource = await WebDavUtils.getRequestedResource(req);
2322

24-
if (resource.type === 'file') throw new MethodNotAllowed('Files cannot be created with MKCOL. Use PUT instead.');
25-
2623
webdavLogger.info(`[MKCOL] Request received for ${resource.type} at ${resource.url}`);
2724

2825
const parentResource = await WebDavUtils.getRequestedResource(resource.parentPath);
@@ -34,7 +31,7 @@ export class MKCOLRequestHandler implements WebDavMethodHandler {
3431
})) as DriveFolderItem;
3532

3633
const [createFolder] = driveFolderService.createFolder({
37-
plainName: resource.name,
34+
plainName: resource.path.base,
3835
parentFolderUuid: parentFolderItem.uuid,
3936
});
4037

0 commit comments

Comments
 (0)