Implement JsFileStorage using IndexedDB

pull/498/head
Alinson S. Xavier 7 years ago
parent f310eaf7d9
commit ddd363917c

@ -94,7 +94,6 @@ suspend fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log
for (v in (currentVersion + 1)..newVersion) {
val sv = if(v < 10) "00$v" else if (v<100) "0$v" else "$v"
val filename = "migrations/$sv.sql"
println(filename)
val migrationFile = fileOpener.openResourceFile(filename)
for (line in migrationFile.lines()) {
if (line.isEmpty()) continue

@ -26,9 +26,11 @@ interface FileOpener {
*
* The path is relative to the assets folder. For example, to open
* assets/main/migrations/09.sql you should provide migrations/09.sql
* as the filename.
* as the path.
*
* This function always succeed, even if the file does not exist.
*/
fun openResourceFile(filename: String): ResourceFile
fun openResourceFile(path: String): ResourceFile
/**
* Opens a file which was not shipped with the application, such as
@ -36,26 +38,54 @@ interface FileOpener {
*
* The path is relative to the user folder. For example, if the application
* stores the user data at /home/user/.loop/ and you wish to open the file
* /home/user/.loop/crash.log, you should provide crash.log as the filename.
* /home/user/.loop/crash.log, you should provide crash.log as the path.
*
* This function always succeed, even if the file does not exist.
*/
fun openUserFile(filename: String): UserFile
fun openUserFile(path: String): UserFile
}
/**
* Represents a file that was created after the application was installed, as a
* result of some user action, such as databases and logs. These files can be
* deleted.
* result of some user action, such as databases and logs.
*/
interface UserFile {
fun delete()
fun exists(): Boolean
/**
* Deletes the user file. If the file does not exist, nothing happens.
*/
suspend fun delete()
/**
* Returns true if the file exists.
*/
suspend fun exists(): Boolean
/**
* Returns the lines of the file. If the file does not exist, throws an
* exception.
*/
suspend fun lines(): List<String>
}
/**
* Represents a file that was shipped with the application, such as migration
* files or translations. These files cannot be deleted.
* files or database templates.
*/
interface ResourceFile {
fun copyTo(dest: UserFile)
/**
* Copies the resource file to the specified user file. If the user file
* already exists, it is replaced. If not, a new file is created.
*/
suspend fun copyTo(dest: UserFile)
/**
* Returns the lines of the resource file. If the file does not exist,
* throws an exception.
*/
suspend fun lines(): List<String>
/**
* Returns true if the file exists.
*/
suspend fun exists(): Boolean
}

@ -19,7 +19,6 @@
package org.isoron
open class BaseTest {
val resolver = DependencyResolver()
val fileOpener = resolver.getFileOpener()
}
val resolver = DependencyResolver()
open class BaseTest

@ -23,7 +23,7 @@ import org.isoron.platform.gui.*
import org.isoron.platform.io.*
expect class DependencyResolver() {
fun getFileOpener(): FileOpener
suspend fun getFileOpener(): FileOpener
suspend fun getDatabase(): Database
fun createCanvas(width: Int, height: Int): Canvas
fun exportCanvas(canvas: Canvas, filename: String)

@ -24,12 +24,29 @@ import kotlin.test.*
class FilesTest() : BaseTest() {
suspend fun testLines() {
val fileOpener = resolver.getFileOpener()
assertFalse(fileOpener.openUserFile("non-existing.txt").exists())
assertFalse(fileOpener.openResourceFile("non-existing.txt").exists())
val hello = fileOpener.openResourceFile("hello.txt")
var lines = hello.lines()
assertEquals("Hello World!", lines[0])
assertEquals("This is a resource.", lines[1])
val helloCopy = fileOpener.openUserFile("hello-copy.txt")
hello.copyTo(helloCopy)
lines = helloCopy.lines()
assertEquals("Hello World!", lines[0])
assertEquals("This is a resource.", lines[1])
assertTrue(helloCopy.exists())
helloCopy.delete()
assertFalse(helloCopy.exists())
val migration = fileOpener.openResourceFile("migrations/012.sql")
assertTrue(migration.exists())
lines = migration.lines()
assertEquals("delete from Score", lines[0])
}

@ -72,7 +72,6 @@ class JsPreparedStatement(val stmt: dynamic) : PreparedStatement {
class JsDatabase(val db: dynamic) : Database {
override fun prepareStatement(sql: String): PreparedStatement {
println(sql)
return JsPreparedStatement(db.prepare(sql))
}

@ -20,31 +20,111 @@
package org.isoron.platform.io
import kotlinx.coroutines.*
import org.w3c.dom.events.*
import org.w3c.xhr.*
import kotlin.js.*
class JsFileOpener : FileOpener {
override fun openUserFile(filename: String): UserFile {
return JsUserFile(filename)
class JsFileStorage {
private val indexedDB = eval("indexedDB")
private var db: dynamic = null
private val DB_NAME = "Main"
private val OS_NAME = "Files"
suspend fun init() {
console.log("Initializing JsFileStorage...")
Promise<Int> { resolve, reject ->
val req = indexedDB.open(DB_NAME, 2)
req.onerror = { reject(Exception("could not open IndexedDB")) }
req.onupgradeneeded = {
console.log("Creating document store for JsFileStorage...")
req.result.createObjectStore(OS_NAME)
}
req.onsuccess = {
console.log("JsFileStorage is ready.")
db = req.result
resolve(0)
}
}.await()
}
suspend fun delete(path: String) {
Promise<Int> { resolve, reject ->
val transaction = db.transaction(OS_NAME, "readwrite")
val os = transaction.objectStore(OS_NAME)
val req = os.delete(path)
req.onerror = { reject(Exception("could not delete $path")) }
req.onsuccess = { resolve(0) }
}.await()
}
override fun openResourceFile(filename: String): ResourceFile {
return JsResourceFile(filename)
suspend fun put(path: String, content: String) {
Promise<Int> { resolve, reject ->
val transaction = db.transaction(OS_NAME, "readwrite")
val os = transaction.objectStore(OS_NAME)
val req = os.put(content, path)
req.onerror = { reject(Exception("could not put $path")) }
req.onsuccess = { resolve(0) }
}.await()
}
suspend fun get(path: String): String {
return Promise<String> { resolve, reject ->
val transaction = db.transaction(OS_NAME, "readonly")
val os = transaction.objectStore(OS_NAME)
val req = os.get(path)
req.onerror = { reject(Exception("could not get $path")) }
req.onsuccess = { resolve(req.result) }
}.await()
}
suspend fun exists(path: String): Boolean {
return Promise<Boolean> { resolve, reject ->
val transaction = db.transaction(OS_NAME, "readonly")
val os = transaction.objectStore(OS_NAME)
val req = os.count(path)
req.onerror = { reject(Exception("could not count $path")) }
req.onsuccess = { resolve(req.result > 0) }
}.await()
}
}
class JsUserFile(filename: String) : UserFile {
override fun delete() {
TODO()
class JsFileOpener(val fileStorage: JsFileStorage) : FileOpener {
override fun openUserFile(path: String): UserFile {
return JsUserFile(fileStorage, path)
}
override fun exists(): Boolean {
TODO()
override fun openResourceFile(path: String): ResourceFile {
return JsResourceFile(path)
}
}
class JsUserFile(val fs: JsFileStorage,
val filename: String) : UserFile {
override suspend fun lines(): List<String> {
return fs.get(filename).lines()
}
override suspend fun delete() {
fs.delete(filename)
}
override suspend fun exists(): Boolean {
return fs.exists(filename)
}
}
class JsResourceFile(val filename: String) : ResourceFile {
override suspend fun exists(): Boolean {
return Promise<Boolean> { resolve, reject ->
val xhr = XMLHttpRequest()
xhr.open("GET", "/assets/$filename", true)
xhr.onload = { resolve(xhr.status.toInt() != 404) }
xhr.onerror = { reject(Exception()) }
xhr.send()
}.await()
}
override suspend fun lines(): List<String> {
return Promise<List<String>> { resolve, reject ->
val xhr = XMLHttpRequest()
@ -55,7 +135,8 @@ class JsResourceFile(val filename: String) : ResourceFile {
}.await()
}
override fun copyTo(dest: UserFile) {
TODO()
override suspend fun copyTo(dest: UserFile) {
val fs = (dest as JsUserFile).fs
fs.put(dest.filename, lines().joinToString("\n"))
}
}

@ -26,7 +26,16 @@ import org.w3c.dom.*
import kotlin.browser.*
actual class DependencyResolver {
actual fun getFileOpener(): FileOpener = JsFileOpener()
var fs: JsFileStorage? = null
actual suspend fun getFileOpener(): FileOpener {
if (fs == null) {
fs = JsFileStorage()
fs?.init()
}
return JsFileOpener(fs!!)
}
actual suspend fun getDatabase(): Database {
val nativeDB = eval("new SQL.Database()")

@ -26,7 +26,7 @@ import kotlin.test.*
class JsAsyncTests {
@Test
fun testLines() = GlobalScope.promise { FilesTest().testLines() }
fun testFiles() = GlobalScope.promise { FilesTest().testLines() }
@Test
fun testDatabase() = GlobalScope.promise { DatabaseTest().testUsage() }

@ -22,45 +22,54 @@ package org.isoron.platform.io
import java.io.*
import java.nio.file.*
class JavaResourceFile(private val path: Path) : ResourceFile {
class JavaResourceFile(private val path: String) : ResourceFile {
private val javaPath: Path
get() {
val mainPath = Paths.get("assets/main/$path")
val testPath = Paths.get("assets/test/$path")
if (Files.exists(mainPath)) return mainPath
else return testPath
}
override suspend fun exists(): Boolean {
return Files.exists(javaPath)
}
override suspend fun lines(): List<String> {
return Files.readAllLines(path)
return Files.readAllLines(javaPath)
}
override fun copyTo(dest: UserFile) {
Files.copy(path, (dest as JavaUserFile).path)
override suspend fun copyTo(dest: UserFile) {
if (dest.exists()) dest.delete()
Files.copy(javaPath, (dest as JavaUserFile).path)
}
fun stream(): InputStream {
return Files.newInputStream(path)
return Files.newInputStream(javaPath)
}
}
class JavaUserFile(val path: Path) : UserFile {
override fun exists(): Boolean {
override suspend fun lines(): List<String> {
return Files.readAllLines(path)
}
override suspend fun exists(): Boolean {
return Files.exists(path)
}
override fun delete() {
override suspend fun delete() {
Files.delete(path)
}
}
class JavaFileOpener : FileOpener {
override fun openUserFile(filename: String): UserFile {
val path = Paths.get("/tmp/$filename")
override fun openUserFile(path: String): UserFile {
val path = Paths.get("/tmp/$path")
return JavaUserFile(path)
}
override fun openResourceFile(filename: String): ResourceFile {
val rootFolders = listOf("assets/main",
"assets/test")
for (root in rootFolders) {
val path = Paths.get("$root/$filename")
if (Files.exists(path) && Files.isReadable(path)) {
return JavaResourceFile(path)
}
}
throw RuntimeException("file not found")
override fun openResourceFile(path: String): ResourceFile {
return JavaResourceFile(path)
}
}

@ -33,7 +33,7 @@ actual class DependencyResolver actual constructor() {
val fileOpener = JavaFileOpener()
val databaseOpener = JavaDatabaseOpener(log)
actual fun getFileOpener(): FileOpener = fileOpener
actual suspend fun getFileOpener(): FileOpener = fileOpener
actual suspend fun getDatabase(): Database {
val dbFile = fileOpener.openUserFile("test.sqlite3")

@ -26,7 +26,7 @@ import org.junit.*
class JavaAsyncTests {
@Test
fun testLines() = runBlocking { FilesTest().testLines() }
fun testFiles() = runBlocking { FilesTest().testLines() }
@Test
fun testDatabase() = runBlocking { DatabaseTest().testUsage() }

@ -19,6 +19,7 @@
package org.isoron.uhabits
import kotlinx.coroutines.*
import org.isoron.platform.gui.*
import org.isoron.platform.io.*
import org.isoron.uhabits.components.*
@ -55,28 +56,30 @@ open class BaseViewTest {
expectedPath: String,
component: Component,
threshold: Double = 1e-3) {
val actual = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
val canvas = JavaCanvas(actual)
val expectedFile: JavaResourceFile
val actualPath = "/tmp/${expectedPath}"
component.draw(canvas)
try {
expectedFile = JavaFileOpener().openResourceFile(expectedPath) as JavaResourceFile
} catch(e: RuntimeException) {
File(actualPath).parentFile.mkdirs()
ImageIO.write(actual, "png", File(actualPath))
//fail("Expected file is missing. Actual render saved to $actualPath")
return
}
expectedFile = JavaFileOpener().openResourceFile(expectedPath) as JavaResourceFile
val expected = ImageIO.read(expectedFile.stream())
val d = distance(actual, expected)
if (d >= threshold) {
File(actualPath).parentFile.mkdirs()
ImageIO.write(actual, "png", File(actualPath))
ImageIO.write(expected, "png", File(actualPath.replace(".png", ".expected.png")))
//fail("Images differ (distance=${d}). Actual rendered saved to ${actualPath}.")
runBlocking<Unit> {
if (expectedFile.exists()) {
val expected = ImageIO.read(expectedFile.stream())
val d = distance(actual, expected)
if (d >= threshold) {
File(actualPath).parentFile.mkdirs()
ImageIO.write(actual, "png", File(actualPath))
ImageIO.write(expected, "png", File(actualPath.replace(".png", ".expected.png")))
//fail("Images differ (distance=${d}). Actual rendered saved to ${actualPath}.")
}
} else {
File(actualPath).parentFile.mkdirs()
ImageIO.write(actual, "png", File(actualPath))
//fail("Expected file is missing. Actual render saved to $actualPath")
}
}
}
}
Loading…
Cancel
Save