forked from UdashFramework/udash-core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
FileService: a way to work with file at frontend
The usecase of this code is something like this: let image that user is making a protonmail or any another application where content inside frontend (=browser storage) some secret user's data that shouldn't be exposed to backend. Secret key for example or anything like that. This code allows to developer to convert any `Seq[Array[Byte]]` from frontend to URL as simple call `FileService.createURL`, asynchronously convert any uploaded `File` to `Array[Byte]` as `FileService.asBytesArray`, or to `InputStream` via `FileService.asInputStream` inside worker. Unfortunately scalatags doesn't support `download` attribute and I need to make it by hand. I've opened a PR[^1] to introduce it, but it might be a while until it is included to release. `FileService.asInputStream` is using `FileReaderSync` that is also missed inside scala-js-dom. I've opened a PR[^2] but it might be a while. Also, this draft API but it is supported by majority of modern browsers[^3]. [^1]: com-lihaoyi/scalatags#212 [^2]: scala-js/scala-js-dom#424 [^3]: https://caniuse.com/?search=FileReaderSync
- Loading branch information
Showing
3 changed files
with
181 additions
and
2 deletions.
There are no files selected for viewing
154 changes: 154 additions & 0 deletions
154
core/.js/src/main/scala/io/udash/utils/FileService.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
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 | ||
} | ||
|
||
final class FileBufferedInputStream(file: File, bufferSize: Int) extends InputStream { | ||
val fileReaderSync = new FileReaderSync() | ||
|
||
var filePos: Int = 0 | ||
var pos: Int = 0 | ||
|
||
var buffer: Array[Byte] = Array.empty | ||
|
||
override def available(): Int = { | ||
val res = file.size.toInt - filePos | ||
if (res < 0) 0 else res | ||
} | ||
|
||
override def read(): Int = { | ||
if (pos >= buffer.length) { | ||
import js.typedarray._ | ||
|
||
if (filePos >= file.size) | ||
return -1 | ||
|
||
val len = math.min(filePos + bufferSize, 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 | ||
} | ||
|
||
override def close(): Unit = filePos = file.size.toInt | ||
|
||
override def markSupported(): Boolean = true | ||
|
||
var markPos: Option[Int] = None | ||
|
||
override def mark(readlimit: Int): Unit = | ||
markPos = Some(pos + filePos - buffer.length) | ||
|
||
override def reset(): Unit = markPos match { | ||
case Some(p) => | ||
filePos = p | ||
pos = 0 | ||
buffer = Array.empty | ||
markPos = None | ||
|
||
case _ => | ||
// do nothing | ||
} | ||
} | ||
|
||
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 revoked via `org.scalajs.dom.revokeObjectURL(url)`. | ||
*/ | ||
def createURL(bytesArrays: Seq[Array[Byte]], mimeType: String): String = { | ||
import js.typedarray._ | ||
|
||
val jsBytesArrays = js.Array[js.Any](bytesArrays.map(_.toTypedArray) :_ *) | ||
val blob = new Blob(jsBytesArrays, BlobPropertyBag(mimeType)) | ||
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 revoked via `org.scalajs.dom.revokeObjectURL(url)`. | ||
*/ | ||
def createURL(bytesArrays: Seq[Array[Byte]]): String = | ||
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 revoked via `org.scalajs.dom.revokeObjectURL(url)`. | ||
*/ | ||
def createURL(byteArray: Array[Byte], mimeType: String): String = | ||
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 revoked via `org.scalajs.dom.revokeObjectURL(url)`. | ||
*/ | ||
def createURL(byteArray: Array[Byte]): String = | ||
createURL(Seq(byteArray), OctetStreamType) | ||
|
||
/** | ||
* 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, bufferSize: Int = 1024): InputStream = | ||
new FileBufferedInputStream(file, bufferSize) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters