Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track changed files and only download files that need to be updated #43

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 54 additions & 15 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@ import {
sanitizedDateString,
setContext,
toastStatusBarMessage,
verifyFileHeader
verifyFileHeader,
localPathToRemote,
WorkspaceChangeTracker,
FileUpdateInfo
} from './utils';

let output: vscode.OutputChannel;
let resourceDir: string;
let helperExePath: string;
let ev3devBrowserProvider: Ev3devBrowserProvider;
let changeTracker: WorkspaceChangeTracker = null;

// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
Expand Down Expand Up @@ -74,6 +78,13 @@ async function pickDevice(): Promise<void> {
try {
await device.connect();
toastStatusBarMessage(`Connected`);

device.onDidDisconnect(() => {
if (changeTracker) {
changeTracker.dispose();
}
changeTracker = null;
});
}
catch (err) {
vscode.window.showErrorMessage(`Failed to connect to ${device.name}: ${err.message}`);
Expand Down Expand Up @@ -173,48 +184,76 @@ async function download(): Promise<boolean> {
title: 'Sending'
}, async progress => {
try {
const files = await vscode.workspace.findFiles(includeFiles, excludeFiles);
let fileIndex = 1;
let fileUpdates: FileUpdateInfo;
if(changeTracker) {
fileUpdates = changeTracker.getFileUpdatesAndReset();
}
else {
// If we lack tracking info, initialize the tracker and assume all files are "created" and must be re-deployed
changeTracker = new WorkspaceChangeTracker();
const uris = await vscode.workspace.findFiles(includeFiles, excludeFiles);
const allFiles = uris.map(uri => uri.fsPath);
fileUpdates = { created: allFiles, updated: [], deleted: [] };
}

const reportProgress = (message: string) => progress.report({ message: message });
const totalChangeCount = fileUpdates.created.length + fileUpdates.updated.length + fileUpdates.deleted.length;

if(totalChangeCount <= 0) {
toastStatusBarMessage("Download complete; there was nothing to do!");
success = true;
return;
}

for (const f of files) {
const baseProgressMessage = `(${fileIndex}/${files.length}) ${f.fsPath}`;
let currentChangeCount = 1;

for (const filePath of [ ...fileUpdates.created, ...fileUpdates.updated ]) {
const baseProgressMessage = `(${currentChangeCount}/${Object.keys(fileUpdates).length}) ${filePath}`;
reportProgress(baseProgressMessage);

const basename = path.basename(f.fsPath);
const relativeDir = path.dirname(vscode.workspace.asRelativePath(f.fsPath));
const remoteDir = path.posix.join(remoteBaseDir, relativeDir);
const remotePath = path.posix.resolve(remoteDir, basename);
const remoteInfo = localPathToRemote(filePath, remoteBaseDir);

// File permission handling:
// - If the file starts with a shebang, then assume it should be
// executable.
// - Otherwise use the existing file permissions. On Windows
// all files will be executable.
let mode: string = undefined;
if (await verifyFileHeader(f.fsPath, new Buffer('#!/'))) {
if (await verifyFileHeader(filePath, new Buffer('#!/'))) {
mode = '755';
}
else {
const stat = fs.statSync(f.fsPath);
const stat = fs.statSync(filePath);
mode = stat.mode.toString(8);
}

// make sure the directory exists
await device.mkdir_p(remoteDir);
await device.mkdir_p(remoteInfo.remoteDir);
// then we can copy the file
await device.put(f.fsPath, remotePath, mode,
await device.put(filePath, remoteInfo.remotePath, mode,
percentage => reportProgress(`${baseProgressMessage} - ${percentage}%`));

fileIndex++;
currentChangeCount++;
}

for (const filePath of fileUpdates.deleted) {
reportProgress(`Deleting ${filePath}...`);
const remoteInfo = localPathToRemote(filePath, remoteBaseDir);

await device.rm(remoteInfo.remotePath);
}

// make sure any new files show up in the browser
ev3devBrowserProvider.fireDeviceChanged();
success = true;
vscode.window.setStatusBarMessage(`Done sending project to ${device.name}.`, 5000);
toastStatusBarMessage(`Done sending project to ${device.name}.`);
}
catch (err) {
vscode.window.showErrorMessage(`Error sending file: ${err.message}`);
if(changeTracker) {
changeTracker.dispose();
}
changeTracker = null;
}
});

Expand Down
71 changes: 71 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as vscode from 'vscode';
import * as temp from 'temp';
import * as fs from 'fs';
import * as path from 'path';
import { isArray } from 'util';

const toastDuration = 5000;
Expand Down Expand Up @@ -74,3 +75,73 @@ export function toastStatusBarMessage(message: string): void {
export function setContext(context: string, state: boolean): void {
vscode.commands.executeCommand('setContext', context, state);
}

export function localPathToRemote(localPath: string, remoteBaseDir: string): { remoteDir: string, remotePath: string } {
const basename = path.basename(localPath);
const relativeDir = path.dirname(vscode.workspace.asRelativePath(localPath));
const remoteDir = path.posix.join(remoteBaseDir, relativeDir);
const remotePath = path.posix.resolve(remoteDir, basename);

return { remoteDir: remoteDir, remotePath: remotePath };
}

export type FileUpdateInfo = { created: string[], updated: string[], deleted: string[] };

export class WorkspaceChangeTracker {
private watcher: vscode.FileSystemWatcher;
private fileUpdates = { created: new Set<string>(), updated: new Set<string>(), deleted: new Set<string>() };

constructor() {
this.watcher = vscode.workspace.createFileSystemWatcher("**");

this.watcher.onDidCreate(uri => {
const filePath = uri.fsPath;
if (!fs.statSync(filePath).isFile()) {
return;
}

this.fileUpdates.deleted.delete(filePath);
this.fileUpdates.created.add(filePath);
});

this.watcher.onDidChange(uri => {
const filePath = uri.fsPath;
if (!fs.statSync(filePath).isFile()) {
return;
}
if (!this.fileUpdates.created.has(filePath) && !this.fileUpdates.deleted.has(filePath)) {
this.fileUpdates.updated.add(filePath);
}
});

this.watcher.onDidDelete(uri => {
const filePath = uri.fsPath;

if (!this.fileUpdates.created.delete(filePath)) {
this.fileUpdates.updated.delete(filePath);
this.fileUpdates.deleted.add(filePath);
}
});
}

public reset() {
this.fileUpdates.created.clear();
this.fileUpdates.updated.clear();
this.fileUpdates.deleted.clear();
}

public getFileUpdatesAndReset(): FileUpdateInfo {
const updateInfo = {
created: Array.from(this.fileUpdates.created.values()),
updated: Array.from(this.fileUpdates.updated.values()),
deleted: Array.from(this.fileUpdates.deleted.values())
};
this.reset();

return updateInfo;
}

public dispose() {
this.watcher.dispose()
}
}