summaryrefslogtreecommitdiff
path: root/src/jsMain/kotlin/io/files.kt
blob: 4c53c4e744980324c6597835e173c9d56343de5e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
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
	}

}