blob: 9bafa1147a4c82b737a33252e33763f323c078c6 [file] [log] [blame]
// Copyright (c) 2017 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
nassh.sftp.fsp = {};
/**
* @fileoverview: This file manages the File System Provider API requests and
* currently active SFTP instances. File system requests are
* handled by their respective SFTP clients.
*/
// Map of file system ids to their SFTP instances
nassh.sftp.fsp.sftpInstances = {};
// Maximum amount of bytes of data transferred in read/write data packets.
// SFTP v3 supports a max of 32768 bytes.
nassh.sftp.fsp.DATA_LIMIT = 32768;
/**
* Creates a new SFTP CommandInstance in the background page and connects to
* the provided connection. Utilizes the same hterm.Terminal.IO as the
* foreground page to authenticate the user before commencing SFTP communication.
*/
nassh.sftp.fsp.createSftpInstance = function(args) {
var sftpInstance = new nassh.CommandInstance(args.argv);
return sftpInstance.connectTo(args.connectOptions);
};
/**
* Sanitizes the provided file's metadata to the requirements specified in
* 'options'.
*/
nassh.sftp.fsp.sanitizeMetadata = function(file, options) {
var metadata = {};
if (options.name) {
if (file.filename) {
metadata.name = file.filename;
} else if (options.directoryPath) {
metadata.name = options.directoryPath.split('/').pop();
} else {
metadata.name = options.entryPath.split('/').pop();
}
}
if (options.isDirectory) {
metadata.isDirectory = (file.permissions & 0x4000) == 0x4000;
}
if (options.size) {
metadata.size = file.size;
}
if (options.modificationTime) {
metadata.modificationTime = new Date(file.last_modified*1000);
}
return metadata;
};
/**
* Metadata Requested handler. Retrieves the file attributes of the requested
* file path.
*/
nassh.sftp.fsp.onGetMetadataRequested = function(options, onSuccess, onError) {
if (!nassh.sftp.fsp.checkInstanceExists(options.fileSystemId, onError)) {
return;
}
var client = nassh.sftp.fsp.sftpInstances[options.fileSystemId].sftpClient;
var path = '.' + options.entryPath; // relative path
client.fileStatus(path)
.then(metadata => nassh.sftp.fsp.sanitizeMetadata(metadata, options))
.then(onSuccess)
.catch(response => {
// If file not found
if (response instanceof nassh.sftp.StatusError &&
response.code == nassh.sftp.packets.StatusCodes.NO_SUCH_FILE) {
onError('NOT_FOUND');
return;
}
console.warn(response.name + ': ' + response.message);
onError('FAILED');
});
};
/**
* Reads the remote directory handle and returns a list of metadata entries,
* which are sanitized if applicable.
*/
nassh.sftp.fsp.readDirectory = function(directoryHandle, client, sanitizeOptions) {
var metadataEntries = []; // directory entries
function nextRead() {
return client.readDirectory(directoryHandle)
.then(response => {
// If EOF, return all the directory entries
if (response instanceof nassh.sftp.packets.StatusPacket
&& response.code == nassh.sftp.packets.StatusCodes.EOF) {
return metadataEntries;
}
for(var i = 0; i < response.fileCount; i++) {
var file = response.files[i];
// Skip over the file if it's a '.', '..' or link file
if (file.filename == '.' || file.filename == '..' ||
file.permissions & 0x2000) {
continue;
}
if (sanitizeOptions) {
file = nassh.sftp.fsp.sanitizeMetadata(file, sanitizeOptions);
}
metadataEntries.push(file);
}
return nextRead();
});
}
return nextRead();
};
/**
* Read Directory Requested handler. Retrieves the entries of the requested
* directory.
*/
nassh.sftp.fsp.onReadDirectoryRequested = function(options, onSuccess, onError) {
if (!nassh.sftp.fsp.checkInstanceExists(options.fileSystemId, onError)) {
return;
}
var client = nassh.sftp.fsp.sftpInstances[options.fileSystemId].sftpClient;
var directoryHandle;
var path = '.' + options.directoryPath; // relative path
client.openDirectory(path)
.then(handle => { directoryHandle = handle; })
.then(() => nassh.sftp.fsp.readDirectory(directoryHandle, client, options))
.then(entries => { onSuccess(entries, false); })
.catch(response => {
console.warn(response.name + ': ' + response.message);
onError('FAILED');
})
.then(() => client.closeFile(directoryHandle));
};
/**
* Write File Requested handler. Writes to the requested file.
*/
nassh.sftp.fsp.onWriteFileRequested = function(options, onSuccess, onError) {
if (!nassh.sftp.fsp.checkInstanceExists(options.fileSystemId, onError)) {
return;
}
var client = nassh.sftp.fsp.sftpInstances[options.fileSystemId].sftpClient;
var fileHandle = client.openedFiles[options.openRequestId];
if (!fileHandle) {
onError('INVALID_OPERATION');
return;
}
var writePromises = [];
// Splits up the data to be written into nassh.sftp.fsp.DATA_LIMIT sized chunks
// and places them into multiple promises which will be resolved asynchronously.
for (var i = 0; i < options.data.byteLength; i += nassh.sftp.fsp.DATA_LIMIT) {
var endSlice = i + nassh.sftp.fsp.DATA_LIMIT;
var array = new Uint8Array(options.data.slice(i, endSlice));
var dataChunk = String.fromCharCode.apply(null, array);
var offset = options.offset + i;
var writePromise = client.writeFile(fileHandle, offset, dataChunk);
writePromises.push(writePromise);
}
Promise.all(writePromises)
.then(onSuccess)
.catch(response => {
console.warn(response.name + ': ' + response.message);
onError('FAILED');
});
};
/**
* Open File Requested handler. Opens the requested file and saves its handle.
*/
nassh.sftp.fsp.onOpenFileRequested = function(options, onSuccess, onError) {
if (!nassh.sftp.fsp.checkInstanceExists(options.fileSystemId, onError)) {
return;
}
var client = nassh.sftp.fsp.sftpInstances[options.fileSystemId].sftpClient;
var pflags = 0;
if (options.mode == "READ") {
pflags |= nassh.sftp.packets.OpenFlags.READ;
} else if (options.mode == "WRITE") {
pflags |= nassh.sftp.packets.OpenFlags.WRITE;
}
var path = '.' + options.filePath; // relative path
client.openFile(path, pflags)
.then(handle => { client.openedFiles[options.requestId] = handle; })
.then(onSuccess)
.catch(response => {
console.warn(response.name + ': ' + response.message);
onError('FAILED');
});
};
/**
* Create File Requested handler. Creates a file at the requested file path and
* saves its handle.
*/
nassh.sftp.fsp.onCreateFileRequested = function(options, onSuccess, onError) {
if (!nassh.sftp.fsp.checkInstanceExists(options.fileSystemId, onError)) {
return;
}
var client = nassh.sftp.fsp.sftpInstances[options.fileSystemId].sftpClient;
var pflags = nassh.sftp.packets.OpenFlags.CREAT |
nassh.sftp.packets.OpenFlags.EXCL;
var path = '.' + options.filePath; // relative path
client.openFile(path, pflags)
.then(handle => { client.openedFiles[options.requestId] = handle; })
.then(onSuccess)
.catch(response => {
console.warn(response.name + ': ' + response.message);
onError('FAILED');
});
};
/**
* Truncate File Requested handler. Truncates the requested file path to 0
* length and then closes it.
*/
nassh.sftp.fsp.onTruncateRequested = function(options, onSuccess, onError) {
if (!nassh.sftp.fsp.checkInstanceExists(options.fileSystemId, onError)) {
return;
}
var client = nassh.sftp.fsp.sftpInstances[options.fileSystemId].sftpClient;
var pflags = nassh.sftp.packets.OpenFlags.CREAT |
nassh.sftp.packets.OpenFlags.TRUNC;
var path = '.' + options.filePath; // relative path
client.openFile(path, pflags)
.then(handle => { client.closeFile(handle); })
.then(onSuccess)
.catch(response => {
console.warn(response.name + ': ' + response.message);
onError('FAILED');
});
};
/**
* Delete Entry Requested handler. Deletes the entry of the requested file path.
* If the entry is a directory, deletes all sub-entries recursively.
*/
nassh.sftp.fsp.onDeleteEntryRequested = function(options, onSuccess, onError) {
if (!nassh.sftp.fsp.checkInstanceExists(options.fileSystemId, onError)) {
return;
}
var client = nassh.sftp.fsp.sftpInstances[options.fileSystemId].sftpClient;
var path = '.' + options.entryPath; // relative path
if (options.recursive) {
nassh.sftp.fsp.removeDirectory(path, client)
.then(onSuccess)
.catch(response => {
// If file not found
if (response instanceof nassh.sftp.StatusError &&
response.code == nassh.sftp.packets.StatusCodes.NO_SUCH_FILE) {
onError('NOT_FOUND');
return;
}
console.warn(response.name + ': ' + response.message);
onError('FAILED');
});
} else {
client.removeFile(path)
.then(onSuccess)
.catch(response => {
// If file not found
if (response instanceof nassh.sftp.StatusError &&
response.code == nassh.sftp.packets.StatusCodes.NO_SUCH_FILE) {
onError('NOT_FOUND');
return;
}
console.warn(response.name + ': ' + response.message);
onError('FAILED');
});
}
};
/**
* Reads the remote directory and removes all of its entries before removing
* itself.
*/
nassh.sftp.fsp.removeDirectory = function(path, client) {
var directoryHandle;
return client.openDirectory(path)
.then(handle => { directoryHandle = handle; })
.then(() => readDirectory(directoryHandle, client))
.then(entries => {
var removePromises = [];
// Recursively delete contents
for(var i = 0; i < entries.length; i++) {
var file = entries[i];
var filename = file.filename;
if (filename == '.' || filename == '..') {
continue;
}
filename = path + '/' + filename;
if (file.permissions & 0x4000) {
removePromises.push(removeDirectory(filename, client));
} else {
removePromises.push(client.removeFile(filename));
}
}
return Promise.all(removePromises);
})
.then(() => client.closeFile(directoryHandle))
.then(() => client.removeDirectory(path));
};
/**
* Close File Requested handler. Closes the requested file and removes its
* handle.
*/
nassh.sftp.fsp.onCloseFileRequested = function(options, onSuccess, onError) {
if (!nassh.sftp.fsp.checkInstanceExists(options.fileSystemId, onError)) {
return;
}
var client = nassh.sftp.fsp.sftpInstances[options.fileSystemId].sftpClient;
if (!client.openedFiles[options.openRequestId]) {
console.warn('File handle not found');
onError('INVALID_OPERATION');
return;
}
client.closeFile(client.openedFiles[options.openRequestId])
.then(() => { delete client.openedFiles[options.openRequestId]; })
.then(onSuccess)
.catch(response => {
console.warn(response.name + ': ' + response.message);
onError('FAILED');
});
};
/**
* Create Directory Requested handler. Creates a directory at the requested
* file path.
*/
nassh.sftp.fsp.onCreateDirectoryRequested = function(options, onSuccess, onError) {
if (!nassh.sftp.fsp.checkInstanceExists(options.fileSystemId, onError)) {
return;
}
var client = nassh.sftp.fsp.sftpInstances[options.fileSystemId].sftpClient;
if (options.recursive) { // Not supported/implemented.
onError('INVALID_OPERATION');
return;
}
var path = '.' + options.directoryPath; // relative path
client.makeDirectory(path)
.then(onSuccess)
.catch(response => {
console.warn(response.name + ': ' + response.message);
onError('FAILED');
});
};
/**
* Move Requested handler. Moves (renames) the requested source file path to the
* target file path.
*/
nassh.sftp.fsp.onMoveEntryRequested = function(options, onSuccess, onError) {
if (!nassh.sftp.fsp.checkInstanceExists(options.fileSystemId, onError)) {
return;
}
var client = nassh.sftp.fsp.sftpInstances[options.fileSystemId].sftpClient;
var sourcePath = '.' + options.sourcePath; // relative path
var targetPath = '.' + options.targetPath; // relative path
client.renameFile(sourcePath, targetPath)
.then(onSuccess)
.catch(response => {
console.warn(response.name + ': ' + response.message);
onError('FAILED');
});
};
/**
* Read File Requested handler. Reads the requested file and returns its data.
*/
nassh.sftp.fsp.onReadFileRequested = function(options, onSuccess, onError) {
if (!nassh.sftp.fsp.checkInstanceExists(options.fileSystemId, onError)) {
return;
}
var client = nassh.sftp.fsp.sftpInstances[options.fileSystemId].sftpClient;
var fileHandle = client.openedFiles[options.openRequestId];
if (!fileHandle) {
onError('INVALID_OPERATION');
return;
}
var readPromises = [];
var readLimit = options.offset + options.length;
// Splits up the data to be read into nassh.sftp.fsp.DATA_LIMIT sized chunks
// and places them into multiple promises which will be resolved asynchronously.
for (var i = options.offset; i < readLimit; i += nassh.sftp.fsp.DATA_LIMIT) {
readPromises.push(client.readFile(fileHandle, i, nassh.sftp.fsp.DATA_LIMIT));
}
Promise.all(readPromises)
.then(dataChunks => {
// join all resolved data chunks together and return them as an ArrayBuffer
var data = dataChunks.join('');
var array = new Uint8Array(data.length);
for (var i = 0; i < array.length; i++) {
array[i] = data.charCodeAt(i);
}
return array.buffer;
})
.then(data => { onSuccess(data, false); } )
.catch(response => {
console.warn(response.name + ': ' + response.message);
onError('FAILED');
});
};
/**
* Copy Entry Requested handler. Copies the entry of the requested file path.
* If the entry is a directory, copies all sub-entries recursively.
*/
nassh.sftp.fsp.onCopyEntryRequested = function(options, onSuccess, onError) {
if (!nassh.sftp.fsp.checkInstanceExists(options.fileSystemId, onError)) {
return;
}
var client = nassh.sftp.fsp.sftpInstances[options.fileSystemId].sftpClient;
var sourcePath = '.' + options.sourcePath; // relative path
var targetPath = '.' + options.targetPath; // relative path
client.fileStatus(sourcePath)
.then(metadata => {
// if the file has the 0x4000 permission flag set, it's a directory
if (metadata.permissions & 0x4000) {
return nassh.sftp.fsp.copyDirectory(sourcePath, targetPath, client);
} else {
return nassh.sftp.fsp.copyFile(sourcePath, targetPath, metadata.size, client);
}
})
.then(onSuccess)
.catch(response => {
console.warn(response.name + ': ' + response.message);
onError('FAILED');
});
};
/**
* Copies the file at the remote source path to the remote target path.
*/
nassh.sftp.fsp.copyFile = function(sourcePath, targetPath, size, client) {
var sourceHandle;
var targetHandle;
return client.openFile(sourcePath)
.then(handle => {
sourceHandle = handle;
var pflags = nassh.sftp.packets.OpenFlags.WRITE |
nassh.sftp.packets.OpenFlags.APPEND |
nassh.sftp.packets.OpenFlags.CREAT |
nassh.sftp.packets.OpenFlags.EXCL;
return client.openFile(targetPath, pflags);
})
.then(handle => {
targetHandle = handle;
var readWritePromises = [];
// Splits up the data to be read and written into nassh.sftp.fsp.DATA_LIMIT
// sized chunks and places them into multiple promises which will be
// resolved asynchronously.
for (var i = 0; i < size; i += nassh.sftp.fsp.DATA_LIMIT) {
var offset = i;
var readWritePromise = client.readFile(sourceHandle,
offset, nassh.sftp.fsp.DATA_LIMIT)
.then(data => client.writeFile(targetHandle, offset, data));
readWritePromises.push(readWritePromise);
}
return Promise.all(readWritePromises);
})
.then(() => client.closeFile(sourceHandle))
.then(() => client.closeFile(targetHandle));
};
/**
* Reads the remote directory and copies all of its entries before copying
* itself.
*/
nassh.sftp.fsp.copyDirectory = function(sourcePath, targetPath, client) {
var sourceHandle;
return client.openDirectory(sourcePath)
.then(handle => { sourceHandle = handle; })
.then(() => client.makeDirectory(targetPath))
.then(() => readDirectory(sourceHandle, client))
.then(entries => {
var copyPromises = [];
for(var i = 0; i < entries.length; i++) {
var file = entries[i];
if (file.filename == '.' || file.filename == '..') {
continue;
}
var fileSourcePath = sourcePath + '/' + file.filename;
var fileTargetPath = targetPath + '/' + file.filename;
if (file.permissions & 0x4000) {
copyPromises.push(nassh.sftp.fsp.copyDirectory(fileSourcePath,
fileTargetPath, client));
} else {
copyPromises.push(nassh.sftp.fsp.copyFile(fileSourcePath, fileTargetPath,
file.size, client));
}
}
return Promise.all(copyPromises);
})
.then(() => client.closeFile(sourceHandle));
};
/**
* Mount Requested handler. Opens the Secure Shell main page which prompts the
* user to mount a remote connection.
*
* Note: This is only called upon first installation of Secure Shell and when
* the user clicks "Add New Service" from the File App.
*/
nassh.sftp.fsp.onMountRequested = function(options, onSuccess, onError) {
window.open('html/nassh.html');
};
/**
* Unmount Requested handler. Closes the SFTP connection and removes the SFTP
* instance before unmounting the file system.
*/
nassh.sftp.fsp.onUnmountRequested = function(options, onSuccess, onError) {
// We don't return immediately on errors. If the caller is trying to unmount
// us, then usually it means they think we're mounted even if we don't think
// we are. This can happen if the Secure Shell background page is killed, but
// the Files app remembers all the connections. Either way, it's more robust
// for us to always unmount with the FSP layer.
if (nassh.sftp.fsp.checkInstanceExists(options.fileSystemId, onError)) {
// Only clear local state if we know about the mount.
var sftpInstance = nassh.sftp.fsp.sftpInstances[options.fileSystemId];
sftpInstance.exit(0); // exit NaCl plugin
delete nassh.sftp.fsp.sftpInstances[options.fileSystemId];
}
chrome.fileSystemProvider.unmount(
{fileSystemId: options.fileSystemId}, () => {
if (chrome.runtime.lastError) {
console.warn(chrome.runtime.lastError.message);
onError('FAILED');
} else {
onSuccess();
if (chrome.runtime.lastError) {} //HACK: catches silly lastError (bug?)
//TODO(mcdermottm): talk to mtomasz@
}
});
};
/**
* Checks if the file system id has an associated SFTP instance.
*/
nassh.sftp.fsp.checkInstanceExists = function(fsId, onError) {
if (!nassh.sftp.fsp.sftpInstances[fsId]) {
console.warn('SFTP Instance for file system id "' + fsId + '" not found!');
onError('FAILED');
return false;
}
return true;
};
/**
* Object containing all supported File System Provider API methods.
*/
nassh.sftp.fsp.providerMethods = [
'onGetMetadataRequested',
'onReadDirectoryRequested',
'onOpenFileRequested',
'onCloseFileRequested',
'onReadFileRequested',
'onMountRequested',
'onUnmountRequested',
'onMoveEntryRequested',
'onDeleteEntryRequested',
'onWriteFileRequested',
'onCreateFileRequested',
'onTruncateRequested',
'onCopyEntryRequested',
'onCreateDirectoryRequested'
];
// Loop over the provider methods and link them to their handlers.
if (chrome.fileSystemProvider) {
nassh.sftp.fsp.providerMethods.forEach(function(provider) {
chrome.fileSystemProvider[provider].addListener(nassh.sftp.fsp[provider]);
});
}