提交 4e9cd88e 编写于 作者: B Benjamin Pasero

save as admin - handle case for readonly files

上级 8f18e874
......@@ -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,
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({ writeElevated: true }).done(null, errors.onUnexpectedError);
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
));
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册