From 2aaf0475a03f771904cec84b101f43f135b9fdc5 Mon Sep 17 00:00:00 2001 From: ThinLiquid Date: Sun, 14 Jan 2024 19:51:08 +0000 Subject: [PATCH] =?UTF-8?q?[=F0=9F=92=A5]=20Custom=20virtual=20filesystem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 1 - package.json | 2 +- src/builtin/apps/editor.ts | 4 +- src/builtin/apps/files.ts | 190 ++++++++-------- src/builtin/apps/settings.ts | 5 +- src/builtin/apps/store.ts | 32 ++- src/filer-types.ts | 171 --------------- src/fs.ts | 414 +++++++++++++++++++++++++++++++++++ src/index.ts | 52 +---- src/instances/Flow.ts | 21 +- src/utils.ts | 3 +- 11 files changed, 546 insertions(+), 349 deletions(-) delete mode 100644 src/filer-types.ts create mode 100644 src/fs.ts diff --git a/index.html b/index.html index d1730f7..1ad808a 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,6 @@ - diff --git a/package.json b/package.json index a8def40..4fbf157 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowos", - "version": "1.0.0", + "version": "1.0.0-indev.0", "description": "The most aesthetic webOS.", "main": "src/index.ts", "scripts": { diff --git a/src/builtin/apps/editor.ts b/src/builtin/apps/editor.ts index 5f08f77..98b0f3a 100644 --- a/src/builtin/apps/editor.ts +++ b/src/builtin/apps/editor.ts @@ -146,7 +146,7 @@ export default class EditorApp implements App { const fileExtension = data.path.split('.').pop()?.toLowerCase() as string const language = fileLanguageMap[fileExtension] ?? 'text' - const value = (await window.fs.promises.readFile(data.path)).toString() + const value = Buffer.from(await window.fs.readFile(data.path)).toString() const editor = fullEditor( win.content.querySelector('.editor') as HTMLElement, { @@ -191,7 +191,7 @@ export default class EditorApp implements App { editor.extensions.searchWidget?.open() } (win.content.querySelector('#save') as HTMLElement).onclick = async () => { - await window.fs.promises.writeFile(data.path, editor.value) + await window.fs.writeFile(data.path, editor.value) } } else { await window.flow.openApp('flow.files') diff --git a/src/builtin/apps/files.ts b/src/builtin/apps/files.ts index 770e0f5..ca76156 100644 --- a/src/builtin/apps/files.ts +++ b/src/builtin/apps/files.ts @@ -3,8 +3,6 @@ import { App } from '../../types' import FlowWindow from '../../structures/FlowWindow' -import { Stats } from 'fs' - export default class FilesApp implements App { meta = { name: 'Files', @@ -26,10 +24,10 @@ export default class FilesApp implements App { win.content.style.flexDirection = 'column' async function setDir (dir: string): Promise { - await window.fs.readdir(dir, (e: NodeJS.ErrnoException, files: string[]) => { - const back = dir === '/' ? 'first_page' : 'chevron_left' + const files = await window.fs.readdir(dir) + const back = dir === '/' ? 'first_page' : 'chevron_left' - win.content.innerHTML = ` + win.content.innerHTML = `
${back}${dir}
@@ -38,110 +36,104 @@ export default class FilesApp implements App {
` - if (back !== 'first_page') { - (win.content.querySelector('.back') as HTMLElement).onclick = async () => { - if (dir.split('/')[1] === dir.replace('/', '')) { - await setDir(`/${dir.split('/')[0]}`) - } else { - await setDir(`/${dir.split('/')[1]}`) + if (back !== 'first_page') { + (win.content.querySelector('.back') as HTMLElement).onclick = async () => { + await setDir(dir.split('/').slice(0, -1).join('/')) + } + } + + (win.content.querySelector('.file') as HTMLElement).onclick = async () => { + const title: string = prompt('Enter file name') ?? 'new-file.txt' + await window.fs.writeFile(`${dir}/${title}`, '') + await setDir(dir) + } + + (win.content.querySelector('.folder') as HTMLElement).onclick = async () => { + const title: string = prompt('Enter folder name') ?? 'new-folder' + await window.fs.mkdir(`${dir}/${title}`) + await setDir(dir) + } + + for (const file of files) { + const seperator = dir === '/' ? '' : '/' + const fileStat = await window.fs.stat(dir + seperator + file) + const element = document.createElement('div') + element.setAttribute('style', 'display: flex;gap: 5px;align-items:center;padding: 5px;border-bottom: 1px solid var(--text);display:flex;align-items:center;gap: 5px;') + + const genIcon = (): string => { + switch (file.split('.').at(-1)) { + case 'js': + case 'mjs': + case 'cjs': { + return 'javascript' + } + + case 'html': + case 'htm': { + return 'html' + } + + case 'css': { + return 'css' + } + + case 'json': { + return 'code' + } + + case 'md': { + return 'markdown' + } + + case 'txt': + case 'text': { + return 'description' + } + + case 'png': + case 'apng': + case 'jpg': + case 'jpeg': + case 'gif': { + return 'image' + } + + default: { + return 'draft' } } } + const icon = fileStat.isDirectory() ? 'folder' : genIcon() - (win.content.querySelector('.file') as HTMLElement).onclick = async () => { - const title: string = prompt('Enter file name') ?? 'new-file.txt' - await window.fs.promises.open(`${dir}/${title}`, 'w') + element.innerHTML += `${icon} ${file}delete_foreveredit`; + (element.querySelector('.rename') as HTMLElement).onclick = async () => { + const value = (prompt('Rename') as string) + if (value !== null || value !== undefined) { + await window.fs.rename(dir + seperator + file, dir + seperator + value) + await setDir(dir) + } + } + (element.querySelector('.delete') as HTMLElement).onclick = async () => { + if (fileStat.isDirectory()) { + await window.fs.rmdir(dir + seperator + file) + } else { + await window.fs.unlink(dir + seperator + file) + } await setDir(dir) } - - (win.content.querySelector('.folder') as HTMLElement).onclick = async () => { - const title: string = prompt('Enter folder name') ?? 'new-folder' - await window.fs.promises.mkdir(`${dir}/${title}`) - await setDir(dir) + element.ondblclick = async () => { + if (fileStat.isDirectory()) { + await setDir(dir + seperator + file) + } else { + await window.flow.openApp('flow.editor', { path: dir + seperator + file }) + } } - for (const file of files) { - const separator = dir === '/' ? '' : '/' - window.fs.stat(dir + separator + file, (e: NodeJS.ErrnoException, fileStat: Stats) => { - const element = document.createElement('div') - element.setAttribute('style', 'display: flex;gap: 5px;align-items:center;padding: 5px;border-bottom: 1px solid var(--text);display:flex;align-items:center;gap: 5px;') - - const genIcon = (): string => { - switch (file.split('.').at(-1)) { - case 'js': - case 'mjs': - case 'cjs': { - return 'javascript' - } - - case 'html': - case 'htm': { - return 'html' - } - - case 'css': { - return 'css' - } - - case 'json': { - return 'code' - } - - case 'md': { - return 'markdown' - } - - case 'txt': - case 'text': { - return 'description' - } - - case 'png': - case 'apng': - case 'jpg': - case 'jpeg': - case 'gif': { - return 'image' - } - - default: { - return 'draft' - } - } - } - const icon = fileStat.isDirectory() ? 'folder' : genIcon() - - element.innerHTML += `${icon} ${file}delete_foreveredit`; - (element.querySelector('.rename') as HTMLElement).onclick = async () => { - const value = (prompt('Rename') as string) - if (value !== null || value !== undefined) { - await window.fs.promises.rename(dir + separator + file, dir + separator + value) - await setDir(dir) - } - } - (element.querySelector('.delete') as HTMLElement).onclick = async () => { - if (fileStat.isDirectory()) { - await window.fs.rmdir(dir + separator + file, () => {}) - } else { - await window.fs.promises.unlink(dir + separator + file) - } - await setDir(dir) - } - element.ondblclick = async () => { - if (fileStat.isDirectory()) { - await setDir(dir + separator + file) - } else { - await window.flow.openApp('flow.editor', { path: dir + separator + file }) - } - } - - win.content.querySelector('.files')?.appendChild(element) - }) - } - }) + win.content.querySelector('.files')?.appendChild(element) + } } - await setDir('/') + await setDir('/home') return win } diff --git a/src/builtin/apps/settings.ts b/src/builtin/apps/settings.ts index c911052..bee65ea 100644 --- a/src/builtin/apps/settings.ts +++ b/src/builtin/apps/settings.ts @@ -57,7 +57,8 @@ export default class SettingsApp implements App { '24HR_CLOCK': '24hr Clock' } - const config = await window.config() + // TODO: settings + const config = {} for (const key of Object.keys(config)) { const container = document.createElement('div') @@ -87,7 +88,7 @@ export default class SettingsApp implements App { } win.content.querySelector('.save')?.addEventListener('click', () => { - window.fs.promises.writeFile('/.config/flow.json', JSON.stringify(config)) + window.fs.writeFile('/.config/flow.json', JSON.stringify(config)) .then(null) .catch(e => console.error(e)) }) diff --git a/src/builtin/apps/store.ts b/src/builtin/apps/store.ts index 68f4769..5fc55a8 100644 --- a/src/builtin/apps/store.ts +++ b/src/builtin/apps/store.ts @@ -23,9 +23,8 @@ export default class MusicApp implements App { win.content.style.background = 'var(--base)' - const config = await window.config() - - fetch(config.SERVER_URL + '/apps/list/') + // TODO: Allow customization of server URL + fetch('https://server.flow-works.me' + '/apps/list/') .then(async (res) => await res.json()) .then(handle) .catch(e => console.error(e)) @@ -61,38 +60,33 @@ export default class MusicApp implements App {
` - window.fs.exists(`/Applications/${app.url.split('/').at(-1) as string}`, (exists) => { + window.fs.exists(`/home/Applications/${app.url.split('/').at(-1) as string}`).then((exists) => { if (exists) { (win.content.querySelector(`div[data-pkg="${sanitize(app.pkg)}"] div > .material-symbols-rounded`) as HTMLElement).innerHTML = 'delete'; - (win.content.querySelector(`div[data-pkg="${sanitize(app.pkg)}"] div > .material-symbols-rounded`) as HTMLElement).onclick = () => { - window.fs.unlink(`/Applications/${app.url.split('/').at(-1) as string}`, () => { - window.location.reload() - }) + (win.content.querySelector(`div[data-pkg="${sanitize(app.pkg)}"] div > .material-symbols-rounded`) as HTMLElement).onclick = async () => { + await window.fs.unlink(`/Applications/${app.url.split('/').at(-1) as string}`) + window.location.reload() } } else { (win.content.querySelector(`div[data-pkg="${sanitize(app.pkg)}"] div > .material-symbols-rounded`) as HTMLElement).onclick = () => { install(app.url) } } - }); - - (win.content.querySelector(`div[data-pkg="${sanitize(app.pkg)}"] div > .material-symbols-rounded`) as HTMLElement).onclick = () => { - install(app.url) - } + }).catch(e => console.error(e)) }) }) } function install (url: string): void { fetch(url).then(async (res) => await res.text()) - .then((data) => { - window.fs.exists('/Applications', (exists) => { - if (!exists) window.fs.promises.mkdir('/Applications').catch(console.error) + .then(async (data) => { + const exists = await window.fs.exists('/home/Applications') - window.fs.promises.writeFile(`/Applications/${url.split('/').at(-1) as string}`, data).then(() => window.location.reload()).catch(console.error) - }) - }).catch(console.error) + if (!exists) window.fs.mkdir('/home/Applications').catch(console.error) + + window.fs.writeFile(`/home/Applications/${url.split('/').at(-1) as string}`, data).then(() => window.location.reload()).catch(console.error) + }).catch(e => console.error(e)) } return win diff --git a/src/filer-types.ts b/src/filer-types.ts deleted file mode 100644 index 7358753..0000000 --- a/src/filer-types.ts +++ /dev/null @@ -1,171 +0,0 @@ -import * as fs from 'fs' -import { PlatformPath } from 'path' - -export interface Path extends PlatformPath { - basename: (path: string, suffix?: string | undefined) => string - normalize: (path: string) => string - isNull: (path: string) => boolean - addTrailing: (path: string) => string - removeTrailing: (path: string) => string -} - -interface FilerError extends Error { - code: string - errno: number - path?: string -} - -export interface Errors { - EACCES: FilerError - EBADF: FilerError - EBUSY: FilerError - EINVAL: FilerError - ENOTDIR: FilerError - EISDIR: FilerError - ENOENT: FilerError - EEXIST: FilerError - EPERM: FilerError - ELOOP: FilerError - ENOTEMPTY: FilerError - EIO: FilerError - ENOTMOUNTED: FilerError - EFILESYSTEMERROR: FilerError - ENOATTR: FilerError -} - -export type FileSystemOptionsFlags = 'FORMAT' | 'NOCTIME' | 'NOMTIME' - -export interface FileSystemOptions { - name: string - flags: FileSystemOptionsFlags[] - /* TO-DO */ - provider: any -} - -export type FileSystemCallback = (err: FilerError | null, fs: FileSystem) => {} - -export interface FileSystem { - appendFile: typeof fs.appendFile - access: typeof fs.access - chown: typeof fs.chown - chmod: typeof fs.chmod - close: typeof fs.close - exists: typeof fs.exists - fchown: typeof fs.fchown - fchmod: typeof fs.fchmod - fstat: typeof fs.fstat - fsync: typeof fs.fsync - ftruncate: typeof fs.ftruncate - futimes: typeof fs.futimes - link: typeof fs.link - lstat: typeof fs.lstat - mkdir: typeof fs.mkdir - mkdtemp: typeof fs.mkdtemp - open: typeof fs.open - readdir: typeof fs.readdir - read: typeof fs.read - readFile: typeof fs.readFile - readlink: typeof fs.readlink - rename: typeof fs.rename - rmdir: typeof fs.rmdir - stat: typeof fs.stat - symlink: typeof fs.symlink - truncate: typeof fs.truncate - unlink: typeof fs.unlink - utimes: typeof fs.utimes - writeFile: typeof fs.writeFile - write: typeof fs.write - - promises: FileSystemPromises -} - -export interface FileSystemPromises { - appendFile: typeof fs.promises.appendFile - access: typeof fs.promises.access - chown: typeof fs.promises.chown - chmod: typeof fs.promises.chmod - link: typeof fs.promises.link - lstat: typeof fs.promises.lstat - mkdir: typeof fs.promises.mkdir - mkdtemp: typeof fs.promises.mkdtemp - open: typeof fs.promises.open - readdir: typeof fs.promises.readdir - readFile: typeof fs.promises.readFile - readlink: typeof fs.promises.readlink - rename: typeof fs.promises.rename - rmdir: typeof fs.promises.rmdir - stat: typeof fs.promises.stat - symlink: typeof fs.promises.symlink - truncate: typeof fs.promises.truncate - unlink: typeof fs.promises.unlink - utimes: typeof fs.promises.utimes - writeFile: typeof fs.promises.writeFile -} - -export interface FileSystemShell { - cd: (path: string, callback: (err: FilerError | null) => void) => void - pwd: () => string - find: ( - dir: string, - options: { - exec?: (path: string, next: () => void) => void - regex?: RegExp - name?: string - } | ((err: FilerError | null, found: any[]) => void) | undefined | null, - /* INCOMPLETE? */ - callback: (err: FilerError | null, found: any[]) => void - ) => void - ls: ( - dir: string, - options: { - recursive?: boolean - } | ((err: FilerError | null) => void) | undefined | null, - callback: (err: FilerError | null) => void - ) => void - exec: ( - path: string, - args: Array | ((err: FilerError | null, result: string) => void) | undefined | null, - callback: (err: FilerError | null, result: string) => void - ) => void - touch: ( - path: string, - options: { - updateOnly?: boolean - date: Date - } | ((err: FilerError | null) => void) | undefined | null, - callback: (err: FilerError | null) => void - ) => void - cat: ( - files: string[], - callback: (err: FilerError | null, data: string) => void - ) => void - rm: ( - path: string, - options: { - recursive?: boolean - } | ((err: FilerError | null) => void) | undefined | null, - callback: (err: FilerError | null) => void - ) => void - tempDir: ( - callback: (err: FilerError | null, tmp: string) => void - ) => void - mkdirp: ( - path: string, - callback: (err: FilerError | null) => void - ) => void -} - -export interface FileSystemShellOptions { - env: { - [key: string]: string - } -} - -export default interface Filer { - FileSystem: (options: FileSystemOptions, callback: (err: FilerError | null, guid: string) => void) => FileSystem - Buffer: Buffer - Path: Path - path: Path - Errors: Errors - Shell: (fs: FileSystem, options: FileSystemShellOptions) => FileSystemShell -} diff --git a/src/fs.ts b/src/fs.ts new file mode 100644 index 0000000..3f82d1f --- /dev/null +++ b/src/fs.ts @@ -0,0 +1,414 @@ +export enum Errors { + ENOENT = 'ENOENT', + EISDIR = 'EISDIR', + EEXIST = 'EEXIST', + EPERM = 'EPERM', + ENOTDIR = 'ENOTDIR', + EACCES = 'EACCES' +} + +export enum Permission { + ROOT, + ADMIN, + USER +} + +export interface Directory { + type: 'directory' + permission: Permission + children: { + [key: string]: Directory | File + } +} + +export interface File { + type: 'file' + permission: Permission + content: Buffer +} + +const defaultFS: { root: Directory } = { + root: { + type: 'directory', + permission: Permission.ROOT, + children: { + home: { + type: 'directory', + permission: Permission.ROOT, + children: { + Downloads: { + type: 'directory', + permission: Permission.ADMIN, + children: {} + }, + Applications: { + type: 'directory', + permission: Permission.ADMIN, + children: {} + }, + Desktop: { + type: 'directory', + permission: Permission.ADMIN, + children: { + 'README.md': { + type: 'file', + permission: Permission.USER, + content: Buffer.from('# Welcome to FlowOS!') + } + } + }, + Pictures: { + type: 'directory', + permission: Permission.ADMIN, + children: {} + }, + Videos: { + type: 'directory', + permission: Permission.ADMIN, + children: {} + }, + Documents: { + type: 'directory', + permission: Permission.ADMIN, + children: {} + }, + Music: { + type: 'directory', + permission: Permission.ADMIN, + children: {} + } + } + }, + var: { + type: 'directory', + permission: Permission.ROOT, + children: {} + }, + etc: { + type: 'directory', + permission: Permission.ROOT, + children: {} + }, + boot: { + type: 'directory', + permission: Permission.ROOT, + children: {} + } + } + } +} + +export class VirtualFS { + private db: IDBDatabase + private fileSystem: { root: Directory } + + constructor (dbName = 'virtualfs') { + this.initializeDatabase(dbName) + .then(async () => { + this.db.onerror = (event: Event) => { + const target = event.target as IDBRequest + const errorMessage = target.error !== null ? target.error.message : 'Unknown error' + throw new Error(`[VirtualFS] ${target.error?.name ?? 'Unknown Error'}: ${errorMessage}`) + } + navigator.storage.persist().catch(e => console.error(e)) + const fs = await this.read() + fs === undefined ? await this.write(defaultFS) : this.fileSystem = fs + console.log('[VirtualFS] Database initialized') + }) + .catch(e => console.error(e)) + } + + /** + * The function initializes a database using IndexedDB in TypeScript. + * @param {string} dbName - The `dbName` parameter is a string that represents the name of the + * database that you want to initialize. + * @returns a Promise. + */ + private async initializeDatabase (dbName: string): Promise { + return await new Promise((resolve, reject) => { + const request = window.indexedDB.open(dbName) + + request.onupgradeneeded = (event: Event) => { + const target = event.target as IDBRequest + const db = target.result + db.createObjectStore('fs') + } + + request.onerror = (event: Event) => { + reject(new Error('[VirtualFS] Error opening database.')) + } + + request.onsuccess = () => { + this.db = request.result + resolve(true) + } + }) + } + + /** + * The function reads data from an object store in a transaction and returns a promise that resolves + * with the result. + * @returns a Promise that resolves to the result of the `getRequest` operation. + */ + private async read (): Promise { + const transaction = this.db.transaction(['fs'], 'readonly') + const store = transaction.objectStore('fs') + const getRequest = store.get('fs') + + return await new Promise((resolve, reject) => { + getRequest.onsuccess = () => { + resolve(getRequest.result) + } + + getRequest.onerror = () => { + reject(getRequest.error) + } + }) + } + + /** + * The `write` function is a private asynchronous method that takes a `fileSystem` object as a + * parameter, sets it as the current file system, and then saves it. + * @param fileSystem - The `fileSystem` parameter is an object that represents the file system. It + * has a property `root` which is a `Directory` object representing the root directory of the file + * system. + */ + private async write (fileSystem: { root: Directory }): Promise { + this.fileSystem = fileSystem + await this.save() + } + + private async save (): Promise { + const transaction = this.db.transaction(['fs'], 'readwrite') + const store = transaction.objectStore('fs') + const putRequest = store.put(this.fileSystem, 'fs') + + return await new Promise((resolve, reject) => { + putRequest.onsuccess = () => { + resolve() + } + + putRequest.onerror = () => { + reject(putRequest.error) + } + }) + } + + /** + * The function handles permissions for a given path and permission level, throwing an error if the + * current permission level is higher than the requested permission level. + * @param {string} path - A string representing the path to a resource or file. + * @param {Permission} permission - The `permission` parameter is of type `Permission`, which is an + * enum representing different levels of permissions. It is used to determine if the current user has + * sufficient permissions to access a certain path. + */ + private async handlePermissions (path: string, permission: Permission): Promise { + const { current } = await this.navigatePath(path) + + if (current.permission === Permission.ADMIN && current.permission <= permission) throw new Error(Errors.EACCES) + if (current.permission === Permission.ROOT && current.permission <= permission) throw new Error(Errors.EPERM) + } + + /** + * The function `navigatePath` takes a path as input and returns the current directory or file object + * and the individual parts of the path. + * @param {string} path - A string representing the path to navigate in the file system. + * @returns an object with two properties: "current" and "parts". The "current" property represents + * the current directory or file that was navigated to based on the given path. The "parts" property + * is an array of strings representing the individual parts of the path. + */ + private async navigatePath (path: string): Promise<{ current: Directory | File, parts: string[] }> { + const parts = path.split('/').filter(x => x !== '') + let current = this.fileSystem.root + for (const part of parts) { + current = current.children[part] as Directory + } + return { current, parts } + } + + /** + * The function `navigatePathParent` takes a path as input and returns the current directory, the + * parts of the path, and the filename. + * @param {string} path - The `path` parameter is a string that represents the file path. It is used + * to navigate through the file system and locate a specific file or directory. + * @returns an object with three properties: "current", "parts", and "filename". + */ + private async navigatePathParent (path: string): Promise<{ current: Directory, parts: string[], filename: string }> { + const parts = path.split('/').filter(x => x !== '') + const filename = parts.pop() as string + let current = this.fileSystem.root + for (const part of parts) { + current = current.children[part] as Directory + } + return { current, parts, filename } + } + + /** + * The `unlink` function deletes a file from a given path and saves the changes. + * @param {string} path - The `path` parameter is a string that represents the file or directory path + * that you want to unlink (delete). + * @param permission - The `permission` parameter is an optional parameter that specifies the level + * of permission required to perform the unlink operation. It has a default value of + * `Permission.USER`, which means that the operation can be performed by the user. + */ + async unlink (path: string, permission = Permission.USER): Promise { + const { current, filename } = await this.navigatePathParent(path) + + await this.handlePermissions(path, permission) + + Reflect.deleteProperty(current.children, filename) + await this.save() + } + + /** + * The function reads a file from a given path and returns its content as a Buffer. + * @param {string} path - The `path` parameter is a string that represents the file path of the file + * to be read. It specifies the location of the file in the file system. + * @param permission - The `permission` parameter is an optional parameter that specifies the + * permission level required to read the file. It has a default value of `Permission.USER`, which + * means that the file can be read by the current user. + * @returns a Promise that resolves to a Buffer. + */ + async readFile (path: string, permission = Permission.USER): Promise { + const { current } = await this.navigatePath(path) + + await this.handlePermissions(path, permission) + + if (current.type !== 'file') throw new Error(Errors.EISDIR) + return current.content + } + + /** + * The `writeFile` function writes content to a file at a specified path, with an optional permission + * parameter. + * @param {string} path - The `path` parameter is a string that represents the file path where the + * file will be written to. + * @param {string | Buffer} content - The `content` parameter is the data that you want to write to + * the file. It can be either a string or a Buffer object. + * @param permission - The `permission` parameter is an optional parameter that specifies the + * permission level for the file being written. It has a default value of `Permission.USER`, which + * indicates that the file can be accessed and modified only by the user who wrote it. + */ + async writeFile (path: string, content: string | Buffer, permission = Permission.USER): Promise { + const { current, filename } = await this.navigatePathParent(path) + + await this.handlePermissions(path, permission) + + current.children[filename] = { + type: 'file', + permission: Permission.USER, + content: Buffer.from(content) + } + await this.save() + } + + /** + * The `mkdir` function creates a new directory at the specified path with the given permission. + * @param {string} path - The `path` parameter is a string that represents the directory path where + * the new directory will be created. It specifies the location where the new directory will be + * created. + * @param permission - The `permission` parameter is an optional parameter that specifies the + * permission level for the newly created directory. It has a default value of `Permission.USER`, + * which indicates that only the user has permission to access the directory. + */ + async mkdir (path: string, permission = Permission.USER): Promise { + const { current, filename } = await this.navigatePathParent(path) + + await this.handlePermissions(path, permission) + + current.children[filename] = { + type: 'directory', + permission: Permission.USER, + children: {} + } + await this.save() + } + + /** + * The `rmdir` function removes a directory at the specified path, after checking permissions and + * ensuring that the path is a directory. + * @param {string} path - The `path` parameter is a string that represents the path of the directory + * to be removed. + * @param permission - The `permission` parameter is an optional parameter that specifies the level + * of permission required to remove a directory. It has a default value of `Permission.USER`, which + * means that the user must have permission to remove the directory. + */ + async rmdir (path: string, permission = Permission.USER): Promise { + const { current, filename } = await this.navigatePathParent(path) + + await this.handlePermissions(path, permission) + + if (current.children[filename].type !== 'directory') throw new Error(Errors.ENOTDIR) + Reflect.deleteProperty(current.children, filename) + await this.save() + } + + /** + * The `readdir` function reads the contents of a directory and returns an array of file names. + * @param {string} path - The `path` parameter is a string that represents the directory path from + * which you want to read the files. + * @param permission - The `permission` parameter is an optional parameter that specifies the + * permission level for accessing the directory. It has a default value of `Permission.USER`. + * @returns The function `readdir` returns a Promise that resolves to an array of strings. + */ + async readdir (path: string, permission = Permission.USER): Promise { + console.log(this.fileSystem) + const { current } = await this.navigatePath(path) + + if (current.type === 'file') throw new Error(Errors.ENOTDIR) + const result = await Promise.all(Object.keys(current.children ?? {})) + return result + } + + /** + * The `stat` function in TypeScript returns an object with two methods, `isDirectory` and `isFile`, + * which determine if a given path is a directory or a file, respectively. + * @param {string} path - A string representing the path to a file or directory. + * @param permission - The `permission` parameter is an optional parameter that specifies the + * permission level for accessing the file or directory. It has a default value of `Permission.USER`, + * which means that the function will check the permission level for the current user. + * @returns an object with two methods: `isDirectory` and `isFile`. + */ + async stat (path: string, permission = Permission.USER): Promise<{ isDirectory: () => boolean, isFile: () => boolean }> { + const { current } = await this.navigatePath(path) + return { + isDirectory: () => current.type === 'directory', + isFile: () => current.type === 'file' + } + } + + /** + * The `rename` function renames a file or directory by moving it from the old path to the new path, + * while also handling permissions and updating the file system structure. + * @param {string} oldPath - The old path of the file or directory that needs to be renamed. + * @param {string} newPath - The `newPath` parameter is a string that represents the new path or + * location where the file or directory will be renamed to. + * @param permission - The `permission` parameter is an optional parameter that specifies the + * permission level for the file or directory being renamed. It has a default value of + * `Permission.USER`, which indicates that the user has permission to perform the rename operation. + */ + async rename (oldPath: string, newPath: string, permission = Permission.USER): Promise { + const { current: oldCurrent, filename: oldFilename } = await this.navigatePathParent(oldPath) + const { current: newCurrent, filename: newFilename } = await this.navigatePathParent(newPath) + + await this.handlePermissions(oldPath, permission) + await this.handlePermissions(newPath, permission) + + newCurrent.children[newFilename] = oldCurrent.children[oldFilename] + Reflect.deleteProperty(oldCurrent.children, oldFilename) + await this.save() + } + + /** + * The function checks if a file or directory exists at a given path. + * @param {string} path - A string representing the path to check for existence. + * @param permission - The `permission` parameter is an optional parameter that specifies the + * permission level for checking the existence of the path. It has a default value of + * `Permission.USER`. + * @returns a Promise that resolves to a boolean value. + */ + async exists (path: string, permission = Permission.USER): Promise { + const { current } = await this.navigatePath(path) + return current !== undefined + } +} diff --git a/src/index.ts b/src/index.ts index ff95d7c..89b059d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,17 +6,23 @@ import StatusBar from './instances/StatusBar' import WindowManager from './instances/WindowManager' import Flow from './instances/Flow' -import { FileSystem } from './filer-types' -import { FlowConfig } from './types' +import { VirtualFS } from './fs' + +import { version } from '../package.json' + +const flowDetails = { + version, + codename: 'Mochi' +} declare global { interface Window { + flowDetails: typeof flowDetails preloader: Preloader flow: Flow - fs: FileSystem + fs: VirtualFS statusBar: StatusBar wm: WindowManager - config: () => Promise } } @@ -39,47 +45,13 @@ window.wm = new WindowManager(); (async function () { window.preloader.setPending('filesystem') - window.fs = new (window as any).Filer.FileSystem() - - const defaultConfig = { - SERVER_URL: 'https://server.flow-works.me', - HOSTNAME: 'flow', - USERNAME: 'user', - '24HR_CLOCK': false - } - - window.fs.exists('/.config', (exists) => { - if (!exists) window.fs.promises.mkdir('/.config').then(null).catch(e => console.error) - - window.fs.exists('/.config/flow.json', (exists) => { - if (!exists) { - window.fs.promises.writeFile('/.config/flow.json', JSON.stringify(defaultConfig)).then(null).catch(e => console.error) - } - }) - }) - - /** - * Gets the current FlowOS config. - * - * @returns The current FlowOS config. - */ - window.config = async (): Promise => { - return await new Promise((resolve, reject) => { - window.fs.exists('/.config/flow.json', (exists) => { - if (exists) { - window.fs.promises.readFile('/.config/flow.json') - .then(content => { resolve(JSON.parse(content.toString())) }) - .catch(() => reject(new Error('Unable to read config file.'))) - } else reject(new Error('Config file does not exist.')) - }) - }) - } + window.fs = new VirtualFS() const registrations = await navigator.serviceWorker.getRegistrations() for (const registration of registrations) { await registration.unregister() } - await navigator.serviceWorker.register('/uv-sw.js?url=' + encodeURIComponent(btoa((await window.config()).SERVER_URL)), { + await navigator.serviceWorker.register('/uv-sw.js?url=' + encodeURIComponent(btoa('https://server.flow-works.me')), { scope: '/service/' }) diff --git a/src/instances/Flow.ts b/src/instances/Flow.ts index af29e55..63fd54b 100644 --- a/src/instances/Flow.ts +++ b/src/instances/Flow.ts @@ -26,18 +26,15 @@ class Flow { window.preloader.setPending('apps') window.preloader.setStatus('importing apps...') - window.fs.exists('/Applications', (exists) => { - if (!exists) window.fs.promises.mkdir('/Applications').catch(e => console.error(e)) - window.fs.promises.readdir('/Applications').then((list) => { - list.forEach((file) => { - window.fs.promises.readFile('/Applications/' + file).then(content => { - if (!file.endsWith('.js') && !file.endsWith('.mjs')) return - if (content.toString() === '') return - this.appList.push(`data:text/javascript;base64,${btoa(content.toString())}`) - }).catch((e) => console.error(e)) - }) - }).catch(e => console.error(e)) - }) + window.fs.readdir('/home/Applications/').then((list) => { + list.forEach((file) => { + window.fs.readFile('/home/Applications/' + file).then(content => { + if (!file.endsWith('.js') && !file.endsWith('.mjs')) return + if (content.toString() === '') return + this.appList.push(`data:text/javascript;base64,${btoa(content.toString())}`) + }).catch((e) => console.error(e)) + }) + }).catch(e => console.error(e)) for (const appPath of this.defaultAppList) { window.preloader.setStatus(`importing default apps\n${appPath}`) diff --git a/src/utils.ts b/src/utils.ts index 4ae9181..99261d3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,8 +4,7 @@ * @returns The time. */ export const getTime = async (): Promise => { - const config = await window.config() - const use24hrs = config['24HOUR_CLOCK'] + const use24hrs = false const now = new Date() let hours: string | number = now.getHours()