mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-26 21:28:04 +00:00
save as admin - handle case for readonly files
This commit is contained in:
@@ -3,11 +3,11 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
# If root, ensure that --user-data-dir or --write-elevated-helper is specified
|
||||
# If root, ensure that --user-data-dir or --sudo-write is specified
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
for i in $@
|
||||
do
|
||||
if [[ $i == --user-data-dir=* || $i == --write-elevated-helper ]]; then
|
||||
if [[ $i == --user-data-dir=* || $i == --sudo-write ]]; then
|
||||
CAN_LAUNCH_AS_ROOT=1
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function main(argv: string[]): TPromise<any> {
|
||||
}
|
||||
|
||||
// Write Elevated
|
||||
else if (args['write-elevated-helper']) {
|
||||
else if (args['sudo-write']) {
|
||||
const source = args._[0];
|
||||
const target = args._[1];
|
||||
|
||||
@@ -68,14 +68,31 @@ export async function main(argv: string[]): TPromise<any> {
|
||||
!fs.existsSync(source) || !fs.statSync(source).isFile() || // make sure source exists as file
|
||||
!fs.existsSync(target) || !fs.statSync(target).isFile() // make sure target exists as file
|
||||
) {
|
||||
return TPromise.wrapError(new Error('Using --write-elevated-helper with invalid arguments.'));
|
||||
return TPromise.wrapError(new Error('Using --sudo-write with invalid arguments.'));
|
||||
}
|
||||
|
||||
// Write source to target
|
||||
try {
|
||||
|
||||
// Check for readonly status and chmod if so if we are told so
|
||||
let targetMode: number;
|
||||
let restoreMode = false;
|
||||
if (!!args['sudo-chmod']) {
|
||||
targetMode = fs.statSync(target).mode;
|
||||
if (!(targetMode & 128) /* readonly */) {
|
||||
fs.chmodSync(target, targetMode | 128);
|
||||
restoreMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Write source to target
|
||||
writeFileAndFlushSync(target, fs.readFileSync(source));
|
||||
|
||||
// Restore previous mode as needed
|
||||
if (restoreMode) {
|
||||
fs.chmodSync(target, targetMode);
|
||||
}
|
||||
} catch (error) {
|
||||
return TPromise.wrapError(new Error(`Using --write-elevated-helper resulted in an error: ${error}`));
|
||||
return TPromise.wrapError(new Error(`Using --sudo-write resulted in an error: ${error}`));
|
||||
}
|
||||
|
||||
return TPromise.as(null);
|
||||
|
||||
@@ -52,7 +52,8 @@ export interface ParsedArgs {
|
||||
'disable-updates'?: string;
|
||||
'disable-crash-reporter'?: string;
|
||||
'skip-add-to-recently-opened'?: boolean;
|
||||
'write-elevated-helper'?: boolean;
|
||||
'sudo-write'?: boolean;
|
||||
'sudo-chmod'?: boolean;
|
||||
}
|
||||
|
||||
export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
|
||||
|
||||
@@ -55,7 +55,8 @@ const options: minimist.Opts = {
|
||||
'disable-crash-reporter',
|
||||
'skip-add-to-recently-opened',
|
||||
'status',
|
||||
'write-elevated-helper'
|
||||
'sudo-write',
|
||||
'sudo-chmod'
|
||||
],
|
||||
alias: {
|
||||
add: 'a',
|
||||
|
||||
@@ -562,7 +562,7 @@ export interface IImportResult {
|
||||
}
|
||||
|
||||
export class FileOperationError extends Error {
|
||||
constructor(message: string, public fileOperationResult: FileOperationResult) {
|
||||
constructor(message: string, public fileOperationResult: FileOperationResult, public options?: IResolveContentOptions & IUpdateContentOptions & ICreateFileOptions) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,10 +101,12 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi
|
||||
|
||||
public onSaveError(error: any, model: ITextFileEditorModel): void {
|
||||
let message: IMessageWithAction | string;
|
||||
|
||||
const fileOperationError = error as FileOperationError;
|
||||
const resource = model.getResource();
|
||||
|
||||
// Dirty write prevention
|
||||
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
|
||||
if (fileOperationError.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
|
||||
|
||||
// If the user tried to save from the opened conflict editor, show its message again
|
||||
// Otherwise show the message that will lead the user into the save conflict editor.
|
||||
@@ -117,21 +119,48 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi
|
||||
|
||||
// Any other save error
|
||||
else {
|
||||
const isReadonly = (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_READ_ONLY;
|
||||
const isPermissionDenied = (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED;
|
||||
const actions: Action[] = [];
|
||||
|
||||
const isReadonly = fileOperationError.fileOperationResult === FileOperationResult.FILE_READ_ONLY;
|
||||
const triedToMakeWriteable = isReadonly && fileOperationError.options && fileOperationError.options.overwriteReadonly;
|
||||
const isPermissionDenied = fileOperationError.fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED;
|
||||
|
||||
// Save Elevated
|
||||
if (isPermissionDenied) {
|
||||
actions.push(new Action('workbench.files.action.saveElevated', nls.localize('saveElevated', "Retry as Admin..."), null, true, () => {
|
||||
if (isPermissionDenied || triedToMakeWriteable) {
|
||||
actions.push(new Action('workbench.files.action.saveElevated', triedToMakeWriteable ? nls.localize('overwriteElevated', "Overwrite as Admin...") : nls.localize('saveElevated', "Retry as Admin..."), null, true, () => {
|
||||
if (!model.isDisposed()) {
|
||||
model.save({ writeElevated: true }).done(null, errors.onUnexpectedError);
|
||||
model.save({
|
||||
writeElevated: true,
|
||||
overwriteReadonly: triedToMakeWriteable
|
||||
}).done(null, errors.onUnexpectedError);
|
||||
}
|
||||
|
||||
return TPromise.as(true);
|
||||
}));
|
||||
}
|
||||
|
||||
// Overwrite
|
||||
else if (isReadonly) {
|
||||
actions.push(new Action('workbench.files.action.overwrite', nls.localize('overwrite', "Overwrite"), null, true, () => {
|
||||
if (!model.isDisposed()) {
|
||||
model.save({ overwriteReadonly: true }).done(null, errors.onUnexpectedError);
|
||||
}
|
||||
|
||||
return TPromise.as(true);
|
||||
}));
|
||||
}
|
||||
|
||||
// Retry
|
||||
else {
|
||||
actions.push(new Action('workbench.files.action.retry', nls.localize('retry', "Retry"), null, true, () => {
|
||||
const saveFileAction = this.instantiationService.createInstance(SaveFileAction, SaveFileAction.ID, SaveFileAction.LABEL);
|
||||
saveFileAction.setResource(resource);
|
||||
saveFileAction.run().done(() => saveFileAction.dispose(), errors.onUnexpectedError);
|
||||
|
||||
return TPromise.as(true);
|
||||
}));
|
||||
}
|
||||
|
||||
// Save As
|
||||
actions.push(new Action('workbench.files.action.saveAs', SaveFileAsAction.LABEL, null, true, () => {
|
||||
const saveAsAction = this.instantiationService.createInstance(SaveFileAsAction, SaveFileAsAction.ID, SaveFileAsAction.LABEL);
|
||||
@@ -150,31 +179,18 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi
|
||||
return TPromise.as(true);
|
||||
}));
|
||||
|
||||
// Retry
|
||||
if (isReadonly) {
|
||||
actions.push(new Action('workbench.files.action.overwrite', nls.localize('overwrite', "Overwrite"), null, true, () => {
|
||||
if (!model.isDisposed()) {
|
||||
model.save({ overwriteReadonly: true }).done(null, errors.onUnexpectedError);
|
||||
}
|
||||
|
||||
return TPromise.as(true);
|
||||
}));
|
||||
} else if (!isPermissionDenied) {
|
||||
actions.push(new Action('workbench.files.action.retry', nls.localize('retry', "Retry"), null, true, () => {
|
||||
const saveFileAction = this.instantiationService.createInstance(SaveFileAction, SaveFileAction.ID, SaveFileAction.LABEL);
|
||||
saveFileAction.setResource(resource);
|
||||
saveFileAction.run().done(() => saveFileAction.dispose(), errors.onUnexpectedError);
|
||||
|
||||
return TPromise.as(true);
|
||||
}));
|
||||
}
|
||||
|
||||
// Cancel
|
||||
actions.push(CancelAction);
|
||||
|
||||
let errorMessage: string;
|
||||
if (isReadonly) {
|
||||
errorMessage = nls.localize('readonlySaveError', "Failed to save '{0}': File is write protected. Select 'Overwrite' to attempt to remove protection.", paths.basename(resource.fsPath));
|
||||
if (triedToMakeWriteable) {
|
||||
errorMessage = nls.localize('readonlySaveErrorAdmin', "Failed to save '{0}': File is write protected. Select 'Overwrite as Admin' to retry as administrator.", paths.basename(resource.fsPath));
|
||||
} else {
|
||||
errorMessage = nls.localize('readonlySaveError', "Failed to save '{0}': File is write protected. Select 'Overwrite' to attempt to remove protection.", paths.basename(resource.fsPath));
|
||||
}
|
||||
} else if (isPermissionDenied) {
|
||||
errorMessage = nls.localize('permissionDeniedSaveError', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Admin' to retry as administrator.", paths.basename(resource.fsPath));
|
||||
} else {
|
||||
errorMessage = nls.localize('genericSaveError', "Failed to save '{0}': {1}", paths.basename(resource.fsPath), toErrorMessage(error, false));
|
||||
}
|
||||
|
||||
@@ -249,7 +249,8 @@ export class RemoteFileService extends FileService {
|
||||
if (options.acceptTextOnly && detected.mimes.indexOf(MIME_BINARY) >= 0) {
|
||||
return TPromise.wrapError<IStreamContent>(new FileOperationError(
|
||||
localize('fileBinaryError', "File seems to be binary and cannot be opened as text"),
|
||||
FileOperationResult.FILE_IS_BINARY
|
||||
FileOperationResult.FILE_IS_BINARY,
|
||||
options
|
||||
));
|
||||
}
|
||||
|
||||
@@ -324,7 +325,7 @@ export class RemoteFileService extends FileService {
|
||||
|
||||
return prepare.then(exists => {
|
||||
if (exists && options && !options.overwrite) {
|
||||
return TPromise.wrapError(new FileOperationError('EEXIST', FileOperationResult.FILE_MODIFIED_SINCE));
|
||||
return TPromise.wrapError(new FileOperationError('EEXIST', FileOperationResult.FILE_MODIFIED_SINCE, options));
|
||||
}
|
||||
return this._doUpdateContent(provider, resource, content || '', {});
|
||||
}).then(fileStat => {
|
||||
|
||||
@@ -262,7 +262,8 @@ export class FileService implements IFileService {
|
||||
if (resource.scheme !== 'file' || !resource.fsPath) {
|
||||
return TPromise.wrapError<IStreamContent>(new FileOperationError(
|
||||
nls.localize('fileInvalidPath', "Invalid file resource ({0})", resource.toString(true)),
|
||||
FileOperationResult.FILE_INVALID_PATH
|
||||
FileOperationResult.FILE_INVALID_PATH,
|
||||
options
|
||||
));
|
||||
}
|
||||
|
||||
@@ -298,7 +299,8 @@ export class FileService implements IFileService {
|
||||
if (stat.isDirectory) {
|
||||
return onStatError(new FileOperationError(
|
||||
nls.localize('fileIsDirectoryError', "File is directory"),
|
||||
FileOperationResult.FILE_IS_DIRECTORY
|
||||
FileOperationResult.FILE_IS_DIRECTORY,
|
||||
options
|
||||
));
|
||||
}
|
||||
|
||||
@@ -306,7 +308,8 @@ export class FileService implements IFileService {
|
||||
if (options && options.etag && options.etag === stat.etag) {
|
||||
return onStatError(new FileOperationError(
|
||||
nls.localize('fileNotModifiedError', "File not modified since"),
|
||||
FileOperationResult.FILE_NOT_MODIFIED_SINCE
|
||||
FileOperationResult.FILE_NOT_MODIFIED_SINCE,
|
||||
options
|
||||
));
|
||||
}
|
||||
|
||||
@@ -314,7 +317,8 @@ export class FileService implements IFileService {
|
||||
if (typeof stat.size === 'number' && stat.size > MAX_FILE_SIZE) {
|
||||
return onStatError(new FileOperationError(
|
||||
nls.localize('fileTooLargeError', "File too large to open"),
|
||||
FileOperationResult.FILE_TOO_LARGE
|
||||
FileOperationResult.FILE_TOO_LARGE,
|
||||
options
|
||||
));
|
||||
}
|
||||
|
||||
@@ -325,7 +329,8 @@ export class FileService implements IFileService {
|
||||
if (err.code === 'ENOENT') {
|
||||
return onStatError(new FileOperationError(
|
||||
nls.localize('fileNotFoundError', "File not found ({0})", resource.toString(true)),
|
||||
FileOperationResult.FILE_NOT_FOUND
|
||||
FileOperationResult.FILE_NOT_FOUND,
|
||||
options
|
||||
));
|
||||
}
|
||||
|
||||
@@ -374,7 +379,8 @@ export class FileService implements IFileService {
|
||||
// Wrap file not found errors
|
||||
err = new FileOperationError(
|
||||
nls.localize('fileNotFoundError', "File not found ({0})", resource.toString(true)),
|
||||
FileOperationResult.FILE_NOT_FOUND
|
||||
FileOperationResult.FILE_NOT_FOUND,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
@@ -391,7 +397,8 @@ export class FileService implements IFileService {
|
||||
// Wrap EISDIR errors (fs.open on a directory works, but you cannot read from it)
|
||||
err = new FileOperationError(
|
||||
nls.localize('fileIsDirectoryError', "File is directory"),
|
||||
FileOperationResult.FILE_IS_DIRECTORY
|
||||
FileOperationResult.FILE_IS_DIRECTORY,
|
||||
options
|
||||
);
|
||||
}
|
||||
if (decoder) {
|
||||
@@ -442,7 +449,8 @@ export class FileService implements IFileService {
|
||||
// stop when reading too much
|
||||
finish(new FileOperationError(
|
||||
nls.localize('fileTooLargeError', "File too large to open"),
|
||||
FileOperationResult.FILE_TOO_LARGE
|
||||
FileOperationResult.FILE_TOO_LARGE,
|
||||
options
|
||||
));
|
||||
} else if (err) {
|
||||
// some error happened
|
||||
@@ -464,7 +472,8 @@ export class FileService implements IFileService {
|
||||
// Return error early if client only accepts text and this is not text
|
||||
finish(new FileOperationError(
|
||||
nls.localize('fileBinaryError', "File seems to be binary and cannot be opened as text"),
|
||||
FileOperationResult.FILE_IS_BINARY
|
||||
FileOperationResult.FILE_IS_BINARY,
|
||||
options
|
||||
));
|
||||
|
||||
} else {
|
||||
@@ -553,7 +562,8 @@ export class FileService implements IFileService {
|
||||
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
||||
return TPromise.wrapError(new FileOperationError(
|
||||
nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)),
|
||||
FileOperationResult.FILE_PERMISSION_DENIED
|
||||
FileOperationResult.FILE_PERMISSION_DENIED,
|
||||
options
|
||||
));
|
||||
}
|
||||
|
||||
@@ -600,7 +610,14 @@ export class FileService implements IFileService {
|
||||
return (import('sudo-prompt')).then(sudoPrompt => {
|
||||
return new TPromise<void>((c, e) => {
|
||||
const promptOptions = { name: this.options.elevationSupport.promptTitle.replace('-', ''), icns: this.options.elevationSupport.promptIcnsPath };
|
||||
sudoPrompt.exec(`"${this.options.elevationSupport.cliPath}" --write-elevated-helper "${tmpPath}" "${absolutePath}"`, promptOptions, (error: string, stdout: string, stderr: string) => {
|
||||
|
||||
const sudoCommand: string[] = [`"${this.options.elevationSupport.cliPath}"`];
|
||||
if (options.overwriteReadonly) {
|
||||
sudoCommand.push('--sudo-chmod');
|
||||
}
|
||||
sudoCommand.push('--sudo-write', `"${tmpPath}"`, `"${absolutePath}"`);
|
||||
|
||||
sudoPrompt.exec(sudoCommand.join(' '), promptOptions, (error: string, stdout: string, stderr: string) => {
|
||||
if (error || stderr) {
|
||||
e(error || stderr);
|
||||
} else {
|
||||
@@ -622,7 +639,8 @@ export class FileService implements IFileService {
|
||||
if (!(error instanceof FileOperationError)) {
|
||||
error = new FileOperationError(
|
||||
nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)),
|
||||
FileOperationResult.FILE_PERMISSION_DENIED
|
||||
FileOperationResult.FILE_PERMISSION_DENIED,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
@@ -645,7 +663,8 @@ export class FileService implements IFileService {
|
||||
if (exists && !options.overwrite) {
|
||||
return TPromise.wrapError<IFileStat>(new FileOperationError(
|
||||
nls.localize('fileExists', "File to create already exists ({0})", resource.toString(true)),
|
||||
FileOperationResult.FILE_MODIFIED_SINCE
|
||||
FileOperationResult.FILE_MODIFIED_SINCE,
|
||||
options
|
||||
));
|
||||
}
|
||||
|
||||
@@ -914,14 +933,14 @@ export class FileService implements IFileService {
|
||||
|
||||
// Find out if content length has changed
|
||||
if (options.etag !== etag(stat.size, options.mtime)) {
|
||||
return TPromise.wrapError<boolean>(new FileOperationError(nls.localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE));
|
||||
return TPromise.wrapError<boolean>(new FileOperationError(nls.localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE, options));
|
||||
}
|
||||
}
|
||||
|
||||
// Throw if file is readonly and we are not instructed to overwrite
|
||||
if (!(stat.mode & 128) /* readonly */) {
|
||||
if (!options.overwriteReadonly) {
|
||||
return this.readOnlyError<boolean>();
|
||||
return this.readOnlyError<boolean>(options);
|
||||
}
|
||||
|
||||
// Try to change mode to writeable
|
||||
@@ -932,7 +951,7 @@ export class FileService implements IFileService {
|
||||
// Make sure to check the mode again, it could have failed
|
||||
return pfs.stat(absolutePath).then(stat => {
|
||||
if (!(stat.mode & 128) /* readonly */) {
|
||||
return this.readOnlyError<boolean>();
|
||||
return this.readOnlyError<boolean>(options);
|
||||
}
|
||||
|
||||
return exists;
|
||||
@@ -948,10 +967,11 @@ export class FileService implements IFileService {
|
||||
});
|
||||
}
|
||||
|
||||
private readOnlyError<T>(): TPromise<T> {
|
||||
private readOnlyError<T>(options: IUpdateContentOptions): TPromise<T> {
|
||||
return TPromise.wrapError<T>(new FileOperationError(
|
||||
nls.localize('fileReadOnlyError', "File is Read Only"),
|
||||
FileOperationResult.FILE_READ_ONLY
|
||||
FileOperationResult.FILE_READ_ONLY,
|
||||
options
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user