From 40ca5ec9ac63c1a6fac98c7f3bac92d2170d8a4e Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Fri, 29 Aug 2025 22:25:35 -0700 Subject: [PATCH 01/12] Make all projects stored under "/projects/". --- src/storage/client_side_storage.ts | 52 +++++++++++++++++++++++++++++- src/storage/names.ts | 20 ++++++------ 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/storage/client_side_storage.ts b/src/storage/client_side_storage.ts index e61ef120..9d0d8024 100644 --- a/src/storage/client_side_storage.ts +++ b/src/storage/client_side_storage.ts @@ -56,7 +56,57 @@ export async function openClientSideStorage(): Promise { }; 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 { + 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(); + } }; }); } diff --git a/src/storage/names.ts b/src/storage/names.ts index 04d5c1c4..ec37fa7c 100644 --- a/src/storage/names.ts +++ b/src/storage/names.ts @@ -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'. @@ -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 /project.info.json. - * The file path of a module is /..json. + * The file path of the project info file is /projects//project.info.json. + * The file path of a module is /projects//..json. */ // The class name of the Robot module that is created automatically when a new project is created. @@ -78,13 +80,13 @@ const REGEX_CLASS_NAME = '^' + REGEX_CLASS_NAME_PART + '$' 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 REGEX_ROBOT_MODULE_PATH = '^/projects/' + REGEX_PROJECT_NAME_PART + '/' + escapeRegExp(ROBOT_MODULE_FILE_NAME) + '$'; // 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) + ')$'; +const REGEX_FILE_PATH = '^/projects/(' + 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 + ')' + +const REGEX_MODULE_PATH = '^/projects/' + 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. @@ -137,7 +139,7 @@ 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) + '/' + + return '^/projects/' + escapeRegExp(projectName) + '/' + '.*' + escapeRegExp(JSON_FILE_EXTENSION) + '$'; } @@ -145,7 +147,7 @@ export function makeFilePathRegexPattern(projectName: string): string { * Returns a regex pattern that matches all module paths in the given project. */ export function makeModulePathRegexPattern(projectName: string): string { - return '^' + escapeRegExp(projectName) + '/' + + return '^/projects/' + escapeRegExp(projectName) + '/' + REGEX_CLASS_NAME_PART + REGEX_MODULE_TYPE_PART + escapeRegExp(JSON_FILE_EXTENSION) + '$'; } @@ -160,7 +162,7 @@ function escapeRegExp(text: string): string { * Returns the file path for the given project name and file name. */ export function makeFilePath(projectName: string, fileName: string): string { - return projectName + '/' + fileName; + return '/projects/' + projectName + '/' + fileName; } /** From 61a8fcb75c15180ba7591aa9b55ea42b08781365 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Fri, 29 Aug 2025 22:30:02 -0700 Subject: [PATCH 02/12] Added list function to Storage interface. --- src/storage/client_side_storage.ts | 36 ++++++++++++++++++++++++++++++ src/storage/common_storage.ts | 3 +++ 2 files changed, 39 insertions(+) diff --git a/src/storage/client_side_storage.ts b/src/storage/client_side_storage.ts index 9d0d8024..20e7336c 100644 --- a/src/storage/client_side_storage.ts +++ b/src/storage/client_side_storage.ts @@ -174,6 +174,42 @@ class ClientSideStorage implements commonStorage.Storage { }); } + async list(path: string): Promise { + if (!path.endsWith('/')) { + path += '/'; + } + return new Promise((resolve, reject) => { + const results: string[] = []; + const openKeyCursorRequest = this.db.transaction([FILES_STORE_NAME], 'readonly') + .objectStore(FILES_STORE_NAME) + .openKeyCursor(); + openKeyCursorRequest.onerror = () => { + console.log('IndexedDB openKeyCursor request failed. openKeyCursorRequest.error is...'); + console.log(openKeyCursorRequest.error); + reject(new Error('IndexedDB openKeyCursor request failed.')); + }; + openKeyCursorRequest.onsuccess = () => { + const cursor = openKeyCursorRequest.result; + if (cursor && cursor.key) { + const filePath: string = cursor.key as string; + 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; + results.push(result); + } + cursor.continue(); + } else { + // The cursor is done. We have finished reading all the files. + resolve(results); + } + }; + }); + } + + // TODO(lizlooney): remove listFilePaths async listFilePaths(opt_filePathRegexPattern?: string): Promise { const regExp = opt_filePathRegexPattern diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index 96b70e4e..fbd7699c 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -28,6 +28,9 @@ export interface Storage { // Functions for storing files. + list(path: string): Promise; + + // TODO(lizlooney): remove listFilePaths listFilePaths(opt_filePathRegexPattern?: string): Promise; fetchFileContentText(filePath: string): Promise; From c52879e725d6a561103c145197949b70da0cdd87 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Fri, 29 Aug 2025 22:54:45 -0700 Subject: [PATCH 03/12] Added rename function to Storage interface. --- src/storage/client_side_storage.ts | 99 ++++++++++++++++++++++++++++++ src/storage/common_storage.ts | 2 + 2 files changed, 101 insertions(+) diff --git a/src/storage/client_side_storage.ts b/src/storage/client_side_storage.ts index 20e7336c..e1c5b55c 100644 --- a/src/storage/client_side_storage.ts +++ b/src/storage/client_side_storage.ts @@ -241,6 +241,105 @@ class ClientSideStorage implements commonStorage.Storage { }); } + async rename(oldPath: string, newPath: string): Promise { + if (oldPath.endsWith('/')) { + return this.renameDirectory(oldPath, newPath); + } + return this.renameFile(oldPath, newPath); + } + + async renameDirectory(oldPath: string, newPath: string): Promise { + 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(); + } + }; + }); + } + + async renameFile(oldPath: string, newPath: string): Promise { + 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.'); + }; + }; + }; + }); + } + async fetchFileContentText(filePath: string): Promise { return new Promise((resolve, reject) => { const getRequest = this.db.transaction([FILES_STORE_NAME], 'readonly') diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index fbd7699c..407a94d5 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -33,6 +33,8 @@ export interface Storage { // TODO(lizlooney): remove listFilePaths listFilePaths(opt_filePathRegexPattern?: string): Promise; + rename(oldPath: string, newPath: string): Promise; + fetchFileContentText(filePath: string): Promise; saveFile(filePath: string, fileContentText: string): Promise; From d6699cba55bf89a34d92f352cf7024c1b3e90395 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Fri, 29 Aug 2025 22:55:05 -0700 Subject: [PATCH 04/12] In project.ts, change renameProject and renameModuleInProject to use storage.rename. --- src/storage/names.ts | 9 +++- src/storage/project.ts | 120 ++++++++++++++++++----------------------- 2 files changed, 60 insertions(+), 69 deletions(-) diff --git a/src/storage/names.ts b/src/storage/names.ts index ec37fa7c..aec899e4 100644 --- a/src/storage/names.ts +++ b/src/storage/names.ts @@ -158,11 +158,18 @@ function escapeRegExp(text: string): string { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } +/** + * Returns the project directory path for the given project name. + */ +export function makeProjectDirectoryPath(projectName: string): string { + return '/projects/' + projectName + '/'; +} + /** * Returns the file path for the given project name and file name. */ export function makeFilePath(projectName: string, fileName: string): string { - return '/projects/' + projectName + '/' + fileName; + return makeProjectDirectoryPath(projectName) + fileName; } /** diff --git a/src/storage/project.ts b/src/storage/project.ts index 1fc22afc..95934ca7 100644 --- a/src/storage/project.ts +++ b/src/storage/project.ts @@ -141,7 +141,9 @@ export async function createProject( */ export async function renameProject( storage: commonStorage.Storage, projectName: string, newProjectName: string): Promise { - await renameOrCopyProject(storage, projectName, newProjectName, true); + const oldPath = storageNames.makeProjectDirectoryPath(projectName); + const newPath = storageNames.makeProjectDirectoryPath(newProjectName); + await storage.rename(oldPath, newPath); } /** @@ -153,12 +155,6 @@ export async function renameProject( */ export async function copyProject( storage: commonStorage.Storage, projectName: string, newProjectName: string): Promise { - await renameOrCopyProject(storage, projectName, newProjectName, false); -} - -async function renameOrCopyProject( - storage: commonStorage.Storage, projectName: string, newProjectName: string, - rename: boolean): Promise { const modulePaths: string[] = await storage.listFilePaths( storageNames.makeModulePathRegexPattern(projectName)); @@ -168,14 +164,8 @@ async function renameOrCopyProject( const newModulePath = storageNames.makeModulePath(newProjectName, className, moduleType); const moduleContentText = await storage.fetchFileContentText(modulePath); await storage.saveFile(newModulePath, moduleContentText); - if (rename) { - await storage.deleteFile(modulePath); - } } await saveProjectInfo(storage, newProjectName); - if (rename) { - await deleteProjectInfo(storage, projectName); - } } /** @@ -269,14 +259,36 @@ export async function removeModuleFromProject( */ export async function renameModuleInProject( storage: commonStorage.Storage, project: Project, newClassName: string, oldModulePath: string): Promise { - const module = findModuleByModulePath(project, oldModulePath); - if (!module) { + const oldModule = findModuleByModulePath(project, oldModulePath); + if (!oldModule) { throw new Error('Failed to find module with path ' + oldModulePath); } - if (module.moduleType == storageModule.ModuleType.ROBOT) { + if (oldModule.moduleType == storageModule.ModuleType.ROBOT) { throw new Error('Renaming the robot module is not allowed.'); } - return await renameOrCopyModule(storage, project, newClassName, module, true); + const newModulePath = storageNames.makeModulePath(project.projectName, newClassName, oldModule.moduleType); + await storage.rename(oldModulePath, newModulePath); + + // Update the project's mechanisms or opModes. + switch (oldModule.moduleType) { + case storageModule.ModuleType.MECHANISM: + const mechanism = project.mechanisms.find(m => m.modulePath === oldModule.modulePath); + if (mechanism) { + mechanism.modulePath = newModulePath; + mechanism.className = newClassName; + } + break; + case storageModule.ModuleType.OPMODE: + const opMode = project.opModes.find(o => o.modulePath === oldModule.modulePath); + if (opMode) { + opMode.modulePath = newModulePath; + opMode.className = newClassName; + } + break; + } + await saveProjectInfo(storage, project.projectName); + + return newModulePath; } /** @@ -289,65 +301,37 @@ export async function renameModuleInProject( */ export async function copyModuleInProject( storage: commonStorage.Storage, project: Project, newClassName: string, oldModulePath: string): Promise { - const module = findModuleByModulePath(project, oldModulePath); - if (!module) { + const oldModule = findModuleByModulePath(project, oldModulePath); + if (!oldModule) { throw new Error('Failed to find module with path ' + oldModulePath); } - if (module.moduleType == storageModule.ModuleType.ROBOT) { + if (oldModule.moduleType == storageModule.ModuleType.ROBOT) { throw new Error('Copying the robot module is not allowed.'); } - return await renameOrCopyModule(storage, project, newClassName, module, false); -} - -async function renameOrCopyModule( - storage: commonStorage.Storage, project: Project, newClassName: string, - oldModule: storageModule.Module, rename: boolean): Promise { const newModulePath = storageNames.makeModulePath(project.projectName, newClassName, oldModule.moduleType); + + // Change the ids in the module. let moduleContentText = await storage.fetchFileContentText(oldModule.modulePath); - if (!rename) { - // Change the ids in the module. - const moduleContent = storageModuleContent.parseModuleContentText(moduleContentText); - moduleContent.changeIds(); - moduleContentText = moduleContent.getModuleContentText(); - } + const moduleContent = storageModuleContent.parseModuleContentText(moduleContentText); + moduleContent.changeIds(); + moduleContentText = moduleContent.getModuleContentText(); + await storage.saveFile(newModulePath, moduleContentText); - if (rename) { - // For rename, delete the old module. - await storage.deleteFile(oldModule.modulePath); - // Update the project's mechanisms or opModes. - switch (oldModule.moduleType) { - case storageModule.ModuleType.MECHANISM: - const mechanism = project.mechanisms.find(m => m.modulePath === oldModule.modulePath); - if (mechanism) { - mechanism.modulePath = newModulePath; - mechanism.className = newClassName; - } - break; - case storageModule.ModuleType.OPMODE: - const opMode = project.opModes.find(o => o.modulePath === oldModule.modulePath); - if (opMode) { - opMode.modulePath = newModulePath; - opMode.className = newClassName; - } - break; - } - } else { // copy - // Update the project's mechanisms or opModes. - const newModule = { - modulePath: newModulePath, - moduleType: oldModule.moduleType, - projectName: project.projectName, - className: newClassName - }; - switch (oldModule.moduleType) { - case storageModule.ModuleType.MECHANISM: - project.mechanisms.push(newModule as storageModule.Mechanism); - break; - case storageModule.ModuleType.OPMODE: - project.opModes.push(newModule as storageModule.OpMode); - break; - } + // Update the project's mechanisms or opModes. + const newModule = { + modulePath: newModulePath, + moduleType: oldModule.moduleType, + projectName: project.projectName, + className: newClassName + }; + switch (oldModule.moduleType) { + case storageModule.ModuleType.MECHANISM: + project.mechanisms.push(newModule as storageModule.Mechanism); + break; + case storageModule.ModuleType.OPMODE: + project.opModes.push(newModule as storageModule.OpMode); + break; } await saveProjectInfo(storage, project.projectName); From 5df623e800ad21c2504356cfa2354619525dcb00 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Fri, 29 Aug 2025 23:23:13 -0700 Subject: [PATCH 05/12] Fixed client_side_storage list function implementation so it doesn't return duplicates. --- src/storage/client_side_storage.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/storage/client_side_storage.ts b/src/storage/client_side_storage.ts index e1c5b55c..ec248f04 100644 --- a/src/storage/client_side_storage.ts +++ b/src/storage/client_side_storage.ts @@ -179,7 +179,7 @@ class ClientSideStorage implements commonStorage.Storage { path += '/'; } return new Promise((resolve, reject) => { - const results: string[] = []; + const resultsSet: Set = new Set(); const openKeyCursorRequest = this.db.transaction([FILES_STORE_NAME], 'readonly') .objectStore(FILES_STORE_NAME) .openKeyCursor(); @@ -198,12 +198,12 @@ class ClientSideStorage implements commonStorage.Storage { const result = (slash != -1) ? relativePath.substring(0, slash + 1) // Include the trailing slash. : relativePath; - results.push(result); + resultsSet.add(result); } cursor.continue(); } else { // The cursor is done. We have finished reading all the files. - resolve(results); + resolve([...resultsSet]); } }; }); From 068e174758b9d40aa8cb2b7ecf94e7e9854e2475 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Fri, 29 Aug 2025 23:26:05 -0700 Subject: [PATCH 06/12] Modified listProjectNames to use storage.list instead of storage.listFilePaths. --- src/storage/names.ts | 16 ++++++++++------ src/storage/project.ts | 14 +++++++++----- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/storage/names.ts b/src/storage/names.ts index aec899e4..17bb17d6 100644 --- a/src/storage/names.ts +++ b/src/storage/names.ts @@ -79,15 +79,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 = '^/projects/' + 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 = '^/projects/(' + REGEX_PROJECT_NAME_PART + ')/(.*' + escapeRegExp(JSON_FILE_EXTENSION) + ')$'; +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 = '^/projects/' + 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 + ')' + @@ -138,6 +140,7 @@ export function snakeCaseToPascalCase(snakeCaseName: string): string { /** * Returns a regex pattern that matches all file paths in the given project. */ +// TODO(lizlooney): remove this. export function makeFilePathRegexPattern(projectName: string): string { return '^/projects/' + escapeRegExp(projectName) + '/' + '.*' + escapeRegExp(JSON_FILE_EXTENSION) + '$'; @@ -146,6 +149,7 @@ export function makeFilePathRegexPattern(projectName: string): string { /** * Returns a regex pattern that matches all module paths in the given project. */ +// TODO(lizlooney): remove this. export function makeModulePathRegexPattern(projectName: string): string { return '^/projects/' + escapeRegExp(projectName) + '/' + REGEX_CLASS_NAME_PART + REGEX_MODULE_TYPE_PART + escapeRegExp(JSON_FILE_EXTENSION) + '$'; @@ -162,7 +166,7 @@ function escapeRegExp(text: string): string { * Returns the project directory path for the given project name. */ export function makeProjectDirectoryPath(projectName: string): string { - return '/projects/' + projectName + '/'; + return PROJECTS_DIRECTORY_PATH + projectName + '/'; } /** diff --git a/src/storage/project.ts b/src/storage/project.ts index 95934ca7..4c926c3b 100644 --- a/src/storage/project.ts +++ b/src/storage/project.ts @@ -32,7 +32,7 @@ import { upgradeProjectIfNecessary } from './upgrade_project'; export type Project = { projectName: string, // For example, WackyWheelerRobot robot: storageModule.Robot, - mechanisms: storageModule.Mechanism[] + mechanisms: storageModule.Mechanism[], opModes: storageModule.OpMode[], }; @@ -47,12 +47,16 @@ type ProjectInfo = { * Returns the list of project names. */ export async function listProjectNames(storage: commonStorage.Storage): Promise { - const filePathRegexPattern = storageNames.REGEX_ROBOT_MODULE_PATH; - const robotModulePaths: string[] = await storage.listFilePaths(filePathRegexPattern); + const projectDirectoryNames: string[] = await storage.list(storageNames.PROJECTS_DIRECTORY_PATH); const projectNames: string[] = []; - for (const robotModulePath of robotModulePaths) { - projectNames.push(storageNames.getProjectName(robotModulePath)) + for (const projectDirectoryName of projectDirectoryNames) { + if (projectDirectoryName.endsWith('/')) { + // TODO(lizlooney): Should we check that the Robot.robot.json and project.info.json files + // exist in the directory? + const projectName = projectDirectoryName.slice(0, projectDirectoryName.length - 1); + projectNames.push(projectName); + } } return projectNames; } From 6df919e1a5c00a918bf63d81b9677f91f420357c Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Fri, 29 Aug 2025 23:33:33 -0700 Subject: [PATCH 07/12] Updated fetchProject to use storage.list instead of storage.listFilePaths. --- src/storage/project.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/storage/project.ts b/src/storage/project.ts index 4c926c3b..3cb1e6cd 100644 --- a/src/storage/project.ts +++ b/src/storage/project.ts @@ -68,14 +68,18 @@ export async function fetchProject( storage: commonStorage.Storage, projectName: string): Promise { await upgradeProjectIfNecessary(storage, projectName); - const modulePaths: string[] = await storage.listFilePaths( - storageNames.makeModulePathRegexPattern(projectName)); + const projectFileNames: string[] = await storage.list( + storageNames.makeProjectDirectoryPath(projectName)); let project: Project | null = null; - const mechanisms: storageModule.Mechanism[] = [] + const mechanisms: storageModule.Mechanism[] = []; const opModes: storageModule.OpMode[] = []; - for (const modulePath of modulePaths) { + for (const projectFileName of projectFileNames) { + if (!storageNames.isValidModuleFileName(projectFileName)) { + continue; + } + const modulePath = storageNames.makeFilePath(projectName, projectFileName); const moduleContentText = await storage.fetchFileContentText(modulePath); const moduleContent: storageModuleContent.ModuleContent = storageModuleContent.parseModuleContentText(moduleContentText); From a48688d4025ea96d480187755345f50e8864c2c8 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Fri, 29 Aug 2025 23:51:02 -0700 Subject: [PATCH 08/12] Updated copyProject to use storage.list instead of storage.listFilePaths. --- src/storage/project.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/storage/project.ts b/src/storage/project.ts index 3cb1e6cd..6ea1b218 100644 --- a/src/storage/project.ts +++ b/src/storage/project.ts @@ -163,17 +163,15 @@ export async function renameProject( */ export async function copyProject( storage: commonStorage.Storage, projectName: string, newProjectName: string): Promise { - const modulePaths: string[] = await storage.listFilePaths( - storageNames.makeModulePathRegexPattern(projectName)); + const projectFileNames: string[] = await storage.list( + storageNames.makeProjectDirectoryPath(projectName)); - for (const modulePath of modulePaths) { - const className = storageNames.getClassName(modulePath); - const moduleType = storageNames.getModuleType(modulePath); - const newModulePath = storageNames.makeModulePath(newProjectName, className, moduleType); - const moduleContentText = await storage.fetchFileContentText(modulePath); - await storage.saveFile(newModulePath, moduleContentText); + for (const projectFileName of projectFileNames) { + const filePath = storageNames.makeFilePath(projectName, projectFileName); + const newFilePath = storageNames.makeFilePath(newProjectName, projectFileName); + const fileContentText = await storage.fetchFileContentText(filePath); + await storage.saveFile(newFilePath, fileContentText); } - await saveProjectInfo(storage, newProjectName); } /** From b0a2b2f67ca4aa5df57427c2240efc8b058887e4 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Fri, 29 Aug 2025 23:54:06 -0700 Subject: [PATCH 09/12] Updated deleteProject to use storage.list instead of storage.listFilePaths. Removed deleteProjectInfo. (No longer used.) --- src/storage/names.ts | 9 --------- src/storage/project.ts | 16 +++++----------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/storage/names.ts b/src/storage/names.ts index 17bb17d6..864c9895 100644 --- a/src/storage/names.ts +++ b/src/storage/names.ts @@ -146,15 +146,6 @@ export function makeFilePathRegexPattern(projectName: string): string { '.*' + escapeRegExp(JSON_FILE_EXTENSION) + '$'; } -/** - * Returns a regex pattern that matches all module paths in the given project. - */ -// TODO(lizlooney): remove this. -export function makeModulePathRegexPattern(projectName: string): string { - return '^/projects/' + escapeRegExp(projectName) + '/' + - REGEX_CLASS_NAME_PART + REGEX_MODULE_TYPE_PART + escapeRegExp(JSON_FILE_EXTENSION) + '$'; -} - /** * Escapes the given text so it can be used literally in a regular expression. */ diff --git a/src/storage/project.ts b/src/storage/project.ts index 6ea1b218..661c4c70 100644 --- a/src/storage/project.ts +++ b/src/storage/project.ts @@ -182,13 +182,13 @@ export async function copyProject( */ export async function deleteProject( storage: commonStorage.Storage, projectName: string): Promise { - const modulePaths: string[] = await storage.listFilePaths( - storageNames.makeModulePathRegexPattern(projectName)); + const projectFileNames: string[] = await storage.list( + storageNames.makeProjectDirectoryPath(projectName)); - for (const modulePath of modulePaths) { - await storage.deleteFile(modulePath); + for (const projectFileName of projectFileNames) { + const filePath = storageNames.makeFilePath(projectName, projectFileName); + await storage.deleteFile(filePath); } - await deleteProjectInfo(storage, projectName); } /** @@ -529,12 +529,6 @@ function parseProjectInfoContentText(projectInfoContentText: string): ProjectInf return projectInfo; } -async function deleteProjectInfo( - storage: commonStorage.Storage, projectName: string): Promise { - const projectInfoPath = storageNames.makeProjectInfoPath(projectName); - await storage.deleteFile(projectInfoPath); -} - export async function fetchProjectInfo( storage: commonStorage.Storage, projectName: string): Promise { const projectInfoPath = storageNames.makeProjectInfoPath(projectName); From 2866631879a46e3531e35120b917ce1fbc3137d8 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Sat, 30 Aug 2025 00:02:03 -0700 Subject: [PATCH 10/12] Updated downloadProject to use storage.list instead of storage.listFilePaths. --- src/storage/names.ts | 9 --------- src/storage/project.ts | 8 ++++---- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/storage/names.ts b/src/storage/names.ts index 864c9895..007426a7 100644 --- a/src/storage/names.ts +++ b/src/storage/names.ts @@ -137,15 +137,6 @@ export function snakeCaseToPascalCase(snakeCaseName: string): string { return pascalCaseName; } -/** - * Returns a regex pattern that matches all file paths in the given project. - */ -// TODO(lizlooney): remove this. -export function makeFilePathRegexPattern(projectName: string): string { - return '^/projects/' + escapeRegExp(projectName) + '/' + - '.*' + escapeRegExp(JSON_FILE_EXTENSION) + '$'; -} - /** * Escapes the given text so it can be used literally in a regular expression. */ diff --git a/src/storage/project.ts b/src/storage/project.ts index 661c4c70..0ea4909e 100644 --- a/src/storage/project.ts +++ b/src/storage/project.ts @@ -413,12 +413,12 @@ export function findModuleByModulePath(project: Project, modulePath: string): st */ export async function downloadProject( storage: commonStorage.Storage, projectName: string): Promise { - const filePaths: string[] = await storage.listFilePaths( - storageNames.makeFilePathRegexPattern(projectName)); + const fileNames: string[] = await storage.list( + storageNames.makeProjectDirectoryPath(projectName)); const fileNameToFileContentText: {[fileName: string]: string} = {}; // value is file content text - for (const filePath of filePaths) { - const fileName = storageNames.getFileName(filePath); + for (const fileName of fileNames) { + const filePath = storageNames.makeFilePath(projectName, fileName); const fileContentText = await storage.fetchFileContentText(filePath); fileNameToFileContentText[fileName] = fileContentText; } From f511e85d272fc17177303197ce7d0a348bc139f4 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Sat, 30 Aug 2025 00:13:15 -0700 Subject: [PATCH 11/12] Removed listFilePaths from storage interface. Removed unused constants and functions in names.ts. --- src/storage/client_side_storage.ts | 32 ------------------------------ src/storage/common_storage.ts | 3 --- src/storage/names.ts | 21 +++----------------- 3 files changed, 3 insertions(+), 53 deletions(-) diff --git a/src/storage/client_side_storage.ts b/src/storage/client_side_storage.ts index ec248f04..2c5ad1c8 100644 --- a/src/storage/client_side_storage.ts +++ b/src/storage/client_side_storage.ts @@ -209,38 +209,6 @@ class ClientSideStorage implements commonStorage.Storage { }); } - // TODO(lizlooney): remove listFilePaths - async listFilePaths(opt_filePathRegexPattern?: string): Promise { - - const regExp = opt_filePathRegexPattern - ? new RegExp(opt_filePathRegexPattern) - : null; - return new Promise((resolve, reject) => { - const filePaths: string[] = []; - const openKeyCursorRequest = this.db.transaction([FILES_STORE_NAME], 'readonly') - .objectStore(FILES_STORE_NAME) - .openKeyCursor(); - openKeyCursorRequest.onerror = () => { - console.log('IndexedDB openKeyCursor request failed. openKeyCursorRequest.error is...'); - console.log(openKeyCursorRequest.error); - reject(new Error('IndexedDB openKeyCursor request failed.')); - }; - openKeyCursorRequest.onsuccess = () => { - const cursor = openKeyCursorRequest.result; - if (cursor && cursor.key) { - const filePath: string = cursor.key as string; - if (!regExp || regExp.test(filePath)) { - filePaths.push(filePath); - } - cursor.continue(); - } else { - // The cursor is done. We have finished reading all the files. - resolve(filePaths); - } - }; - }); - } - async rename(oldPath: string, newPath: string): Promise { if (oldPath.endsWith('/')) { return this.renameDirectory(oldPath, newPath); diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index 407a94d5..7e797659 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -30,9 +30,6 @@ export interface Storage { list(path: string): Promise; - // TODO(lizlooney): remove listFilePaths - listFilePaths(opt_filePathRegexPattern?: string): Promise; - rename(oldPath: string, newPath: string): Promise; fetchFileContentText(filePath: string): Promise; diff --git a/src/storage/names.ts b/src/storage/names.ts index 007426a7..e53e6ea7 100644 --- a/src/storage/names.ts +++ b/src/storage/names.ts @@ -56,7 +56,7 @@ 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'; @@ -64,9 +64,6 @@ 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]*'; @@ -81,9 +78,9 @@ const REGEX_MODULE_TYPE_PART = '\.(robot|mechanism|opmode)'; export const PROJECTS_DIRECTORY_PATH = '/projects/'; -// This regex is used to extract the project name and file name from a file path. +// 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) + ')$'; + '(' + 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 = '^' + escapeRegExp(PROJECTS_DIRECTORY_PATH) + @@ -192,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. */ From 8e7e54ab66f59b67c2987b702ab4a1f065b20826 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Sat, 30 Aug 2025 19:02:24 -0700 Subject: [PATCH 12/12] In storage API, rename deleteFile to delete. Changed client_side_storage implementation of delete to work for directories and files. Update project to call storage.delete for a whole project and for a single module. --- src/storage/client_side_storage.ts | 51 ++++++++++++++++++++++++++++-- src/storage/common_storage.ts | 2 +- src/storage/project.ts | 10 ++---- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/storage/client_side_storage.ts b/src/storage/client_side_storage.ts index 2c5ad1c8..8382f4c1 100644 --- a/src/storage/client_side_storage.ts +++ b/src/storage/client_side_storage.ts @@ -216,7 +216,7 @@ class ClientSideStorage implements commonStorage.Storage { return this.renameFile(oldPath, newPath); } - async renameDirectory(oldPath: string, newPath: string): Promise { + private async renameDirectory(oldPath: string, newPath: string): Promise { return new Promise((resolve, reject) => { const transaction = this.db.transaction([FILES_STORE_NAME], 'readwrite'); transaction.oncomplete = () => { @@ -265,7 +265,7 @@ class ClientSideStorage implements commonStorage.Storage { }); } - async renameFile(oldPath: string, newPath: string): Promise { + private async renameFile(oldPath: string, newPath: string): Promise { return new Promise((resolve, reject) => { const transaction = this.db.transaction([FILES_STORE_NAME], 'readwrite'); transaction.oncomplete = () => { @@ -366,7 +366,52 @@ class ClientSideStorage implements commonStorage.Storage { }); } - async deleteFile(filePath: string): Promise { + async delete(path: string): Promise { + if (path.endsWith('/')) { + return this.deleteDirectory(path); + } + return this.deleteFile(path); + } + + private async deleteDirectory(path: string): Promise { + 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 openKeyCursorRequest = filesObjectStore.openKeyCursor(); + openKeyCursorRequest.onerror = () => { + console.log('IndexedDB openKeyCursor request failed. openKeyCursorRequest.error is...'); + console.log(openKeyCursorRequest.error); + throw new Error('IndexedDB openKeyCursor request failed.'); + }; + openKeyCursorRequest.onsuccess = () => { + const cursor = openKeyCursorRequest.result; + if (cursor && cursor.key) { + const filePath: string = cursor.key as string; + if (filePath.startsWith(path)) { + const deleteRequest = filesObjectStore.delete(filePath); + 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(); + } + }; + }); + } + + private async deleteFile(filePath: string): Promise { return new Promise((resolve, reject) => { const transaction = this.db.transaction([FILES_STORE_NAME], 'readwrite'); transaction.oncomplete = () => { diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index 7e797659..13d3dd09 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -36,5 +36,5 @@ export interface Storage { saveFile(filePath: string, fileContentText: string): Promise; - deleteFile(filePath: string): Promise; + delete(path: string): Promise; } diff --git a/src/storage/project.ts b/src/storage/project.ts index 0ea4909e..7292fbd1 100644 --- a/src/storage/project.ts +++ b/src/storage/project.ts @@ -182,13 +182,7 @@ export async function copyProject( */ export async function deleteProject( storage: commonStorage.Storage, projectName: string): Promise { - const projectFileNames: string[] = await storage.list( - storageNames.makeProjectDirectoryPath(projectName)); - - for (const projectFileName of projectFileNames) { - const filePath = storageNames.makeFilePath(projectName, projectFileName); - await storage.deleteFile(filePath); - } + await storage.delete(storageNames.makeProjectDirectoryPath(projectName)); } /** @@ -250,7 +244,7 @@ export async function removeModuleFromProject( project.opModes = project.opModes.filter(o => o.modulePath !== modulePath); break; } - await storage.deleteFile(modulePath); + await storage.delete(modulePath); await saveProjectInfo(storage, project.projectName); } }