Skip to content

FileService: a way to work with file at frontend #656

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions core/.js/src/main/scala/io/udash/utils/FileService.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package io.udash.utils

import com.avsystem.commons.misc.AbstractCase

import java.io.IOException
import org.scalajs.dom._
import org.scalajs.dom.raw.Blob

import scala.scalajs.js
import scala.concurrent.{Future, Promise}
import scala.scalajs.js.annotation.JSGlobal
import scala.scalajs.js.typedarray.ArrayBuffer
import scala.util.Try

@js.native
@JSGlobal
private[utils] final class FileReaderSync() extends js.Object {
def readAsArrayBuffer(blob: Blob): ArrayBuffer = js.native
}

final case class CloseableUrl(value: String) extends AbstractCase with AutoCloseable {
override def close(): Unit = {
URL.revokeObjectURL(value)
}
}

object FileService {

final val OctetStreamType = "application/octet-stream"

/**
* Converts specified bytes arrays to string that contains URL
* that representing the array given in the parameter with specified mime-type.
*
* Keep in mind that returned URL should be closed.
*/
def createURL(bytesArrays: Seq[Array[Byte]], mimeType: String): CloseableUrl = {
import js.typedarray._

val jsBytesArrays = js.Array[js.Any](bytesArrays.map(_.toTypedArray) :_ *)
val blob = new Blob(jsBytesArrays, BlobPropertyBag(mimeType))
CloseableUrl(URL.createObjectURL(blob))
}

/**
* Converts specified bytes arrays to string that contains URL
* that representing the array given in the parameter with `application/octet-stream` mime-type.
*
* Keep in mind that returned URL should be closed.
*/
def createURL(bytesArrays: Seq[Array[Byte]]): CloseableUrl =
createURL(bytesArrays, OctetStreamType)

/**
* Converts specified bytes array to string that contains URL
* that representing the array given in the parameter with specified mime-type.
*
* Keep in mind that returned URL should be closed.
*/
def createURL(byteArray: Array[Byte], mimeType: String): CloseableUrl =
createURL(Seq(byteArray), mimeType)

/**
* Converts specified bytes array to string that contains URL
* that representing the array given in the parameter with `application/octet-stream` mime-type.
*
* Keep in mind that returned URL should be closed.
*/
def createURL(byteArray: Array[Byte]): CloseableUrl =
createURL(Seq(byteArray), OctetStreamType)

/**
* Asynchronously convert specified part of file to bytes array.
*/
def asBytesArray(file: File, start: Double, end: Double): Future[Array[Byte]] = {
import js.typedarray._

val fileReader = new FileReader()
val promise = Promise[Array[Byte]]()

fileReader.onerror = (e: Event) =>
promise.failure(new IOException(e.toString))

fileReader.onabort = (e: Event) =>
promise.failure(new IOException(e.toString))

fileReader.onload = (_: UIEvent) =>
promise.complete(Try(
new Int8Array(fileReader.result.asInstanceOf[ArrayBuffer]).toArray
))

val slice = file.slice(start, end)
fileReader.readAsArrayBuffer(slice)

promise.future
}

/**
* Asynchronously convert specified file to bytes array.
*/
def asBytesArray(file: File): Future[Array[Byte]] =
asBytesArray(file, 0, file.size)

/**
* Synchronously convert specified part of file to bytes array.
*
* Because it is using synchronous I/O this API can be used only inside worker.
*
* This method is using FileReaderSync that is part of Working Draft File API.
* Anyway it is supported for majority of modern browsers
*/
def asBytesArraySync(file: File, start: Double, end: Double): Array[Byte] = {
import js.typedarray._

val fileReaderSync = new FileReaderSync()
val slice = file.slice(start, end)

val int8Array = new Int8Array(fileReaderSync.readAsArrayBuffer(slice))

int8Array.toArray
}

/**
* Synchronously convert file to bytes array.
*
* Because it is using synchronous I/O this API can be used only inside worker.
*
* This method is using FileReaderSync that is part of Working Draft File API.
* Anyway it is supported for majority of modern browsers
*/
def asBytesArraySync(file: File): Array[Byte] =
asBytesArraySync(file, 0, file.size)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class FrontendFilesView extends View {
),
p("You can find a working demo application in the ", a(href := References.UdashFilesDemoRepo, target := "_blank")("Udash Demos"), " repositiory."),
h3("Frontend forms"),
p(i("FileService"), " is an object that allows to convert ", i("Array[Byte]")," to URL, save it as file from frontend ",
" and asynchronously convert ", i("File"), " to ", i("Future[Array[Byte]]"), "."),
p(i("FileInput"), " is the file HTML input wrapper providing a property containing selected files. "),
fileInputSnippet,
p("Take a look at the following live demo:"),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.udash.web.guide.views.frontend.demos

import io.udash.css.CssView
import io.udash.utils.FileService
import io.udash.web.guide.demos.AutoDemo
import io.udash.web.guide.styles.partials.GuideStyles
import scalatags.JsDom.all._
Expand All @@ -12,14 +13,35 @@ object FileInputDemo extends AutoDemo with CssView {
import org.scalajs.dom.File
import scalatags.JsDom.all._

val acceptMultipleFiles = Property(true)
import scala.concurrent.ExecutionContext.Implicits.global

val acceptMultipleFiles = true.toProperty
val selectedFiles = SeqProperty.blank[File]

div(
FileInput(selectedFiles, acceptMultipleFiles)("files"),
h4("Selected files"),
ul(repeat(selectedFiles)(file => {
li(file.get.name).render
val content = Property(Array.empty[Byte])

FileService.asBytesArray(file.get) foreach { bytes =>
content.set(bytes)
}

val name = file.get.name
li(showIfElse(content.transform(_.isEmpty))(
span(name).render,
{
val url = FileService.createURL(content.get)
val download = a(href := url.value, attr("download") := name)(name)
val revoke = a(href := "#", onclick := { () =>
content.set(Array.empty[Byte])
url.close()
})("revoke")

Seq(download, span(" or "), revoke).render
}
)).render
}))
)
}.withSourceCode
Expand Down