[💥] Custom virtual filesystem

This commit is contained in:
ThinLiquid 2024-01-14 19:51:08 +00:00
parent f7570368f4
commit 2aaf0475a0
No known key found for this signature in database
GPG key ID: D5085759953E6CAA
11 changed files with 546 additions and 349 deletions

View file

@ -7,7 +7,6 @@
<link rel="shortcut icon" href="./src/assets/flow.png" type="image/png">
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/filer"></script>
<script src="./src/index.ts" type="module"></script>
</body>
</html>

View file

@ -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": {

View file

@ -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')

View file

@ -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<void> {
await window.fs.readdir(dir, (e: NodeJS.ErrnoException, files: string[]) => {
const back = dir === '/' ? '<span class="material-symbols-rounded">first_page</span>' : '<span class="back material-symbols-rounded">chevron_left</span>'
const files = await window.fs.readdir(dir)
const back = dir === '/' ? '<span class="material-symbols-rounded">first_page</span>' : '<span class="back material-symbols-rounded">chevron_left</span>'
win.content.innerHTML = `
win.content.innerHTML = `
<div style="padding: 5px;display: flex;align-items: center;gap: 5px;">
${back}${dir}
<div style="flex:1;"></div>
@ -38,110 +36,104 @@ export default class FilesApp implements App {
<div class="files" style="background: var(--base);flex: 1;border-radius: 10px;display: flex;flex-direction: column;"></div>
`
if (back !== '<span class="material-symbols-rounded">first_page</span>') {
(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 !== '<span class="material-symbols-rounded">first_page</span>') {
(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 '<span class="material-symbols-rounded">javascript</span>'
}
case 'html':
case 'htm': {
return '<span class="material-symbols-rounded">html</span>'
}
case 'css': {
return '<span class="material-symbols-rounded">css</span>'
}
case 'json': {
return '<span class="material-symbols-rounded">code</span>'
}
case 'md': {
return '<span class="material-symbols-rounded">markdown</span>'
}
case 'txt':
case 'text': {
return '<span class="material-symbols-rounded">description</span>'
}
case 'png':
case 'apng':
case 'jpg':
case 'jpeg':
case 'gif': {
return '<span class="material-symbols-rounded">image</span>'
}
default: {
return '<span class="material-symbols-rounded">draft</span>'
}
}
}
const icon = fileStat.isDirectory() ? '<span class="material-symbols-rounded">folder</span>' : 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} <span style="flex:1;">${file}</span><span class="material-symbols-rounded delete">delete_forever</span><span class="material-symbols-rounded rename">edit</span>`;
(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 '<span class="material-symbols-rounded">javascript</span>'
}
case 'html':
case 'htm': {
return '<span class="material-symbols-rounded">html</span>'
}
case 'css': {
return '<span class="material-symbols-rounded">css</span>'
}
case 'json': {
return '<span class="material-symbols-rounded">code</span>'
}
case 'md': {
return '<span class="material-symbols-rounded">markdown</span>'
}
case 'txt':
case 'text': {
return '<span class="material-symbols-rounded">description</span>'
}
case 'png':
case 'apng':
case 'jpg':
case 'jpeg':
case 'gif': {
return '<span class="material-symbols-rounded">image</span>'
}
default: {
return '<span class="material-symbols-rounded">draft</span>'
}
}
}
const icon = fileStat.isDirectory() ? '<span class="material-symbols-rounded">folder</span>' : genIcon()
element.innerHTML += `${icon} <span style="flex:1;">${file}</span><span class="material-symbols-rounded delete">delete_forever</span><span class="material-symbols-rounded rename">edit</span>`;
(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
}

View file

@ -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))
})

View file

@ -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 {
</div>
`
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

View file

@ -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<string | number> | ((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
}

414
src/fs.ts Normal file
View file

@ -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<boolean>.
*/
private async initializeDatabase (dbName: string): Promise<boolean> {
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<any> {
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<void> {
this.fileSystem = fileSystem
await this.save()
}
private async save (): Promise<void> {
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<void> {
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<void> {
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<Buffer> {
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<void> {
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<void> {
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<void> {
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<string[]> {
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<void> {
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<boolean> {
const { current } = await this.navigatePath(path)
return current !== undefined
}
}

View file

@ -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<FlowConfig>
}
}
@ -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<FlowConfig> => {
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/'
})

View file

@ -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}`)

View file

@ -4,8 +4,7 @@
* @returns The time.
*/
export const getTime = async (): Promise<string> => {
const config = await window.config()
const use24hrs = config['24HOUR_CLOCK']
const use24hrs = false
const now = new Date()
let hours: string | number = now.getHours()