Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
173 changes: 163 additions & 10 deletions src/storage/client_side_storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,57 @@ export async function openClientSideStorage(): Promise<commonStorage.Storage> {
};
openRequest.onsuccess = () => {
const db = openRequest.result;
resolve(ClientSideStorage.create(db));
fixOldFiles(db).then(() => {
resolve(ClientSideStorage.create(db));
})
};
});
}

// The following function allows Alan and Liz to load older projects.
// TODO(lizlooney): Remove this function.
async function fixOldFiles(db: IDBDatabase): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = db.transaction([FILES_STORE_NAME], 'readwrite');
transaction.oncomplete = () => {
resolve();
};
transaction.onabort = () => {
console.log('IndexedDB transaction aborted.');
reject(new Error('IndexedDB transaction aborted.'));
};
const filesObjectStore = transaction.objectStore(FILES_STORE_NAME);
const openCursorRequest = filesObjectStore.openCursor();
openCursorRequest.onerror = () => {
console.log('IndexedDB openCursor request failed. openCursorRequest.error is...');
console.log(openCursorRequest.error);
reject(new Error('IndexedDB openCursor request failed.'));
};
openCursorRequest.onsuccess = () => {
const cursor = openCursorRequest.result;
if (cursor) {
const value = cursor.value;
if (!value.path.startsWith('/projects/')) {
const oldFilePath = value.path;
value.path = '/projects/' + value.path;
const putRequest = filesObjectStore.put(value);
putRequest.onerror = () => {
console.log('IndexedDB put request failed. putRequest.error is...');
console.log(putRequest.error);
throw new Error('IndexedDB put request failed.');
};
const deleteRequest = filesObjectStore.delete(oldFilePath);
deleteRequest.onerror = () => {
console.log('IndexedDB delete request failed. deleteRequest.error is...');
console.log(deleteRequest.error);
throw new Error('IndexedDB delete request failed.');
};
}
cursor.continue();
} else {
// The cursor is done. We have finished reading all the files.
resolve();
}
};
});
}
Expand Down Expand Up @@ -124,13 +174,12 @@ class ClientSideStorage implements commonStorage.Storage {
});
}

async listFilePaths(opt_filePathRegexPattern?: string): Promise<string[]> {

const regExp = opt_filePathRegexPattern
? new RegExp(opt_filePathRegexPattern)
: null;
async list(path: string): Promise<string[]> {
if (!path.endsWith('/')) {
path += '/';
}
return new Promise((resolve, reject) => {
const filePaths: string[] = [];
const resultsSet: Set<string> = new Set();
const openKeyCursorRequest = this.db.transaction([FILES_STORE_NAME], 'readonly')
.objectStore(FILES_STORE_NAME)
.openKeyCursor();
Expand All @@ -143,14 +192,118 @@ class ClientSideStorage implements commonStorage.Storage {
const cursor = openKeyCursorRequest.result;
if (cursor && cursor.key) {
const filePath: string = cursor.key as string;
if (!regExp || regExp.test(filePath)) {
filePaths.push(filePath);
if (filePath.startsWith(path)) {
const relativePath = filePath.substring(path.length);
const slash = relativePath.indexOf('/');
const result = (slash != -1)
? relativePath.substring(0, slash + 1) // Include the trailing slash.
: relativePath;
resultsSet.add(result);
}
cursor.continue();
} else {
// The cursor is done. We have finished reading all the files.
resolve([...resultsSet]);
}
};
});
}

async rename(oldPath: string, newPath: string): Promise<void> {
if (oldPath.endsWith('/')) {
return this.renameDirectory(oldPath, newPath);
}
return this.renameFile(oldPath, newPath);
}

async renameDirectory(oldPath: string, newPath: string): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([FILES_STORE_NAME], 'readwrite');
transaction.oncomplete = () => {
resolve();
};
transaction.onabort = () => {
console.log('IndexedDB transaction aborted.');
reject(new Error('IndexedDB transaction aborted.'));
};
const filesObjectStore = transaction.objectStore(FILES_STORE_NAME);
const openCursorRequest = filesObjectStore.openCursor();
openCursorRequest.onerror = () => {
console.log('IndexedDB openCursor request failed. openCursorRequest.error is...');
console.log(openCursorRequest.error);
throw new Error('IndexedDB openCursor request failed.');
};
openCursorRequest.onsuccess = () => {
const cursor = openCursorRequest.result;
if (cursor) {
const value = cursor.value;
if (value.path.startsWith(oldPath)) {
const relativePath = value.path.substring(oldPath.length);
const oldFilePath = value.path;
value.path = newPath + relativePath;
const putRequest = filesObjectStore.put(value);
putRequest.onerror = () => {
console.log('IndexedDB put request failed. putRequest.error is...');
console.log(putRequest.error);
throw new Error('IndexedDB put request failed.');
};
putRequest.onsuccess = () => {
const deleteRequest = filesObjectStore.delete(oldFilePath);
deleteRequest.onerror = () => {
console.log('IndexedDB delete request failed. deleteRequest.error is...');
console.log(deleteRequest.error);
throw new Error('IndexedDB delete request failed.');
};
}
}
cursor.continue();
} else {
// The cursor is done. We have finished reading all the files.
resolve(filePaths);
resolve();
}
};
});
}

