summaryrefslogtreecommitdiff
path: root/src/main/kotlin
diff options
context:
space:
mode:
authornea <romangraef@gmail.com>2021-08-20 15:33:32 +0200
committernea <romangraef@gmail.com>2021-08-20 15:33:58 +0200
commit5042498c8fe12751c1b6560ae1fde07e7cd78259 (patch)
tree688aa93978ffffd0b15c639768fdad6f17609bdd /src/main/kotlin
parent65a6ffeb6761994d4d69011c87f23fe074f7a1f7 (diff)
downloadwebos-5042498c8fe12751c1b6560ae1fde07e7cd78259.tar.gz
webos-5042498c8fe12751c1b6560ae1fde07e7cd78259.tar.bz2
webos-5042498c8fe12751c1b6560ae1fde07e7cd78259.zip
aaaaaaaaHEADmaster
Diffstat (limited to 'src/main/kotlin')
-rw-r--r--src/main/kotlin/WebOS.kt366
-rw-r--r--src/main/kotlin/asyncio.kt61
-rw-r--r--src/main/kotlin/io/Path.kt78
-rw-r--r--src/main/kotlin/io/files.kt203
-rw-r--r--src/main/kotlin/util/sequence.kt5
5 files changed, 713 insertions, 0 deletions
diff --git a/src/main/kotlin/WebOS.kt b/src/main/kotlin/WebOS.kt
new file mode 100644
index 0000000..5bb788c
--- /dev/null
+++ b/src/main/kotlin/WebOS.kt
@@ -0,0 +1,366 @@
+import io.FileService
+import io.Path
+import io.PrimitiveFileService
+import io.PrimitiveINode
+import kotlinx.browser.document
+import kotlinx.browser.window
+import org.w3c.dom.Element
+import org.w3c.dom.Node
+import org.w3c.dom.asList
+import org.w3c.dom.events.Event
+import org.w3c.dom.events.KeyboardEvent
+import kotlin.properties.Delegates
+
+fun main() {
+ console.log("Hello from Kotlin")
+
+ val runner = IORunner.runOnIO {
+ println("ON START")
+ wait()
+ println("WAIT CALLED ONCE")
+ val x = getOneKey()
+ println("INPUT GET: $x")
+ println("EXITING NOW")
+ }
+ println("runOnIO called -> CALLING WHATEVER")
+ runner._CALLED_BY_JS_onWhatever()
+ println("CALLED WHATEVER -> CALLING INPUT")
+ runner._CALLED_BY_JS_onInput(window.prompt("INPUT") ?: "NO INPUT")
+ println("CALLED INPUT")
+ return
+
+
+
+
+
+
+ val webos = WebOS()
+ document.body?.addEventListener("load", {
+ document.body?.querySelectorAll(".webosconsole")?.asList()?.forEach {
+ if (it !is Element) return@forEach
+ webos.registerConsole(it)
+ }
+ })
+}
+
+data class CharacterRun(val text: String, val color: String)
+
+abstract class Activity(val console: Console) {
+
+ open fun handleKeyPress(key: Key) {}
+ open fun update() {}
+ abstract fun render(columns: Int, rows: Int): List<List<CharacterRun>>
+}
+
+class ShellRunner(console: Console) : Activity(console) {
+
+ val history = mutableListOf<String>()
+
+ val shellProgramStack = ArrayDeque<ShellProgram>()
+
+ var lastLine = ""
+
+ fun newLine() {
+ history.add(lastLine)
+ lastLine = ""
+ console.invalidateRender()
+ }
+
+ fun println(it: String = "") {
+ print(it)
+ newLine()
+ }
+
+ fun print(it: String) {
+ lastLine += it
+ console.invalidateRender()
+ }
+
+ override fun update() {
+ shellProgramStack.lastOrNull()?.update()
+ }
+
+ fun getInput(): String? {
+ if (inputBuffer.contains("\n")) {
+ val r = inputBuffer.substringBefore("\n")
+ inputBuffer = inputBuffer.substringAfter("\n")
+ return r
+ }
+ return null
+ }
+
+ override fun render(columns: Int, rows: Int): List<List<CharacterRun>> =
+ history.map { listOf(CharacterRun(it.take(columns), "white")) }.takeLast(rows - 1) + listOf(
+ listOf(
+ CharacterRun(
+ lastLine + inputBuffer,
+ "white"
+ )
+ )
+ )
+
+ var inputBuffer = ""
+
+ override fun handleKeyPress(key: Key) = when (key) {
+ is Key.Printable -> {
+ inputBuffer += key.char
+ }
+ is Key.Enter -> {
+ inputBuffer += "\n"
+ }
+ else -> Unit
+ }
+
+ fun <T : ShellProgram> openShellProgram(program: (ShellRunner) -> T): ShellRunner = openShellProgram(program(this))
+
+ fun <T : ShellProgram> openShellProgram(program: T): ShellRunner {
+ shellProgramStack.addLast(program)
+ return this
+ }
+
+}
+
+abstract class ShellProgram(val shellRunner: ShellRunner) {
+
+ abstract fun update(): Unit
+}
+
+sealed class Key {
+ object Alt : Key()
+ object AltGraph : Key()
+ object CapsLock : Key()
+ object Control : Key()
+ object Function : Key()
+ object FunctionLock : Key()
+ object Hyper : Key()
+ object Meta : Key()
+ object NumLock : Key()
+ object Shift : Key()
+ object Super : Key()
+ object Symbol : Key()
+ object SymbolLock : Key()
+ object Enter : Key()
+ object Tab : Key()
+ sealed class Arrow : Key() {
+ object Down : Key()
+ object Left : Key()
+ object Right : Key()
+ object Up : Key()
+ }
+
+ object End : Key()
+ object Home : Key()
+ object PageUp : Key()
+ object PageDown : Key()
+ object Backspace : Key()
+ object Clear : Key()
+ object Copy : Key()
+ object CrSel : Key()
+ object Cut : Key()
+ object Delete : Key()
+ object EraseEof : Key()
+ object ExSel : Key()
+ object Insert : Key()
+ object Paste : Key()
+ object Redo : Key()
+ object Undo : Key()
+ object Accept : Key()
+ object Again : Key()
+ object Attn : Key()
+ object Cancel : Key()
+ object ContextMenu : Key()
+ object Escape : Key()
+ object Execute : Key()
+ object Find : Key()
+ object Finish : Key()
+ object Help : Key()
+ class Printable(val char: Char) : Key()
+ class FunctionN(val n: Int) : Key()
+ companion object {
+
+ fun from(string: String) = when (string) {
+ "Alt" -> Alt
+ "AltGraph" -> AltGraph
+ "CapsLock" -> CapsLock
+ "Control" -> Control
+ "Fn" -> Function
+ "FnLock" -> FunctionLock
+ "Hyper" -> Hyper
+ "Meta" -> Meta
+ "NumLock" -> NumLock
+ "Shift" -> Shift
+ "Super" -> Super
+ "Symbol" -> Symbol
+ "SymbolLock" -> SymbolLock
+ "Enter" -> Enter
+ "Tab" -> Tab
+ "Down" -> Arrow.Down
+ "Left" -> Arrow.Left
+ "Right" -> Arrow.Right
+ "Up" -> Arrow.Up
+ "End" -> End
+ "Home" -> Home
+ "PageUp" -> PageUp
+ "PageDown" -> PageDown
+ "Backspace" -> Backspace
+ "Clear" -> Clear
+ "Copy" -> Copy
+ "CrSel" -> CrSel
+ "Cut" -> Cut
+ "Delete" -> Delete
+ "EraseEof" -> EraseEof
+ "ExSel" -> ExSel
+ "Insert" -> Insert
+ "Paste" -> Paste
+ "Redo" -> Redo
+ "Undo" -> Undo
+ "Accept" -> Accept
+ "Again" -> Again
+ "Attn" -> Attn
+ "Cancel" -> Cancel
+ "ContextMenu" -> ContextMenu
+ "Escape" -> Escape
+ "Execute" -> Execute
+ "Find" -> Find
+ "Finish" -> Finish
+ "Help" -> Help
+ else -> if (string.length == 1)
+ Printable(string.first())
+ else if (string.first() == 'F')
+ FunctionN(string.substring(1).toInt())
+ else throw TODO()
+ }
+ }
+}
+
+class Login(shellRunner: ShellRunner) : ShellProgram(shellRunner) {
+
+ override fun update() {
+ while (true)
+ when (state) {
+ 0 -> {
+ shellRunner.print("Username: ")
+ state = 1
+ }
+ 1 -> {
+ val inp = shellRunner.getInput() ?: return
+ shellRunner.println(inp)
+ shellRunner.print("Password: ")
+ username = inp
+ state = 2
+ }
+ 2 -> {
+ val inp = shellRunner.getInput() ?: return
+ shellRunner.println(inp)
+ shellRunner.println("Login complete)")
+ password = inp
+ // TODO: check password and set user
+ state = 3
+ }
+ 3 -> {
+ TODO()
+ }
+ }
+
+ }
+
+ var state = 0
+ var username = ""
+ var password = ""
+
+}
+
+class Console(val os: WebOS, val renderElement: Element?) {
+
+ val isVirtual get() = renderElement == null
+ val activityStack = ArrayDeque<Activity>()
+
+ var columns: Int = 80
+ var rows: Int = 46
+
+ var shouldRerender = true
+
+ var currentUser: User? = null
+
+ private var _workingDirectory: Path.Absolute? = null
+
+ var workingDirectory: Path.Absolute
+ get() = _workingDirectory ?: currentUser?.homeDirectory ?: Path.root
+ set(value) {
+ _workingDirectory = value
+ }
+
+ init {
+ renderElement?.addEventListener("keydown", ::computeKeypressEvent)
+ }
+
+ fun computeKeypressEvent(event: Event) {
+ if (!isInFocus()) return
+ if (event !is KeyboardEvent) return
+ activityStack.lastOrNull()?.handleKeyPress(Key.from(event.key))
+ }
+
+ private fun isInFocus(): Boolean = renderElement.containsOrIs(document.activeElement)
+
+ private fun Node?.containsOrIs(node: Node?) = this == node || this?.contains(node) ?: false
+
+ fun <T : Activity> openActivity(activity: (Console) -> T): Console = openActivity(activity(this))
+
+ fun <T : Activity> openActivity(activity: T): Console {
+ activityStack.addLast(activity)
+ invalidateRender()
+ return this
+ }
+
+ fun render() {
+ if (renderElement == null) return
+ if (!shouldRerender) return
+ shouldRerender = false
+ val x = activityStack.lastOrNull()?.render(columns, rows)
+ console.log(x)
+ }
+
+ fun invalidateRender() {
+ shouldRerender = true
+ window.requestAnimationFrame { render() }
+ }
+
+ fun resize(newColumns: Int, newRows: Int) {
+ invalidateRender()
+ }
+
+ // TODO: Handle resizes of the renderElement
+
+ fun update(): Unit {
+ activityStack.lastOrNull()?.update()
+ }
+
+ var updateInterval by Delegates.notNull<Int>()
+
+ fun start(): Console {
+ updateInterval = window.setInterval(::update, 10)
+ return openActivity(ShellRunner(this).also { it.openShellProgram(Login(it)) })
+ }
+
+ fun destroy() {
+ window.clearInterval(updateInterval)
+ }
+
+}
+
+class WebOS {
+
+ val hostname: String = "host"
+ private val _consoles = mutableListOf<Console>()
+ val consoles get() = _consoles.toList()
+ val files: FileService<PrimitiveINode> = PrimitiveFileService()
+ fun registerConsole(element: Element) {
+ _consoles.add(Console(this, element))
+ }
+}
+
+data class User(
+ val name: String,
+ val homeDirectory: Path.Absolute,
+ val isRoot: Boolean = false,
+)
diff --git a/src/main/kotlin/asyncio.kt b/src/main/kotlin/asyncio.kt
new file mode 100644
index 0000000..e14fea9
--- /dev/null
+++ b/src/main/kotlin/asyncio.kt
@@ -0,0 +1,61 @@
+import kotlin.coroutines.*
+
+class IORunner : Continuation<Unit> {
+ var nextStep: Continuation<Unit>? = null
+ val eventQueue = ArrayDeque<String>()
+ fun _CALLED_BY_JS_onInput(str: String) {
+ eventQueue.addLast(str)
+ awake()
+ }
+
+ fun _CALLED_BY_JS_onWhatever() {
+ awake()
+ }
+
+ private fun awake() {
+ val step = nextStep
+ if (step != null) {
+ nextStep = null
+ step.resume(Unit)
+ }
+ }
+
+ suspend fun wait() {
+ return suspendCoroutine { c ->
+ console.log("SUSPENDING COROUTINE")
+ nextStep = c
+ }
+ }
+
+ override val context: CoroutineContext
+ get() = EmptyCoroutineContext
+
+ override fun resumeWith(result: Result<Unit>) {
+ if (result.isFailure) {
+ result.exceptionOrNull()?.printStackTrace()
+ } else {
+ println("IORunner exited successfully")
+ }
+ }
+
+ companion object {
+ fun runOnIO(block: suspend IORunner.() -> Unit): IORunner {
+ val r = IORunner()
+ block.startCoroutine(r, r)
+ return r
+ }
+ }
+}
+
+
+suspend fun IORunner.getOneKey(): String {
+ while (true) {
+ val x = eventQueue.removeFirstOrNull()
+ if (x == null)
+ wait()
+ else {
+ return x
+ }
+ }
+}
+
diff --git a/src/main/kotlin/io/Path.kt b/src/main/kotlin/io/Path.kt
new file mode 100644
index 0000000..241c559
--- /dev/null
+++ b/src/main/kotlin/io/Path.kt
@@ -0,0 +1,78 @@
+package io
+
+sealed class Path {
+ abstract val parts: List<String>
+ fun toAbsolutePath(relativeTo: Absolute): Absolute {
+ return relativeTo.resolve(this)
+ }
+
+ abstract fun resolve(path: Path): Path
+
+ abstract val stringPath: String
+
+ companion object {
+ val root = Absolute(listOf())
+
+ fun ofShell(string: String, userHome: Absolute): Path =
+ ofShell(string.split("/"), userHome)
+
+ fun ofShell(vararg parts: String, userHome: Absolute): Path =
+ ofShell(parts.toList(), userHome)
+
+ fun of(vararg parts: String): Path =
+ of(parts.toList())
+
+ fun of(string: String): Path =
+ of(string.split("/"))
+
+ fun ofShell(parts: List<String>, userHome: Absolute): Path {
+ if (parts.firstOrNull() == "~")
+ return userHome.resolve(Relative(parts.subList(1, parts.size).filter { it.isNotEmpty() }))
+ return of(parts)
+ }
+
+ fun of(parts: List<String>): Path {
+ if (parts.isEmpty())
+ return root
+ if (parts[0] == "") // Starts with a /
+ return Absolute(parts.subList(1, parts.size).filter { it.isNotEmpty() })
+ return Relative(parts.filter { it.isNotEmpty() })
+ }
+ }
+
+ data class Relative internal constructor(override val parts: List<String>) : Path() {
+ override fun resolve(path: Path): Path {
+ if (path is Absolute) return path
+ return Relative(this.parts + path.parts)
+ }
+
+ override val stringPath: String get() = parts.joinToString("/")
+ }
+
+ data class Absolute internal constructor(override val parts: List<String>) : Path() {
+ override fun resolve(path: Path): Absolute {
+ if (path is Absolute) return path
+ return Absolute(this.parts + path.parts)
+ }
+
+ override val stringPath: String get() = "/" + parts.joinToString("/")
+
+ fun relativize(path: Path): Relative = when (path) {
+ is Relative -> path
+ is Absolute -> {
+ var idx = 0
+ while (idx < path.parts.size && idx < parts.size && path.parts[idx] == parts[idx]) {
+ idx++
+ }
+ val returns = if (idx < parts.size) {
+ parts.size - idx
+ } else {
+ 0
+ }
+ Relative(List(returns) { ".." } + path.parts.subList(idx, path.parts.size))
+ }
+ }
+ }
+
+ override fun toString(): String = "Path($stringPath)"
+}
diff --git a/src/main/kotlin/io/files.kt b/src/main/kotlin/io/files.kt
new file mode 100644
index 0000000..4c53c4e
--- /dev/null
+++ b/src/main/kotlin/io/files.kt
@@ -0,0 +1,203 @@
+package io
+
+import User
+
+sealed interface CreateFileResult {
+ object Created : CreateFileResult
+ sealed interface Failure : CreateFileResult {
+ object NoPermission : Failure
+ object NoParent : Failure
+ object AlreadyExists : Failure
+ }
+}
+
+sealed interface WriteFileResult {
+ object Written : WriteFileResult
+ sealed interface Failure : WriteFileResult {
+ object NoPermission : Failure
+ object NotAFile : Failure
+ data class NotEnoughSpace(
+ val dataSize: Long,
+ val spaceLeft: Long
+ ) : Failure
+ }
+}
+
+sealed interface ReadFileResult {
+ data class Read(val data: ByteArray) : ReadFileResult {
+ override fun equals(other: Any?): Boolean = (other as? Read)?.let { it.data.contentEquals(this.data) } ?: false
+
+ override fun hashCode(): Int {
+ return data.contentHashCode()
+ }
+ }
+
+ sealed interface Failure : ReadFileResult {
+ object NoPermission : Failure
+ object NotAFile : Failure
+ }
+}
+
+sealed interface DeleteFileResult {
+ sealed interface Failure : DeleteFileResult {
+ object NoPermission : Failure
+ object NotAFile : Failure
+ }
+
+ object Deleted : DeleteFileResult
+}
+
+interface FileService<INode> {
+ fun getPath(iNode: INode): Path.Absolute
+ fun getINode(path: Path.Absolute): INode
+ fun createFile(iNode: INode, user: User): CreateFileResult
+ fun createSymlink(iNode: INode, user: User, path: Path): CreateFileResult
+ fun createDirectory(iNode: INode, user: User): CreateFileResult
+ fun writeToFile(iNode: INode, user: User, data: ByteArray): WriteFileResult
+ fun readFromFile(iNode: INode, user: User): ReadFileResult
+ fun deleteFile(iNode: INode, user: User): DeleteFileResult
+ fun exists(iNode: INode): Boolean
+ fun isFile(iNode: INode): Boolean
+ fun isDirectory(iNode: INode): Boolean
+ fun isSymlink(iNode: INode): Boolean
+ fun resolve(iNode: INode, fragment: String): INode
+ fun resolve(iNode: INode, path: Path.Relative): INode
+ fun changePermissions(iNode: INode, user: User, permissionUpdate: Map<User, Permission>)
+}
+
+data class Permission(
+ val read: Boolean,
+ val write: Boolean,
+ val execute: Boolean
+) {
+ companion object {
+ val default get() = Permission(read = true, write = true, execute = false)
+ }
+}
+
+sealed interface PrimitiveStorageBlob {
+ val permissions: MutableMap<User, Permission>
+
+ class File(var data: ByteArray, override val permissions: MutableMap<User, Permission>) : PrimitiveStorageBlob
+ class Symlink(val path: Path, override val permissions: MutableMap<User, Permission>) : PrimitiveStorageBlob
+ class Directory(override val permissions: MutableMap<User, Permission>) : PrimitiveStorageBlob
+}
+
+data class PrimitiveINode internal constructor(internal val internalPath: String)
+class PrimitiveFileService : FileService<PrimitiveINode> {
+ private val storageBlobs = mutableMapOf<String, PrimitiveStorageBlob>(
+ "/" to PrimitiveStorageBlob.Directory(mutableMapOf())
+ )
+
+ override fun getPath(iNode: PrimitiveINode): Path.Absolute = Path.of(iNode.internalPath) as Path.Absolute
+
+ override fun getINode(path: Path.Absolute): PrimitiveINode {
+ return resolve(PrimitiveINode("/"), Path.Relative(path.parts))
+ }
+
+ override fun resolve(iNode: PrimitiveINode, fragment: String): PrimitiveINode {
+ if (fragment == "..") {
+ val up = iNode.internalPath.substringBeforeLast('/')
+ if (up.isEmpty()) return PrimitiveINode("/")
+ return PrimitiveINode(up)
+ }
+ if (fragment.isEmpty() || fragment == ".")
+ return iNode
+ val blob = storageBlobs[iNode.internalPath]
+ return when (blob) {
+ is PrimitiveStorageBlob.Symlink -> {
+ when (blob.path) {
+ is Path.Absolute -> getINode(blob.path)
+ is Path.Relative -> resolve(resolve(iNode, ".."), blob.path)
+ }
+ }
+ else -> {
+ PrimitiveINode(iNode.internalPath + "/" + fragment)
+ }
+ }
+ }
+
+ override fun resolve(iNode: PrimitiveINode, path: Path.Relative): PrimitiveINode =
+ path.parts.fold(iNode) { node, fragment -> resolve(node, fragment) }
+
+ private fun getStorageBlob(iNode: PrimitiveINode): PrimitiveStorageBlob? = storageBlobs[iNode.internalPath]
+
+ override fun writeToFile(iNode: PrimitiveINode, user: User, data: ByteArray): WriteFileResult {
+ val file = getStorageBlob(iNode) as? PrimitiveStorageBlob.File ?: return WriteFileResult.Failure.NotAFile
+ if (!hasPermission(user, file) { read })
+ return WriteFileResult.Failure.NoPermission
+ file.data = data
+ return WriteFileResult.Written
+ }
+
+ override fun readFromFile(iNode: PrimitiveINode, user: User): ReadFileResult {
+ val file = getStorageBlob(iNode) as? PrimitiveStorageBlob.File ?: return ReadFileResult.Failure.NotAFile
+ if (!hasPermission(user, file) { read })
+ return ReadFileResult.Failure.NoPermission
+ return ReadFileResult.Read(file.data)
+ }
+
+ override fun exists(iNode: PrimitiveINode): Boolean =
+ getStorageBlob(iNode) != null
+
+ override fun isFile(iNode: PrimitiveINode): Boolean =
+ getStorageBlob(iNode) is PrimitiveStorageBlob.File
+
+ override fun isDirectory(iNode: PrimitiveINode): Boolean =
+ getStorageBlob(iNode) is PrimitiveStorageBlob.Directory
+
+ override fun isSymlink(iNode: PrimitiveINode): Boolean =
+ getStorageBlob(iNode) is PrimitiveStorageBlob.Symlink
+
+ override fun changePermissions(iNode: PrimitiveINode, user: User, permissionUpdate: Map<User, Permission>) {
+ val file = getStorageBlob(iNode) ?: return // TODO Results
+ if (!hasPermission(user, file) { write })
+ return // TODO Results
+ file.permissions.putAll(permissionUpdate)
+ return // TODO Results
+ }
+
+ override fun deleteFile(iNode: PrimitiveINode, user: User): DeleteFileResult {
+ val file = getStorageBlob(iNode) ?: return DeleteFileResult.Failure.NotAFile
+ if (!hasPermission(user, file) { write })
+ return DeleteFileResult.Failure.NoPermission
+ (storageBlobs.keys.filter { it.startsWith(iNode.internalPath + "/") } + listOf(iNode.internalPath)).forEach {
+ storageBlobs.remove(it)
+ }
+ return DeleteFileResult.Deleted
+ }
+
+ private fun hasPermission(user: User, blob: PrimitiveStorageBlob, check: Permission.() -> Boolean): Boolean {
+ return user.isRoot || blob.permissions[user]?.let(check) ?: false
+ }
+
+ private fun checkCreationPreconditions(iNode: PrimitiveINode, user: User): CreateFileResult? {
+ if (storageBlobs.containsKey(iNode.internalPath)) return CreateFileResult.Failure.AlreadyExists
+ val parent = getStorageBlob(resolve(iNode, ".."))
+ if (parent !is PrimitiveStorageBlob.Directory) return CreateFileResult.Failure.NoParent
+ if (!hasPermission(user, parent) { write }) return CreateFileResult.Failure.NoPermission
+ return null
+ }
+
+ override fun createFile(iNode: PrimitiveINode, user: User): CreateFileResult {
+ val preconditions = checkCreationPreconditions(iNode, user)
+ if (preconditions != null) return preconditions
+ storageBlobs[iNode.internalPath] = PrimitiveStorageBlob.File(byteArrayOf(), mutableMapOf(user to Permission.default))
+ return CreateFileResult.Created
+ }
+
+ override fun createSymlink(iNode: PrimitiveINode, user: User, path: Path): CreateFileResult {
+ val preconditions = checkCreationPreconditions(iNode, user)
+ if (preconditions != null) return preconditions
+ storageBlobs[iNode.internalPath] = PrimitiveStorageBlob.Symlink(path, mutableMapOf(user to Permission.default))
+ return CreateFileResult.Created
+ }
+
+ override fun createDirectory(iNode: PrimitiveINode, user: User): CreateFileResult {
+ val preconditions = checkCreationPreconditions(iNode, user)
+ if (preconditions != null) return preconditions
+ storageBlobs[iNode.internalPath] = PrimitiveStorageBlob.Directory(mutableMapOf(user to Permission.default))
+ return CreateFileResult.Created
+ }
+
+}
diff --git a/src/main/kotlin/util/sequence.kt b/src/main/kotlin/util/sequence.kt
new file mode 100644
index 0000000..72b07dc
--- /dev/null
+++ b/src/main/kotlin/util/sequence.kt
@@ -0,0 +1,5 @@
+package util
+
+fun <T> Iterable<T>.expandWith(t: T): Sequence<T> =
+ this.asSequence() + generateSequence { t }.asSequence()
+