Skip to content

Commit d04de3d

Browse files
jsjoeiocode-asher
authored andcommitted
feat: add option for disabling file downloads (coder#5055)
* feat(cli): add disable-file-downloads to cli * feat(e2e): add download test * feat(e2e): add downloads disabled test * refactor(e2e): explain how to debug unexpected close * feat(patches): add disable file downloads * wip: update diff * Update src/node/cli.ts Co-authored-by: Asher <[email protected]> * fixup! add missing common/contextkeys file to patch * fixup!: update patch * fixup!: default disable-file-downloads undefined * fixup!: combine e2e tests * fixup!: use different test names * feat: add CS_DISABLE_FILE_DOWNLOADS * fixup!: make explicit and cleanup test * fixup!: use beforeEach Co-authored-by: Asher <[email protected]>
1 parent dea642b commit d04de3d

File tree

7 files changed

+259
-1
lines changed

7 files changed

+259
-1
lines changed

patches/disable-downloads.diff

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
Add option to disable file downloads via CLI
2+
3+
This patch adds support for a new CLI flag called `--disable-file-downloads`
4+
which allows a user to remove the "Download..." option that shows up when you
5+
right-click files in Code. The default value for this is `false`.
6+
7+
To test this, start code-server with `--disable-file-downloads`, open editor,
8+
right-click on a file (not a folder) and you should **not** see the
9+
"Download..." option.
10+
11+
Index: code-server/lib/vscode/src/vs/workbench/browser/web.api.ts
12+
===================================================================
13+
--- code-server.orig/lib/vscode/src/vs/workbench/browser/web.api.ts
14+
+++ code-server/lib/vscode/src/vs/workbench/browser/web.api.ts
15+
@@ -210,6 +210,11 @@ export interface IWorkbenchConstructionO
16+
*/
17+
readonly userDataPath?: string
18+
19+
+ /**
20+
+ * Whether the "Download..." option is enabled for files.
21+
+ */
22+
+ readonly isEnabledFileDownloads?: boolean
23+
+
24+
//#endregion
25+
26+
27+
Index: code-server/lib/vscode/src/vs/workbench/services/environment/browser/environmentService.ts
28+
===================================================================
29+
--- code-server.orig/lib/vscode/src/vs/workbench/services/environment/browser/environmentService.ts
30+
+++ code-server/lib/vscode/src/vs/workbench/services/environment/browser/environmentService.ts
31+
@@ -30,6 +30,11 @@ export interface IBrowserWorkbenchEnviro
32+
* Options used to configure the workbench.
33+
*/
34+
readonly options?: IWorkbenchConstructionOptions;
35+
+
36+
+ /**
37+
+ * Enable downloading files via menu actions.
38+
+ */
39+
+ readonly isEnabledFileDownloads?: boolean;
40+
}
41+
42+
export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvironmentService {
43+
@@ -61,6 +66,13 @@ export class BrowserWorkbenchEnvironment
44+
return this.options.userDataPath;
45+
}
46+
47+
+ get isEnabledFileDownloads(): boolean {
48+
+ if (typeof this.options.isEnabledFileDownloads === "undefined") {
49+
+ throw new Error('isEnabledFileDownloads was not provided to the browser');
50+
+ }
51+
+ return this.options.isEnabledFileDownloads;
52+
+ }
53+
+
54+
@memoize
55+
get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); }
56+
57+
Index: code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts
58+
===================================================================
59+
--- code-server.orig/lib/vscode/src/vs/server/node/serverEnvironmentService.ts
60+
+++ code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts
61+
@@ -15,6 +15,7 @@ export const serverOptions: OptionDescri
62+
'disable-update-check': { type: 'boolean' },
63+
'auth': { type: 'string' },
64+
'locale': { type: 'string' },
65+
+ 'disable-file-downloads': { type: 'boolean' },
66+
67+
/* ----- server setup ----- */
68+
69+
@@ -92,6 +93,7 @@ export interface ServerParsedArgs {
70+
'disable-update-check'?: boolean;
71+
'auth'?: string
72+
'locale'?: string
73+
+ 'disable-file-downloads'?: boolean;
74+
75+
/* ----- server setup ----- */
76+
77+
Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts
78+
===================================================================
79+
--- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts
80+
+++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts
81+
@@ -290,6 +290,7 @@ export class WebClientServer {
82+
logLevel: this._logService.getLevel(),
83+
},
84+
userDataPath: this._environmentService.userDataPath,
85+
+ isEnabledFileDownloads: !this._environmentService.args['disable-file-downloads'],
86+
settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined,
87+
productConfiguration: <Partial<IProductConfiguration>>{
88+
rootEndpoint: base,
89+
Index: code-server/lib/vscode/src/vs/workbench/browser/contextkeys.ts
90+
===================================================================
91+
--- code-server.orig/lib/vscode/src/vs/workbench/browser/contextkeys.ts
92+
+++ code-server/lib/vscode/src/vs/workbench/browser/contextkeys.ts
93+
@@ -7,12 +7,11 @@ import { Event } from 'vs/base/common/ev
94+
import { Disposable } from 'vs/base/common/lifecycle';
95+
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
96+
import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext } from 'vs/platform/contextkey/common/contextkeys';
97+
-import { SplitEditorsVertically, InEditorZenModeContext, ActiveEditorCanRevertContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, EditorTabsVisibleContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, EditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext } from 'vs/workbench/common/contextkeys';
98+
+import { SplitEditorsVertically, InEditorZenModeContext, ActiveEditorCanRevertContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, EditorTabsVisibleContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, EditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, IsEnabledFileDownloads } from 'vs/workbench/common/contextkeys';
99+
import { TEXT_DIFF_EDITOR_ID, EditorInputCapabilities, SIDE_BY_SIDE_EDITOR_ID, DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor';
100+
import { trackFocus, addDisposableListener, EventType } from 'vs/base/browser/dom';
101+
import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
102+
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
103+
-import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
104+
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
105+
import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
106+
import { IWorkbenchLayoutService, Parts, positionToString } from 'vs/workbench/services/layout/browser/layoutService';
107+
@@ -24,6 +23,7 @@ import { IEditorResolverService } from '
108+
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
109+
import { Schemas } from 'vs/base/common/network';
110+
import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess';
111+
+import { IBrowserWorkbenchEnvironmentService } from '../services/environment/browser/environmentService';
112+
113+
export class WorkbenchContextKeysHandler extends Disposable {
114+
private inputFocusedContext: IContextKey<boolean>;
115+
@@ -75,7 +75,7 @@ export class WorkbenchContextKeysHandler
116+
@IContextKeyService private readonly contextKeyService: IContextKeyService,
117+
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
118+
@IConfigurationService private readonly configurationService: IConfigurationService,
119+
- @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
120+
+ @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService,
121+
@IEditorService private readonly editorService: IEditorService,
122+
@IEditorResolverService private readonly editorResolverService: IEditorResolverService,
123+
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
124+
@@ -194,6 +194,9 @@ export class WorkbenchContextKeysHandler
125+
this.auxiliaryBarVisibleContext = AuxiliaryBarVisibleContext.bindTo(this.contextKeyService);
126+
this.auxiliaryBarVisibleContext.set(this.layoutService.isVisible(Parts.AUXILIARYBAR_PART));
127+
128+
+ // code-server
129+
+ IsEnabledFileDownloads.bindTo(this.contextKeyService).set(this.environmentService.isEnabledFileDownloads ?? true)
130+
+
131+
this.registerListeners();
132+
}
133+
134+
Index: code-server/lib/vscode/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts
135+
===================================================================
136+
--- code-server.orig/lib/vscode/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts
137+
+++ code-server/lib/vscode/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts
138+
@@ -21,7 +21,7 @@ import { CLOSE_SAVED_EDITORS_COMMAND_ID,
139+
import { AutoSaveAfterShortDelayContext } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
140+
import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService';
141+
import { Schemas } from 'vs/base/common/network';
142+
-import { DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, WorkbenchStateContext, WorkspaceFolderCountContext, SidebarFocusContext, ActiveEditorCanRevertContext, ActiveEditorContext, ResourceContextKey } from 'vs/workbench/common/contextkeys';
143+
+import { DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, WorkbenchStateContext, WorkspaceFolderCountContext, SidebarFocusContext, ActiveEditorCanRevertContext, ActiveEditorContext, ResourceContextKey, IsEnabledFileDownloads } from 'vs/workbench/common/contextkeys';
144+
import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys';
145+
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
146+
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
147+
@@ -475,13 +475,16 @@ MenuRegistry.appendMenuItem(MenuId.Explo
148+
id: DOWNLOAD_COMMAND_ID,
149+
title: DOWNLOAD_LABEL
150+
},
151+
- when: ContextKeyExpr.or(
152+
- // native: for any remote resource
153+
- ContextKeyExpr.and(IsWebContext.toNegated(), ResourceContextKey.Scheme.notEqualsTo(Schemas.file)),
154+
- // web: for any files
155+
- ContextKeyExpr.and(IsWebContext, ExplorerFolderContext.toNegated(), ExplorerRootContext.toNegated()),
156+
- // web: for any folders if file system API support is provided
157+
- ContextKeyExpr.and(IsWebContext, HasWebFileSystemAccess)
158+
+ when: ContextKeyExpr.and(
159+
+ IsEnabledFileDownloads,
160+
+ ContextKeyExpr.or(
161+
+ // native: for any remote resource
162+
+ ContextKeyExpr.and(IsWebContext.toNegated(), ResourceContextKey.Scheme.notEqualsTo(Schemas.file)),
163+
+ // web: for any files
164+
+ ContextKeyExpr.and(IsWebContext, ExplorerFolderContext.toNegated(), ExplorerRootContext.toNegated()),
165+
+ // web: for any folders if file system API support is provided
166+
+ ContextKeyExpr.and(IsWebContext, HasWebFileSystemAccess)
167+
+ )
168+
)
169+
}));
170+
171+
Index: code-server/lib/vscode/src/vs/workbench/common/contextkeys.ts
172+
===================================================================
173+
--- code-server.orig/lib/vscode/src/vs/workbench/common/contextkeys.ts
174+
+++ code-server/lib/vscode/src/vs/workbench/common/contextkeys.ts
175+
@@ -30,6 +30,8 @@ export const IsFullscreenContext = new R
176+
177+
export const HasWebFileSystemAccess = new RawContextKey<boolean>('hasWebFileSystemAccess', false, true); // Support for FileSystemAccess web APIs (https://wicg.github.io/file-system-access)
178+
179+
+export const IsEnabledFileDownloads = new RawContextKey<boolean>('isEnabledFileDownloads', true, true);
180+
+
181+
//#endregion
182+
183+

patches/series

+1
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ local-storage.diff
1818
service-worker.diff
1919
connection-type.diff
2020
sourcemaps.diff
21+
disable-downloads.diff

src/node/cli.ts

+9
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export interface UserProvidedCodeArgs {
4949
category?: string
5050
"github-auth"?: string
5151
"disable-update-check"?: boolean
52+
"disable-file-downloads"?: boolean
5253
}
5354

5455
/**
@@ -157,6 +158,10 @@ export const options: Options<Required<UserProvidedArgs>> = {
157158
"Disable update check. Without this flag, code-server checks every 6 hours against the latest github release and \n" +
158159
"then notifies you once every week that a new release is available.",
159160
},
161+
"disable-file-downloads": {
162+
type: "boolean",
163+
description: "Disable file downloads from Code.",
164+
},
160165
// --enable can be used to enable experimental features. These features
161166
// provide no guarantees.
162167
enable: { type: "string[]" },
@@ -537,6 +542,10 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config
537542
args.password = process.env.PASSWORD
538543
}
539544

545+
if (process.env.CS_DISABLE_FILE_DOWNLOADS === "1") {
546+
args["disable-file-downloads"] = true
547+
}
548+
540549
const usingEnvHashedPassword = !!process.env.HASHED_PASSWORD
541550
if (process.env.HASHED_PASSWORD) {
542551
args["hashed-password"] = process.env.HASHED_PASSWORD

test/e2e/downloads.test.ts

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as path from "path"
2+
import { promises as fs } from "fs"
3+
import { clean } from "../utils/helpers"
4+
import { describe, test, expect } from "./baseFixture"
5+
6+
describe("Downloads (enabled)", true, [], {}, async () => {
7+
const testName = "downloads-enabled"
8+
test.beforeAll(async () => {
9+
await clean(testName)
10+
})
11+
12+
test("should see the 'Download...' option", async ({ codeServerPage }) => {
13+
// Setup
14+
const workspaceDir = await codeServerPage.workspaceDir
15+
const tmpFilePath = path.join(workspaceDir, "unique-file.txt")
16+
await fs.writeFile(tmpFilePath, "hello world")
17+
18+
// Action
19+
const fileInExplorer = await codeServerPage.page.waitForSelector("text=unique-file.txt")
20+
await fileInExplorer.click({
21+
button: "right",
22+
})
23+
24+
expect(await codeServerPage.page.isVisible("text=Download...")).toBe(true)
25+
})
26+
})
27+
28+
describe("Downloads (disabled)", true, ["--disable-file-downloads"], {}, async () => {
29+
const testName = "downloads-disabled"
30+
test.beforeAll(async () => {
31+
await clean(testName)
32+
})
33+
34+
test("should not see the 'Download...' option", async ({ codeServerPage }) => {
35+
// Setup
36+
const workspaceDir = await codeServerPage.workspaceDir
37+
const tmpFilePath = path.join(workspaceDir, "unique-file.txt")
38+
await fs.writeFile(tmpFilePath, "hello world")
39+
40+
// Action
41+
const fileInExplorer = await codeServerPage.page.waitForSelector("text=unique-file.txt")
42+
await fileInExplorer.click({
43+
button: "right",
44+
})
45+
46+
expect(await codeServerPage.page.isVisible("text=Download...")).toBe(false)
47+
})
48+
})

test/e2e/models/CodeServer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export class CodeServer {
134134
})
135135

136136
proc.on("close", (code) => {
137-
const error = new Error("code-server closed unexpectedly")
137+
const error = new Error("code-server closed unexpectedly. Try running with LOG_LEVEL=debug to see more info.")
138138
if (!this.closed) {
139139
this.logger.error(error.message, field("code", code))
140140
}

test/unit/node/cli.test.ts

+16
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ describe("parser", () => {
4242
beforeEach(() => {
4343
delete process.env.LOG_LEVEL
4444
delete process.env.PASSWORD
45+
delete process.env.CS_DISABLE_FILE_DOWNLOADS
4546
console.log = jest.fn()
4647
})
4748

@@ -92,6 +93,8 @@ describe("parser", () => {
9293

9394
"--port=8081",
9495

96+
"--disable-file-downloads",
97+
9598
["--host", "0.0.0.0"],
9699
"4",
97100
"--",
@@ -108,6 +111,7 @@ describe("parser", () => {
108111
cert: {
109112
value: path.resolve("path/to/cert"),
110113
},
114+
"disable-file-downloads": true,
111115
enable: ["feature1", "feature2"],
112116
help: true,
113117
host: "0.0.0.0",
@@ -346,6 +350,18 @@ describe("parser", () => {
346350
expect(process.env.GITHUB_TOKEN).toBe(undefined)
347351
})
348352

353+
it("should use env var CS_DISABLE_FILE_DOWNLOADS", async () => {
354+
process.env.CS_DISABLE_FILE_DOWNLOADS = "1"
355+
const args = parse([])
356+
expect(args).toEqual({})
357+
358+
const defaultArgs = await setDefaults(args)
359+
expect(defaultArgs).toEqual({
360+
...defaults,
361+
"disable-file-downloads": true,
362+
})
363+
})
364+
349365
it("should error if password passed in", () => {
350366
expect(() => parse(["--password", "supersecret123"])).toThrowError(
351367
"--password can only be set in the config file or passed in via $PASSWORD",

test/unit/node/plugin.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ describe("plugin", () => {
3838
"proxy-domain": [],
3939
config: "~/.config/code-server/config.yaml",
4040
verbose: false,
41+
"disable-file-downloads": false,
4142
usingEnvPassword: false,
4243
usingEnvHashedPassword: false,
4344
"extensions-dir": "",

0 commit comments

Comments
 (0)