diff --git a/build.sbt b/build.sbt
index 7f281628..3e67b453 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,3 +1,4 @@
+import com.karasiq.scalajsbundler.compilers.{AssetCompilers, ConcatCompiler}
import com.typesafe.sbt.packager.docker.Cmd
import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject}
@@ -360,12 +361,15 @@ lazy val `server-static-routes` = (project in file("server") / "static-routes")
WebDeps.toastrJS,
WebDeps.pellJS,
WebDeps.multiSelectJS,
- scalaJsApplication(webapp, fastOpt = false, launcher = false).value
+ scalaJsApplication(webapp, fastOpt = true, launcher = false).value
)
},
scalaJsBundlerCompile in Compile := (scalaJsBundlerCompile in Compile)
- .dependsOn(fullOptJS in Compile in webapp)
- .value
+ .dependsOn(fastOptJS in Compile in webapp)
+ .value,
+ scalaJsBundlerCompilers in Compile := AssetCompilers {
+ case com.karasiq.scalajsbundler.dsl.Mimes.javascript ⇒ ConcatCompiler
+ }.<<=(AssetCompilers.default)
)
.dependsOn(`server-api-routes`)
.enablePlugins(SJSAssetBundlerPlugin)
diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf
index f6bd2b47..019e902d 100644
--- a/core/src/main/resources/reference.conf
+++ b/core/src/main/resources/reference.conf
@@ -1,6 +1,8 @@
include "sc-akka-serialization.conf"
shadowcloud {
+ base-dir = ${user.home}/.shadowcloud
+
default-storage {
immutable = false // Prohibits delete of data
// chunk-key = hash
diff --git a/core/src/main/scala/com/karasiq/shadowcloud/ShadowCloud.scala b/core/src/main/scala/com/karasiq/shadowcloud/ShadowCloud.scala
index 6856199b..09b8cb51 100644
--- a/core/src/main/scala/com/karasiq/shadowcloud/ShadowCloud.scala
+++ b/core/src/main/scala/com/karasiq/shadowcloud/ShadowCloud.scala
@@ -27,8 +27,8 @@ import com.karasiq.shadowcloud.streams.chunk.ChunkProcessingStreams
import com.karasiq.shadowcloud.streams.file.FileStreams
import com.karasiq.shadowcloud.streams.metadata.MetadataStreams
import com.karasiq.shadowcloud.streams.region.RegionStreams
-import com.karasiq.shadowcloud.ui.UIProvider
import com.karasiq.shadowcloud.ui.passwords.PasswordProvider
+import com.karasiq.shadowcloud.ui.{ChallengeHub, UIProvider}
import com.karasiq.shadowcloud.utils.{ProviderInstantiator, SCProviderInstantiator}
import com.typesafe.config.Config
@@ -180,6 +180,8 @@ class ShadowCloudExtension(_actorSystem: ExtendedActorSystem) extends Extension
// -----------------------------------------------------------------------
// User interface
// -----------------------------------------------------------------------
+ object challenges extends ChallengeHub
+
object ui extends UIProvider with PasswordProvider {
private[this] lazy val passProvider: PasswordProvider = provInstantiator.getInstance(config.ui.passwordProvider)
private[this] lazy val uiProvider: UIProvider = provInstantiator.getInstance(config.ui.uiProvider)
diff --git a/core/src/main/scala/com/karasiq/shadowcloud/ui/ChallengeHub.scala b/core/src/main/scala/com/karasiq/shadowcloud/ui/ChallengeHub.scala
new file mode 100644
index 00000000..bfbc9a6c
--- /dev/null
+++ b/core/src/main/scala/com/karasiq/shadowcloud/ui/ChallengeHub.scala
@@ -0,0 +1,55 @@
+package com.karasiq.shadowcloud.ui
+
+import java.time.Instant
+import java.util.UUID
+
+import akka.actor.ActorSystem
+import akka.event.Logging
+import akka.util.ByteString
+import com.karasiq.shadowcloud.ui.Challenge.AnswerFormat
+
+import scala.collection.concurrent.TrieMap
+import scala.concurrent.duration._
+import scala.concurrent.{Future, Promise, TimeoutException}
+
+class ChallengeHub(implicit as: ActorSystem) {
+ import as.dispatcher
+ private[this] val log = Logging(as, classOf[ChallengeHub])
+ private[this] val challenges = TrieMap.empty[UUID, (Challenge, Promise[ByteString], Deadline)]
+ private[this] val schedule = as.scheduler.scheduleAtFixedRate(1 second, 1 second) { () ⇒
+ challenges.values.collect {
+ case (challenge, promise, deadline) if deadline.isOverdue() ⇒
+ log.warning("Challenge timed out: {}", challenge.title)
+ promise.tryFailure(new TimeoutException(s"Challenge timed out: ${challenge.title}"))
+ }
+ }
+
+ def list(): Seq[Challenge] =
+ challenges.values.map(_._1).toVector.sortBy(_.time.toEpochMilli)
+
+ def create(
+ title: String,
+ html: String = "",
+ answerFormat: AnswerFormat = AnswerFormat.String,
+ deadline: FiniteDuration = 5 minutes
+ ): Future[ByteString] = {
+ val challenge = Challenge(UUID.randomUUID(), Instant.now(), title, html, answerFormat)
+ val promise = Promise[ByteString]
+ log.info("Challenge created: {}", title)
+ challenges(challenge.id) = (challenge, promise, Deadline.now + deadline)
+ promise.future.onComplete(_ ⇒ challenges -= challenge.id)
+ promise.future
+ }
+
+ def solve(id: UUID, response: ByteString = ByteString.empty): Unit = challenges.remove(id).foreach {
+ case (challenge, promise, _) ⇒
+ log.info("Challenge solved: {}", challenge.title)
+ promise.trySuccess(response)
+ }
+
+ override def finalize(): Unit = {
+ schedule.cancel()
+ challenges.values.foreach { case (_, p, _) ⇒ p.tryFailure(new RuntimeException("Challenge hub closed")) }
+ super.finalize()
+ }
+}
diff --git a/model/src/main/scala/com/karasiq/shadowcloud/ui/Challenge.scala b/model/src/main/scala/com/karasiq/shadowcloud/ui/Challenge.scala
new file mode 100644
index 00000000..cf020b89
--- /dev/null
+++ b/model/src/main/scala/com/karasiq/shadowcloud/ui/Challenge.scala
@@ -0,0 +1,19 @@
+package com.karasiq.shadowcloud.ui
+
+import java.time.Instant
+import java.util.UUID
+
+import com.karasiq.shadowcloud.model.SCEntity
+import com.karasiq.shadowcloud.ui.Challenge.AnswerFormat
+
+@SerialVersionUID(0L)
+final case class Challenge(id: UUID, time: Instant, title: String, html: String, answerFormat: AnswerFormat) extends SCEntity
+
+object Challenge {
+ sealed trait AnswerFormat
+ object AnswerFormat {
+ case object Ack extends AnswerFormat
+ case object String extends AnswerFormat
+ case object Binary extends AnswerFormat
+ }
+}
diff --git a/persistence/src/main/resources/reference.conf b/persistence/src/main/resources/reference.conf
index 208a68e5..0141bcc8 100644
--- a/persistence/src/main/resources/reference.conf
+++ b/persistence/src/main/resources/reference.conf
@@ -1,5 +1,5 @@
shadowcloud.persistence.h2 {
- path = ${user.home}/.shadowcloud/shadowcloud
+ path = ${shadowcloud.base-dir}/shadowcloud
cipher = AES
compress = true
init-script = "classpath:sc-persistence-h2-init.sql"
diff --git a/serialization/src/main/scala/com/karasiq/shadowcloud/serialization/boopickle/SCBooPickleEncoders.scala b/serialization/src/main/scala/com/karasiq/shadowcloud/serialization/boopickle/SCBooPickleEncoders.scala
index 7c665bfd..02940a39 100644
--- a/serialization/src/main/scala/com/karasiq/shadowcloud/serialization/boopickle/SCBooPickleEncoders.scala
+++ b/serialization/src/main/scala/com/karasiq/shadowcloud/serialization/boopickle/SCBooPickleEncoders.scala
@@ -1,19 +1,21 @@
package com.karasiq.shadowcloud.serialization.boopickle
+import java.time.Instant
+
import akka.Done
import akka.util.ByteString
import boopickle._
-import scalapb.{GeneratedEnum, GeneratedEnumCompanion, GeneratedMessage, GeneratedMessageCompanion}
-
import com.karasiq.shadowcloud.config.SerializedProps
-import com.karasiq.shadowcloud.index.{ChunkIndex, FolderIndex, IndexData}
import com.karasiq.shadowcloud.index.diffs.{ChunkIndexDiff, FolderDiff, FolderIndexDiff, IndexDiff}
+import com.karasiq.shadowcloud.index.{ChunkIndex, FolderIndex, IndexData}
import com.karasiq.shadowcloud.model._
import com.karasiq.shadowcloud.model.crypto._
import com.karasiq.shadowcloud.model.keys.{KeyChain, KeyProps, KeySet}
-import com.karasiq.shadowcloud.model.utils._
import com.karasiq.shadowcloud.model.utils.GCReport.{RegionGCState, StorageGCState}
import com.karasiq.shadowcloud.model.utils.RegionStateReport.{RegionStatus, StorageStatus}
+import com.karasiq.shadowcloud.model.utils._
+import com.karasiq.shadowcloud.ui.Challenge
+import scalapb.{GeneratedEnum, GeneratedEnumCompanion, GeneratedMessage, GeneratedMessageCompanion}
//noinspection TypeAnnotation
trait SCBooPickleEncoders extends Base with BasicImplicitPicklers with TransformPicklers with TuplePicklers {
@@ -45,48 +47,60 @@ trait SCBooPickleEncoders extends Base with BasicImplicitPicklers with Transform
}
}
- implicit val akkaDoneFormat: Pickler[Done] = ConstPickler(Done)
- implicit val akkaDoneFormat1 = ConstPickler(Done)
- implicit val pathFormat = generatePickler[Path]
- implicit val serializedPropsFormat = generatePickler[SerializedProps]
- implicit val encryptionMethodFormat = generatePickler[EncryptionMethod]
- implicit val hashingMethodFormat = generatePickler[HashingMethod]
- implicit val symmetricEncryptionParametersFormat = generatePickler[SymmetricEncryptionParameters]
+ implicit object instantFormat extends Pickler[Instant] {
+ override def pickle(obj: Instant)(implicit state: PickleState): Unit = {
+ state.enc.writeLong(obj.toEpochMilli)
+ }
+
+ override def unpickle(implicit state: UnpickleState): Instant = {
+ Instant.ofEpochMilli(state.dec.readLong)
+ }
+ }
+
+ implicit val akkaDoneFormat: Pickler[Done] = ConstPickler(Done)
+ implicit val akkaDoneFormat1 = ConstPickler(Done)
+ implicit val pathFormat = generatePickler[Path]
+ implicit val serializedPropsFormat = generatePickler[SerializedProps]
+ implicit val encryptionMethodFormat = generatePickler[EncryptionMethod]
+ implicit val hashingMethodFormat = generatePickler[HashingMethod]
+ implicit val symmetricEncryptionParametersFormat = generatePickler[SymmetricEncryptionParameters]
implicit val asymmetricEncryptionParametersFormat = generatePickler[AsymmetricEncryptionParameters]
- implicit val encryptionParametersFormat = generatePickler[EncryptionParameters]
+ implicit val encryptionParametersFormat = generatePickler[EncryptionParameters]
- implicit val signMethodFormat = generatePickler[SignMethod]
+ implicit val signMethodFormat = generatePickler[SignMethod]
implicit val signParametersFormat = generatePickler[SignParameters]
- implicit val timestampFormat = generatePickler[Timestamp]
- implicit val dataFormat = generatePickler[Data]
- implicit val checksumFormat = generatePickler[Checksum]
- implicit val chunkFormat = generatePickler[Chunk]
- implicit val fileFormat = generatePickler[File]
- implicit val folderFormat = generatePickler[Folder]
-
- implicit val chunkIndexFormat = generatePickler[ChunkIndex]
- implicit val folderIndexFormat = generatePickler[FolderIndex]
- implicit val folderDiffFormat = generatePickler[FolderDiff]
- implicit val folderIndexDiffFormat = generatePickler[FolderIndexDiff]
- implicit val chunkIndexDiffFormat = generatePickler[ChunkIndexDiff]
- implicit val indexDiffFormat = generatePickler[IndexDiff]
- implicit val indexDataFormat = generatePickler[IndexData]
- implicit val fileAvailabilityFormat = generatePickler[FileAvailability]
- implicit val storageGCStateFormat = generatePickler[StorageGCState]
- implicit val regionGCStateFormat = generatePickler[RegionGCState]
- implicit val gCReportFormat = generatePickler[GCReport]
- implicit val syncReportFormat = generatePickler[SyncReport]
- implicit val storageStatusFormat = generatePickler[StorageStatus]
- implicit val regionStatusFormat = generatePickler[RegionStatus]
- implicit val regionStateReportFormat = generatePickler[RegionStateReport]
- implicit val storageHealthFormat = generatePickler[StorageHealth]
- implicit val regionHealthFormat = generatePickler[RegionHealth]
- implicit val indexScopeFormat = generatePickler[IndexScope]
- implicit val keySetFormat = generatePickler[KeySet]
- implicit val keyPropsFormat = generatePickler[KeyProps]
- implicit val keyChainFormat = generatePickler[KeyChain]
-
- implicit def generatedMessagePickler[T <: GeneratedMessage with scalapb.Message[T] : GeneratedMessageCompanion]: Pickler[T] = new Pickler[T] {
+ implicit val timestampFormat = generatePickler[Timestamp]
+ implicit val dataFormat = generatePickler[Data]
+ implicit val checksumFormat = generatePickler[Checksum]
+ implicit val chunkFormat = generatePickler[Chunk]
+ implicit val fileFormat = generatePickler[File]
+ implicit val folderFormat = generatePickler[Folder]
+
+ implicit val chunkIndexFormat = generatePickler[ChunkIndex]
+ implicit val folderIndexFormat = generatePickler[FolderIndex]
+ implicit val folderDiffFormat = generatePickler[FolderDiff]
+ implicit val folderIndexDiffFormat = generatePickler[FolderIndexDiff]
+ implicit val chunkIndexDiffFormat = generatePickler[ChunkIndexDiff]
+ implicit val indexDiffFormat = generatePickler[IndexDiff]
+ implicit val indexDataFormat = generatePickler[IndexData]
+ implicit val fileAvailabilityFormat = generatePickler[FileAvailability]
+ implicit val storageGCStateFormat = generatePickler[StorageGCState]
+ implicit val regionGCStateFormat = generatePickler[RegionGCState]
+ implicit val gCReportFormat = generatePickler[GCReport]
+ implicit val syncReportFormat = generatePickler[SyncReport]
+ implicit val storageStatusFormat = generatePickler[StorageStatus]
+ implicit val regionStatusFormat = generatePickler[RegionStatus]
+ implicit val regionStateReportFormat = generatePickler[RegionStateReport]
+ implicit val storageHealthFormat = generatePickler[StorageHealth]
+ implicit val regionHealthFormat = generatePickler[RegionHealth]
+ implicit val indexScopeFormat = generatePickler[IndexScope]
+ implicit val keySetFormat = generatePickler[KeySet]
+ implicit val keyPropsFormat = generatePickler[KeyProps]
+ implicit val keyChainFormat = generatePickler[KeyChain]
+ implicit val challengeAnswerFormatFormat = generatePickler[Challenge.AnswerFormat]
+ implicit val challengeFormat = generatePickler[Challenge]
+
+ implicit def generatedMessagePickler[T <: GeneratedMessage with scalapb.Message[T]: GeneratedMessageCompanion]: Pickler[T] = new Pickler[T] {
def pickle(obj: T)(implicit state: PickleState): Unit = {
state.enc.writeByteArray(obj.toByteArray)
}
@@ -96,7 +110,7 @@ trait SCBooPickleEncoders extends Base with BasicImplicitPicklers with Transform
}
}
- implicit def generatedEnumPickler[T <: GeneratedEnum : GeneratedEnumCompanion]: Pickler[T] = new Pickler[T] {
+ implicit def generatedEnumPickler[T <: GeneratedEnum: GeneratedEnumCompanion]: Pickler[T] = new Pickler[T] {
def pickle(obj: T)(implicit state: PickleState): Unit = {
state.enc.writeInt(obj.value)
}
diff --git a/server/api-routes/src/main/scala/com/karasiq/shadowcloud/server/http/api/ShadowCloudApiImpl.scala b/server/api-routes/src/main/scala/com/karasiq/shadowcloud/server/http/api/ShadowCloudApiImpl.scala
index 48227847..8fea2a2b 100644
--- a/server/api-routes/src/main/scala/com/karasiq/shadowcloud/server/http/api/ShadowCloudApiImpl.scala
+++ b/server/api-routes/src/main/scala/com/karasiq/shadowcloud/server/http/api/ShadowCloudApiImpl.scala
@@ -1,7 +1,10 @@
package com.karasiq.shadowcloud.server.http.api
+import java.util.UUID
+
import akka.Done
import akka.stream.scaladsl.Sink
+import akka.util.ByteString
import com.karasiq.shadowcloud.ShadowCloudExtension
import com.karasiq.shadowcloud.actors.RegionGC.GCStrategy
import com.karasiq.shadowcloud.actors.internal.RegionTracker
@@ -18,6 +21,7 @@ import com.karasiq.shadowcloud.model.utils.{IndexScope, RegionStateReport}
import com.karasiq.shadowcloud.storage.props.StorageProps
import com.karasiq.shadowcloud.storage.replication.ChunkWriteAffinity
import com.karasiq.shadowcloud.streams.region.RegionRepairStream
+import com.karasiq.shadowcloud.ui.Challenge
import scala.concurrent.Future
import scala.language.implicitConversions
@@ -281,6 +285,15 @@ private[server] final class ShadowCloudApiImpl(sc: ShadowCloudExtension) extends
} yield Done
}
+ override def getChallenges(): Future[Seq[Challenge]] = Future.successful {
+ sc.challenges.list()
+ }
+
+ override def solveChallenge(id: UUID, answer: ByteString): Future[Done] = Future.successful {
+ sc.challenges.solve(id, answer)
+ Done
+ }
+
// -----------------------------------------------------------------------
// Utils
// -----------------------------------------------------------------------
diff --git a/server/autowire-api/src/main/scala/com/karasiq/shadowcloud/api/ShadowCloudApi.scala b/server/autowire-api/src/main/scala/com/karasiq/shadowcloud/api/ShadowCloudApi.scala
index 50aea376..a5a07332 100644
--- a/server/autowire-api/src/main/scala/com/karasiq/shadowcloud/api/ShadowCloudApi.scala
+++ b/server/autowire-api/src/main/scala/com/karasiq/shadowcloud/api/ShadowCloudApi.scala
@@ -1,11 +1,15 @@
package com.karasiq.shadowcloud.api
+import java.util.UUID
+
import akka.Done
+import akka.util.ByteString
import com.karasiq.shadowcloud.config.SerializedProps
import com.karasiq.shadowcloud.metadata.Metadata
import com.karasiq.shadowcloud.model._
import com.karasiq.shadowcloud.model.keys.{KeyChain, KeyId, KeySet}
import com.karasiq.shadowcloud.model.utils._
+import com.karasiq.shadowcloud.ui.Challenge
import scala.concurrent.Future
@@ -68,4 +72,10 @@ trait ShadowCloudApi {
def deleteFiles(regionId: RegionId, path: Path): Future[Set[File]]
def deleteFile(regionId: RegionId, file: File): Future[File]
def repairFile(regionId: RegionId, file: File, storages: Seq[StorageId], scope: IndexScope = IndexScope.default): Future[Done]
+
+ // -----------------------------------------------------------------------
+ // Сhallenges
+ // -----------------------------------------------------------------------
+ def getChallenges(): Future[Seq[Challenge]]
+ def solveChallenge(id: UUID, answer: ByteString): Future[Done]
}
diff --git a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/api/AjaxApi.scala b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/api/AjaxApi.scala
index 919a0749..0575759e 100644
--- a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/api/AjaxApi.scala
+++ b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/api/AjaxApi.scala
@@ -1,5 +1,9 @@
package com.karasiq.shadowcloud.webapp.api
+import java.util.UUID
+
+import akka.Done
+import akka.util.ByteString
import autowire._
import com.karasiq.shadowcloud.api.js.SCAjaxBooPickleApiClient
import com.karasiq.shadowcloud.api.{SCApiMeta, ShadowCloudApi}
@@ -9,6 +13,7 @@ import com.karasiq.shadowcloud.metadata.Metadata.Tag
import com.karasiq.shadowcloud.model._
import com.karasiq.shadowcloud.model.keys.{KeyId, KeySet}
import com.karasiq.shadowcloud.model.utils.IndexScope
+import com.karasiq.shadowcloud.ui.Challenge
import com.karasiq.shadowcloud.webapp.context.AppContext
import scala.concurrent.{ExecutionContext, Future}
@@ -25,10 +30,10 @@ object AjaxApi extends ShadowCloudApi with FileApi with SCApiMeta {
val payloadContentType = clientFactory.payloadContentType
import encoding.implicits._ // Should not be deleted
-
- private[this] val apiClient = clientFactory[ShadowCloudApi]
private[this] implicit val implicitExecutionContext: ExecutionContext = AppContext.JsExecutionContext
+ private[this] val apiClient = clientFactory[ShadowCloudApi]
+
// -----------------------------------------------------------------------
// Regions
// -----------------------------------------------------------------------
@@ -212,4 +217,12 @@ object AjaxApi extends ShadowCloudApi with FileApi with SCApiMeta {
def repairFile(regionId: RegionId, file: File, storages: Seq[StorageId], scope: IndexScope) = {
apiClient.repairFile(regionId, file, storages, scope).call()
}
+
+ override def getChallenges(): Future[Seq[Challenge]] = {
+ apiClient.getChallenges().call()
+ }
+
+ override def solveChallenge(id: UUID, answer: ByteString): Future[Done] = {
+ apiClient.solveChallenge(id, answer).call()
+ }
}
diff --git a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/ChallengePanel.scala b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/ChallengePanel.scala
new file mode 100644
index 00000000..d7dc36c5
--- /dev/null
+++ b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/ChallengePanel.scala
@@ -0,0 +1,76 @@
+package com.karasiq.shadowcloud.webapp.components
+
+import java.util.UUID
+
+import akka.util.ByteString
+import com.karasiq.bootstrap.Bootstrap.default._
+import com.karasiq.shadowcloud.ui.Challenge
+import com.karasiq.shadowcloud.webapp.api.AjaxApi
+import com.karasiq.shadowcloud.webapp.components.common.Toastr
+import com.karasiq.shadowcloud.webapp.context.AppContext
+import com.karasiq.shadowcloud.webapp.utils.{Blobs, RxWithUpdate}
+import org.scalajs.dom
+import rx.{Rx, Var}
+import scalaTags.all._
+
+import scala.collection.mutable
+import scala.concurrent.Future
+import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue
+
+object ChallengePanel {
+ def apply()(implicit context: AppContext): ChallengePanel = new ChallengePanel
+}
+
+class ChallengePanel(implicit context: AppContext) extends BootstrapHtmlComponent {
+ private[this] val list = RxWithUpdate(Seq.empty[Challenge])(_ ⇒ AjaxApi.getChallenges())
+ dom.window.setInterval(() ⇒ list.update(), 10000)
+
+ override def renderTag(md: ModifierT*): TagT = {
+ val map = mutable.HashMap.empty[UUID, TableRow]
+
+ val source = list.toRx
+ source.triggerLater {
+ map.keys
+ .filter(id ⇒ !source.now.exists(_.id == id))
+ .foreach(map -= _)
+ }
+
+ def renderChallenge(challenge: Challenge): TableRow =
+ map.getOrElseUpdate(
+ challenge.id, {
+ Toastr.info(challenge.title, "Challenge")
+ val string = Var("")
+ val files = Var(Seq.empty[dom.File])
+
+ TableRow(
+ Seq(
+ challenge.title,
+ raw(challenge.html),
+ FormInput.text("", string.reactiveInput),
+ FormInput.file("", files.reactiveInputRead),
+ Button(block = true)(
+ context.locale.submit,
+ onclick := Callback.onClick { _ ⇒
+ val data = files.now.headOption match {
+ case Some(value) ⇒
+ Blobs.toBytes(value)
+ case None ⇒
+ Future.successful(ByteString(string.now))
+ }
+
+ data
+ .flatMap(AjaxApi.solveChallenge(challenge.id, _))
+ .foreach(_ ⇒ list.update())
+ }
+ )
+ )
+ )
+ }
+ )
+
+ Table(
+ Rx(Seq(context.locale.name, context.locale.content, context.locale.pasteText, context.locale.file, context.locale.submit)),
+ source.map(_.map(renderChallenge))
+ )
+ }
+}
diff --git a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/SCFrontend.scala b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/SCFrontend.scala
index bf60f2cf..18d550c7 100644
--- a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/SCFrontend.scala
+++ b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/SCFrontend.scala
@@ -1,20 +1,18 @@
package com.karasiq.shadowcloud.webapp.components
-import scala.util.control.NonFatal
-
-import org.scalajs.dom
-
import com.karasiq.bootstrap.Bootstrap.default._
-import scalaTags.all._
-
import com.karasiq.shadowcloud.webapp.components.common.AppIcons
import com.karasiq.shadowcloud.webapp.components.folder.FoldersPanel
import com.karasiq.shadowcloud.webapp.components.keys.KeysContext
-import com.karasiq.shadowcloud.webapp.components.region.{RegionContext, RegionsStoragesPanel, RegionSwitcher}
+import com.karasiq.shadowcloud.webapp.components.region.{RegionContext, RegionSwitcher, RegionsStoragesPanel}
import com.karasiq.shadowcloud.webapp.components.themes.ThemeSelector
import com.karasiq.shadowcloud.webapp.context.{AppContext, FolderContext}
import com.karasiq.shadowcloud.webapp.controllers.{FileController, FolderController}
import com.karasiq.shadowcloud.webapp.utils.RxLocation
+import org.scalajs.dom
+import scalaTags.all._
+
+import scala.util.control.NonFatal
object SCFrontend {
def apply()(implicit appContext: AppContext): SCFrontend = {
@@ -67,7 +65,8 @@ class SCFrontend()(implicit val context: AppContext) {
.withTabs(
NavigationTab(context.locale.foldersView, "folders", AppIcons.foldersView, renderFoldersPanel()),
NavigationTab(context.locale.regionsView, "regions", AppIcons.regionsView, renderRegionsPanel()),
- NavigationTab(context.locale.logs, "logs", AppIcons.logs, LogPanel())
+ NavigationTab(context.locale.logs, "logs", AppIcons.logs, LogPanel()),
+ NavigationTab("Challenges", "challengees", AppIcons.rename, ChallengePanel())
)
}
diff --git a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/keys/KeysView.scala b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/keys/KeysView.scala
index a83ad570..c4e69f57 100644
--- a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/keys/KeysView.scala
+++ b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/keys/KeysView.scala
@@ -81,27 +81,12 @@ class KeysView()(implicit context: AppContext, kc: KeysContext, rc: RegionContex
.show()
}
- def showImportDialog(): Unit = {
- AppComponents.importDialog(context.locale.importKey) { result =>
- val key = ExportUtils.decodeKey(result)
- context.api.addKey(key).foreach(_ ⇒ kc.updateAll())
- }.show()
- }
-
val generateButton = Button(ButtonStyle.success, block = true)(
context.locale.generateKey,
onclick := Callback.onClick(_ ⇒ showGenerateDialog())
)
- val importButton = Button(ButtonStyle.primary, block = true)(
- context.locale.importKey,
- onclick := Callback.onClick(_ ⇒ showImportDialog())
- )
-
- GridSystem.row(
- GridSystem.col.md(6)(generateButton),
- GridSystem.col.md(6)(importButton)
- )
+ generateButton
}
private[this] def showExportDialog(key: KeySet): Unit = {
diff --git a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/region/ExportImportModal.scala b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/region/ExportImportModal.scala
index a9d5b9c6..88145faf 100644
--- a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/region/ExportImportModal.scala
+++ b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/region/ExportImportModal.scala
@@ -49,11 +49,11 @@ object ExportImportModal {
val filteredSnapshot = snapshot.copy(
snapshot.regions.collect {
case (regionId, status) if regions(regionId) ⇒
- regionId → status.copy(storages = status.storages.filter(storages))
+ regionId → status.copy(storages = status.storages)
},
snapshot.storages.collect {
case (storageId, status) if storages(storageId) ⇒
- storageId → status.copy(regions = status.regions.filter(regions))
+ storageId → status.copy(regions = status.regions)
}
)
diff --git a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/utils/Blobs.scala b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/utils/Blobs.scala
index a3cba926..26413a33 100644
--- a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/utils/Blobs.scala
+++ b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/utils/Blobs.scala
@@ -1,13 +1,14 @@
package com.karasiq.shadowcloud.webapp.utils
-import scala.concurrent.{ExecutionContext, Future, Promise}
-import scala.scalajs.js
-import scala.scalajs.js.typedarray.Uint8Array
-import scalatags.JsDom.all._
-
+import akka.util.ByteString
import org.scalajs.dom
-import org.scalajs.dom.{Blob, Event}
import org.scalajs.dom.raw._
+import org.scalajs.dom.{Blob, Event}
+import scalatags.JsDom.all._
+
+import scala.concurrent.{ExecutionContext, Future, Promise}
+import scala.scalajs.js
+import scala.scalajs.js.typedarray.{ArrayBuffer, TypedArrayBuffer, Uint8Array}
/**
* Blob/file utility
@@ -79,4 +80,20 @@ object Blobs {
promise.future
}
+
+ def toBytes(blob: Blob): Future[ByteString] = {
+ val promise = Promise[ByteString]
+ val reader = new FileReader
+ reader.readAsArrayBuffer(blob)
+ reader.onloadend = (_: ProgressEvent) ⇒ {
+ val buffer = TypedArrayBuffer.wrap(reader.result.asInstanceOf[ArrayBuffer])
+ promise.success(ByteString.fromByteBuffer(buffer))
+ }
+
+ reader.onerror = (errorEvent: Event) ⇒ {
+ promise.failure(new IllegalArgumentException(errorEvent.toString))
+ }
+
+ promise.future
+ }
}
diff --git a/storage/telegram/src/main/resources/reference.conf b/storage/telegram/src/main/resources/reference.conf
index b8cc4f49..7ab946a8 100644
--- a/storage/telegram/src/main/resources/reference.conf
+++ b/storage/telegram/src/main/resources/reference.conf
@@ -1,6 +1,7 @@
shadowcloud.storage {
providers.telegram = com.karasiq.shadowcloud.storage.telegram.TelegramStorageProvider
telegram {
+ temp-dir = ${shadowcloud.base-dir}/tgcloud
secrets {
api-hash = "b18441a1ff607e10a989891a5462e627"
api-id = 2040
diff --git a/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramScripts.scala b/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramScripts.scala
index 4440657a..8586b99a 100644
--- a/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramScripts.scala
+++ b/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramScripts.scala
@@ -7,25 +7,31 @@ import java.nio.file.attribute.BasicFileAttributes
import akka.util.ByteString
import com.karasiq.shadowcloud.model.StorageId
import com.karasiq.shadowcloud.storage.telegram.TelegramStorageConfig.Secrets
-import com.karasiq.shadowcloud.ui.UIProvider
+import com.karasiq.shadowcloud.ui.Challenge.AnswerFormat
+import com.karasiq.shadowcloud.ui.ChallengeHub
+import scala.concurrent.Await
+import scala.concurrent.duration.Duration
import scala.util.Try
object TelegramScripts {
- def createSession(storageId: StorageId, secrets: Secrets, uiProvider: UIProvider): ByteString = {
- require(uiProvider.canBlock, "Please create session manually and paste it as base64 in storage config session key")
- val baseDir = Paths.get(sys.props("user.home"), s"tgcloud-temp-$storageId")
+ def createSession(storageId: StorageId, secrets: Secrets, tempDir: String, uiProvider: ChallengeHub): ByteString = {
+ val baseDir = Paths.get(tempDir, storageId)
deleteDir(baseDir)
extract(baseDir)
writeSecrets(baseDir, secrets)
- uiProvider.showNotification(
- s"""Please execute the following action depending on your OS, then press OK
- |Windows: Execute create_session.bat in $baseDir
- |Linux/MacOS: Run in terminal: $baseDir/create_session
- |""".stripMargin
+ val future = uiProvider.create(
+ s"Telegram login ($storageId)",
+ s"""Please execute the following action depending on your OS
+ |Windows: Execute create_session.bat
in $baseDir
+ |Linux/MacOS: Run in terminal: bash $baseDir/create_session
+ |Then upload created ${secrets.entity}.session
file
+ |""".stripMargin,
+ AnswerFormat.Binary
)
- val result = readSession(baseDir, secrets.entity)
+
+ val result = Await.result(future, Duration.Inf)
deleteDir(baseDir)
result
}
@@ -54,7 +60,9 @@ object TelegramScripts {
val files = Seq(
"download_service.py",
"requirements.txt",
- "telegram_create_session.py"
+ "telegram_create_session.py",
+ "create_session",
+ "create_session.bat"
)
Files.createDirectories(directory)
files.foreach { f ⇒
diff --git a/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStorageConfig.scala b/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStorageConfig.scala
index a24b2c15..788a6aba 100644
--- a/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStorageConfig.scala
+++ b/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStorageConfig.scala
@@ -4,7 +4,7 @@ import com.karasiq.common.configs.ConfigImplicits._
import com.karasiq.shadowcloud.storage.telegram.TelegramStorageConfig.Secrets
import com.typesafe.config.Config
-case class TelegramStorageConfig(pythonPath: Option[String], entity: String, port: Option[Int], secrets: Secrets)
+case class TelegramStorageConfig(pythonPath: Option[String], entity: String, port: Option[Int], secrets: Secrets, tempDir: String)
object TelegramStorageConfig {
case class Secrets(apiId: Int, apiHash: String, entity: String)
@@ -23,7 +23,8 @@ object TelegramStorageConfig {
config.optional(_.getString("python-path")),
config.withDefault("tgcloud", _.getString("entity")),
config.optional(_.getInt("port")),
- Secrets(config.getConfig("secrets"))
+ Secrets(config.getConfig("secrets")),
+ config.getString("temp-dir")
)
}
}
diff --git a/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStoragePlugin.scala b/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStoragePlugin.scala
index e70c17f6..e58f8c1d 100644
--- a/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStoragePlugin.scala
+++ b/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStoragePlugin.scala
@@ -16,8 +16,8 @@ import com.karasiq.shadowcloud.storage.props.StorageProps
import com.karasiq.shadowcloud.storage.utils.StoragePluginBuilder
import scala.collection.JavaConverters._
+import scala.concurrent.Await
import scala.concurrent.duration._
-import scala.concurrent.{Await, Future}
import scala.sys.ShutdownHookThread
import scala.util.control.NonFatal
import scala.util.{Random, Try}
@@ -48,10 +48,7 @@ class TelegramStoragePlugin extends StoragePlugin {
val session = Try(sc.sessions.getRawBlocking(storageId, "tgcloud-session")).toOption
.orElse(props.rootConfig.optional(_.getString("session")).map(Base64.decode))
.filter(_.nonEmpty)
- .getOrElse {
- val future = Future(TelegramScripts.createSession(storageId, config.secrets, sc.ui))(sc.ui.executionContext)
- Await.result(future, 5 minutes)
- }
+ .getOrElse(TelegramScripts.createSession(storageId, config.secrets, config.tempDir, sc.challenges))
require(session.nonEmpty, "Session is empty")
sc.sessions.setRawBlocking(storageId, "tgcloud-session", session)