diff --git a/libraries/Files/Directory.ts b/libraries/Files/Directory.ts new file mode 100644 index 0000000000..7f8138bddc --- /dev/null +++ b/libraries/Files/Directory.ts @@ -0,0 +1,109 @@ +import { Item } from './abstracts/Item.abstract' +import { FileSystemErrors } from './errors/Errors' +import { DIRECTORY_TYPE } from './types/directory' + +export class Directory extends Item { + private _type: DIRECTORY_TYPE.DEFAULT + private _children = new Map() + + /** + * @param {string=''} name Name of the new directory + * @param {DIRECTORY_TYPE=DIRECTORY_TYPE.DEFAULT} type directory type of the new folder + * @returns {Directory} + */ + constructor(name: string = '', type: DIRECTORY_TYPE = DIRECTORY_TYPE.DEFAULT) { + super(name || 'un-named directory') + this._type = DIRECTORY_TYPE[type] ? type : DIRECTORY_TYPE.DEFAULT + } + + /** + * @getter content + * @returns {Array} containing directory contents + */ + get content(): Array { + return Array.from(this._children.values()) + } + /** + * @getter type + * @returns {DIRECTORY_TYPE} returns the type of directory + */ + get type(): DIRECTORY_TYPE { + return this._type + } + + /** + * @getter + * @returns {Directory} returns a cloned copy of this directory + */ + get copy(): Directory { + const dirCopy = new Directory(`${this.name} copy`, this.type) + + this.content.forEach(item => { + const itemCopy = item.copy + itemCopy.name = item.name + dirCopy.addChild(itemCopy) + }) + + return dirCopy + } + + /** + * @method hasChild + * @param {string} childName the name of the child to search for + * @returns {boolean} returns true or false depending on if a child exists in the directory + */ + hasChild(childName: string): boolean { + return this._children.has(childName) + } + + /** + * @method addChild + * @param {Item} child the child to add to the parent directory + * @returns {boolean} returns true or false depending on if a child exists in the directory + */ + addChild(child: Item): boolean { + if (this.hasChild(child.name)) return false + + if( child === this) throw new Error(FileSystemErrors.DIR_PARADOX) + + let parent = this.parent + + while (parent !== null) { + if (parent === child) + throw new Error(FileSystemErrors.DIR_PARENT_PARADOX) + parent = parent.parent + } + + this._children.set(child.name, child) + child.parent = this + + return this.hasChild(child.name) + } + + + /** + * @method getChild + * @param {string} childName the name of the child to fetch + * @returns {Item} returns the child if it exists, otherwise returns null + */ + getChild(childName: string): Item { + return this._children.get(childName) || null + } + + /** + * @method removeChild + * @param {string} childName the name of the child to remove + * @returns {Item} returns true if the child has been successfully removed + */ + removeChild(childName: string): boolean { + if (this.getChild(childName) === null) return false + const child = this.getChild(childName) + + if (child) { + this._children.delete(childName) + child.parent = null + } + + return !this.hasChild(childName) + } +} diff --git a/libraries/Files/Fil.ts b/libraries/Files/Fil.ts new file mode 100644 index 0000000000..17776cfe35 --- /dev/null +++ b/libraries/Files/Fil.ts @@ -0,0 +1,63 @@ +import { Item } from "./abstracts/Item.abstract" + +import { FILE_TYPE } from './types/file' + +export class Fil extends Item { + private _type = FILE_TYPE.GENERIC + private _hash: string = '' + private _description: string = '' + + /** + * Create a new file instance + * @constructor + * @param {string} name - Name of the file. + * @param {string} description - Short description or associated text for the file + * @param {string} hash - Hash location of the file on chain (Usually IPFS Hash) + */ + constructor(name: string = '', description: string = '', hash: string = '') { + super(name || 'un-named file') + this._description = description + this._hash = hash + } + + /** + * Get the file description + * @getter + */ + get description(): string { + return this._description + } + + /** + * Get the file type in plain text + * @getter + */ + get type(): FILE_TYPE { + return this._type + } + + /** + * Returns the hash of the file (usually IPFS) + * @getter + */ + get hash(): string { + return this._hash + } + + /** + * Get a new copy of the file + * @getter + */ + get copy(): Fil { + return new Fil(`${this.name} copy`, this._description, this._hash) + } + + /** + * Update the files description text + * @setter + * @param {string} content the content to set the file description to + */ + set description(content: string) { + this._description = `${content || ''}` + } +} \ No newline at end of file diff --git a/libraries/Files/FileSystem.ts b/libraries/Files/FileSystem.ts new file mode 100644 index 0000000000..2bc2cb9a4c --- /dev/null +++ b/libraries/Files/FileSystem.ts @@ -0,0 +1,402 @@ +import { Directory } from './Directory' +import { DIRECTORY_TYPE } from './types/directory' +import { Fil } from './Fil' +import { Item } from './abstracts/Item.abstract' + +export class FileSystem { + private _self = new Directory('root') + private _currentDirectory = this._self + private _currentDirectoryPath = [this._currentDirectory] // as stack + + /** + * @getter currentDirectory + * @returns {Directory} containing the current active directory + */ + get currentDirectory(): Directory { + return this._currentDirectory + } + + /** + * @getter currentDirectoryPath + * @returns {string[]} returns string array of directory pathing usually joined by '/' + */ + get currentDirectoryPath(): string[] { + return this._currentDirectoryPath.map((dir: Directory) => dir.name) + } + + /** + * @getter root + * @returns {Directory} returns the root directory + */ + get root(): Directory { + return this._self + } + + /** + * @getter parent + * @returns null - filesystem always has no parent this is just to fit the interface + */ + get parent(): null { + return null + } + + /** + * @getter name + * @returns {string} returns own name + */ + get name(): string { + return this.root.name + } + + /** + * @getter copy + * @returns {FileSystem} Returns a copy of the entire filesystem + */ + get copy(): FileSystem { + const fsCopy = new FileSystem() + + this.root.content.forEach((item) => { + const itemCopy = item.copy + itemCopy.name = item.name + fsCopy.addChild(itemCopy) + }) + + return fsCopy + } + + /** + * @getter copy + * @returns {any[]} Returns an array of all content within the CURRENT directory + */ + get content(): any[] { + return this.currentDirectory.content + } + + /** + * @method createFile + * @argument {string} fileName name of the new file to create + * @argument {any[]} options list of additional arguments to pass to new file + * @returns {Fil | null} Returns the new file if successfully created, else null + */ + public createFile(fileName: string, ...options: any[]): Fil | null { + const newFile = new Fil(fileName, ...options) + const inserted = this.addChild(newFile) + return inserted ? newFile : null + } + + /** + * @method createDirectory + * @argument {string} dirName name of the new directory to create + * @argument {type} DIRECTORY_TYPE Default for now + * @returns {Fil | null} Returns the new directory if successfully created, else null + */ + public createDirectory(dirName: string, type = DIRECTORY_TYPE.DEFAULT): Directory | null { + const newDir = new Directory(dirName, type) + const inserted = this.currentDirectory.addChild(newDir) + return inserted ? newDir : null + } + + /** + * @method addChild + * @argument {Item} child item to add to the filesystem + * @returns {boolean} returns truthy if the child was added + */ + public addChild(child: Item): boolean { + return this.currentDirectory.addChild(child) + } + + /** + * @method getChild + * @argument {string} childName name of the child to fetch + * @returns {Directory | Item} returns directory or Fil + */ + public getChild(childName: string): Directory | Item { + return this.currentDirectory.getChild(childName) + } + + /** + * @method hasChild + * @argument {string} childName name of the child to check for + * @returns {boolean} returns truthy if child by name exists in filesystem + */ + public hasChild(childName: string): boolean { + return this.currentDirectory.hasChild(childName) + } + + /** + * @method removeChild + * @argument {string} childName name of the child to remove + * @returns {boolean} returns truthy if child was removed + */ + public removeChild(childName: string): boolean { + return this.currentDirectory.removeChild(childName) + } + + /** + * @method removeChild + * @argument {string} childName name of the child to remove + * @returns {boolean} returns truthy if child was removed + */ + public renameChild(currentName: string, newName: string): Item | null { + const item = this.getChild(currentName) + + if (item) { + item.name = newName + this.removeChild(currentName) + this.addChild(item) + return item + } + + return null + } + + /** + * @method copyChild + * @argument {string} childName name of the child to copy + * @returns {Item | null} returns newly copied child, or null if it fails + */ + public copyChild(childName: string): Item | null { + const item = this.getChild(childName) as Fil + + if (item) { + const itemCopy = item.copy + this.addChild(itemCopy) + return itemCopy + } + + return null + } + + /** + * @method printCurrentDirectory + */ + public printCurrentDirectory(): void { + console.log( + `\n[${this.currentDirectoryPath.join('/')}]:` + + (this.currentDirectory.content + .map( + (item) => + `\n[${item.constructor.name.substring(0, 1)}]-> ${item.name}`, + ) + .join('') || '\n(empty)'), + ) + } + + /** + * @method openDirectory + * This will navigate the filesystem to the directory at this path + * @argument {string} path path to navigate to + * @returns {Directory | null} returns the opened directory + */ + public openDirectory(path: string): Directory | null { + if (!path) return null + + let dir = this.getDirectoryFromPath(path as string) + if (!(dir && dir instanceof Directory)) return null + + const dirPath = [dir] + let parent = dir.parent + + while (parent) { + dirPath.unshift(parent) + parent = parent.parent + } + + this._currentDirectory = dir + this._currentDirectoryPath = dirPath + + return dir + } + + /** + * @method goBack + * This will navigate the filesystem to the directory at this path + * @argument {string} steps number of steps to go back (will go to root if too many are provided) + * @returns {Directory | null} returns the opened directory + */ + public goBack(steps: number = 1): Directory | null { + if (isNaN(steps) || steps <= 0 || steps >= this.currentDirectoryPath.length) + return null + + let dir = this.currentDirectory + let stepsMoved = steps + + while (dir && stepsMoved > 0) { + if (dir.parent) dir = dir.parent + stepsMoved -= 1 + } + + if (dir && dir !== this.currentDirectory) { + this._currentDirectory = dir + this._currentDirectoryPath = this._currentDirectoryPath.slice( + 0, + this._currentDirectoryPath.length - (steps - stepsMoved), + ) + } + + return dir + } + + /** + * @method goBackToDirectory + * Navigates to a specific directory + * @argument {string} dirName directory to navigate to + * @returns {Directory | null} returns the opened directory + */ + public goBackToDirectory(dirName: string): Directory | null { + const dirIndex = this.currentDirectoryPath.lastIndexOf( + dirName, + this.currentDirectoryPath.length - 2, + ) + + if (dirIndex < 0) return null + + const dir = + dirIndex === 0 ? this.root : this._currentDirectoryPath[dirIndex] + + this._currentDirectory = dir + this._currentDirectoryPath = this._currentDirectoryPath.slice( + 0, + dirIndex + 1, + ) + + return dir + } + + /** + * @method findItem + * Find a specific item inside the filesystem + */ + public findItem( + itemNameOrValidatorFunc: any, + fromDirectory: Directory = this.root, + ): any { + return this.setupAndFind(itemNameOrValidatorFunc, fromDirectory) + } + + /** + * @method findAllItems + * Find a all item inside the filesystem + */ + public findAllItems( + itemNameOrValidatorFunc: any, + fromDirectory: Directory = this.root, + ): Directory | Item[] | null { + return this.setupAndFind(itemNameOrValidatorFunc, fromDirectory, true) + } + + /** + * @method moveItemTo + * move an item into a specific directory + * @argument {string} childName name of item to move + * @argument {string} dirPath name of directory to move item into + * @returns {Directory | null} directory the item has been moved to + */ + public moveItemTo(childName: string, dirPath: string): Directory | null { + const item = this.getChild(childName) + + if (item) { + const dir = this.getDirectoryFromPath(dirPath as string) + + if (dir && dir instanceof Directory) { + dir.addChild(item) + return dir + } + } + + return null + } + + /**f + * @method moveItemTo + * move an item into a specific directory + * @argument {string} childName name of item to move + * @argument {string} dirPath name of directory to move item into + * @returns {Directory | null} directory the item has been moved to + */ + private _findItem( + isItem: any, + dir: Directory, + multiple: boolean = false, + ): Item[] | null { + let match = multiple ? ([] as Item[]) : null + let directories = [] + + for (const item of dir.content) { + if (isItem(item)) { + if (multiple !== null) { + match?.push(item) + } else { + match = item + break + } + } + + if (item instanceof Directory) + directories.push(item) + } + + if ((match === null || multiple) && directories.length) { + for (const subDir of directories) { + const found = this._findItem(isItem, subDir, multiple) + if (multiple && found !== null) { + match?.push(...(found as Item[])) + } else if (found !== null) { + match = found + break + } + } + } + + return match + } + + + /** + * @method setupAndFind + * Find an item in the filesystem + */ + private setupAndFind( + itemNameOrValidatorFunc: string | CallableFunction, + fromDirectory: Directory, + multiple?: boolean, + ): Directory | null | Item[] { + if (typeof itemNameOrValidatorFunc === 'function') { + return this._findItem(itemNameOrValidatorFunc, fromDirectory, multiple) + } + + const func = (item: Item) => item.name === itemNameOrValidatorFunc + return this._findItem(func, fromDirectory, multiple) + } + + /** + * @method setupAndFind + * Get a directory given a string path to the directory + * @argument {string} dirPath string path to the directory to get + * @returns {Directory | null} returns the directory or null if it can't be found + */ + private getDirectoryFromPath (dirPath: string): Directory | null { + if (dirPath.match(/^(root\/?|\/)$/g)) + return this.root + + if (dirPath.match(/^\.\/?$/g)) + return this.currentDirectory + + let dir = dirPath.match(/^(root\/?|\/)/g) + ? this.root + : this.currentDirectory + const paths = dirPath.replace(/^(root\/|\.\/|\/)/g, '').split('/') + + while (paths.length) { + dir = dir.getChild(paths.shift() as string) as Directory + + if (!dir || !(dir instanceof Directory)) + return null + } + + if (paths.length === 0) + return dir + + return null + } +} diff --git a/libraries/Files/abstracts/Item.abstract.ts b/libraries/Files/abstracts/Item.abstract.ts new file mode 100644 index 0000000000..8b2642733b --- /dev/null +++ b/libraries/Files/abstracts/Item.abstract.ts @@ -0,0 +1,109 @@ +import { v4 as uuidv4 } from 'uuid' +import { Directory } from '../Directory' +import { FileSystemErrors } from '../errors/Errors' +import { ItemInterface } from '../interface/Item.interface' +import { DIRECTORY_TYPE } from '../types/directory' + +export abstract class Item implements ItemInterface { + private _id: string = uuidv4() + private _name: string = '' + private _parent: Directory | null | undefined = null + + /** + * Update the parent directory for this item + * @constructor + * @param {string} name - Name of the item. + * @param {Parent} parent - Optional parent of the item. + */ + constructor(name: string, parent?: Directory | null) { + if (this.constructor.name === 'Item') + throw new Error(FileSystemErrors.ITEM_ABSTRACT_ONLY) + + this._name = name + this._parent = parent + } + + /** + * @getter + * @returns the path of the item + */ + get path(): string { + if (this.validateParent(this.parent)) + return `${this._parent?.path}/${this._name}` + + return this._name + } + + /** + * @getter + * @returns the item name + */ + get name() { + return this._name + } + + /** + * @getter + * @returns a unique identifier for the item + */ + get id() { + return this._id + } + + /** + * @getter + * @returns the parent directory + */ + get parent() { + // Make sure we always return either null or the parent. Never undefined. + return this._parent !== undefined ? this._parent : null + } + + /** + * Validate that the parent is of the correct instance type + * @method + * @param {string} newName - The new name of the associated file. + */ + private validateParent(parent: Directory | null): boolean { + // In the future we may want shared directory types and more + return ( + parent !== null && (parent as Directory).type === DIRECTORY_TYPE.DEFAULT + ) + } + + /** + * Set a new name for this item. + * @setter + * @param {string} newName - The new name of the associated file. + */ + set name(newName: string) { + const filenameTest = new RegExp('[\\/:"*?<>|]+') + + if (!newName || typeof newName !== 'string' || !newName.trim().length) + throw new Error(FileSystemErrors.NO_EMPTY_STRING) + + if (filenameTest.test(newName)) + throw new Error(FileSystemErrors.INVALID_SYMBOL) + + if (this.validateParent(this.parent) && this.parent?.hasChild(newName)) + throw new Error(FileSystemErrors.DUPLICATE_NAME) + + this._name = newName.trim() + } + + /** + * Update the parent directory for this item + * @method + * @param {Directory} newPARENT - The new name of the associated file. + */ + set parent(newParent: Directory | null) { + if (newParent !== this._parent) { + const prevParent = this._parent + this._parent = newParent + + if (prevParent) prevParent.removeChild(this.name) + + if (newParent) newParent.addChild(this) + } + } +} diff --git a/libraries/Files/errors/Errors.ts b/libraries/Files/errors/Errors.ts new file mode 100644 index 0000000000..d402c07f4f --- /dev/null +++ b/libraries/Files/errors/Errors.ts @@ -0,0 +1,14 @@ +export enum TextileErrors { + MISSING_WALLET = 'A wallet is mandatory for using Textile methods.' +} + +export enum FileSystemErrors { + METHOD_MISSING = 'Method not implemented.', + RFM_ABSTRACT_ONLY = 'RFM class is Abstract. It can only be extended', + ITEM_ABSTRACT_ONLY = 'Item class is Abstract. It can only be extended', + NO_EMPTY_STRING = 'Item name must be a non empty string', + INVALID_SYMBOL = 'Item name contains invalid symbol', + DUPLICATE_NAME = 'Item with name already exists in this directory', + DIR_PARADOX = 'Directory cannot contain itself', + DIR_PARENT_PARADOX = 'Directory cannot contain one of its ancestors', +} \ No newline at end of file diff --git a/libraries/Files/interface/Item.interface.ts b/libraries/Files/interface/Item.interface.ts new file mode 100644 index 0000000000..e198f1b015 --- /dev/null +++ b/libraries/Files/interface/Item.interface.ts @@ -0,0 +1,8 @@ +import { Directory } from "../Directory" + +export interface ItemInterface { + path: string + name: string + id: string + parent: Directory | null +} \ No newline at end of file diff --git a/libraries/Files/remote/abstracts/RFM.abstract.ts b/libraries/Files/remote/abstracts/RFM.abstract.ts new file mode 100644 index 0000000000..8f6e95a016 --- /dev/null +++ b/libraries/Files/remote/abstracts/RFM.abstract.ts @@ -0,0 +1,29 @@ +// Remote file management +import { Item } from "../../abstracts/Item.abstract" +import { FileSystemErrors } from "../../errors/Errors" +import { Fil } from "../../Fil" +import { RFMInterface } from "../interface/RFM.interface" + +export abstract class RFM implements RFMInterface{ + private _fileSystem: FileSystem + + constructor(fileSystem: FileSystem) { + if (this.constructor.name === 'RFM') + throw new Error(FileSystemErrors.RFM_ABSTRACT_ONLY) + + this._fileSystem = fileSystem + } + updateIndex(index: FileSystem): void { + throw new Error(FileSystemErrors.METHOD_MISSING) + } + getIndex(): Promise { + throw new Error(FileSystemErrors.METHOD_MISSING) + } + delete(file: Fil): boolean { + throw new Error(FileSystemErrors.METHOD_MISSING) + } + + upload(file: File, name: string, meta: any): string { + throw new Error(FileSystemErrors.METHOD_MISSING) + } +} \ No newline at end of file diff --git a/libraries/Files/remote/interface/RFM.interface.ts b/libraries/Files/remote/interface/RFM.interface.ts new file mode 100644 index 0000000000..55b7dac950 --- /dev/null +++ b/libraries/Files/remote/interface/RFM.interface.ts @@ -0,0 +1,9 @@ +import { Item } from "../../abstracts/Item.abstract" +import { Fil } from "../../Fil" + +export interface RFMInterface { + updateIndex(index: FileSystem): void + getIndex(): Array | Promise> + upload(file: File, name: string, meta: any): string + delete(file: Fil): boolean +} \ No newline at end of file diff --git a/libraries/Files/remote/textile/Bucket.ts b/libraries/Files/remote/textile/Bucket.ts new file mode 100644 index 0000000000..dc1c10cb5d --- /dev/null +++ b/libraries/Files/remote/textile/Bucket.ts @@ -0,0 +1,51 @@ +import { RFM } from "../abstracts/RFM.abstract" +import { RFMInterface } from "../interface/RFM.interface" + +import { TextileConfig, TextileInitializationData } from '~/types/textile/manager' +import IdentityManager from '~/libraries/Textile/IdentityManager' +import { TextileErrors } from '../../errors/Errors' + +export class Bucket extends RFM implements RFMInterface { + private creds: { id: any, pass: any } = { id: null, pass: null } + private identityManager: IdentityManager + private _textile: TextileInitializationData | null = null + + constructor(fileSystem: FileSystem) { + super(fileSystem) + this.identityManager = new IdentityManager() + } + + get textile(): TextileInitializationData | null { + return this._textile + } + + /** + * @method + * Initialization function that creates a Textile identity + * and initializes the Mailbox + * @param param0 Textile Configuration that includes id, password and SolanaWallet instance + * @returns a promise that resolves when the initialization completes + */ + async init({ id, pass, wallet }: TextileConfig): Promise { + if (!wallet) { + throw new Error(TextileErrors.MISSING_WALLET) + } + + const identity = await this.identityManager.initFromWallet(wallet) + const { client, users } = await this.identityManager.authorize(identity) + + this.creds = { + id, + pass, + } + + this._textile = { + identity, + client, + wallet, + users, + } + + return this._textile + } +} \ No newline at end of file diff --git a/libraries/Files/test/Directory.test.ts b/libraries/Files/test/Directory.test.ts new file mode 100644 index 0000000000..6da9839a91 --- /dev/null +++ b/libraries/Files/test/Directory.test.ts @@ -0,0 +1,46 @@ +import { Directory } from '../Directory' +import { Fil } from '../Fil' +import { DIRECTORY_TYPE } from '../types/directory' + +describe('Test FileSystem Directory', () => { + const mockFileData = { + name: 'TestFile.png', + descrption: 'Test file description', + hash: '0x0aef', + } + + const mockDirectoryData = { + name: 'Test Directory', + type: DIRECTORY_TYPE.DEFAULT + } + + const file = new Fil(...Object.values(mockFileData)) + const directory = new Directory(...Object.values(mockDirectoryData)) + + it('Correctly returns a directory name', () => + expect(directory.name).toEqual(mockDirectoryData.name)) + it('Correctly returns a directory type', () => + expect(directory.type).toEqual(mockDirectoryData.type)) + it('Correctly adds a child file or folder', () => { + directory.addChild(file) + expect(directory.hasChild(file.name)).toBe(true) + + const child = directory.getChild(file.name) + expect(child.id).toEqual(file.id) + }) + it('Correctly displays content', () => { + const content = directory.content + expect(content).not.toEqual(null) + }) + it('Correctly clones a folder', () => { + const clone = directory.copy + expect(clone.hasChild(file.name)).toBe(true) + }) + it('Correctly removes a child file or folder', () => { + directory.removeChild(file.name) + expect(directory.hasChild(file.name)).toBe(false) + + const child = directory.getChild(file.name) + expect(child).toBe(null) + }) +}) diff --git a/libraries/Files/test/Fil.test.ts b/libraries/Files/test/Fil.test.ts new file mode 100644 index 0000000000..1dfb8f35c3 --- /dev/null +++ b/libraries/Files/test/Fil.test.ts @@ -0,0 +1,26 @@ +import { Fil } from '../Fil' +import { FILE_TYPE } from '../types/file' + +describe('Test FileSystem File', () => { + const mockFileData = { + name: 'TestFile.png', + description: 'Test file description', + hash: '0x0aef', + } + + const file = new Fil(...Object.values(mockFileData)) + + it(`Correctly returns a file name (${mockFileData.name})`, () => + expect(file.name).toEqual(mockFileData.name)) + it(`Correctly returns a file description (${mockFileData.description})`, () => + expect(file.description).toEqual(mockFileData.description)) + it(`Correctly returns a file type (${FILE_TYPE.GENERIC})`, () => + expect(file.type).toEqual(FILE_TYPE.GENERIC)) + it(`Correctly returns a file hash (${mockFileData.hash})`, () => + expect(file.hash).toEqual(mockFileData.hash)) + it('Correctly clones a file', () => { + const clonedFile: Fil = file.copy + expect(clonedFile.hash).toEqual(file.hash) + expect(clonedFile.id).not.toEqual(file.id) + }) +}) diff --git a/libraries/Files/test/FileSystem.test.ts b/libraries/Files/test/FileSystem.test.ts new file mode 100644 index 0000000000..6b72f65d8f --- /dev/null +++ b/libraries/Files/test/FileSystem.test.ts @@ -0,0 +1,49 @@ +import { Directory } from '../Directory' +import { Fil } from '../Fil' +import { FileSystem } from '../FileSystem' +import { DIRECTORY_TYPE } from '../types/directory' + +const mockFileData = { + name: 'TestFile.png', + descrption: 'Test file description', + hash: '0x0aef', +} +const mockDirectoryData = { + name: 'Test Directory', + type: DIRECTORY_TYPE.DEFAULT +} +const mockFileSystemData = { + name: 'root', +} + +describe('Test FileSystem', () => { + const filesystem = new FileSystem() + const file = new Fil(...Object.values(mockFileData)) + const directory = new Directory(...Object.values(mockDirectoryData)) + directory.addChild(file) + filesystem.addChild(directory) + + it(`Correctly returns a filesystem name (${mockFileSystemData.name})`, () => + expect(filesystem.name).toEqual(mockFileSystemData.name)) + const newDirectory = filesystem.copyChild('Test Directory') + if (newDirectory) { + it(`Correctly rejects duplicate entries`, () => + expect(filesystem.addChild(newDirectory)).toBe(false)) + } + + it(`Correctly creates a new directory`, () => + expect(filesystem.createDirectory('test_dir_3')).not.toBe(null)) + it(`Correctly creates a new file`, () => + expect(filesystem.createFile('test_fil')).not.toBe(null)) + it(`Correctly deletes a directory`, () => + expect(filesystem.removeChild('test_dir_3')).toBe(true)) + it(`Correctly deletes a file`, () => + expect(filesystem.removeChild('test_fil')).toBe(true)) + + it(`Correctly renames a child`, () => { + filesystem.createFile('test_fil') + filesystem.renameChild('test_fil', 'test_fil_rename') + expect(filesystem.hasChild('test_fil')).toBe(false) + expect(filesystem.hasChild('test_fil_rename')).toBe(true) + }) +}) diff --git a/libraries/Files/types/directory.ts b/libraries/Files/types/directory.ts new file mode 100644 index 0000000000..9f6e24edee --- /dev/null +++ b/libraries/Files/types/directory.ts @@ -0,0 +1,3 @@ +export enum DIRECTORY_TYPE { + DEFAULT = 'DEFAULT' +} diff --git a/libraries/Files/types/file.ts b/libraries/Files/types/file.ts new file mode 100644 index 0000000000..7b6ba9758d --- /dev/null +++ b/libraries/Files/types/file.ts @@ -0,0 +1,9 @@ +/* Describes all supported filetypes +* this will be useful for applying icons to the tree later on +* if we don't have a filetype supported, we can just default to generic. +*/ +export const enum FILE_TYPE { + GENERIC = 'generic', + IMAGE_PNG = 'img/png', + ARCHIVE = 'archive/zip', +} \ No newline at end of file diff --git a/locales b/locales index adcafb6501..b356584e1f 160000 --- a/locales +++ b/locales @@ -1 +1 @@ -Subproject commit adcafb6501345500970fdaf72fcf9b43654f30b0 +Subproject commit b356584e1f85690481b342976752a01e37625760