async renameFile(oldPath: string, newPath: string): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([FILES_STORE_NAME], 'readwrite');
transaction.oncomplete = () => {
resolve();
};
transaction.onabort = () => {
console.log('IndexedDB transaction aborted.');
reject(new Error('IndexedDB transaction aborted.'));
};
const filesObjectStore = transaction.objectStore(FILES_STORE_NAME);
const getRequest = filesObjectStore.get(oldPath);
getRequest.onerror = () => {
console.log('IndexedDB get request failed. getRequest.error is...');
console.log(getRequest.error);
throw new Error('IndexedDB get request failed.');
};
getRequest.onsuccess = () => {
if (getRequest.result === undefined) {
console.log('IndexedDB get request succeeded, but the file does not exist.');
throw new Error('IndexedDB get request succeeded, but the file does not exist.');
return;
}
const value = getRequest.result;
value.path = newPath;
const putRequest = filesObjectStore.put(value);
putRequest.onerror = () => {
console.log('IndexedDB put request failed. putRequest.error is...');
console.log(putRequest.error);
throw new Error('IndexedDB put request failed.');
};
putRequest.onsuccess = () => {
const deleteRequest = filesObjectStore.delete(oldPath);
deleteRequest.onerror = () => {
console.log('IndexedDB delete request failed. deleteRequest.error is...');
console.log(deleteRequest.error);
throw new Error('IndexedDB delete request failed.');
};
};
};
});
}
Expand Down
4 changes: 3 additions & 1 deletion src/storage/common_storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ export interface Storage {

// Functions for storing files.

listFilePaths(opt_filePathRegexPattern?: string): Promise<string[]>;
list(path: string): Promise<string[]>;

rename(oldPath: string, newPath: string): Promise<void>;

fetchFileContentText(filePath: string): Promise<string>;

Expand Down
62 changes: 21 additions & 41 deletions src/storage/names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import * as storageModule from './module';
/**
* Paths and file names for Blocks Projects
*
* Files in a project are stored in a directory whose name is the project name. All files have
* All projects are stored in a directory called '/projects/'.
*
* Files in a project are stored in a subdirectory whose name is the project name. All files have
* the extension '.json' and contain JSON text.
*
* Project information is stored in a file called 'project.info.json'.
Expand All @@ -43,8 +45,8 @@ import * as storageModule from './module';
* zero or more mechanisms, with the extension '.mechanism.json'
* zero or more opmodes, with the extension '.opmode.json'
*
* The file path of the project info file is <ProjectName>/project.info.json.
* The file path of a module is <ProjectName>/<ClassName>.<ModuleType>.json.
* The file path of the project info file is /projects/<ProjectName>/project.info.json.
* The file path of a module is /projects/<ProjectName>/<ClassName>.<ModuleType>.json.
*/

// The class name of the Robot module that is created automatically when a new project is created.
Expand All @@ -54,17 +56,14 @@ export const CLASS_NAME_ROBOT = 'Robot';
export const CLASS_NAME_TELEOP = 'Teleop';

// The extension of all JSON files is .json.
export const JSON_FILE_EXTENSION = '.json';
const JSON_FILE_EXTENSION = '.json';

// The extension of a downloaded project is .blocks.
export const UPLOAD_DOWNLOAD_FILE_EXTENSION = '.blocks';

// The file name of the project info file.
const PROJECT_INFO_FILE_NAME = 'project.info.json';

// The file name of the project info file.
const ROBOT_MODULE_FILE_NAME = 'Robot.robot.json';

// A project name starts with an uppercase letter, followed by alphanumeric characters.
const REGEX_PROJECT_NAME_PART = '[A-Z][A-Za-z0-9]*';

Expand All @@ -77,15 +76,17 @@ const REGEX_CLASS_NAME = '^' + REGEX_CLASS_NAME_PART + '$'
// The module type of a module path is either .robot, .mechanism, or .opmode.
const REGEX_MODULE_TYPE_PART = '\.(robot|mechanism|opmode)';

// This regex is used to match the robot module path in any project.
export const REGEX_ROBOT_MODULE_PATH = '^' + REGEX_PROJECT_NAME_PART + '/' + escapeRegExp(ROBOT_MODULE_FILE_NAME) + '$';
export const PROJECTS_DIRECTORY_PATH = '/projects/';

// This regex is used to extract the project name and file name from a file path.
const REGEX_FILE_PATH = '^(' + REGEX_PROJECT_NAME_PART + ')/(.*' + escapeRegExp(JSON_FILE_EXTENSION) + ')$';
// This regex is used to extract the project name from a file path.
const REGEX_FILE_PATH = '^' + escapeRegExp(PROJECTS_DIRECTORY_PATH) +
'(' + REGEX_PROJECT_NAME_PART + ')/.*' + escapeRegExp(JSON_FILE_EXTENSION) + '$';

// This regex is used to extract the class name from a module path.
const REGEX_MODULE_PATH = '^' + REGEX_PROJECT_NAME_PART + '/(' + REGEX_CLASS_NAME_PART + ')' +
REGEX_MODULE_TYPE_PART + escapeRegExp(JSON_FILE_EXTENSION) + '$';
const REGEX_MODULE_PATH = '^' + escapeRegExp(PROJECTS_DIRECTORY_PATH) +
REGEX_PROJECT_NAME_PART + '/' +
'(' + REGEX_CLASS_NAME_PART + ')' + REGEX_MODULE_TYPE_PART + escapeRegExp(JSON_FILE_EXTENSION) +
'$';

// This regex is used to extract the class name from a module file name.
const REGEX_MODULE_FILE_NAME = '^(' + REGEX_CLASS_NAME_PART + ')' +
Expand Down Expand Up @@ -134,33 +135,24 @@ export function snakeCaseToPascalCase(snakeCaseName: string): string {
}

/**
* Returns a regex pattern that matches all file paths in the given project.
*/
export function makeFilePathRegexPattern(projectName: string): string {
return '^' + escapeRegExp(projectName) + '/' +
'.*' + escapeRegExp(JSON_FILE_EXTENSION) + '$';
}

/**
* Returns a regex pattern that matches all module paths in the given project.
* Escapes the given text so it can be used literally in a regular expression.
*/
export function makeModulePathRegexPattern(projectName: string): string {
return '^' + escapeRegExp(projectName) + '/' +
REGEX_CLASS_NAME_PART + REGEX_MODULE_TYPE_PART + escapeRegExp(JSON_FILE_EXTENSION) + '$';
function escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/**
* Escapes the given text so it can be used literally in a regular expression.
* Returns the project directory path for the given project name.
*/
function escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
export function makeProjectDirectoryPath(projectName: string): string {
return PROJECTS_DIRECTORY_PATH + projectName + '/';
}

/**
* Returns the file path for the given project name and file name.
*/
export function makeFilePath(projectName: string, fileName: string): string {
return projectName + '/' + fileName;
return makeProjectDirectoryPath(projectName) + fileName;
}

/**
Expand Down Expand Up @@ -197,18 +189,6 @@ export function getProjectName(filePath: string): string {
return result[1];
}

/**
* Returns the file name for given file path.
*/
export function getFileName(filePath: string): string {
const regex = new RegExp(REGEX_FILE_PATH);
const result = regex.exec(filePath)
if (!result) {
throw new Error('Unable to extract the file name from "' + filePath + '"');
}
return result[2];
}

/**
* Returns true if the given file name is a valid module file name.
*/
Expand Down
Loading