diff --git a/jupyter_drives/handlers.py b/jupyter_drives/handlers.py index b01847f..7a279a7 100644 --- a/jupyter_drives/handlers.py +++ b/jupyter_drives/handlers.py @@ -84,6 +84,25 @@ async def patch(self, drive: str = "", path: 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() + 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 + 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 df0f946..90ceaaf 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 @@ -165,6 +166,9 @@ async def get_contents(self, drive_name, path): """ if path == '/': path = '' + else: + path = path.strip('/') + try : data = [] isDir = False @@ -237,23 +241,198 @@ 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 """ - 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, options_format, content_format, content_type): + """Save file with new content. + + Args: + 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: + # eliminate leading and trailing backslashes + path = path.strip('/') + + if options_format == 'json': + 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) + + 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 + formatted_content = BytesIO(b"".join(byte_arrays)) + formatted_content.seek(0) # reset cursor for any further reading + elif options_format == 'text': + formatted_content = content.encode("utf-8") + else: + formatted_content = content + + 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 = { + "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): + 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 + new_path: path of new file name """ - 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. + + 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 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 + """ + try: + # eliminate leading and trailing backslashes + path = path.strip('/') + await obs.head_async(self._content_managers[drive_name], path) + except Exception: + raise tornado.web.HTTPError( + status_code= httpx.codes.NOT_FOUND, + reason="Object does not already exist within drive.", + ) + + return + + 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, diff --git a/src/contents.ts b/src/contents.ts index 3c52da0..67f140f 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -1,8 +1,25 @@ import { JupyterFrontEnd } from '@jupyterlab/application'; import { Signal, ISignal } from '@lumino/signaling'; import { Contents, ServerConnection } from '@jupyterlab/services'; -import { IDriveInfo, IRegisteredFileTypes } from './token'; -import { getContents, mountDrive } from './requests'; +import { PathExt } from '@jupyterlab/coreutils'; + +import { + extractCurrentDrive, + formatPath, + IDriveInfo, + IRegisteredFileTypes +} from './token'; +import { + saveObject, + getContents, + mountDrive, + createObject, + checkObject, + deleteObjects, + countObjectNameAppearances, + renameObjects, + copyObjects +} from './requests'; let data: Contents.IModel = { name: '', @@ -194,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) { @@ -214,18 +223,12 @@ 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 } } - // 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 { @@ -273,30 +276,39 @@ 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 { - /*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 !== '') { + const currentDrive = extractCurrentDrive(path, this._drivesList); + + // 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({ @@ -317,6 +329,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) { @@ -363,21 +378,17 @@ 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 { - /*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; - }*/ + if (localPath !== '') { + const currentDrive = extractCurrentDrive(localPath, this._drivesList); + + await deleteObjects(currentDrive.name, { + path: formatPath(localPath) + }); + } else { + // create new element at root would mean modifying a drive + console.warn('Operation not supported.'); + } this._fileChanged.emit({ type: 'delete', @@ -404,27 +415,81 @@ 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; + if (oldLocalPath !== '') { + const currentDrive = extractCurrentDrive(oldLocalPath, this._drivesList); + + // eliminate drive name from path + const relativePath = formatPath(oldLocalPath); + const newRelativePath = formatPath(newLocalPath); + + // extract new file name + let newFileName = PathExt.basename(newRelativePath); + + try { + // check if object with chosen name already exists + await checkObject(currentDrive.name, { + path: newRelativePath + }); + newFileName = await this.incrementName( + newRelativePath, + currentDrive.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.'); } - 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; } + + /** + * 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. * @@ -444,19 +509,18 @@ 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; + if (localPath !== '') { + const currentDrive = extractCurrentDrive(localPath, this._drivesList); + + data = await saveObject(currentDrive.name, { + path: formatPath(localPath), + param: options, + registeredFileTypes: this._registeredFileTypes + }); + } else { + // create new element at root would mean modifying a drive + console.warn('Operation not supported.'); } - const data = await response.json();*/ Contents.validateContentsModel(data); this._fileChanged.emit({ @@ -468,88 +532,81 @@ 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; + if (path !== '') { + const currentDrive = extractCurrentDrive(path, this._drivesList); + + // eliminate drive name from path + const relativePath = formatPath(path); + const toRelativePath = formatPath(toDir); + + // 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.'); } - const data = await response.json();*/ this._fileChanged.emit({ type: 'new', diff --git a/src/requests.ts b/src/requests.ts index 2edb2c7..242157c 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. */ @@ -87,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, @@ -101,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), @@ -122,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, @@ -137,3 +140,390 @@ 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 saveObject( + driveName: string, + options: { + path: string; + param: Partial; + 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, + options_format: options.param.format, + content_format: fileFormat, + content_type: fileType + } + ); + + 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; +} + +/** + * 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(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; +} + +/** + * 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); + }) + ); + } + 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 + } +} + +/** + * Rename an object. + * + * @param driveName + * @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. + */ +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; +} + +/** + * 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. + * + * @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; + } +) { + await requestAPI('drives/' + driveName + '/' + options.path, 'HEAD'); +} + +/** + * 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; + path = path.substring(0, path.lastIndexOf('/')); + + const response = await requestAPI( + 'drives/' + driveName + '/' + path, + 'GET' + ); + + if (response.data && response.data.length !== 0) { + response.data.forEach((c: any) => { + const fileName = c.path.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 + * 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'); + } + + /** + * Helping function for renaming files inside + * a directory, in the case of deleting the directory. + * + * @param driveName + * @param objectPath complete path of object to rename + */ + export async function renameSingleObject( + driveName: string, + objectPath: string, + newObjectPath: string + ) { + return await requestAPI( + 'drives/' + driveName + '/' + objectPath, + 'PATCH', + { + new_path: newObjectPath + } + ); + } + + /** + * 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 + } + ); + } +} 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) : ''; +}