diff --git a/web/packages/shared/components/DesktopSession/DesktopSession.story.tsx b/web/packages/shared/components/DesktopSession/DesktopSession.story.tsx
index 6ea5c4fd50dca..53b29c246554f 100644
--- a/web/packages/shared/components/DesktopSession/DesktopSession.story.tsx
+++ b/web/packages/shared/components/DesktopSession/DesktopSession.story.tsx
@@ -26,6 +26,7 @@ import {
} from 'shared/hooks/useAsync';
import {
BitmapFrame,
+ BrowserFileSystem,
ClientScreenSpec,
TdpClient,
TdpClientEvent,
@@ -54,7 +55,7 @@ const meta: Meta = {
export default meta;
const fakeClient = () => {
- const client = new TdpClient(() => null);
+ const client = new TdpClient(() => null, new BrowserFileSystem());
// Don't try to connect to a websocket.
client.connect = async spec => {
emitFrame(client, spec);
diff --git a/web/packages/shared/components/DesktopSession/DesktopSession.test.tsx b/web/packages/shared/components/DesktopSession/DesktopSession.test.tsx
index f59a0aa425f36..cf079f8c4fbd3 100644
--- a/web/packages/shared/components/DesktopSession/DesktopSession.test.tsx
+++ b/web/packages/shared/components/DesktopSession/DesktopSession.test.tsx
@@ -22,7 +22,7 @@ import { screen } from '@testing-library/react';
import { render } from 'design/utils/testing';
import { makeSuccessAttempt } from 'shared/hooks/useAsync';
-import { TdpClient } from 'shared/libs/tdp';
+import { BrowserFileSystem, TdpClient } from 'shared/libs/tdp';
import { wait } from 'shared/utils/wait';
import { DesktopSession } from './DesktopSession';
@@ -72,7 +72,10 @@ const getMockTransport = () => {
test('reconnect button reinitializes the connection', async () => {
const transport = getMockTransport();
- const tpdClient = new TdpClient(transport.getTransport);
+ const tpdClient = new TdpClient(
+ transport.getTransport,
+ new BrowserFileSystem()
+ );
jest.spyOn(tpdClient, 'connect');
jest.spyOn(tpdClient, 'shutdown');
const { unmount } = render(
diff --git a/web/packages/shared/components/DesktopSession/Withholder.test.ts b/web/packages/shared/components/DesktopSession/Withholder.test.ts
index bc597e81b51c0..43ee1267cf4f3 100644
--- a/web/packages/shared/components/DesktopSession/Withholder.test.ts
+++ b/web/packages/shared/components/DesktopSession/Withholder.test.ts
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import { ButtonState, TdpClient } from 'shared/libs/tdp';
+import { BrowserFileSystem, ButtonState, TdpClient } from 'shared/libs/tdp';
import { Withholder } from './Withholder';
@@ -53,7 +53,7 @@ describe('withholder', () => {
const params = {
e: { key: 'Enter' } as KeyboardEvent as KeyboardEvent,
state: ButtonState.DOWN,
- cli: new TdpClient(() => null),
+ cli: new TdpClient(() => null, new BrowserFileSystem()),
};
withholder.handleKeyboardEvent(params, mockHandleKeyboardEvent);
expect(mockHandleKeyboardEvent).toHaveBeenCalledWith(params);
@@ -63,19 +63,19 @@ describe('withholder', () => {
const metaDown = {
e: { key: 'Meta' } as KeyboardEvent,
state: ButtonState.DOWN,
- cli: new TdpClient(() => null),
+ cli: new TdpClient(() => null, new BrowserFileSystem()),
};
const metaUp = {
e: { key: 'Meta' } as KeyboardEvent,
state: ButtonState.UP,
- cli: new TdpClient(() => null),
+ cli: new TdpClient(() => null, new BrowserFileSystem()),
};
const enterDown = {
e: { key: 'Enter' } as KeyboardEvent as KeyboardEvent,
state: ButtonState.DOWN,
- cli: new TdpClient(() => null),
+ cli: new TdpClient(() => null, new BrowserFileSystem()),
};
withholder.handleKeyboardEvent(metaDown, mockHandleKeyboardEvent);
@@ -95,12 +95,12 @@ describe('withholder', () => {
const metaParams = {
e: { key: 'Meta' } as KeyboardEvent,
state: ButtonState.UP,
- cli: new TdpClient(() => null),
+ cli: new TdpClient(() => null, new BrowserFileSystem()),
};
const altParams = {
e: { key: 'Alt' } as KeyboardEvent,
state: ButtonState.UP,
- cli: new TdpClient(() => null),
+ cli: new TdpClient(() => null, new BrowserFileSystem()),
};
withholder.handleKeyboardEvent(metaParams, mockHandleKeyboardEvent);
@@ -119,7 +119,7 @@ describe('withholder', () => {
const metaParams = {
e: { key: 'Meta' } as KeyboardEvent,
state: ButtonState.UP,
- cli: new TdpClient(() => null),
+ cli: new TdpClient(() => null, new BrowserFileSystem()),
};
withholder.handleKeyboardEvent(metaParams, mockHandleKeyboardEvent);
expect((withholder as any).withheldKeys).toHaveLength(1);
diff --git a/web/packages/shared/components/DesktopSession/useDesktopSession.tsx b/web/packages/shared/components/DesktopSession/useDesktopSession.tsx
index 15ea9e1db1bf5..e68041a114a2e 100644
--- a/web/packages/shared/components/DesktopSession/useDesktopSession.tsx
+++ b/web/packages/shared/components/DesktopSession/useDesktopSession.tsx
@@ -121,32 +121,13 @@ export default function useDesktopSession(
}
}
- const onShareDirectory = () => {
+ const onShareDirectory = async () => {
try {
- window
- .showDirectoryPicker()
- .then(sharedDirHandle => {
- // Permissions granted and/or directory selected
- setDirectorySharingState(prevState => ({
- ...prevState,
- directorySelected: true,
- }));
- tdpClient.addSharedDirectory(sharedDirHandle);
- tdpClient.sendSharedDirectoryAnnounce();
- })
- .catch(e => {
- setDirectorySharingState(prevState => ({
- ...prevState,
- directorySelected: false,
- }));
- addAlert({
- severity: 'warn',
- content: {
- title: 'Failed to open the directory picker',
- description: e.message,
- },
- });
- });
+ await tdpClient.shareDirectory();
+ setDirectorySharingState(prevState => ({
+ ...prevState,
+ directorySelected: true,
+ }));
} catch (e) {
setDirectorySharingState(prevState => ({
...prevState,
@@ -154,19 +135,9 @@ export default function useDesktopSession(
}));
addAlert({
severity: 'warn',
- // This is a gross error message, but should be infrequent enough that its worth just telling
- // the user the likely problem, while also displaying the error message just in case that's not it.
- // In a perfect world, we could check for which error message this is and display
- // context appropriate directions.
content: {
- title: 'Encountered an error while attempting to share a directory',
- description:
- e.message +
- '. \n\nYour user role supports directory sharing over desktop access, \
- however this feature is only available by default on some Chromium \
- based browsers like Google Chrome or Microsoft Edge. Brave users can \
- use the feature by navigating to brave://flags/#file-system-access-api \
- and selecting "Enable". If you\'re not already, please switch to a supported browser.',
+ title: 'Could not share a directory',
+ description: e.message,
},
});
}
diff --git a/web/packages/shared/libs/tdp/client.ts b/web/packages/shared/libs/tdp/client.ts
index cb961087bb7a8..f942ef1bd93c3 100644
--- a/web/packages/shared/libs/tdp/client.ts
+++ b/web/packages/shared/libs/tdp/client.ts
@@ -51,9 +51,9 @@ import Codec, {
} from './codec';
import {
PathDoesNotExistError,
- SharedDirectoryManager,
+ SharedDirectoryAccess,
type FileOrDirInfo,
-} from './sharedDirectoryManager';
+} from './sharedDirectoryAccess';
export enum TdpClientEvent {
TDP_CLIENT_SCREEN_SPEC = 'tdp client screen spec',
@@ -110,18 +110,17 @@ export class TdpClient extends EventEmitter {
protected codec: Codec;
protected transport: TdpTransport | undefined;
private transportAbortController: AbortController | undefined;
- private sdManager: SharedDirectoryManager;
private fastPathProcessor: FastPathProcessor | undefined;
private wasmReady: Promise | undefined;
private logger = Logger.create('TDPClient');
constructor(
- private getTransport: (signal: AbortSignal) => Promise
+ private getTransport: (signal: AbortSignal) => Promise,
+ private sharedDirectoryAccess: SharedDirectoryAccess
) {
super();
this.codec = new Codec();
- this.sdManager = new SharedDirectoryManager();
}
/**
@@ -479,20 +478,22 @@ export class TdpClient extends EventEmitter {
// Since this is not a fatal error, we emit a warning but otherwise
// keep the sesion alive.
this.handleWarning(
- `Failed to share directory '${this.sdManager.getName()}', drive redirection may be disabled on the RDP server.`,
+ `Failed to share directory '${this.sharedDirectoryAccess.getDirectoryName()}', drive redirection may be disabled on the RDP server.`,
TdpClientEvent.TDP_WARNING
);
return;
}
- this.logger.info('Started sharing directory: ' + this.sdManager.getName());
+ this.logger.info(
+ `Started sharing directory: ${this.sharedDirectoryAccess.getDirectoryName()}`
+ );
}
async handleSharedDirectoryInfoRequest(buffer: ArrayBuffer) {
const req = this.codec.decodeSharedDirectoryInfoRequest(buffer);
const path = req.path;
try {
- const info = await this.sdManager.getInfo(path);
+ const info = await this.sharedDirectoryAccess.stat(path);
this.sendSharedDirectoryInfoResponse({
completionId: req.completionId,
errCode: SharedDirectoryErrCode.Nil,
@@ -521,8 +522,8 @@ export class TdpClient extends EventEmitter {
const req = this.codec.decodeSharedDirectoryCreateRequest(buffer);
try {
- await this.sdManager.create(req.path, req.fileType);
- const info = await this.sdManager.getInfo(req.path);
+ await this.sharedDirectoryAccess.create(req.path, req.fileType);
+ const info = await this.sharedDirectoryAccess.stat(req.path);
this.sendSharedDirectoryCreateResponse({
completionId: req.completionId,
errCode: SharedDirectoryErrCode.Nil,
@@ -548,7 +549,7 @@ export class TdpClient extends EventEmitter {
const req = this.codec.decodeSharedDirectoryDeleteRequest(buffer);
try {
- await this.sdManager.delete(req.path);
+ await this.sharedDirectoryAccess.delete(req.path);
this.sendSharedDirectoryDeleteResponse({
completionId: req.completionId,
errCode: SharedDirectoryErrCode.Nil,
@@ -564,7 +565,7 @@ export class TdpClient extends EventEmitter {
async handleSharedDirectoryReadRequest(buffer: ArrayBuffer) {
const req = this.codec.decodeSharedDirectoryReadRequest(buffer);
- const readData = await this.sdManager.readFile(
+ const readData = await this.sharedDirectoryAccess.read(
req.path,
req.offset,
req.length
@@ -579,7 +580,7 @@ export class TdpClient extends EventEmitter {
async handleSharedDirectoryWriteRequest(buffer: ArrayBuffer) {
const req = this.codec.decodeSharedDirectoryWriteRequest(buffer);
- const bytesWritten = await this.sdManager.writeFile(
+ const bytesWritten = await this.sharedDirectoryAccess.write(
req.path,
req.offset,
req.writeData
@@ -610,7 +611,8 @@ export class TdpClient extends EventEmitter {
const req = this.codec.decodeSharedDirectoryListRequest(buffer);
const path = req.path;
- const infoList: FileOrDirInfo[] = await this.sdManager.listContents(path);
+ const infoList: FileOrDirInfo[] =
+ await this.sharedDirectoryAccess.readDir(path);
const fsoList: FileSystemObject[] = infoList.map(info => this.toFso(info));
this.sendSharedDirectoryListResponse({
@@ -622,7 +624,7 @@ export class TdpClient extends EventEmitter {
async handleSharedDirectoryTruncateRequest(buffer: ArrayBuffer) {
const req = this.codec.decodeSharedDirectoryTruncateRequest(buffer);
- await this.sdManager.truncateFile(req.path, req.endOfFile);
+ await this.sharedDirectoryAccess.truncate(req.path, req.endOfFile);
this.sendSharedDirectoryTruncateResponse({
completionId: req.completionId,
errCode: SharedDirectoryErrCode.Nil,
@@ -706,12 +708,13 @@ export class TdpClient extends EventEmitter {
this.send(msg);
}
- addSharedDirectory(sharedDirectory: FileSystemDirectoryHandle) {
- this.sdManager.add(sharedDirectory);
+ async shareDirectory() {
+ await this.sharedDirectoryAccess.selectDirectory();
+ this.sendSharedDirectoryAnnounce();
}
sendSharedDirectoryAnnounce() {
- const name = this.sdManager.getName();
+ const name = this.sharedDirectoryAccess.getDirectoryName();
this.send(
this.codec.encodeSharedDirectoryAnnounce({
discard: 0, // This is always the first request.
diff --git a/web/packages/shared/libs/tdp/index.ts b/web/packages/shared/libs/tdp/index.ts
index 602656d77b041..dc59db2619bf7 100644
--- a/web/packages/shared/libs/tdp/index.ts
+++ b/web/packages/shared/libs/tdp/index.ts
@@ -23,3 +23,4 @@ export {
type BitmapFrame,
} from './client';
export * from './codec';
+export * from './sharedDirectoryAccess';
diff --git a/web/packages/shared/libs/tdp/sharedDirectoryManager.ts b/web/packages/shared/libs/tdp/sharedDirectoryAccess.ts
similarity index 77%
rename from web/packages/shared/libs/tdp/sharedDirectoryManager.ts
rename to web/packages/shared/libs/tdp/sharedDirectoryAccess.ts
index 53f700974424e..dc9168c5f4902 100644
--- a/web/packages/shared/libs/tdp/sharedDirectoryManager.ts
+++ b/web/packages/shared/libs/tdp/sharedDirectoryAccess.ts
@@ -18,16 +18,54 @@
import { FileType } from './codec';
-// SharedDirectoryManager manages a FileSystemDirectoryHandle for use
-// by the TDP client. Most of its methods can potentially throw errors
-// and so should be wrapped in try/catch blocks.
-export class SharedDirectoryManager {
+export interface SharedDirectoryAccess {
+ /** Prompts the user to select a directory to share. */
+ selectDirectory(): Promise;
+ /** Returns the name of the currently shared directory. */
+ getDirectoryName(): string;
+ /** Retrieves metadata about a file or directory at the given path. */
+ stat(path: string): Promise;
+ /** Lists files and directories within the given directory path. */
+ readDir(path: string): Promise;
+ /** Reads a slice of a file. */
+ read(path: string, offset: bigint, length: number): Promise;
+ /** Writes data to a file at a given offset. */
+ write(path: string, offset: bigint, data: Uint8Array): Promise;
+ /** Truncates a file to the specified size. */
+ truncate(path: string, size: number): Promise;
+ /** Creates a new file or directory at the given path. */
+ create(path: string, fileType: FileType): Promise;
+ /** Deletes a file or directory at the given path. */
+ delete(path: string): Promise;
+}
+
+/**
+ * Enables directory sharing using FileSystem API.
+ * Most of the methods can potentially throw errors and so should be wrapped in try/catch blocks.
+ */
+export class BrowserFileSystem implements SharedDirectoryAccess {
private dir: FileSystemDirectoryHandle | undefined;
/**
+ * Opens a directory.
* @throws Will throw an error if a directory is already being shared.
*/
- add(sharedDirectory: FileSystemDirectoryHandle) {
+ async selectDirectory() {
+ if (typeof window.showDirectoryPicker !== 'function') {
+ // This is a gross error message, but should be infrequent enough that its worth just telling
+ // the user the likely problem, while also displaying the error message just in case that's not it.
+ // In a perfect world, we could check for which error message this is and display
+ // context appropriate directions.
+ throw new Error(
+ 'Your user role supports directory sharing over desktop access, \
+ however this feature is only available by default on some Chromium \
+ based browsers like Google Chrome or Microsoft Edge. Brave users can \
+ use the feature by navigating to brave://flags/#file-system-access-api \
+ and selecting "Enable". If you\'re not already, please switch to a supported browser.'
+ );
+ }
+
+ const sharedDirectory = await window.showDirectoryPicker();
if (this.dir) {
throw new Error(
'SharedDirectoryManager currently only supports sharing a single directory'
@@ -37,19 +75,19 @@ export class SharedDirectoryManager {
}
/**
- * @throws Will throw an error if a directory has not already been initialized via add().
+ * @throws Will throw an error if a directory has not already been initialized.
*/
- getName(): string {
+ getDirectoryName(): string {
this.checkReady();
return this.dir.name;
}
/**
* Gets the information for the file or directory at path where path is the relative path from the root directory.
- * @throws Will throw an error if a directory has not already been initialized via add().
+ * @throws Will throw an error if a directory has not already been initialized.
* @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory
*/
- async getInfo(path: string): Promise {
+ async stat(path: string): Promise {
this.checkReady();
const fileOrDir = await this.walkPath(path);
@@ -89,10 +127,10 @@ export class SharedDirectoryManager {
/**
* Gets the FileOrDirInfo for all the children of the directory at path.
- * @throws Will throw an error if a directory has not already been initialized via add().
+ * @throws Will throw an error if a directory has not already been initialized.
* @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory
*/
- async listContents(path: string): Promise {
+ async readDir(path: string): Promise {
this.checkReady();
// Get the directory whose contents we want to list.
@@ -110,7 +148,7 @@ export class SharedDirectoryManager {
} else {
entryPath = entry.name;
}
- infos.push(await this.getInfo(entryPath));
+ infos.push(await this.stat(entryPath));
}
return infos;
@@ -118,10 +156,10 @@ export class SharedDirectoryManager {
/**
* Reads length bytes starting at offset from a file at path.
- * @throws Will throw an error if a directory has not already been initialized via add().
+ * @throws Will throw an error if a directory has not already been initialized.
* @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory
*/
- async readFile(
+ async read(
path: string,
offset: bigint,
length: number
@@ -136,14 +174,10 @@ export class SharedDirectoryManager {
/**
* Writes the bytes in writeData to the file at path starting at offset.
- * @throws Will throw an error if a directory has not already been initialized via add().
+ * @throws Will throw an error if a directory has not already been initialized.
* @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory
*/
- async writeFile(
- path: string,
- offset: bigint,
- data: Uint8Array
- ): Promise {
+ async write(path: string, offset: bigint, data: Uint8Array): Promise {
this.checkReady();
const fileHandle = await this.getFileHandle(path);
@@ -156,10 +190,10 @@ export class SharedDirectoryManager {
/**
* Truncates the file at path to size bytes.
- * @throws Will throw an error if a directory has not already been initialized via add().
+ * @throws Will throw an error if a directory has not already been initialized.
* @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory
*/
- async truncateFile(path: string, size: number): Promise {
+ async truncate(path: string, size: number): Promise {
this.checkReady();
const fileHandle = await this.getFileHandle(path);
const file = await fileHandle.createWritable({ keepExistingData: true });
@@ -276,7 +310,7 @@ export class SharedDirectoryManager {
}
/**
- * @throws Will throw an error if a directory has not already been initialized via add().
+ * @throws Will throw an error if a directory has not already been initialized.
*/
private checkReady() {
if (!this.dir) {
diff --git a/web/packages/teleport/src/DesktopSession/DesktopSession.tsx b/web/packages/teleport/src/DesktopSession/DesktopSession.tsx
index 4d74ea6dbdb79..7bcaeda1a8700 100644
--- a/web/packages/teleport/src/DesktopSession/DesktopSession.tsx
+++ b/web/packages/teleport/src/DesktopSession/DesktopSession.tsx
@@ -24,7 +24,7 @@ import {
DesktopSession as SharedDesktopSession,
} from 'shared/components/DesktopSession';
import { useAsync } from 'shared/hooks/useAsync';
-import { TdpClient } from 'shared/libs/tdp';
+import { BrowserFileSystem, TdpClient } from 'shared/libs/tdp';
import { useTeleport } from 'teleport';
import AuthnDialog from 'teleport/components/AuthnDialog';
@@ -44,17 +44,19 @@ export function DesktopSession() {
const [client] = useState(
() =>
//TODO(gzdunek): It doesn't really matter here, but make TdpClient reactive to addr change.
- new TdpClient(abortSignal =>
- adaptWebSocketToTdpTransport(
- new AuthenticatedWebSocket(
- cfg.api.desktopWsAddr
- .replace(':fqdn', getHostName())
- .replace(':clusterId', clusterId)
- .replace(':desktopName', desktopName)
- .replace(':username', username)
+ new TdpClient(
+ abortSignal =>
+ adaptWebSocketToTdpTransport(
+ new AuthenticatedWebSocket(
+ cfg.api.desktopWsAddr
+ .replace(':fqdn', getHostName())
+ .replace(':clusterId', clusterId)
+ .replace(':desktopName', desktopName)
+ .replace(':username', username)
+ ),
+ abortSignal
),
- abortSignal
- )
+ new BrowserFileSystem()
)
);
const mfa = useMfaEmitter(client, undefined, {
diff --git a/web/packages/teleport/src/lib/tdp/playerClient.ts b/web/packages/teleport/src/lib/tdp/playerClient.ts
index 5633368da7f85..7e7f2055a6b3e 100644
--- a/web/packages/teleport/src/lib/tdp/playerClient.ts
+++ b/web/packages/teleport/src/lib/tdp/playerClient.ts
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import { TdpClient, TdpClientEvent } from 'shared/libs/tdp';
+import { BrowserFileSystem, TdpClient, TdpClientEvent } from 'shared/libs/tdp';
import { base64ToArrayBuffer } from 'shared/utils/base64';
import { throttle } from 'shared/utils/highbar';
@@ -54,8 +54,10 @@ export class PlayerClient extends TdpClient {
private timeout = null;
constructor({ url, setTime, setPlayerStatus, setStatusText }) {
- super(signal =>
- adaptWebSocketToTdpTransport(new AuthenticatedWebSocket(url), signal)
+ super(
+ signal =>
+ adaptWebSocketToTdpTransport(new AuthenticatedWebSocket(url), signal),
+ new BrowserFileSystem()
);
this.setPlayerStatus = setPlayerStatus;
this.setStatusText = setStatusText;