Skip to content
This repository was archived by the owner on Feb 8, 2024. It is now read-only.

Add file transfer to Connect#1225

Merged
gzdunek merged 27 commits intomasterfrom
gzdunek/refactor-file-transfer
Oct 10, 2022
Merged

Add file transfer to Connect#1225
gzdunek merged 27 commits intomasterfrom
gzdunek/refactor-file-transfer

Conversation

@gzdunek
Copy link
Copy Markdown
Contributor

@gzdunek gzdunek commented Sep 30, 2022

Closes https://github.com/gravitational/webapps.e/issues/179
This PR refactors FileTransfer so it can be used in both WebUI and Connect.

The biggest change from the previous version is that FileTransfer no longer makes HTTP requests on its own. Instead, it allows you to provide your own implementations for upload/download functions.
There's also a refreshed UI and, as we discussed at the Design Meeting, there's no longer an Upload button. Uploading starts as soon as you select files.
Additionally, FileTransfer has been moved from teleport to the shared package.

How Web UI works

Upload/download in the WebUI works as before, they use HTTP (XHR) to transfer the files. When a user downloads a file, it is stored in the browser's memory, and when it is finished, the browser's download is triggered.

How Connect works

The upload/download for Connect is quite different. It doesn't physically move the files, but delegates this task to tshd (SFTP). We just specify the source/destination paths via gRPC server-side stream, and all the dirty work is done for us :) Server-side streaming is used to send the transfer progress back to the UI. To specify the destination path to download the file, we use the native OS save dialog.

Info for the reviewers

Review commit-by-commit.

At first I didn't plan to do a refactor, but then I realized that the changes would be significant anyway, so this is a great opportunity to refactor the whole module to modern React, add full TS support and simplify the final usage. I also added tests and better stories.

Although the PR added/updated/removed many files (sorry 😞), I tried to keep the logic in one place. The main logic of FileTransfer is in useFilesStore.tsx and FileTransfer.tsx.
Similarly DocumentSsh, DocumentTerminal, getHttpFileTransferHandlers (the XHR orchestration here is 1:1 to the previous Transfer service) are the places where most of the callers work is done.

Comment thread packages/shared/components/FileTransfer/useFilesStore.ts Outdated
Copy link
Copy Markdown
Member

@ravicious ravicious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a look at the rest of the UI code and the backend code tomorrow.

Comment thread packages/shared/components/FileTransfer/FileTransfer.test.tsx Outdated
Comment thread packages/shared/components/FileTransfer/FileTransfer.test.tsx Outdated
Comment thread packages/shared/components/FileTransfer/FileTransfer.test.tsx Outdated
Comment thread packages/shared/components/FileTransfer/FileTransfer.test.tsx Outdated
Comment thread packages/shared/components/FileTransfer/useFilesStore.ts Outdated
Comment thread packages/shared/components/FileTransfer/useFilesStore.ts Outdated
Comment thread packages/shared/components/FileTransfer/useFilesStore.ts Outdated
Comment thread packages/shared/components/FileTransfer/useFilesStore.ts Outdated
Comment thread packages/shared/components/FileTransfer/useFilesStore.ts Outdated
Comment thread packages/shared/components/FileTransfer/FileTransfer.test.tsx Outdated
Copy link
Copy Markdown
Member

@ravicious ravicious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keyboard navigation works fine now, thanks for fixing this.

Comment thread packages/shared/components/FileTransfer/useFilesStore.ts Outdated
Comment thread packages/design/src/ButtonIcon/ButtonIcon.jsx Outdated
@ravicious
Copy link
Copy Markdown
Member

I didn't get around to testing if the Web UI file transfer still works correctly but I did test it for Connect.

Also, did you compare performance of file transfer through Connect vs Web UI and tsh? I won't be able to do that today but it'd be good to make sure we're not sluggish compared to them for some reason.

Copy link
Copy Markdown
Member

@ravicious ravicious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned in DMs, I somehow totally forgot to take a look at the non-UI code. I might have more followups soon, right now I'm looking at the problem with server ID on the backend side of this.

