Skip to content

Commit 16d762d

Browse files
authored
Updated storage interface (#214)
* Make all projects stored under "/projects/". * Added list function to Storage interface. * Added rename function to Storage interface. * In project.ts, change renameProject and renameModuleInProject to use storage.rename. * Fixed client_side_storage list function implementation so it doesn't return duplicates. * Modified listProjectNames to use storage.list instead of storage.listFilePaths. * Updated fetchProject to use storage.list instead of storage.listFilePaths. * Updated copyProject to use storage.list instead of storage.listFilePaths. * Updated deleteProject to use storage.list instead of storage.listFilePaths. Removed deleteProjectInfo. (No longer used.) * Updated downloadProject to use storage.list instead of storage.listFilePaths. * Removed listFilePaths from storage interface. Removed unused constants and functions in names.ts. * 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.
1 parent b4ecdf0 commit 16d762d

File tree

4 files changed

+316
-158
lines changed

4 files changed

+316
-158
lines changed

src/storage/client_side_storage.ts

Lines changed: 209 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,57 @@ export async function openClientSideStorage(): Promise<commonStorage.Storage> {
5656
};
5757
openRequest.onsuccess = () => {
5858
const db = openRequest.result;
59-
resolve(ClientSideStorage.create(db));
59+
fixOldFiles(db).then(() => {
60+
resolve(ClientSideStorage.create(db));
61+
})
62+
};
63+
});
64+
}
65+
66+
// The following function allows Alan and Liz to load older projects.
67+
// TODO(lizlooney): Remove this function.
68+
async function fixOldFiles(db: IDBDatabase): Promise<void> {
69+
return new Promise((resolve, reject) => {
70+
const transaction = db.transaction([FILES_STORE_NAME], 'readwrite');
71+
transaction.oncomplete = () => {
72+
resolve();
73+
};
74+
transaction.onabort = () => {
75+
console.log('IndexedDB transaction aborted.');
76+
reject(new Error('IndexedDB transaction aborted.'));
77+
};
78+
const filesObjectStore = transaction.objectStore(FILES_STORE_NAME);
79+
const openCursorRequest = filesObjectStore.openCursor();
80+
openCursorRequest.onerror = () => {
81+
console.log('IndexedDB openCursor request failed. openCursorRequest.error is...');
82+
console.log(openCursorRequest.error);
83+
reject(new Error('IndexedDB openCursor request failed.'));
84+
};
85+
openCursorRequest.onsuccess = () => {
86+
const cursor = openCursorRequest.result;
87+
if (cursor) {
88+
const value = cursor.value;
89+
if (!value.path.startsWith('/projects/')) {
90+
const oldFilePath = value.path;
91+
value.path = '/projects/' + value.path;
92+
const putRequest = filesObjectStore.put(value);
93+
putRequest.onerror = () => {
94+
console.log('IndexedDB put request failed. putRequest.error is...');
95+
console.log(putRequest.error);
96+
throw new Error('IndexedDB put request failed.');
97+
};
98+
const deleteRequest = filesObjectStore.delete(oldFilePath);
99+
deleteRequest.onerror = () => {
100+
console.log('IndexedDB delete request failed. deleteRequest.error is...');
101+
console.log(deleteRequest.error);
102+
throw new Error('IndexedDB delete request failed.');
103+
};
104+
}
105+
cursor.continue();
106+
} else {
107+
// The cursor is done. We have finished reading all the files.
108+
resolve();
109+
}
60110
};
61111
});
62112
}
@@ -124,13 +174,12 @@ class ClientSideStorage implements commonStorage.Storage {
124174
});
125175
}
126176

