From 4edd7316a5bd29caceac22c659a3f082265e0664 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Thu, 21 Nov 2024 18:11:37 +0100 Subject: [PATCH 01/23] add handler for saving file operation --- jupyter_drives/handlers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/jupyter_drives/handlers.py b/jupyter_drives/handlers.py index 5fb7d2b..771bda4 100644 --- a/jupyter_drives/handlers.py +++ b/jupyter_drives/handlers.py @@ -84,6 +84,12 @@ async def patch(self, path: str = "", drive: str = ""): result = await self._manager.rename_file(drive, path, **body) self.finish(result) + @tornado.web.authenticated + async def put(self, drive: str = "", path: str = ""): + body = self.get_json_body() + result = await self._manager.save_file(drive, path, **body) + self.finish(result) + handlers = [ ("drives", ListJupyterDrivesHandler) ] From 111c79573afc3010cb2c0936016f52076fd6d991 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Thu, 21 Nov 2024 18:12:38 +0100 Subject: [PATCH 02/23] add backend functionality to save file --- jupyter_drives/manager.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/jupyter_drives/manager.py b/jupyter_drives/manager.py index 3e39d94..7c8d773 100644 --- a/jupyter_drives/manager.py +++ b/jupyter_drives/manager.py @@ -170,6 +170,40 @@ async def new_file(self, drive_name, path, **kwargs): path: path where new content should be created """ print('New file function called.') + + async def save_file(self, drive_name, path, content): + """Save file with new content. + + Args: + drive_name: name of drive where file exists + path: path where new content should be saved + """ + data = {} + try: + # eliminate leading and trailing backslashes + path = path.strip('/') + # format body when dealing with base64 enconding or files of type PDF + formattedContent = content.encode("utf-8") + + await obs.put_async(self._content_managers[drive_name], path, formattedContent, mode = "overwrite") + metadata = await obs.head_async(self._content_managers[drive_name], path) + + data = { + "path": path, + "content": content, + "last_modified": metadata["last_modified"].isoformat(), + "size": metadata["size"] + } + except Exception as e: + raise tornado.web.HTTPError( + status_code= httpx.codes.BAD_REQUEST, + reason=f"The following error occured when saving the file: {e}", + ) + + response = { + "data": data + } + return response async def rename_file(self, drive_name, path, **kwargs): """Rename a file. From 01d4b609a994c64d091a88d5dc9ed9200edf84f7 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Thu, 21 Nov 2024 18:15:02 +0100 Subject: [PATCH 03/23] implement save file request and connect to frontend --- src/contents.ts | 35 +++++++++++++++++++++-------------- src/requests.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/contents.ts b/src/contents.ts index 470d824..4979884 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -5,7 +5,7 @@ import { Signal, ISignal } from '@lumino/signaling'; import { Contents, ServerConnection } from '@jupyterlab/services'; import { PathExt } from '@jupyterlab/coreutils'; import { IDriveInfo } from './token'; -import { mountDrive } from './requests'; +import { saveFile, mountDrive } from './requests'; let data: Contents.IModel = { name: '', @@ -433,19 +433,26 @@ export class Drive implements Contents.IDrive { localPath: string, options: Partial = {} ): Promise { - /*const settings = this.serverSettings; - const url = this._getUrl(localPath); - const init = { - method: 'PUT', - body: JSON.stringify(options) - }; - const response = await ServerConnection.makeRequest(url, init, settings); - // will return 200 for an existing file and 201 for a new file - if (response.status !== 200 && response.status !== 201) { - const err = await ServerConnection.ResponseError.create(response); - throw err; - } - const data = await response.json();*/ + // extract current drive name + const currentDrive = this._drivesList.filter( + x => + x.name === + (localPath.indexOf('/') !== -1 + ? localPath.substring(0, localPath.indexOf('/')) + : localPath) + )[0]; + + // eliminate drive name from path + const relativePath = + localPath.indexOf('/') !== -1 + ? localPath.substring(localPath.indexOf('/') + 1) + : ''; + + const resp = await saveFile(currentDrive.name, { + path: relativePath, + param: options + }); + console.log('contents resp: ', resp); Contents.validateContentsModel(data); this._fileChanged.emit({ diff --git a/src/requests.ts b/src/requests.ts index d6ac6e3..07cc666 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -1,5 +1,6 @@ import { ReadonlyJSONObject } from '@lumino/coreutils'; import { requestAPI } from './handler'; +import { Contents } from '@jupyterlab/services'; /** * Fetch the list of available drives. @@ -24,3 +25,30 @@ export async function mountDrive( }; return await requestAPI('drives', 'POST', body); } + +export async function saveFile( + driveName: string, + options: { + path: string; + param: Partial; + } +) { + // const [fileType, fileMimeType, fileFormat] = getFileType( + // PathExt.extname(PathExt.basename(options.path)), + // options.registeredFileTypes + // ); + + // const formattedBody = Private.formatBody(options.param, fileFormat, fileType, fileMimeType); + // const body: ReadonlyJSONObject = { + // content: formattedBody + // }; + + const response = await requestAPI( + 'drives/' + driveName + '/' + options.path, + 'PUT', + { + content: options.param.content + } + ); + console.log('response: ', response); +} From 547ec44c8b5edf98c9c2c438faeddb7f1323c4b4 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Mon, 25 Nov 2024 14:23:03 +0100 Subject: [PATCH 04/23] remove unused import --- src/contents.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/contents.ts b/src/contents.ts index 21d114b..b3d042b 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -1,7 +1,6 @@ import { JupyterFrontEnd } from '@jupyterlab/application'; import { Signal, ISignal } from '@lumino/signaling'; import { Contents, ServerConnection } from '@jupyterlab/services'; -import { PathExt } from '@jupyterlab/coreutils'; import { IDriveInfo, IRegisteredFileTypes } from './token'; import { saveFile, getContents, mountDrive } from './requests'; From 233ceacf1edb9019c86b58a9b10755538d915d05 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Mon, 25 Nov 2024 19:00:40 +0100 Subject: [PATCH 05/23] iterate on saving functionality --- src/contents.ts | 6 +++--- src/requests.ts | 31 ++++++++++++++++++++----------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/contents.ts b/src/contents.ts index b3d042b..cff3065 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -460,11 +460,11 @@ export class Drive implements Contents.IDrive { ? localPath.substring(localPath.indexOf('/') + 1) : ''; - const resp = await saveFile(currentDrive.name, { + const data = await saveFile(currentDrive.name, { path: relativePath, - param: options + param: options, + registeredFileTypes: this._registeredFileTypes }); - console.log('contents resp: ', resp); Contents.validateContentsModel(data); this._fileChanged.emit({ diff --git a/src/requests.ts b/src/requests.ts index 7ee0308..2b2f67d 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -143,18 +143,9 @@ export async function saveFile( options: { path: string; param: Partial; + registeredFileTypes: IRegisteredFileTypes; } ) { - // const [fileType, fileMimeType, fileFormat] = getFileType( - // PathExt.extname(PathExt.basename(options.path)), - // options.registeredFileTypes - // ); - - // const formattedBody = Private.formatBody(options.param, fileFormat, fileType, fileMimeType); - // const body: ReadonlyJSONObject = { - // content: formattedBody - // }; - const response = await requestAPI( 'drives/' + driveName + '/' + options.path, 'PUT', @@ -162,5 +153,23 @@ export async function saveFile( content: options.param.content } ); - console.log('response: ', response); + + const [fileType, fileMimeType, fileFormat] = getFileType( + PathExt.extname(PathExt.basename(options.path)), + options.registeredFileTypes + ); + + data = { + name: PathExt.basename(options.path), + path: PathExt.join(driveName, options.path), + last_modified: response.data.last_modified, + created: response.data.last_modified, + content: response.data.content, + format: fileFormat as Contents.FileFormat, + mimetype: fileMimeType, + size: response.data.size, + writable: true, + type: fileType + }; + return data; } From bd1c5984b953a2a9a16c9d665bf960f72a6905e3 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Mon, 25 Nov 2024 20:01:14 +0100 Subject: [PATCH 06/23] update docstrings --- src/requests.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/requests.ts b/src/requests.ts index 2b2f67d..e1d2312 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -31,6 +31,7 @@ export async function getDrivesList() { /** * Mount a drive by establishing a connection with it. + * * @param driveName * @param options.provider The provider of the drive to be mounted. * @param options.region The region of the drive to be mounted. @@ -51,8 +52,8 @@ export async function mountDrive( * Get contents of a directory or retrieve contents of a specific file. * * @param driveName - * @param options.path The path of object to be retrived - * @param options.path The list containing all registered file types. + * @param options.path The path of object to be retrived. + * @param options.registeredFileTypes The list containing all registered file types. * * @returns A promise which resolves with the contents model. */ @@ -138,6 +139,16 @@ export async function getContents( return data; } +/** + * Save an object. + * + * @param driveName + * @param options.path The path of the object to be saved. + * @param options.param The options sent when getting the request from the content manager. + * @param options.registeredFileTypes The list containing all registered file types. + * + * @returns A promise which resolves with the contents model. + */ export async function saveFile( driveName: string, options: { From d02f167e05d32f89fc868643959da5208f57eb04 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Tue, 26 Nov 2024 11:02:29 +0100 Subject: [PATCH 07/23] add logic for object creation --- jupyter_drives/manager.py | 30 ++++++++++++++++++-- src/contents.ts | 58 ++++++++++++++++++++++++++------------- src/requests.ts | 45 ++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 21 deletions(-) diff --git a/jupyter_drives/manager.py b/jupyter_drives/manager.py index da76f1f..91a00b3 100644 --- a/jupyter_drives/manager.py +++ b/jupyter_drives/manager.py @@ -237,14 +237,39 @@ async def get_contents(self, drive_name, path): return response - async def new_file(self, drive_name, path, **kwargs): + async def new_file(self, drive_name, path): """Create a new file or directory at the given path. Args: drive_name: name of drive where the new content is created path: path where new content should be created + content: content of object """ - print('New file function called.') + data = {} + try: + # eliminate leading and trailing backslashes + path = path.strip('/') + + # TO DO: switch to mode "created", which is not implemented yet + await obs.put_async(self._content_managers[drive_name], path, b"", mode = "overwrite") + metadata = await obs.head_async(self._content_managers[drive_name], path) + + data = { + "path": path, + "content": "", + "last_modified": metadata["last_modified"].isoformat(), + "size": metadata["size"] + } + except Exception as e: + raise tornado.web.HTTPError( + status_code= httpx.codes.BAD_REQUEST, + reason=f"The following error occured when creating the object: {e}", + ) + + response = { + "data": data + } + return response async def save_file(self, drive_name, path, content): """Save file with new content. @@ -252,6 +277,7 @@ async def save_file(self, drive_name, path, content): Args: drive_name: name of drive where file exists path: path where new content should be saved + content: content of object """ data = {} try: diff --git a/src/contents.ts b/src/contents.ts index cff3065..ee90c88 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -3,7 +3,7 @@ import { Signal, ISignal } from '@lumino/signaling'; import { Contents, ServerConnection } from '@jupyterlab/services'; import { IDriveInfo, IRegisteredFileTypes } from './token'; -import { saveFile, getContents, mountDrive } from './requests'; +import { saveFile, getContents, mountDrive, createObject } from './requests'; let data: Contents.IModel = { name: '', @@ -278,26 +278,43 @@ export class Drive implements Contents.IDrive { async newUntitled( options: Contents.ICreateOptions = {} ): Promise { - /*let body = '{}'; - if (options) { - if (options.ext) { - options.ext = Private.normalizeExtension(options.ext); - } - body = JSON.stringify(options); - } + const path = options.path ?? ''; - const settings = this.serverSettings; - const url = this._getUrl(options.path ?? ''); - const init = { - method: 'POST', - body - }; - const response = await ServerConnection.makeRequest(url, init, settings); - if (response.status !== 201) { - const err = await ServerConnection.ResponseError.create(response); - throw err; + if (path !== '') { + // extract current drive name + const currentDrive = this._drivesList.filter( + x => + x.name === + (path.indexOf('/') !== -1 + ? path.substring(0, path.indexOf('/')) + : path) + )[0]; + + // eliminate drive name from path + const relativePath = + path.indexOf('/') !== -1 ? path.substring(path.indexOf('/') + 1) : ''; + + // get current list of contents of drive + const old_data = await getContents(currentDrive.name, { + path: relativePath, + registeredFileTypes: this._registeredFileTypes + }); + + if (options.type !== undefined) { + // get incremented untitled name + const name = this.incrementUntitledName(old_data, options); + data = await createObject(currentDrive.name, { + name: name, + path: relativePath, + registeredFileTypes: this._registeredFileTypes + }); + } else { + console.warn('Type of new element is undefined'); + } + } else { + // create new element at root would mean creating a new drive + console.warn('Operation not supported.'); } - const data = await response.json();*/ Contents.validateContentsModel(data); this._fileChanged.emit({ @@ -318,6 +335,9 @@ export class Drive implements Contents.IDrive { let countText = 0; let countDir = 0; let countNotebook = 0; + if (options.type === 'notebook') { + options.ext = 'ipynb'; + } content.forEach(item => { if (options.ext !== undefined) { diff --git a/src/requests.ts b/src/requests.ts index e1d2312..b9af825 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -184,3 +184,48 @@ export async function saveFile( }; return data; } + +/** + * Create a new object. + * + * @param driveName + * @param options.path The path of new object. + * @param options.registeredFileTypes The list containing all registered file types. + * + * @returns A promise which resolves with the contents model. + */ +export async function createObject( + driveName: string, + options: { + name: string; + path: string; + registeredFileTypes: IRegisteredFileTypes; + } +) { + const path = options.path + ? PathExt.join(options.path, options.name) + : options.name; + const response = await requestAPI( + 'drives/' + driveName + '/' + path, + 'POST' + ); + + const [fileType, fileMimeType, fileFormat] = getFileType( + PathExt.extname(PathExt.basename(options.name)), + options.registeredFileTypes + ); + + data = { + name: options.name, + path: PathExt.join(driveName, path), + last_modified: response.data.last_modified, + created: response.data.last_modified, + content: response.data.content, + format: fileFormat as Contents.FileFormat, + mimetype: fileMimeType, + size: response.data.size, + writable: true, + type: fileType + }; + return data; +} From f13f3074cf45db7801dba3c12baea360b46535f7 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Tue, 26 Nov 2024 13:41:58 +0100 Subject: [PATCH 08/23] add delete functionality in backend and connect to frontend content manager --- jupyter_drives/handlers.py | 5 ++++ jupyter_drives/manager.py | 20 +++++++++++++++ src/contents.ts | 36 ++++++++++++++++++--------- src/requests.ts | 50 +++++++++++++++++++++++++++++++++++++- 4 files changed, 99 insertions(+), 12 deletions(-) diff --git a/jupyter_drives/handlers.py b/jupyter_drives/handlers.py index f294040..68f441c 100644 --- a/jupyter_drives/handlers.py +++ b/jupyter_drives/handlers.py @@ -89,6 +89,11 @@ async def put(self, drive: str = "", path: str = ""): body = self.get_json_body() result = await self._manager.save_file(drive, path, **body) self.finish(result) + + @tornado.web.authenticated + async def delete(self, drive: str = "", path: str = ""): + result = await self._manager.delete_file(drive, path) + self.finish(result) handlers = [ ("drives", ListJupyterDrivesHandler) diff --git a/jupyter_drives/manager.py b/jupyter_drives/manager.py index 91a00b3..5d7a4f5 100644 --- a/jupyter_drives/manager.py +++ b/jupyter_drives/manager.py @@ -314,6 +314,26 @@ async def rename_file(self, drive_name, path, **kwargs): path: path of file """ print('Rename file function called.') + + async def delete_file(self, drive_name, path): + """Delete an object. + + Args: + drive_name: name of drive where object exists + path: path where content is located + """ + try: + # eliminate leading and trailing backslashes + path = path.strip('/') + await obs.delete_async(self._content_managers[drive_name], path) + + except Exception as e: + raise tornado.web.HTTPError( + status_code= httpx.codes.BAD_REQUEST, + reason=f"The following error occured when deleting the object: {e}", + ) + + return async def _call_provider( self, diff --git a/src/contents.ts b/src/contents.ts index ee90c88..9ae9566 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -3,7 +3,13 @@ import { Signal, ISignal } from '@lumino/signaling'; import { Contents, ServerConnection } from '@jupyterlab/services'; import { IDriveInfo, IRegisteredFileTypes } from './token'; -import { saveFile, getContents, mountDrive, createObject } from './requests'; +import { + saveFile, + getContents, + mountDrive, + createObject, + deleteObjects +} from './requests'; let data: Contents.IModel = { name: '', @@ -389,16 +395,24 @@ export class Drive implements Contents.IDrive { }*/ async delete(localPath: string): Promise { - /*const url = this._getUrl(localPath); - const settings = this.serverSettings; - const init = { method: 'DELETE' }; - const response = await ServerConnection.makeRequest(url, init, settings); - // TODO: update IPEP27 to specify errors more precisely, so - // that error types can be detected here with certainty. - if (response.status !== 204) { - const err = await ServerConnection.ResponseError.create(response); - throw err; - }*/ + // extract current drive name + const currentDrive = this._drivesList.filter( + x => + x.name === + (localPath.indexOf('/') !== -1 + ? localPath.substring(0, localPath.indexOf('/')) + : localPath) + )[0]; + + // eliminate drive name from path + const relativePath = + localPath.indexOf('/') !== -1 + ? localPath.substring(localPath.indexOf('/') + 1) + : ''; + + await deleteObjects(currentDrive.name, { + path: relativePath + }); this._fileChanged.emit({ type: 'delete', diff --git a/src/requests.ts b/src/requests.ts index b9af825..92b26cc 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -211,7 +211,7 @@ export async function createObject( ); const [fileType, fileMimeType, fileFormat] = getFileType( - PathExt.extname(PathExt.basename(options.name)), + PathExt.extname(options.name), options.registeredFileTypes ); @@ -229,3 +229,51 @@ export async function createObject( }; return data; } + +/** + * Delete an object. + * + * @param driveName + * @param options.path The path of object. + * + * @returns A promise which resolves with the contents model. + */ +export async function deleteObjects( + driveName: string, + options: { + path: string; + } +) { + // get list of contents with given prefix (path) + const response = await requestAPI( + 'drives/' + driveName + '/' + options.path, + 'GET' + ); + + // deleting contents of a directory + if (response.data.length !== undefined && response.data.length !== 0) { + await Promise.all( + response.data.map(async (c: any) => { + return Private.deleteSingleObject(driveName, c.path); + }) + ); + } + // always deleting the object (file or main directory) + return Private.deleteSingleObject(driveName, options.path); +} + +namespace Private { + /** + * Helping function for deleting files inside + * a directory, in the case of deleting the directory. + * + * @param driveName + * @param objectPath complete path of object to delete + */ + export async function deleteSingleObject( + driveName: string, + objectPath: string + ) { + await requestAPI('drives/' + driveName + '/' + objectPath, 'DELETE'); + } +} From 9784010e60b28f11cc9619b965711987d4fe7f1f Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Tue, 26 Nov 2024 13:43:50 +0100 Subject: [PATCH 09/23] fix sub directory listing --- src/requests.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/requests.ts b/src/requests.ts index 92b26cc..b80104b 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -88,7 +88,9 @@ export async function getContents( fileList[fileName] = fileList[fileName] ?? { name: fileName, - path: driveName + '/' + row.path, + path: options.path + ? PathExt.join(driveName, options.path, fileName) + : PathExt.join(driveName, fileName), last_modified: row.last_modified, created: '', content: !fileName.split('.')[1] ? [] : null, @@ -102,8 +104,8 @@ export async function getContents( }); data = { - name: options.path ? PathExt.basename(options.path) : '', - path: options.path ? options.path + '/' : '', + name: options.path ? PathExt.basename(options.path) : driveName, + path: PathExt.join(driveName, options.path ? options.path + '/' : ''), last_modified: '', created: '', content: Object.values(fileList), @@ -123,7 +125,7 @@ export async function getContents( data = { name: PathExt.basename(options.path), - path: driveName + '/' + response.data.path, + path: PathExt.join(driveName, response.data.path), last_modified: response.data.last_modified, created: '', content: response.data.content, From 2774654b33264dd87a67551c0292fd05ff8d4410 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Tue, 26 Nov 2024 15:56:40 +0100 Subject: [PATCH 10/23] format path in backend when listing --- jupyter_drives/manager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jupyter_drives/manager.py b/jupyter_drives/manager.py index 5d7a4f5..40c0b50 100644 --- a/jupyter_drives/manager.py +++ b/jupyter_drives/manager.py @@ -165,6 +165,9 @@ async def get_contents(self, drive_name, path): """ if path == '/': path = '' + else: + path = path.strip('/') + try : data = [] isDir = False From ef20987d945afe9f5ddcb612965518ba9483e6f0 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Tue, 26 Nov 2024 23:05:01 +0100 Subject: [PATCH 11/23] add backend functionality to check if object exists in drive and count no. of appearances --- jupyter_drives/handlers.py | 5 ++++ jupyter_drives/manager.py | 21 ++++++++++++++ src/contents.ts | 37 ++++++++++++++++++++++++- src/requests.ts | 57 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 1 deletion(-) diff --git a/jupyter_drives/handlers.py b/jupyter_drives/handlers.py index 68f441c..c3cd77a 100644 --- a/jupyter_drives/handlers.py +++ b/jupyter_drives/handlers.py @@ -95,6 +95,11 @@ async def delete(self, drive: str = "", path: str = ""): result = await self._manager.delete_file(drive, path) self.finish(result) + @tornado.web.authenticated + async def head(self, drive: str = "", path: str = ""): + result = await self._manager.check_file(drive, path) + self.finish(result) + handlers = [ ("drives", ListJupyterDrivesHandler) ] diff --git a/jupyter_drives/manager.py b/jupyter_drives/manager.py index 40c0b50..e854ce5 100644 --- a/jupyter_drives/manager.py +++ b/jupyter_drives/manager.py @@ -338,6 +338,27 @@ async def delete_file(self, drive_name, path): return + async def check_file(self, drive_name, path): + """Check if an object already exists within a drive. + + Args: + drive_name: name of drive where object exists + path: path where content is located + """ + result = False + try: + # eliminate leading and trailing backslashes + path = path.strip('/') + await obs.head_async(self._content_managers[drive_name], path) + result = True + except Exception: + pass + + response = { + result: result + } + return response + async def _call_provider( self, url: str, diff --git a/src/contents.ts b/src/contents.ts index 9ae9566..62b127e 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -1,6 +1,7 @@ import { JupyterFrontEnd } from '@jupyterlab/application'; import { Signal, ISignal } from '@lumino/signaling'; import { Contents, ServerConnection } from '@jupyterlab/services'; +import { PathExt } from '@jupyterlab/coreutils'; import { IDriveInfo, IRegisteredFileTypes } from './token'; import { @@ -8,7 +9,8 @@ import { getContents, mountDrive, createObject, - deleteObjects + deleteObjects, + countObjectNameAppearances } from './requests'; let data: Contents.IModel = { @@ -460,6 +462,39 @@ export class Drive implements Contents.IDrive { Contents.validateContentsModel(data); return data; } + + /** + * Helping function to increment name of existing files or directorties. + * + * @param localPath - Path to file. + * + * @param driveName - The name of the drive where content is counted. + + */ + async incrementName(localPath: string, driveName: string) { + let fileExtension: string = ''; + let originalName: string = ''; + + // extract name from path + originalName = PathExt.basename(localPath); + // eliminate file extension + fileExtension = PathExt.extname(originalName); + originalName = + fileExtension !== '' + ? originalName.split('.')[originalName.split('.').length - 2] + : originalName; + + const counter = await countObjectNameAppearances( + driveName, + localPath, + originalName + ); + let newName = counter ? originalName + counter : originalName; + newName = fileExtension !== '' ? newName + fileExtension : newName; + + return newName; + } + /** * Save a file. * diff --git a/src/requests.ts b/src/requests.ts index b80104b..15cfeab 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -264,6 +264,63 @@ export async function deleteObjects( return Private.deleteSingleObject(driveName, options.path); } +/** + * Check existance of an object. + * + * @param driveName + * @param options.path The path to the object. + * + * @returns A promise which resolves or rejects depending on the object existing. + */ +export async function checkObject( + driveName: string, + options: { + path: string; + } +) { + const response = await requestAPI( + 'drives/' + driveName + '/' + options.path, + 'HEAD' + ); + + return response.result; +} + +/** + * Count number of appeareances of object name. + * + * @param driveName: + * @param path: The path to the object. + * @param originalName: The original name of the object (before it was incremented). + * + * @returns A promise which resolves with the number of appeareances of object. + */ +export const countObjectNameAppearances = async ( + driveName: string, + path: string, + originalName: string +): Promise => { + let counter: number = 0; + + const response = await requestAPI( + 'drives/' + driveName + '/' + path.substring(0, path.lastIndexOf('/')), + 'GET' + ); + + if (response.data && response.data.length !== 0) { + response.data.forEach((c: any) => { + const fileName = c.row.replace(path ? path + '/' : '', '').split('/')[0]; + if ( + fileName.substring(0, originalName.length + 1).includes(originalName) + ) { + counter += 1; + } + }); + } + + return counter; +}; + namespace Private { /** * Helping function for deleting files inside From ff3426ee40a688432cf2af702bdd9e82eee60698 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Tue, 26 Nov 2024 23:07:13 +0100 Subject: [PATCH 12/23] add rename functionaltity in backend and frontend content manager --- jupyter_drives/manager.py | 26 ++++++++++- src/contents.ts | 56 ++++++++++++++++++------ src/requests.ts | 92 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 16 deletions(-) diff --git a/jupyter_drives/manager.py b/jupyter_drives/manager.py index e854ce5..a836c8d 100644 --- a/jupyter_drives/manager.py +++ b/jupyter_drives/manager.py @@ -309,14 +309,36 @@ async def save_file(self, drive_name, path, content): } return response - async def rename_file(self, drive_name, path, **kwargs): + async def rename_file(self, drive_name, path, new_path): """Rename a file. Args: drive_name: name of drive where file is located path: path of file """ - print('Rename file function called.') + data = {} + try: + # eliminate leading and trailing backslashes + path = path.strip('/') + + await obs.rename_async(self._content_managers[drive_name], path, new_path) + metadata = await obs.head_async(self._content_managers[drive_name], new_path) + + data = { + "path": new_path, + "last_modified": metadata["last_modified"].isoformat(), + "size": metadata["size"] + } + except Exception as e: + raise tornado.web.HTTPError( + status_code= httpx.codes.BAD_REQUEST, + reason=f"The following error occured when renaming the object: {e}", + ) + + response = { + "data": data + } + return response async def delete_file(self, drive_name, path): """Delete an object. diff --git a/src/contents.ts b/src/contents.ts index 62b127e..f04a925 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -9,8 +9,10 @@ import { getContents, mountDrive, createObject, + checkObject, deleteObjects, - countObjectNameAppearances + countObjectNameAppearances, + renameObjects } from './requests'; let data: Contents.IModel = { @@ -441,25 +443,51 @@ export class Drive implements Contents.IDrive { newLocalPath: string, options: Contents.ICreateOptions = {} ): Promise { - /*const settings = this.serverSettings; - const url = this._getUrl(oldLocalPath); - const init = { - method: 'PATCH', - body: JSON.stringify({ path: newLocalPath }) - }; - const response = await ServerConnection.makeRequest(url, init, settings); - if (response.status !== 200) { - const err = await ServerConnection.ResponseError.create(response); - throw err; + // extract current drive name + const currentDrive = this._drivesList.filter( + x => + x.name === + (oldLocalPath.indexOf('/') !== -1 + ? oldLocalPath.substring(0, oldLocalPath.indexOf('/')) + : oldLocalPath) + )[0]; + + // eliminate drive name from path + const relativePath = + oldLocalPath.indexOf('/') !== -1 + ? oldLocalPath.substring(oldLocalPath.indexOf('/') + 1) + : ''; + const newRelativePath = + newLocalPath.indexOf('/') !== -1 + ? newLocalPath.substring(newLocalPath.indexOf('/') + 1) + : ''; + + // extract new file name + let newFileName = PathExt.basename(newLocalPath); + + try { + // check if object with chosen name already exists + await checkObject(currentDrive.name, { + path: newLocalPath + }); + newFileName = await this.incrementName(newLocalPath, this._name); + } catch (error) { + // HEAD request failed for this file name, continue, as name doesn't already exist. + } finally { + data = await renameObjects(currentDrive.name, { + path: relativePath, + newPath: newRelativePath, + newFileName: newFileName, + registeredFileTypes: this._registeredFileTypes + }); } - const data = await response.json();*/ + Contents.validateContentsModel(data); this._fileChanged.emit({ type: 'rename', oldValue: { path: oldLocalPath }, - newValue: { path: newLocalPath } + newValue: data }); - Contents.validateContentsModel(data); return data; } diff --git a/src/requests.ts b/src/requests.ts index 15cfeab..a0b9cf8 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -264,6 +264,77 @@ export async function deleteObjects( return Private.deleteSingleObject(driveName, options.path); } +/** + * Rename an object. + * + * @param driveName + * @param options.path The path of object. + * + * @returns A promise which resolves with the contents model. + */ +export async function renameObjects( + driveName: string, + options: { + path: string; + newPath: string; + newFileName: string; + registeredFileTypes: IRegisteredFileTypes; + } +) { + const formattedNewPath = + options.newPath.substring(0, options.newPath.lastIndexOf('/') + 1) + + options.newFileName; + + const [fileType, fileMimeType, fileFormat] = getFileType( + PathExt.extname(PathExt.basename(options.newFileName)), + options.registeredFileTypes + ); + + // get list of contents with given prefix (path) + const response = await requestAPI( + 'drives/' + driveName + '/' + options.path, + 'GET' + ); + + // renaming contents of a directory + if (response.data.length !== undefined && response.data.length !== 0) { + await Promise.all( + response.data.map(async (c: any) => { + const remainingFilePath = c.path.substring(options.path.length); + Private.renameSingleObject( + driveName, + PathExt.join(options.path, remainingFilePath), + PathExt.join(formattedNewPath, remainingFilePath) + ); + }) + ); + } + // always rename the object (file or main directory) + try { + const renamedObject = await Private.renameSingleObject( + driveName, + options.path, + formattedNewPath + ); + data = { + name: options.newFileName, + path: PathExt.join(driveName, formattedNewPath), + last_modified: renamedObject.data.last_modified, + created: '', + content: PathExt.extname(options.newFileName) !== '' ? null : [], // TODO: add dir check + format: fileFormat as Contents.FileFormat, + mimetype: fileMimeType, + size: renamedObject.data.size, + writable: true, + type: fileType + }; + } catch (error) { + // renaming failed if directory didn't exist and was only part of a path + } + + return data; +} + /** * Check existance of an object. * @@ -335,4 +406,25 @@ namespace Private { ) { await requestAPI('drives/' + driveName + '/' + objectPath, 'DELETE'); } + + /** + * Helping function for renaming files inside + * a directory, in the case of deleting the directory. + * + * @param driveName + * @param objectPath complete path of object to delete + */ + export async function renameSingleObject( + driveName: string, + objectPath: string, + newObjectPath: string + ) { + return await requestAPI( + 'drives/' + driveName + '/' + objectPath, + 'PATCH', + { + new_path: newObjectPath + } + ); + } } From ea27266dc6fd8fe0b409bfc8361d8f113c8bd663 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Wed, 27 Nov 2024 13:32:04 +0100 Subject: [PATCH 13/23] add logic to copy object in backend and frontend content manager --- jupyter_drives/handlers.py | 5 +- jupyter_drives/manager.py | 32 +++++++++ src/contents.ts | 134 ++++++++++++++++++------------------- src/requests.ts | 97 ++++++++++++++++++++++++++- 4 files changed, 196 insertions(+), 72 deletions(-) diff --git a/jupyter_drives/handlers.py b/jupyter_drives/handlers.py index c3cd77a..7a279a7 100644 --- a/jupyter_drives/handlers.py +++ b/jupyter_drives/handlers.py @@ -87,7 +87,10 @@ async def patch(self, drive: str = "", path: str = ""): @tornado.web.authenticated async def put(self, drive: str = "", path: str = ""): body = self.get_json_body() - result = await self._manager.save_file(drive, path, **body) + if 'content' in body: + result = await self._manager.save_file(drive, path, **body) + elif 'to_path' in body: + result = await self._manager.copy_file(drive, path, **body) self.finish(result) @tornado.web.authenticated diff --git a/jupyter_drives/manager.py b/jupyter_drives/manager.py index a836c8d..f047dfb 100644 --- a/jupyter_drives/manager.py +++ b/jupyter_drives/manager.py @@ -381,6 +381,38 @@ async def check_file(self, drive_name, path): } return response + async def copy_file(self, drive_name, path, to_path): + """Save file with new content. + + Args: + drive_name: name of drive where file exists + path: path where original content exists + to_path: path where object should be copied + """ + data = {} + try: + # eliminate leading and trailing backslashes + path = path.strip('/') + + await obs.copy_async(self._content_managers[drive_name], path, to_path) + metadata = await obs.head_async(self._content_managers[drive_name], to_path) + + data = { + "path": to_path, + "last_modified": metadata["last_modified"].isoformat(), + "size": metadata["size"] + } + except Exception as e: + raise tornado.web.HTTPError( + status_code= httpx.codes.BAD_REQUEST, + reason=f"The following error occured when copying the: {e}", + ) + + response = { + "data": data + } + return response + async def _call_provider( self, url: str, diff --git a/src/contents.ts b/src/contents.ts index f04a925..ec65e6a 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -12,7 +12,8 @@ import { checkObject, deleteObjects, countObjectNameAppearances, - renameObjects + renameObjects, + copyObjects } from './requests'; let data: Contents.IModel = { @@ -573,88 +574,83 @@ export class Drive implements Contents.IDrive { } /** - * Copy a file into a given directory. + * Helping function for copying an object. * - * @param path - The original file path. + * @param copiedItemPath - The original file path. * - * @param toDir - The destination directory path. + * @param toPath - The path where item will be copied. * - * @returns A promise which resolves with the new contents model when the + * @param driveName - The name of the drive where content is moved. + * + * @returns A promise which resolves with the new name when the * file is copied. */ + async incrementCopyName( + copiedItemPath: string, + toPath: string, + driveName: string + ) { + // extracting original file name + const originalFileName = PathExt.basename(copiedItemPath); - incrementCopyName(contents: Contents.IModel, copiedItemPath: string): string { - const content: Array = contents.content; - let name: string = ''; - let countText = 0; - let countDir = 0; - let countNotebook = 0; - let ext = undefined; - const list1 = copiedItemPath.split('/'); - const copiedItemName = list1[list1.length - 1]; + // constructing new file name and path with -Copy string + const newFileName = + PathExt.extname(originalFileName) === '' + ? originalFileName + '-Copy' + : originalFileName.split('.')[0] + + '-Copy.' + + originalFileName.split('.')[1]; - const list2 = copiedItemName.split('.'); - let rootName = list2[0]; + const newFilePath = PathExt.join(toPath, newFileName); + // copiedItemPath.substring(0, copiedItemPath.lastIndexOf('/') + 1) + newFileName; - content.forEach(item => { - if (item.name.includes(rootName) && item.name.includes('.txt')) { - ext = '.txt'; - if (rootName.includes('-Copy')) { - const list3 = rootName.split('-Copy'); - countText = parseInt(list3[1]) + 1; - rootName = list3[0]; - } else { - countText = countText + 1; - } - } - if (item.name.includes(rootName) && item.name.includes('.ipynb')) { - ext = '.ipynb'; - if (rootName.includes('-Copy')) { - const list3 = rootName.split('-Copy'); - countNotebook = parseInt(list3[1]) + 1; - rootName = list3[0]; - } else { - countNotebook = countNotebook + 1; - } - } else if (item.name.includes(rootName)) { - if (rootName.includes('-Copy')) { - const list3 = rootName.split('-Copy'); - countDir = parseInt(list3[1]) + 1; - rootName = list3[0]; - } else { - countDir = countDir + 1; - } - } - }); + // getting incremented name of Copy in case of duplicates + const incrementedName = await this.incrementName(newFilePath, driveName); - if (ext === '.txt') { - name = rootName + '-Copy' + countText + ext; - } - if (ext === 'ipynb') { - name = rootName + '-Copy' + countText + ext; - } else if (ext === undefined) { - name = rootName + '-Copy' + countDir; - } - - return name; + return incrementedName; } + + /** + * Copy a file into a given directory. + * + * @param path - The original file path. + * + * @param toDir - The destination directory path. + * + * @returns A promise which resolves with the new contents model when the + * file is copied. + */ async copy( - fromFile: string, + path: string, toDir: string, options: Contents.ICreateOptions = {} ): Promise { - /*const settings = this.serverSettings; - const url = this._getUrl(toDir); - const init = { - method: 'POST', - body: JSON.stringify({ copy_from: fromFile }) - }; - const response = await ServerConnection.makeRequest(url, init, settings); - if (response.status !== 201) { - const err = await ServerConnection.ResponseError.create(response); - throw err; - } - const data = await response.json();*/ + // extract current drive name + const currentDrive = this._drivesList.filter( + x => + x.name === + (path.indexOf('/') !== -1 ? path.substring(0, path.indexOf('/')) : path) + )[0]; + + // eliminate drive name from path + const relativePath = + path.indexOf('/') !== -1 ? path.substring(path.indexOf('/') + 1) : ''; + const toRelativePath = + toDir.indexOf('/') !== -1 ? toDir.substring(toDir.indexOf('/') + 1) : ''; + + // construct new file or directory name for the copy + const newFileName = await this.incrementCopyName( + relativePath, + toRelativePath, + currentDrive.name + ); + + data = await copyObjects(currentDrive.name, { + path: relativePath, + toPath: toRelativePath, + newFileName: newFileName, + registeredFileTypes: this._registeredFileTypes + }); this._fileChanged.emit({ type: 'new', diff --git a/src/requests.ts b/src/requests.ts index a0b9cf8..d5abc04 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -335,6 +335,78 @@ export async function renameObjects( return data; } +/** + * Copy an object. + * + * @param driveName + * @param options.path The path of object. + * @param options.toPath The path where object should be copied. + * @param options.newFileName The name of the item to be copied. + * @param options.registeredFileTypes The list containing all registered file types. + * + * @returns A promise which resolves with the contents model. + */ +export async function copyObjects( + driveName: string, + options: { + path: string; + toPath: string; + newFileName: string; + registeredFileTypes: IRegisteredFileTypes; + } +) { + const formattedNewPath = PathExt.join(options.toPath, options.newFileName); + + const [fileType, fileMimeType, fileFormat] = getFileType( + PathExt.extname(PathExt.basename(options.newFileName)), + options.registeredFileTypes + ); + + // get list of contents with given prefix (path) + const response = await requestAPI( + 'drives/' + driveName + '/' + options.path, + 'GET' + ); + + // copying contents of a directory + if (response.data.length !== undefined && response.data.length !== 0) { + await Promise.all( + response.data.map(async (c: any) => { + const remainingFilePath = c.path.substring(options.path.length); + Private.copySingleObject( + driveName, + PathExt.join(options.path, remainingFilePath), + PathExt.join(formattedNewPath, remainingFilePath) + ); + }) + ); + } + // always copy the main object (file or directory) + try { + const copiedObject = await Private.copySingleObject( + driveName, + options.path, + formattedNewPath + ); + data = { + name: options.newFileName, + path: PathExt.join(driveName, formattedNewPath), + last_modified: copiedObject.data.last_modified, + created: '', + content: PathExt.extname(options.newFileName) !== '' ? null : [], // TODO: add dir check + format: fileFormat as Contents.FileFormat, + mimetype: fileMimeType, + size: copiedObject.data.size, + writable: true, + type: fileType + }; + } catch (error) { + // copied failed if directory didn't exist and was only part of a path + } + + return data; +} + /** * Check existance of an object. * @@ -380,7 +452,7 @@ export const countObjectNameAppearances = async ( if (response.data && response.data.length !== 0) { response.data.forEach((c: any) => { - const fileName = c.row.replace(path ? path + '/' : '', '').split('/')[0]; + const fileName = c.path.replace(path ? path + '/' : '', '').split('/')[0]; if ( fileName.substring(0, originalName.length + 1).includes(originalName) ) { @@ -412,7 +484,7 @@ namespace Private { * a directory, in the case of deleting the directory. * * @param driveName - * @param objectPath complete path of object to delete + * @param objectPath complete path of object to rename */ export async function renameSingleObject( driveName: string, @@ -427,4 +499,25 @@ namespace Private { } ); } + + /** + * Helping function for copying files inside + * a directory, in the case of deleting the directory. + * + * @param driveName + * @param objectPath complete path of object to copy + */ + export async function copySingleObject( + driveName: string, + objectPath: string, + newObjectPath: string + ) { + return await requestAPI( + 'drives/' + driveName + '/' + objectPath, + 'PUT', + { + to_path: newObjectPath + } + ); + } } From cea5cc9bac777bdb3547faa0465b3a59e4c1b6f3 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Wed, 27 Nov 2024 13:34:19 +0100 Subject: [PATCH 14/23] update docstrings --- src/requests.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/requests.ts b/src/requests.ts index d5abc04..cacd29f 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -268,7 +268,10 @@ export async function deleteObjects( * Rename an object. * * @param driveName - * @param options.path The path of object. + * @param options.path The original path of object. + * @param options.newPath The new path of object. + * @param options.newFileName The name of the item to be renamed. + * @param options.registeredFileTypes The list containing all registered file types. * * @returns A promise which resolves with the contents model. */ From 103a73cae940a757896f84461cd097d5e0e0b98f Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Wed, 27 Nov 2024 19:16:30 +0100 Subject: [PATCH 15/23] iterate on saving functionality to format body --- jupyter_drives/manager.py | 26 +++++++++++++++++++++++--- src/contents.ts | 4 ++-- src/requests.ts | 17 ++++++++++------- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/jupyter_drives/manager.py b/jupyter_drives/manager.py index f047dfb..f1d12d2 100644 --- a/jupyter_drives/manager.py +++ b/jupyter_drives/manager.py @@ -8,6 +8,7 @@ import httpx import traitlets import base64 +from io import BytesIO from jupyter_server.utils import url_path_join import obstore as obs @@ -274,7 +275,7 @@ async def new_file(self, drive_name, path): } return response - async def save_file(self, drive_name, path, content): + async def save_file(self, drive_name, path, content, options_format, content_format, content_type): """Save file with new content. Args: @@ -286,8 +287,27 @@ async def save_file(self, drive_name, path, content): try: # eliminate leading and trailing backslashes path = path.strip('/') - # format body when dealing with base64 enconding or files of type PDF - formattedContent = content.encode("utf-8") + + if options_format == 'json': + formattedContent = json.dumps(content, indent=2) + formattedContent = formattedContent.encode("utf-8") + elif options_format == 'base64' and (content_format == 'base64' or content_type == 'PDF'): + # transform base64 encoding to a UTF-8 byte array for saving or storing + byte_characters = base64.b64decode(content) + + byte_arrays = [] + for offset in range(0, len(byte_characters), 512): + slice_ = byte_characters[offset:offset + 512] + byte_array = bytearray(slice_) + byte_arrays.append(byte_array) + + # combine byte arrays and wrap in a BytesIO object + formattedContent = BytesIO(b"".join(byte_arrays)) + formattedContent.seek(0) # reset cursor for any further reading + elif options_format == 'text': + formattedContent = content.encode("utf-8") + else: + formattedContent = content await obs.put_async(self._content_managers[drive_name], path, formattedContent, mode = "overwrite") metadata = await obs.head_async(self._content_managers[drive_name], path) diff --git a/src/contents.ts b/src/contents.ts index ec65e6a..569428e 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -5,7 +5,7 @@ import { PathExt } from '@jupyterlab/coreutils'; import { IDriveInfo, IRegisteredFileTypes } from './token'; import { - saveFile, + saveObject, getContents, mountDrive, createObject, @@ -558,7 +558,7 @@ export class Drive implements Contents.IDrive { ? localPath.substring(localPath.indexOf('/') + 1) : ''; - const data = await saveFile(currentDrive.name, { + const data = await saveObject(currentDrive.name, { path: relativePath, param: options, registeredFileTypes: this._registeredFileTypes diff --git a/src/requests.ts b/src/requests.ts index cacd29f..4d7cc43 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -151,7 +151,7 @@ export async function getContents( * * @returns A promise which resolves with the contents model. */ -export async function saveFile( +export async function saveObject( driveName: string, options: { path: string; @@ -159,19 +159,22 @@ export async function saveFile( registeredFileTypes: IRegisteredFileTypes; } ) { + const [fileType, fileMimeType, fileFormat] = getFileType( + PathExt.extname(PathExt.basename(options.path)), + options.registeredFileTypes + ); + const response = await requestAPI( 'drives/' + driveName + '/' + options.path, 'PUT', { - content: options.param.content + content: options.param.content, + options_format: options.param.format, + content_format: fileFormat, + content_type: fileType } ); - const [fileType, fileMimeType, fileFormat] = getFileType( - PathExt.extname(PathExt.basename(options.path)), - options.registeredFileTypes - ); - data = { name: PathExt.basename(options.path), path: PathExt.join(driveName, options.path), From cf31d43ee7dfd7f22cdc99bc8b825a2c0b49e045 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Wed, 27 Nov 2024 19:26:51 +0100 Subject: [PATCH 16/23] check if operations are done outside of a drive --- src/contents.ts | 229 ++++++++++++++++++++++++++---------------------- 1 file changed, 124 insertions(+), 105 deletions(-) diff --git a/src/contents.ts b/src/contents.ts index 569428e..59507c0 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -285,7 +285,6 @@ export class Drive implements Contents.IDrive { * @returns A promise which resolves with the created file content when the * file is created. */ - async newUntitled( options: Contents.ICreateOptions = {} ): Promise { @@ -395,29 +394,30 @@ export class Drive implements Contents.IDrive { * * @returns A promise which resolves when the file is deleted. */ - /*delete(path: string): Promise { - return Promise.reject('Repository is read only'); - }*/ - async delete(localPath: string): Promise { - // extract current drive name - const currentDrive = this._drivesList.filter( - x => - x.name === - (localPath.indexOf('/') !== -1 - ? localPath.substring(0, localPath.indexOf('/')) - : localPath) - )[0]; - - // eliminate drive name from path - const relativePath = - localPath.indexOf('/') !== -1 - ? localPath.substring(localPath.indexOf('/') + 1) - : ''; - - await deleteObjects(currentDrive.name, { - path: relativePath - }); + if (localPath !== '') { + // extract current drive name + const currentDrive = this._drivesList.filter( + x => + x.name === + (localPath.indexOf('/') !== -1 + ? localPath.substring(0, localPath.indexOf('/')) + : localPath) + )[0]; + + // eliminate drive name from path + const relativePath = + localPath.indexOf('/') !== -1 + ? localPath.substring(localPath.indexOf('/') + 1) + : ''; + + await deleteObjects(currentDrive.name, { + path: relativePath + }); + } else { + // create new element at root would mean modifying a drive + console.warn('Operation not supported.'); + } this._fileChanged.emit({ type: 'delete', @@ -444,43 +444,48 @@ export class Drive implements Contents.IDrive { newLocalPath: string, options: Contents.ICreateOptions = {} ): Promise { - // extract current drive name - const currentDrive = this._drivesList.filter( - x => - x.name === - (oldLocalPath.indexOf('/') !== -1 - ? oldLocalPath.substring(0, oldLocalPath.indexOf('/')) - : oldLocalPath) - )[0]; - - // eliminate drive name from path - const relativePath = - oldLocalPath.indexOf('/') !== -1 - ? oldLocalPath.substring(oldLocalPath.indexOf('/') + 1) - : ''; - const newRelativePath = - newLocalPath.indexOf('/') !== -1 - ? newLocalPath.substring(newLocalPath.indexOf('/') + 1) - : ''; - - // extract new file name - let newFileName = PathExt.basename(newLocalPath); - - try { - // check if object with chosen name already exists - await checkObject(currentDrive.name, { - path: newLocalPath - }); - newFileName = await this.incrementName(newLocalPath, this._name); - } catch (error) { - // HEAD request failed for this file name, continue, as name doesn't already exist. - } finally { - data = await renameObjects(currentDrive.name, { - path: relativePath, - newPath: newRelativePath, - newFileName: newFileName, - registeredFileTypes: this._registeredFileTypes - }); + if (oldLocalPath !== '') { + // extract current drive name + const currentDrive = this._drivesList.filter( + x => + x.name === + (oldLocalPath.indexOf('/') !== -1 + ? oldLocalPath.substring(0, oldLocalPath.indexOf('/')) + : oldLocalPath) + )[0]; + + // eliminate drive name from path + const relativePath = + oldLocalPath.indexOf('/') !== -1 + ? oldLocalPath.substring(oldLocalPath.indexOf('/') + 1) + : ''; + const newRelativePath = + newLocalPath.indexOf('/') !== -1 + ? newLocalPath.substring(newLocalPath.indexOf('/') + 1) + : ''; + + // extract new file name + let newFileName = PathExt.basename(newLocalPath); + + try { + // check if object with chosen name already exists + await checkObject(currentDrive.name, { + path: newLocalPath + }); + newFileName = await this.incrementName(newLocalPath, this._name); + } catch (error) { + // HEAD request failed for this file name, continue, as name doesn't already exist. + } finally { + data = await renameObjects(currentDrive.name, { + path: relativePath, + newPath: newRelativePath, + newFileName: newFileName, + registeredFileTypes: this._registeredFileTypes + }); + } + } else { + // create new element at root would mean modifying a drive + console.warn('Operation not supported.'); } Contents.validateContentsModel(data); @@ -543,26 +548,31 @@ export class Drive implements Contents.IDrive { localPath: string, options: Partial = {} ): Promise { - // extract current drive name - const currentDrive = this._drivesList.filter( - x => - x.name === - (localPath.indexOf('/') !== -1 - ? localPath.substring(0, localPath.indexOf('/')) - : localPath) - )[0]; - - // eliminate drive name from path - const relativePath = - localPath.indexOf('/') !== -1 - ? localPath.substring(localPath.indexOf('/') + 1) - : ''; - - const data = await saveObject(currentDrive.name, { - path: relativePath, - param: options, - registeredFileTypes: this._registeredFileTypes - }); + if (localPath !== '') { + // extract current drive name + const currentDrive = this._drivesList.filter( + x => + x.name === + (localPath.indexOf('/') !== -1 + ? localPath.substring(0, localPath.indexOf('/')) + : localPath) + )[0]; + + // eliminate drive name from path + const relativePath = + localPath.indexOf('/') !== -1 + ? localPath.substring(localPath.indexOf('/') + 1) + : ''; + + data = await saveObject(currentDrive.name, { + path: relativePath, + param: options, + registeredFileTypes: this._registeredFileTypes + }); + } else { + // create new element at root would mean modifying a drive + console.warn('Operation not supported.'); + } Contents.validateContentsModel(data); this._fileChanged.emit({ @@ -625,32 +635,41 @@ export class Drive implements Contents.IDrive { toDir: string, options: Contents.ICreateOptions = {} ): Promise { - // extract current drive name - const currentDrive = this._drivesList.filter( - x => - x.name === - (path.indexOf('/') !== -1 ? path.substring(0, path.indexOf('/')) : path) - )[0]; - - // eliminate drive name from path - const relativePath = - path.indexOf('/') !== -1 ? path.substring(path.indexOf('/') + 1) : ''; - const toRelativePath = - toDir.indexOf('/') !== -1 ? toDir.substring(toDir.indexOf('/') + 1) : ''; - - // construct new file or directory name for the copy - const newFileName = await this.incrementCopyName( - relativePath, - toRelativePath, - currentDrive.name - ); + if (path !== '') { + // extract current drive name + const currentDrive = this._drivesList.filter( + x => + x.name === + (path.indexOf('/') !== -1 + ? path.substring(0, path.indexOf('/')) + : path) + )[0]; - data = await copyObjects(currentDrive.name, { - path: relativePath, - toPath: toRelativePath, - newFileName: newFileName, - registeredFileTypes: this._registeredFileTypes - }); + // eliminate drive name from path + const relativePath = + path.indexOf('/') !== -1 ? path.substring(path.indexOf('/') + 1) : ''; + const toRelativePath = + toDir.indexOf('/') !== -1 + ? toDir.substring(toDir.indexOf('/') + 1) + : ''; + + // construct new file or directory name for the copy + const newFileName = await this.incrementCopyName( + relativePath, + toRelativePath, + currentDrive.name + ); + + data = await copyObjects(currentDrive.name, { + path: relativePath, + toPath: toRelativePath, + newFileName: newFileName, + registeredFileTypes: this._registeredFileTypes + }); + } else { + // create new element at root would mean modifying a drive + console.warn('Operation not supported.'); + } this._fileChanged.emit({ type: 'new', From a4e53506b88a41c3d8e73ba092fd9039ef268878 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Wed, 27 Nov 2024 19:37:15 +0100 Subject: [PATCH 17/23] remove log of mounted error --- src/contents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contents.ts b/src/contents.ts index 59507c0..1ad2574 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -226,7 +226,7 @@ export class Drive implements Contents.IDrive { }); this._drivesList.filter(x => x.name === localPath)[0].mounted = true; } catch (e) { - console.log(e); + // it will give an error if drive is already mounted } } From 56ef5414a6943f8fc5f007c2880741e95d750774 Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Wed, 27 Nov 2024 19:45:04 +0100 Subject: [PATCH 18/23] wrap deleting object in try and catch block --- src/requests.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/requests.ts b/src/requests.ts index 4d7cc43..d78e043 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -263,8 +263,12 @@ export async function deleteObjects( }) ); } - // always deleting the object (file or main directory) - return Private.deleteSingleObject(driveName, options.path); + try { + // always deleting the object (file or main directory) + return Private.deleteSingleObject(driveName, options.path); + } catch (error) { + // deleting failed if directory didn't exist and was only part of a path + } } /** From 76516be7a4d3d4010cd6cccf4e2cd06aff4ccf6d Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Wed, 27 Nov 2024 21:40:57 +0100 Subject: [PATCH 19/23] refactor code to use helping functions --- src/contents.ts | 116 ++++++++++++------------------------------------ src/token.ts | 23 ++++++++++ 2 files changed, 51 insertions(+), 88 deletions(-) diff --git a/src/contents.ts b/src/contents.ts index 1ad2574..948ee23 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -3,7 +3,12 @@ import { Signal, ISignal } from '@lumino/signaling'; import { Contents, ServerConnection } from '@jupyterlab/services'; import { PathExt } from '@jupyterlab/coreutils'; -import { IDriveInfo, IRegisteredFileTypes } from './token'; +import { + extractCurrentDrive, + formatPath, + IDriveInfo, + IRegisteredFileTypes +} from './token'; import { saveObject, getContents, @@ -206,16 +211,8 @@ export class Drive implements Contents.IDrive { localPath: string, options?: Contents.IFetchOptions ): Promise { - let relativePath = ''; if (localPath !== '') { - // extract current drive name - const currentDrive = this._drivesList.filter( - x => - x.name === - (localPath.indexOf('/') !== -1 - ? localPath.substring(0, localPath.indexOf('/')) - : localPath) - )[0]; + const currentDrive = extractCurrentDrive(localPath, this._drivesList); // when accessed the first time, mount drive if (currentDrive.mounted === false) { @@ -230,14 +227,8 @@ export class Drive implements Contents.IDrive { } } - // eliminate drive name from path - relativePath = - localPath.indexOf('/') !== -1 - ? localPath.substring(localPath.indexOf('/') + 1) - : ''; - data = await getContents(currentDrive.name, { - path: relativePath, + path: formatPath(localPath), registeredFileTypes: this._registeredFileTypes }); } else { @@ -291,14 +282,7 @@ export class Drive implements Contents.IDrive { const path = options.path ?? ''; if (path !== '') { - // extract current drive name - const currentDrive = this._drivesList.filter( - x => - x.name === - (path.indexOf('/') !== -1 - ? path.substring(0, path.indexOf('/')) - : path) - )[0]; + const currentDrive = extractCurrentDrive(path, this._drivesList); // eliminate drive name from path const relativePath = @@ -396,23 +380,10 @@ export class Drive implements Contents.IDrive { */ async delete(localPath: string): Promise { if (localPath !== '') { - // extract current drive name - const currentDrive = this._drivesList.filter( - x => - x.name === - (localPath.indexOf('/') !== -1 - ? localPath.substring(0, localPath.indexOf('/')) - : localPath) - )[0]; - - // eliminate drive name from path - const relativePath = - localPath.indexOf('/') !== -1 - ? localPath.substring(localPath.indexOf('/') + 1) - : ''; + const currentDrive = extractCurrentDrive(localPath, this._drivesList); await deleteObjects(currentDrive.name, { - path: relativePath + path: formatPath(localPath) }); } else { // create new element at root would mean modifying a drive @@ -445,34 +416,27 @@ export class Drive implements Contents.IDrive { options: Contents.ICreateOptions = {} ): Promise { if (oldLocalPath !== '') { - // extract current drive name - const currentDrive = this._drivesList.filter( - x => - x.name === - (oldLocalPath.indexOf('/') !== -1 - ? oldLocalPath.substring(0, oldLocalPath.indexOf('/')) - : oldLocalPath) - )[0]; + const currentDrive = extractCurrentDrive(oldLocalPath, this._drivesList); // eliminate drive name from path - const relativePath = - oldLocalPath.indexOf('/') !== -1 - ? oldLocalPath.substring(oldLocalPath.indexOf('/') + 1) - : ''; - const newRelativePath = - newLocalPath.indexOf('/') !== -1 - ? newLocalPath.substring(newLocalPath.indexOf('/') + 1) - : ''; + const relativePath = formatPath(oldLocalPath); + const newRelativePath = formatPath(newLocalPath); + console.log('rel: ', relativePath); + console.log('new: ', newRelativePath); // extract new file name - let newFileName = PathExt.basename(newLocalPath); + let newFileName = PathExt.basename(newRelativePath); try { // check if object with chosen name already exists await checkObject(currentDrive.name, { - path: newLocalPath + path: newRelativePath }); - newFileName = await this.incrementName(newLocalPath, this._name); + newFileName = await this.incrementName( + newRelativePath, + currentDrive.name + ); + console.log(newFileName); } catch (error) { // HEAD request failed for this file name, continue, as name doesn't already exist. } finally { @@ -549,23 +513,10 @@ export class Drive implements Contents.IDrive { options: Partial = {} ): Promise { if (localPath !== '') { - // extract current drive name - const currentDrive = this._drivesList.filter( - x => - x.name === - (localPath.indexOf('/') !== -1 - ? localPath.substring(0, localPath.indexOf('/')) - : localPath) - )[0]; - - // eliminate drive name from path - const relativePath = - localPath.indexOf('/') !== -1 - ? localPath.substring(localPath.indexOf('/') + 1) - : ''; + const currentDrive = extractCurrentDrive(localPath, this._drivesList); data = await saveObject(currentDrive.name, { - path: relativePath, + path: formatPath(localPath), param: options, registeredFileTypes: this._registeredFileTypes }); @@ -636,22 +587,11 @@ export class Drive implements Contents.IDrive { options: Contents.ICreateOptions = {} ): Promise { if (path !== '') { - // extract current drive name - const currentDrive = this._drivesList.filter( - x => - x.name === - (path.indexOf('/') !== -1 - ? path.substring(0, path.indexOf('/')) - : path) - )[0]; + const currentDrive = extractCurrentDrive(path, this._drivesList); // eliminate drive name from path - const relativePath = - path.indexOf('/') !== -1 ? path.substring(path.indexOf('/') + 1) : ''; - const toRelativePath = - toDir.indexOf('/') !== -1 - ? toDir.substring(toDir.indexOf('/') + 1) - : ''; + const relativePath = formatPath(path); + const toRelativePath = formatPath(toDir); // construct new file or directory name for the copy const newFileName = await this.incrementCopyName( diff --git a/src/token.ts b/src/token.ts index 9f0137a..6ea725f 100644 --- a/src/token.ts +++ b/src/token.ts @@ -78,3 +78,26 @@ export function getFileType( return [fileType, fileMimetype, fileFormat]; } + +/** + * Helping function to extract current drive. + * @param path + * @param drivesList + * @returns current drive + */ +export function extractCurrentDrive(path: string, drivesList: IDriveInfo[]) { + return drivesList.filter( + x => + x.name === + (path.indexOf('/') !== -1 ? path.substring(0, path.indexOf('/')) : path) + )[0]; +} + +/** + * Helping function to eliminate drive name from path + * @param path + * @returns fornatted path without drive name + */ +export function formatPath(path: string) { + return path.indexOf('/') !== -1 ? path.substring(path.indexOf('/') + 1) : ''; +} From d7562eb0eae78cf369fd3cc7db09eaf5a01442eb Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Wed, 27 Nov 2024 21:42:15 +0100 Subject: [PATCH 20/23] iterate on check object functionality --- jupyter_drives/manager.py | 12 +++++------- src/requests.ts | 10 +++------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/jupyter_drives/manager.py b/jupyter_drives/manager.py index f1d12d2..147a905 100644 --- a/jupyter_drives/manager.py +++ b/jupyter_drives/manager.py @@ -387,19 +387,17 @@ async def check_file(self, drive_name, path): drive_name: name of drive where object exists path: path where content is located """ - result = False try: # eliminate leading and trailing backslashes path = path.strip('/') await obs.head_async(self._content_managers[drive_name], path) - result = True except Exception: - pass + raise tornado.web.HTTPError( + status_code= httpx.codes.NOT_FOUND, + reason="Object does not already exist within drive.", + ) - response = { - result: result - } - return response + return async def copy_file(self, drive_name, path, to_path): """Save file with new content. diff --git a/src/requests.ts b/src/requests.ts index d78e043..242157c 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -431,12 +431,7 @@ export async function checkObject( path: string; } ) { - const response = await requestAPI( - 'drives/' + driveName + '/' + options.path, - 'HEAD' - ); - - return response.result; + await requestAPI('drives/' + driveName + '/' + options.path, 'HEAD'); } /** @@ -454,9 +449,10 @@ export const countObjectNameAppearances = async ( originalName: string ): Promise => { let counter: number = 0; + path = path.substring(0, path.lastIndexOf('/')); const response = await requestAPI( - 'drives/' + driveName + '/' + path.substring(0, path.lastIndexOf('/')), + 'drives/' + driveName + '/' + path, 'GET' ); From dfafbf582faf67ae134707f488be1fecb94cd67b Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Wed, 27 Nov 2024 21:43:54 +0100 Subject: [PATCH 21/23] remove dev comments --- src/contents.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/contents.ts b/src/contents.ts index 948ee23..67f140f 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -421,8 +421,6 @@ export class Drive implements Contents.IDrive { // eliminate drive name from path const relativePath = formatPath(oldLocalPath); const newRelativePath = formatPath(newLocalPath); - console.log('rel: ', relativePath); - console.log('new: ', newRelativePath); // extract new file name let newFileName = PathExt.basename(newRelativePath); @@ -436,7 +434,6 @@ export class Drive implements Contents.IDrive { newRelativePath, currentDrive.name ); - console.log(newFileName); } catch (error) { // HEAD request failed for this file name, continue, as name doesn't already exist. } finally { From ed8fa3730bc2063b3e21839700ab57ff89fa2db0 Mon Sep 17 00:00:00 2001 From: Denisa Checiu <91504950+DenisaCG@users.noreply.github.com> Date: Thu, 28 Nov 2024 18:13:17 +0100 Subject: [PATCH 22/23] Update docstrings and variable names Co-authored-by: Afshin Taylor Darian --- jupyter_drives/manager.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/jupyter_drives/manager.py b/jupyter_drives/manager.py index 147a905..59aa50a 100644 --- a/jupyter_drives/manager.py +++ b/jupyter_drives/manager.py @@ -247,7 +247,6 @@ async def new_file(self, drive_name, path): Args: drive_name: name of drive where the new content is created path: path where new content should be created - content: content of object """ data = {} try: @@ -289,8 +288,8 @@ async def save_file(self, drive_name, path, content, options_format, content_for path = path.strip('/') if options_format == 'json': - formattedContent = json.dumps(content, indent=2) - formattedContent = formattedContent.encode("utf-8") + formatted_content = json.dumps(content, indent=2) + formatted_content = formatted_content.encode("utf-8") elif options_format == 'base64' and (content_format == 'base64' or content_type == 'PDF'): # transform base64 encoding to a UTF-8 byte array for saving or storing byte_characters = base64.b64decode(content) @@ -302,14 +301,14 @@ async def save_file(self, drive_name, path, content, options_format, content_for byte_arrays.append(byte_array) # combine byte arrays and wrap in a BytesIO object - formattedContent = BytesIO(b"".join(byte_arrays)) - formattedContent.seek(0) # reset cursor for any further reading + formatted_content = BytesIO(b"".join(byte_arrays)) + formatted_content.seek(0) # reset cursor for any further reading elif options_format == 'text': - formattedContent = content.encode("utf-8") + formatted_content = content.encode("utf-8") else: - formattedContent = content + formatted_content = content - await obs.put_async(self._content_managers[drive_name], path, formattedContent, mode = "overwrite") + await obs.put_async(self._content_managers[drive_name], path, formatted_content, mode = "overwrite") metadata = await obs.head_async(self._content_managers[drive_name], path) data = { @@ -335,6 +334,7 @@ async def rename_file(self, drive_name, path, new_path): Args: drive_name: name of drive where file is located path: path of file + new_path: path of new file name """ data = {} try: From 75d5d871116ac47b24b1435fd50156daf9cf133a Mon Sep 17 00:00:00 2001 From: DenisaCG Date: Thu, 28 Nov 2024 18:18:53 +0100 Subject: [PATCH 23/23] update docstring of save backend function --- jupyter_drives/manager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jupyter_drives/manager.py b/jupyter_drives/manager.py index 59aa50a..90ceaaf 100644 --- a/jupyter_drives/manager.py +++ b/jupyter_drives/manager.py @@ -281,6 +281,9 @@ async def save_file(self, drive_name, path, content, options_format, content_for drive_name: name of drive where file exists path: path where new content should be saved content: content of object + options_format: format of content (as sent through contents manager request) + content_format: format of content (as defined by the registered file formats in JupyterLab) + content_type: type of content (as defined by the registered file types in JupyterLab) """ data = {} try: