diff --git a/core/.js/src/main/scala/io/udash/utils/FileService.scala b/core/.js/src/main/scala/io/udash/utils/FileService.scala new file mode 100644 index 000000000..c8a1529ea --- /dev/null +++ b/core/.js/src/main/scala/io/udash/utils/FileService.scala @@ -0,0 +1,115 @@ +package io.udash.utils + +import java.io.{IOException, InputStream} + +import org.scalajs.dom._ +import org.scalajs.dom.html.Anchor +import org.scalajs.dom.raw.Blob +import scalatags.JsDom + +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 +sealed class FileReaderSync() extends js.Object { + def readAsArrayBuffer(blob: Blob): ArrayBuffer = js.native +} + +sealed class FileBufferedInputStream(val file: File) extends InputStream { + val fileReaderSync = new FileReaderSync() + + var filePos: Int = 0 + var pos: Int = 0 + + var buffer: Array[Byte] = Array.empty + + override def read(): Int = { + if (pos >= buffer.length) { + import js.typedarray._ + + if (filePos >= file.size) { + return -1 + } + + val len = math.min(filePos + 1024, file.size.toInt) + val slice = file.slice(filePos, len) + buffer = new Int8Array(fileReaderSync.readAsArrayBuffer(slice)).toArray + filePos += buffer.length + pos = 0 + } + val r = buffer(pos).toInt & 0xff + pos += 1 + r + } +} + +object FileService { + + final val OctetStreamType = "application/octet-stream" + + /** + * Converts specified bytes array to string that contains URL + * that representing the array given in the parameter with optionally specified mime-type. + * + * Keep in mind that returned URL should be revoked via `org.scalajs.dom.revokeObjectURL(url)`. + */ + def asURL(bytes: Array[Byte], mimeType: String = OctetStreamType): String = { + import js.typedarray._ + + val jsBytes = js.Array[js.Any](bytes.toTypedArray) + val blob = new Blob(jsBytes, BlobPropertyBag(mimeType)) + URL.createObjectURL(blob) + } + + /** + * Create an anchor element that on click downloads byte array as a file with specified name. + * + * Keep in mind that anchor's href URL should be revoked via `org.scalajs.dom.revokeObjectURL(url)`. + */ + def asAnchor(filename: String, bytes: Array[Byte], mimeType: String = OctetStreamType): JsDom.TypedTag[Anchor] = { + import JsDom.all._ + + val download = attr("download") + a(href := asURL(bytes, mimeType), download := filename) + } + + /** + * Asynchronously convert specified file to bytes array. + */ + def asBytesArray(file: File): 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 + )) + + fileReader.readAsArrayBuffer(file) + + promise.future + } + + /** + * Convert specified file to InputStream with blocking I/O + * + * Because it is using synchronous I/O that could potentially 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 asInputStream(file: File): InputStream = + new FileBufferedInputStream(file) +} diff --git a/guide/guide/.js/src/main/scala/io/udash/web/guide/views/frontend/FrontendFilesView.scala b/guide/guide/.js/src/main/scala/io/udash/web/guide/views/frontend/FrontendFilesView.scala index 4c7f3e89b..5eaef2e97 100644 --- a/guide/guide/.js/src/main/scala/io/udash/web/guide/views/frontend/FrontendFilesView.scala +++ b/guide/guide/.js/src/main/scala/io/udash/web/guide/views/frontend/FrontendFilesView.scala @@ -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("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:"), diff --git a/guide/guide/.js/src/main/scala/io/udash/web/guide/views/frontend/demos/FileInputDemo.scala b/guide/guide/.js/src/main/scala/io/udash/web/guide/views/frontend/demos/FileInputDemo.scala index 481799507..2573bbb75 100644 --- a/guide/guide/.js/src/main/scala/io/udash/web/guide/views/frontend/demos/FileInputDemo.scala +++ b/guide/guide/.js/src/main/scala/io/udash/web/guide/views/frontend/demos/FileInputDemo.scala @@ -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._ @@ -9,8 +10,11 @@ object FileInputDemo extends AutoDemo with CssView { private val (rendered, source) = { import io.udash._ - import org.scalajs.dom.File + import org.scalajs.dom.{File, URL} import scalatags.JsDom.all._ + import org.scalajs.dom.window + + import scala.concurrent.ExecutionContext.Implicits.global val acceptMultipleFiles = Property(true) val selectedFiles = SeqProperty.blank[File] @@ -19,7 +23,27 @@ object FileInputDemo extends AutoDemo with CssView { FileInput(selectedFiles, acceptMultipleFiles)("files"), h4("Selected files"), ul(repeat(selectedFiles)(file => { - li(file.get.name).render + val content = Property(Array.empty[Byte]) + + window.setTimeout(() => + FileService.asBytesArray(file.get) foreach { bytes => + content.set(bytes) + }, 3000) + + val name = file.get.name + li(showIfElse(content.transform(_.isEmpty))( + span(name).render, + { + val anchor = FileService.asAnchor(name, content.get)(name).render + + window.setTimeout(() => { + content.set(Array.empty[Byte]) + URL.revokeObjectURL(anchor.href) + }, 10000) + + anchor + } + )).render })) ) }.withSourceCode