127-
async listFilePaths(opt_filePathRegexPattern?: string): Promise<string[]> {
128-
129-
const regExp = opt_filePathRegexPattern
130-
? new RegExp(opt_filePathRegexPattern)
131-
: null;
177+
async list(path: string): Promise<string[]> {
178+
if (!path.endsWith('/')) {
179+
path += '/';
180+
}
132181
return new Promise((resolve, reject) => {
133-
const filePaths: string[] = [];
182+
const resultsSet: Set<string> = new Set();
134183
const openKeyCursorRequest = this.db.transaction([FILES_STORE_NAME], 'readonly')
135184
.objectStore(FILES_STORE_NAME)
136185
.openKeyCursor();
@@ -143,18 +192,122 @@ class ClientSideStorage implements commonStorage.Storage {
143192
const cursor = openKeyCursorRequest.result;
144193
if (cursor && cursor.key) {
145194
const filePath: string = cursor.key as string;
146-
if (!regExp || regExp.test(filePath)) {
147-
filePaths.push(filePath);
195+
if (filePath.startsWith(path)) {
196+
const relativePath = filePath.substring(path.length);
197+
const slash = relativePath.indexOf('/');
198+
const result = (slash != -1)
199+
? relativePath.substring(0, slash + 1) // Include the trailing slash.
200+
: relativePath;
201+
resultsSet.add(result);
148202
}
149203
cursor.continue();
150204
} else {
151205
// The cursor is done. We have finished reading all the files.
152-
resolve(filePaths);
206+
resolve([...resultsSet]);
153207
}
154208
};
155209
});
156210
}
157211

212+
async rename(oldPath: string, newPath: string): Promise<void> {
213+
if (oldPath.endsWith('/')) {
214+
return this.renameDirectory(oldPath, newPath);
215+
}
216+
return this.renameFile(oldPath, newPath);
217+
}
218+
219+
private async renameDirectory(oldPath: string, newPath: string): Promise<void> {
220+
return new Promise((resolve, reject) => {
221+
const transaction = this.db.transaction([FILES_STORE_NAME], 'readwrite');
222+
transaction.oncomplete = () => {
223+
resolve();
224+
};
225+
transaction.onabort = () => {
226+
console.log('IndexedDB transaction aborted.');
227+
reject(new Error('IndexedDB transaction aborted.'));
228+
};
229+
const filesObjectStore = transaction.objectStore(FILES_STORE_NAME);
230+
const openCursorRequest = filesObjectStore.openCursor();
231+
openCursorRequest.onerror = () => {
232+
console.log('IndexedDB openCursor request failed. openCursorRequest.error is...');
233+
console.log(openCursorRequest.error);
234+
throw new Error('IndexedDB openCursor request failed.');
235+
};
236+
openCursorRequest.onsuccess = () => {
237+
const cursor = openCursorRequest.result;
238+
if (cursor) {
239+
const value = cursor.value;
240+
if (value.path.startsWith(oldPath)) {
241+
const relativePath = value.path.substring(oldPath.length);
242+
const oldFilePath = value.path;
243+
value.path = newPath + relativePath;
244+
const putRequest = filesObjectStore.put(value);
245+
putRequest.onerror = () => {
246+
console.log('IndexedDB put request failed. putRequest.error is...');
247+
console.log(putRequest.error);
248+
throw new Error('IndexedDB put request failed.');
249+
};
250+
putRequest.onsuccess = () => {
251+
const deleteRequest = filesObjectStore.delete(oldFilePath);
252+
deleteRequest.onerror = () => {
253+
console.log('IndexedDB delete request failed. deleteRequest.error is...');
254+
console.log(deleteRequest.error);
255+
throw new Error('IndexedDB delete request failed.');
256+
};
257+
}
258+
}
259+
cursor.continue();
260+
} else {
261+
// The cursor is done. We have finished reading all the files.
262+
resolve();
263+
}
264+
};
265+
});
266+
}
267+
268+
private async renameFile(oldPath: string, newPath: string): Promise<void> {
269+
return new Promise((resolve, reject) => {
270+
const transaction = this.db.transaction([FILES_STORE_NAME], 'readwrite');
271+
transaction.oncomplete = () => {
272+
resolve();
273+
};
274+
transaction.onabort = () => {
275+
console.log('IndexedDB transaction aborted.');
276+
reject(new Error('IndexedDB transaction aborted.'));
277+
};
278+
const filesObjectStore = transaction.objectStore(FILES_STORE_NAME);
279+
const getRequest = filesObjectStore.get(oldPath);
280+
getRequest.onerror = () => {
281+
console.log('IndexedDB get request failed. getRequest.error is...');
282+
console.log(getRequest.error);
283+
throw new Error('IndexedDB get request failed.');
284+
};
285+
getRequest.onsuccess = () => {
286+
if (getRequest.result === undefined) {
287+
console.log('IndexedDB get request succeeded, but the file does not exist.');
288+
throw new Error('IndexedDB get request succeeded, but the file does not exist.');
289+
return;
290+
}
291+
const value = getRequest.result;
292+
value.path = newPath;
293+
const putRequest = filesObjectStore.put(value);
294+
putRequest.onerror = () => {
295+
console.log('IndexedDB put request failed. putRequest.error is...');
296+
console.log(putRequest.error);
297+
throw new Error('IndexedDB put request failed.');
298+
};
299+
putRequest.onsuccess = () => {
300+
const deleteRequest = filesObjectStore.delete(oldPath);
301+
deleteRequest.onerror = () => {
302+
console.log('IndexedDB delete request failed. deleteRequest.error is...');
303+
console.log(deleteRequest.error);
304+
throw new Error('IndexedDB delete request failed.');
305+
};
306+
};
307+
};
308+
});
309+
}
310+
158311
async fetchFileContentText(filePath: string): Promise<string> {
159312
return new Promise((resolve, reject) => {
160313
const getRequest = this.db.transaction([FILES_STORE_NAME], 'readonly')
@@ -213,7 +366,52 @@ class ClientSideStorage implements commonStorage.Storage {
213366
});
214367
}
215368

216-
async deleteFile(filePath: string): Promise<void> {
369+
async delete(path: string): Promise<void> {
370+
if (path.endsWith('/')) {
371+
return this.deleteDirectory(path);
372+
}
373+
return this.deleteFile(path);
374+
}
375+
376+
private async deleteDirectory(path: string): Promise<void> {
377+
return new Promise((resolve, reject) => {
378+
const transaction = this.db.transaction([FILES_STORE_NAME], 'readwrite');
379+
transaction.oncomplete = () => {
380+
resolve();
381+
};
382+
transaction.onabort = () => {
383+
console.log('IndexedDB transaction aborted.');
384+
reject(new Error('IndexedDB transaction aborted.'));
385+
};
386+
const filesObjectStore = transaction.objectStore(FILES_STORE_NAME);
387+
const openKeyCursorRequest = filesObjectStore.openKeyCursor();
388+
openKeyCursorRequest.onerror = () => {
389+
console.log('IndexedDB openKeyCursor request failed. openKeyCursorRequest.error is...');
390+
console.log(openKeyCursorRequest.error);
391+
throw new Error('IndexedDB openKeyCursor request failed.');
392+
};
393+
openKeyCursorRequest.onsuccess = () => {
394+
const cursor = openKeyCursorRequest.result;
395+
if (cursor && cursor.key) {
396+
const filePath: string = cursor.key as string;
397+
if (filePath.startsWith(path)) {
398+
const deleteRequest = filesObjectStore.delete(filePath);
399+
deleteRequest.onerror = () => {
400+
console.log('IndexedDB delete request failed. deleteRequest.error is...');
401+
console.log(deleteRequest.error);
402+
throw new Error('IndexedDB delete request failed.');
403+
};
404+
}
405+
cursor.continue();
406+
} else {
407+
// The cursor is done. We have finished reading all the files.
408+
resolve();
409+
}
410+
};
411+
});
412+
}
413+
414+
private async deleteFile(filePath: string): Promise<void> {
217415
return new Promise((resolve, reject) => {
218416
const transaction = this.db.transaction([FILES_STORE_NAME], 'readwrite');
219417
transaction.oncomplete = () => {

src/storage/common_storage.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ export interface Storage {
2828

2929
// Functions for storing files.
3030

31-
listFilePaths(opt_filePathRegexPattern?: string): Promise<string[]>;
31+
list(path: string): Promise<string[]>;
32+
33+
rename(oldPath: string, newPath: string): Promise<void>;
3234

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

3537
saveFile(filePath: string, fileContentText: string): Promise<void>;
3638

37-
deleteFile(filePath: string): Promise<void>;
39+
delete(path: string): Promise<void>;
3840
}

src/storage/names.ts

Lines changed: 21 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import * as storageModule from './module';
2525
/**
2626
* Paths and file names for Blocks Projects
2727
*
28-
* Files in a project are stored in a directory whose name is the project name. All files have
28+
* All projects are stored in a directory called '/projects/'.
29+
*
30+
* Files in a project are stored in a subdirectory whose name is the project name. All files have
2931
* the extension '.json' and contain JSON text.
3032
*
3133
* Project information is stored in a file called 'project.info.json'.
@@ -43,8 +45,8 @@ import * as storageModule from './module';
4345
* zero or more mechanisms, with the extension '.mechanism.json'
4446
* zero or more opmodes, with the extension '.opmode.json'
4547
*
46-
* The file path of the project info file is <ProjectName>/project.info.json.
47-
* The file path of a module is <ProjectName>/<ClassName>.<ModuleType>.json.
48+
* The file path of the project info file is /projects/<ProjectName>/project.info.json.
49+
* The file path of a module is /projects/<ProjectName>/<ClassName>.<ModuleType>.json.
4850
*/
4951

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

5658
// The extension of all JSON files is .json.
57-
export const JSON_FILE_EXTENSION = '.json';
59+
const JSON_FILE_EXTENSION = '.json';
5860

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

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

65-
// The file name of the project info file.
66-
const ROBOT_MODULE_FILE_NAME = 'Robot.robot.json';
67-
6867
// A project name starts with an uppercase letter, followed by alphanumeric characters.
6968
const REGEX_PROJECT_NAME_PART = '[A-Z][A-Za-z0-9]*';
7069

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

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

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

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

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

136137
/**
137-
* Returns a regex pattern that matches all file paths in the given project.
138-
*/
139-
export function makeFilePathRegexPattern(projectName: string): string {
140-
return '^' + escapeRegExp(projectName) + '/' +
141-
'.*' + escapeRegExp(JSON_FILE_EXTENSION) + '$';
142-
}
143-
144-
/**
145-
* Returns a regex pattern that matches all module paths in the given project.
138+
* Escapes the given text so it can be used literally in a regular expression.
146139
*/
147-
export function makeModulePathRegexPattern(projectName: string): string {
148-
return '^' + escapeRegExp(projectName) + '/' +
149-
REGEX_CLASS_NAME_PART + REGEX_MODULE_TYPE_PART + escapeRegExp(JSON_FILE_EXTENSION) + '$';
140+
function escapeRegExp(text: string): string {
141+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
150142
}
151143

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

159151
/**
160152
* Returns the file path for the given project name and file name.
161153
*/
162154
export function makeFilePath(projectName: string, fileName: string): string {
163-
return projectName + '/' + fileName;
155+
return makeProjectDirectoryPath(projectName) + fileName;
164156
}
165157

166158
/**
@@ -197,18 +189,6 @@ export function getProjectName(filePath: string): string {
197189
return result[1];
198190
}
199191

200-
/**
201-
* Returns the file name for given file path.
202-
*/
203-
export function getFileName(filePath: string): string {
204-
const regex = new RegExp(REGEX_FILE_PATH);
205-
const result = regex.exec(filePath)
206-
if (!result) {
207-
throw new Error('Unable to extract the file name from "' + filePath + '"');
208-
}
209-
return result[2];
210-
}
211-
212192
/**
213193
* Returns true if the given file name is a valid module file name.
214194
*/

0 commit comments

Comments
 (0)