Comment on lines +434 to +446
abortSignal.addEventListener(() => call.cancel());

call.on('data', (data: api.FileTransferProgress) =>
listeners.onProgress(data.getPercentage())
);
call.on('end', () => {
listeners.onComplete();
call.removeAllListeners();
});
call.on('error', error => {
listeners.onError(error);
call.removeAllListeners();
});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if perhaps a better idea would be to make the tshd client return just the call object and then contain the rest of the logic within FileTransferClient.

Right now all FileTransferClient does is just pass some stuff further down – at this point one might wonder why we even bother creating this class at all.

See how I organized the stream stuff in my initial draft PR for tshd-initiated communication. I'm talking about the files in services/tshd. Though here you probably don't want to call FileTransferClient from within tshd client, but rather as I suggested, tsdh client would return the call object and then FileTransferClient could operate on it.

File transfer doesn't need to follow it 1:1. I think the files you created so far are alright it's just a matter of moving the logic around a bit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, context bridge doesn't allow us to return call object, but I followed the structure from linked PR, so now we have a function createFileTransferStream that is returned from tshd client.

Also, as discussed in DM's, we decided not to pass FileTransferListeners as an argument, but instead we return it from the tshd/http calls. It makes the code easier to understand, especially getDownloader/getUploader parts (less arguments, one callback instead of a callback returning another callback).
I also added createFileTransferEventsEmitter utility function that makes working with the events/listeners more convenient.

@ravicious ravicious self-requested a review October 6, 2022 10:35
Copy link
Copy Markdown
Member

@ravicious ravicious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting close to ready here.

constructor(private tshClient: TshClient) {}

transferFile(
options: FileTransferRequest.AsObject,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I've seen, we typically "reexport" those AsObject types:

export type Application = apiApp.App.AsObject;
export type Kube = apiKube.Kube.AsObject;
export type Server = apiServer.Server.AsObject;
export type Gateway = apigateway.Gateway.AsObject;

Could you do the same for those few places where you use FileTransferRequest.AsObject?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

): void {
): FileTransferListeners {
const server = appContext.clustersService.getServer(file.serverUri);
const eventsEmitter = createFileTransferEventsEmitter();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need that eventsEmitter here?

appContext.fileTransferClient.transferFile already returns an object that has all the properties that FileTransferListeners requires so unless I'm missing something, it seems that we can pass whatever we get from transferFile directly.

Once that event emitter is removed, we can remove createFileTransferEventsEmitter altogether by bringing back the inline usage of event emitter in httpFileTransferHandlers.

Copy link
Copy Markdown
Contributor Author

@gzdunek gzdunek Oct 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, appContext.fileTransferClient.transferFile already returns an object that we can use, but I need the event emitter to create a layer between tshClient and FileTransfer. Events returned from the tshClient are used inside useTshFileTransferHandlers and FileTransfer uses these returned from useTshFileTransferHandlers.
It is needed, because the errors needs to go through retryWithRelogin which decides what to do with them first.
For example, when there is ssh: cert has expired error, it should show to the user only when he reject to log in.

I wanted to avoid this, but I don't see any other way not to create an event emitter.

};

return this.tshClient.transferFile(options, listeners, abortSignal);
return this.tshClient.transferFile(options, abortSignal);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like FileTransferClient ended up with very few responsibilities after all. Still, I think it's better to encapsulate calls to tshClient (which has big surface area) within small services like that (which have very small surface area) rather than passing the big tshClient throughout the app.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally agree, we should avoid using tshClient directly.

import { TshClient } from 'teleterm/services/tshd/types';
import { FileTransferRequest } from 'teleterm/services/tshd/v1/service_pb';

export class FileTransferClient {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could probably be named FileTransferService rather than Client, no? It'd fit better with the rest of the classes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the name comes from the first iteration and doesn't have much sense now, changed.

@gzdunek gzdunek merged commit f614b7f into master Oct 10, 2022
@gzdunek gzdunek deleted the gzdunek/refactor-file-transfer branch October 10, 2022 16:38
gzdunek added a commit that referenced this pull request Oct 10, 2022
(cherry picked from commit f614b7f)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants