Add option to disable file downloads and uploads via cli This patch adds support for a new CLI flag called `--disable-file-downloads` which allows a user to remove the "Download..." option that shows up when you right-click files in Code. It also disables the "Show Local" button on the dialog for Save, Save-As and Save Workspace. The default value for this is `false`. This patch also add support for a new CLI flag called `--disable-file-uploads` which disables the drag to upload functionality and the "Upload..." option when you right-click folders in Code. It also disables the "Show Local" button on the dialog for opening a file. The default value for this is `false`. This patch also adds trace log statements for when a file is read and written to disk. To test disabling downloads, start code-server with `--disable-file-downloads`, open editor, right-click on a file (not a folder) and you should **not** see the "Download..." option. When saving a file or workspace, the "Show Local" button should **not** appear on the dialog that comes on screen. To test disabling uploads, start code-server with `--disable-file-uploads`, open editor, right-click on a folder (not a file) and you should **not** see the "Upload..." option. If you drag a file into the file navigator, the file should **not** upload and appear in the file navigator. When opening a file, the "Show Local" button should **not** appear on the dialog that comes on screen. Index: code-server/lib/vscode/src/vs/workbench/browser/web.api.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/workbench/browser/web.api.ts +++ code-server/lib/vscode/src/vs/workbench/browser/web.api.ts @@ -303,6 +303,16 @@ export interface IWorkbenchConstructionO */ readonly userDataPath?: string + /** + * Whether the "Download..." option is enabled for files. + */ + readonly isEnabledFileDownloads?: boolean + + /** + * Whether the "Upload..." button is enabled. + */ + readonly isEnabledFileUploads?: boolean + //#endregion //#region Profile options Index: code-server/lib/vscode/src/vs/workbench/services/environment/browser/environmentService.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/workbench/services/environment/browser/environmentService.ts +++ code-server/lib/vscode/src/vs/workbench/services/environment/browser/environmentService.ts @@ -34,6 +34,16 @@ export interface IBrowserWorkbenchEnviro readonly options?: IWorkbenchConstructionOptions; /** + * Enable downloading files via menu actions. + */ + readonly isEnabledFileDownloads?: boolean; + + /** + * Enable uploading files via menu actions. + */ + readonly isEnabledFileUploads?: boolean; + + /** * Gets whether a resolver extension is expected for the environment. */ readonly expectsResolverExtension: boolean; @@ -111,6 +121,20 @@ export class BrowserWorkbenchEnvironment return this.options.userDataPath; } + get isEnabledFileDownloads(): boolean { + if (typeof this.options.isEnabledFileDownloads === "undefined") { + throw new Error('isEnabledFileDownloads was not provided to the browser'); + } + return this.options.isEnabledFileDownloads; + } + + get isEnabledFileUploads(): boolean { + if (typeof this.options.isEnabledFileUploads === "undefined") { + throw new Error('isEnabledFileUploads was not provided to the browser'); + } + return this.options.isEnabledFileUploads; + } + @memoize get argvResource(): URI { return joinPath(this.userRoamingDataHome, 'argv.json'); } Index: code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/server/node/serverEnvironmentService.ts +++ code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts @@ -16,6 +16,8 @@ export const serverOptions: OptionDescri /* ----- code-server ----- */ 'disable-update-check': { type: 'boolean' }, 'auth': { type: 'string' }, + 'disable-file-downloads': { type: 'boolean' }, + 'disable-file-uploads': { type: 'boolean' }, /* ----- server setup ----- */ @@ -99,6 +101,8 @@ export interface ServerParsedArgs { /* ----- code-server ----- */ 'disable-update-check'?: boolean; 'auth'?: string; + 'disable-file-downloads'?: boolean; + 'disable-file-uploads'?: boolean; /* ----- server setup ----- */ Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts +++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts @@ -336,6 +336,8 @@ export class WebClientServer { serverBasePath: this._basePath, webviewEndpoint: vscodeBase + this._staticRoute + '/out/vs/workbench/contrib/webview/browser/pre', userDataPath: this._environmentService.userDataPath, + isEnabledFileDownloads: !this._environmentService.args['disable-file-downloads'], + isEnabledFileUploads: !this._environmentService.args['disable-file-uploads'], _wrapWebWorkerExtHostInIframe, developmentOptions: { enableSmokeTestDriver: this._environmentService.args['enable-smoke-test-driver'] ? true : undefined, logLevel: this._logService.getLevel() }, settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined, Index: code-server/lib/vscode/src/vs/workbench/browser/contextkeys.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/workbench/browser/contextkeys.ts +++ code-server/lib/vscode/src/vs/workbench/browser/contextkeys.ts @@ -7,11 +7,11 @@ import { Event } from '../../base/common import { Disposable } from '../../base/common/lifecycle.js'; import { IContextKeyService, IContextKey, setConstant as setConstantContextKey } from '../../platform/contextkey/common/contextkey.js'; import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext, ProductQualityContext, IsMobileContext } from '../../platform/contextkey/common/contextkeys.js'; -import { SplitEditorsVertically, InEditorZenModeContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, MainEditorAreaVisibleContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, TitleBarVisibleContext, TitleBarStyleContext, IsAuxiliaryWindowFocusedContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorGroupLockedContext, MultipleEditorGroupsContext, EditorsVisibleContext } from '../common/contextkeys.js'; +import { SplitEditorsVertically, InEditorZenModeContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, MainEditorAreaVisibleContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, TitleBarVisibleContext, TitleBarStyleContext, IsAuxiliaryWindowFocusedContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorGroupLockedContext, MultipleEditorGroupsContext, EditorsVisibleContext, IsEnabledFileDownloads, IsEnabledFileUploads } from '../common/contextkeys.js'; import { trackFocus, addDisposableListener, EventType, onDidRegisterWindow, getActiveWindow, isEditableElement } from '../../base/browser/dom.js'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from '../services/editor/common/editorGroupsService.js'; import { IConfigurationService } from '../../platform/configuration/common/configuration.js'; -import { IWorkbenchEnvironmentService } from '../services/environment/common/environmentService.js'; +import { IBrowserWorkbenchEnvironmentService } from '../services/environment/browser/environmentService.js'; import { WorkbenchState, IWorkspaceContextService, isTemporaryWorkspace } from '../../platform/workspace/common/workspace.js'; import { IWorkbenchLayoutService, Parts, positionToString } from '../services/layout/browser/layoutService.js'; import { getRemoteName } from '../../platform/remote/common/remoteHosts.js'; @@ -70,7 +70,7 @@ export class WorkbenchContextKeysHandler @IContextKeyService private readonly contextKeyService: IContextKeyService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService, @IProductService private readonly productService: IProductService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IEditorService private readonly editorService: IEditorService, @@ -197,6 +197,10 @@ export class WorkbenchContextKeysHandler this.auxiliaryBarVisibleContext = AuxiliaryBarVisibleContext.bindTo(this.contextKeyService); this.auxiliaryBarVisibleContext.set(this.layoutService.isVisible(Parts.AUXILIARYBAR_PART)); + // code-server + IsEnabledFileDownloads.bindTo(this.contextKeyService).set(this.environmentService.isEnabledFileDownloads ?? true) + IsEnabledFileUploads.bindTo(this.contextKeyService).set(this.environmentService.isEnabledFileUploads ?? true) + this.registerListeners(); } Index: code-server/lib/vscode/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ code-server/lib/vscode/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -20,7 +20,7 @@ import { CLOSE_SAVED_EDITORS_COMMAND_ID, import { AutoSaveAfterShortDelayContext } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; import { WorkbenchListDoubleSelection, WorkbenchTreeFindOpen } from '../../../../platform/list/browser/listService.js'; import { Schemas } from '../../../../base/common/network.js'; -import { DirtyWorkingCopiesContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, WorkbenchStateContext, WorkspaceFolderCountContext, SidebarFocusContext, ActiveEditorCanRevertContext, ActiveEditorContext, ResourceContextKey, ActiveEditorAvailableEditorIdsContext, MultipleEditorsSelectedInGroupContext, TwoEditorsSelectedInGroupContext, SelectedEditorsInGroupFileOrUntitledResourceContextKey } from '../../../common/contextkeys.js'; +import { IsEnabledFileDownloads, IsEnabledFileUploads, DirtyWorkingCopiesContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, WorkbenchStateContext, WorkspaceFolderCountContext, SidebarFocusContext, ActiveEditorCanRevertContext, ActiveEditorContext, ResourceContextKey, ActiveEditorAvailableEditorIdsContext, MultipleEditorsSelectedInGroupContext, TwoEditorsSelectedInGroupContext, SelectedEditorsInGroupFileOrUntitledResourceContextKey } from '../../../common/contextkeys.js'; import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; @@ -572,13 +572,16 @@ MenuRegistry.appendMenuItem(MenuId.Explo id: DOWNLOAD_COMMAND_ID, title: DOWNLOAD_LABEL }, - when: ContextKeyExpr.or( - // native: for any remote resource - ContextKeyExpr.and(IsWebContext.toNegated(), ResourceContextKey.Scheme.notEqualsTo(Schemas.file)), - // web: for any files - ContextKeyExpr.and(IsWebContext, ExplorerFolderContext.toNegated(), ExplorerRootContext.toNegated()), - // web: for any folders if file system API support is provided - ContextKeyExpr.and(IsWebContext, HasWebFileSystemAccess) + when: ContextKeyExpr.and( + IsEnabledFileDownloads, + ContextKeyExpr.or( + // native: for any remote resource + ContextKeyExpr.and(IsWebContext.toNegated(), ResourceContextKey.Scheme.notEqualsTo(Schemas.file)), + // web: for any files + ContextKeyExpr.and(IsWebContext, ExplorerFolderContext.toNegated(), ExplorerRootContext.toNegated()), + // web: for any folders if file system API support is provided + ContextKeyExpr.and(IsWebContext, HasWebFileSystemAccess) + ) ) })); @@ -590,6 +593,7 @@ MenuRegistry.appendMenuItem(MenuId.Explo title: UPLOAD_LABEL, }, when: ContextKeyExpr.and( + IsEnabledFileUploads, // only in web IsWebContext, // only on folders Index: code-server/lib/vscode/src/vs/workbench/common/contextkeys.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/workbench/common/contextkeys.ts +++ code-server/lib/vscode/src/vs/workbench/common/contextkeys.ts @@ -40,6 +40,9 @@ export const HasWebFileSystemAccess = ne export const EmbedderIdentifierContext = new RawContextKey('embedderIdentifier', undefined, localize('embedderIdentifier', 'The identifier of the embedder according to the product service, if one is defined')); +export const IsEnabledFileDownloads = new RawContextKey('isEnabledFileDownloads', true, true); +export const IsEnabledFileUploads = new RawContextKey('isEnabledFileUploads', true, true); + //#endregion Index: code-server/lib/vscode/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ code-server/lib/vscode/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -18,7 +18,7 @@ import { IModelService } from '../../../ import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js'; import { Schemas } from '../../../../base/common/network.js'; -import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; +import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js'; import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js'; import { IContextKeyService, IContextKey, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { equalsIgnoreCase, format, startsWithIgnoreCase } from '../../../../base/common/strings.js'; @@ -139,7 +139,7 @@ export class SimpleFileDialog extends Di @IFileDialogService private readonly fileDialogService: IFileDialogService, @IModelService private readonly modelService: IModelService, @ILanguageService private readonly languageService: ILanguageService, - @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService, + @IBrowserWorkbenchEnvironmentService protected readonly environmentService: IBrowserWorkbenchEnvironmentService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @IPathService protected readonly pathService: IPathService, @IKeybindingService private readonly keybindingService: IKeybindingService, @@ -283,20 +283,22 @@ export class SimpleFileDialog extends Di this.filePickBox.sortByLabel = false; this.filePickBox.ignoreFocusOut = true; this.filePickBox.ok = true; - if ((this.scheme !== Schemas.file) && this.options && this.options.availableFileSystems && (this.options.availableFileSystems.length > 1) && (this.options.availableFileSystems.indexOf(Schemas.file) > -1)) { - this.filePickBox.customButton = true; - this.filePickBox.customLabel = nls.localize('remoteFileDialog.local', 'Show Local'); - let action; - if (isSave) { - action = SaveLocalFileCommand; - } else { - action = this.allowFileSelection ? (this.allowFolderSelection ? OpenLocalFileFolderCommand : OpenLocalFileCommand) : OpenLocalFolderCommand; - } - const keybinding = this.keybindingService.lookupKeybinding(action.ID); - if (keybinding) { - const label = keybinding.getLabel(); - if (label) { - this.filePickBox.customHover = format('{0} ({1})', action.LABEL, label); + if ((isSave && this.environmentService.isEnabledFileDownloads) || (!isSave && this.environmentService.isEnabledFileUploads)) { + if ((this.scheme !== Schemas.file) && this.options && this.options.availableFileSystems && (this.options.availableFileSystems.length > 1) && (this.options.availableFileSystems.indexOf(Schemas.file) > -1)) { + this.filePickBox.customButton = true; + this.filePickBox.customLabel = nls.localize('remoteFileDialog.local', 'Show Local'); + let action; + if (isSave) { + action = SaveLocalFileCommand; + } else { + action = this.allowFileSelection ? (this.allowFolderSelection ? OpenLocalFileFolderCommand : OpenLocalFileCommand) : OpenLocalFolderCommand; + } + const keybinding = this.keybindingService.lookupKeybinding(action.ID); + if (keybinding) { + const label = keybinding.getLabel(); + if (label) { + this.filePickBox.customHover = format('{0} ({1})', action.LABEL, label); + } } } } Index: code-server/lib/vscode/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ code-server/lib/vscode/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -65,6 +65,7 @@ import { timeout } from '../../../../../ import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js'; import { mainWindow } from '../../../../../base/browser/window.js'; import { IExplorerFileContribution, explorerFileContribRegistry } from '../explorerFileContrib.js'; +import { IBrowserWorkbenchEnvironmentService } from '../../../../services/environment/browser/environmentService.js'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -1030,7 +1031,8 @@ export class FileDragAndDrop implements @IConfigurationService private configurationService: IConfigurationService, @IInstantiationService private instantiationService: IInstantiationService, @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IBrowserWorkbenchEnvironmentService protected readonly environmentService: IBrowserWorkbenchEnvironmentService ) { const updateDropEnablement = (e: IConfigurationChangeEvent | undefined) => { if (!e || e.affectsConfiguration('explorer.enableDragAndDrop')) { @@ -1255,15 +1257,17 @@ export class FileDragAndDrop implements // External file DND (Import/Upload file) if (data instanceof NativeDragAndDropData) { - // Use local file import when supported - if (!isWeb || (isTemporaryWorkspace(this.contextService.getWorkspace()) && WebFileSystemAccess.supported(mainWindow))) { - const fileImport = this.instantiationService.createInstance(ExternalFileImport); - await fileImport.import(resolvedTarget, originalEvent, mainWindow); - } - // Otherwise fallback to browser based file upload - else { - const browserUpload = this.instantiationService.createInstance(BrowserFileUpload); - await browserUpload.upload(target, originalEvent); + if (this.environmentService.isEnabledFileUploads) { + // Use local file import when supported + if (!isWeb || (isTemporaryWorkspace(this.contextService.getWorkspace()) && WebFileSystemAccess.supported(mainWindow))) { + const fileImport = this.instantiationService.createInstance(ExternalFileImport); + await fileImport.import(resolvedTarget, originalEvent, mainWindow); + } + // Otherwise fallback to browser based file upload + else { + const browserUpload = this.instantiationService.createInstance(BrowserFileUpload); + await browserUpload.upload(target, originalEvent); + } } } Index: code-server/lib/vscode/src/vs/platform/files/node/diskFileSystemProviderServer.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/platform/files/node/diskFileSystemProviderServer.ts +++ code-server/lib/vscode/src/vs/platform/files/node/diskFileSystemProviderServer.ts @@ -92,6 +92,7 @@ export abstract class AbstractDiskFileSy private async readFile(uriTransformer: IURITransformer, _resource: UriComponents, opts?: IFileAtomicReadOptions): Promise { const resource = this.transformIncoming(uriTransformer, _resource, true); + this.logService.trace(`File action: readFile ${resource.path}`); const buffer = await this.provider.readFile(resource, opts); return VSBuffer.wrap(buffer); @@ -110,6 +111,7 @@ export abstract class AbstractDiskFileSy } }); + this.logService.trace(`File action: readFileStream ${resource.path}`); const fileStream = this.provider.readFileStream(resource, opts, cts.token); listenStream(fileStream, { onData: chunk => emitter.fire(VSBuffer.wrap(chunk)), @@ -130,7 +132,7 @@ export abstract class AbstractDiskFileSy private writeFile(uriTransformer: IURITransformer, _resource: UriComponents, content: VSBuffer, opts: IFileWriteOptions): Promise { const resource = this.transformIncoming(uriTransformer, _resource); - + this.logService.trace(`File action: writeFile ${resource.path}`); return this.provider.writeFile(resource, content.buffer, opts); }