app/controllers/ProjectEntryController.scala (1,104 lines of code) (raw):

package controllers import akka.actor.ActorRef import akka.pattern.ask import auth.{BearerTokenAuth, Security} import exceptions.RecordNotFoundException import helpers.{AllowCORSFunctions, S3Helper} import models._ import play.api.Configuration import play.api.cache.SyncCacheApi import play.api.db.slick.DatabaseConfigProvider import play.api.http.HttpEntity import play.api.inject.Injector import play.api.libs.json.{JsError, JsResult, JsValue, Json, Writes} import play.api.mvc._ import services.RabbitMqDeliverable.DeliverableEvent import services.RabbitMqPropagator.ChangeEvent import services.RabbitMqSend.FixEvent import services.actors.Auditor import services.actors.creation.GenericCreationActor.{NewProjectRequest, ProjectCreateTransientData} import services.actors.creation.{CreationMessage, GenericCreationActor} import services.{CreateOperation, UpdateOperation} import slick.dbio.DBIOAction import slick.jdbc.PostgresProfile import slick.jdbc.PostgresProfile.api._ import slick.lifted.TableQuery import play.api.libs.mailer._ import java.io.File import java.nio.file.Paths import java.time.ZonedDateTime import javax.inject.{Inject, Named, Singleton} import scala.concurrent.{Await, ExecutionContext, Future} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ import scala.language.postfixOps import scala.util.{Failure, Success, Try} import vidispine.{VSOnlineOutputMessage, VidispineCommunicator, VidispineConfig} import mes.OnlineOutputMessage import mess.InternalOnlineOutputMessage import akka.actor.ActorSystem import akka.stream.Materializer import java.util.concurrent.{Executors, TimeUnit} import de.geekonaut.slickmdc.MdcExecutionContext import services.RabbitMqSAN.SANEvent import com.om.mxs.client.japi.Vault import akka.stream.scaladsl.{Keep, Sink, Source} import mxscopy.streamcomponents.OMFastContentSearchSource import mxscopy.models.ObjectMatrixEntry import matrixstore.MatrixStoreEnvironmentConfigProvider import mxscopy.MXSConnectionBuilderImpl import mxscopy.MXSConnectionBuilder import services.RabbitMqMatrix.MatrixEvent import java.util.Date import java.sql.Timestamp import helpers.StorageHelper @Singleton class ProjectEntryController @Inject() (@Named("project-creation-actor") projectCreationActor:ActorRef, override implicit val config: Configuration, dbConfigProvider: DatabaseConfigProvider, cacheImpl:SyncCacheApi, @Named("rabbitmq-propagator") implicit val rabbitMqPropagator:ActorRef, @Named("rabbitmq-send") rabbitMqSend:ActorRef, @Named("rabbitmq-deliverable") rabbitMqDeliverable:ActorRef, @Named("rabbitmq-san") rabbitMqSAN:ActorRef, @Named("rabbitmq-matrix") rabbitMqMatrix:ActorRef, @Named("auditor") auditor:ActorRef, override val controllerComponents:ControllerComponents, override val bearerTokenAuth:BearerTokenAuth, storageHelper:StorageHelper) (implicit fileEntryDAO:FileEntryDAO, assetFolderFileEntryDAO:AssetFolderFileEntryDAO, injector: Injector, mat: Materializer, mailerClient: MailerClient) extends GenericDatabaseObjectControllerWithFilter[ProjectEntry,ProjectEntryFilterTerms] with ProjectEntrySerializer with ProjectRequestSerializer with ProjectEntryFilterTermsSerializer with UpdateTitleRequestSerializer with FileEntrySerializer with AssetFolderFileEntrySerializer with Security { override implicit val cache:SyncCacheApi = cacheImpl val dbConfig = dbConfigProvider.get[PostgresProfile] implicit val implicitConfig = config override def deleteid(requestedId: Int) = dbConfig.db.run( TableQuery[ProjectEntryRow].filter(_.id === requestedId).delete.asTry ) override def selectid(requestedId: Int):Future[Try[Seq[ProjectEntry]]] = dbConfig.db.run( TableQuery[ProjectEntryRow].filter(_.id === requestedId).result.asTry ) protected def selectVsid(vsid: String):Future[Try[Seq[ProjectEntry]]] = dbConfig.db.run( TableQuery[ProjectEntryRow].filter(_.vidispineProjectId === vsid).result.asTry ) override def dbupdate(itemId:Int, entry:ProjectEntry) :Future[Try[Int]] = { logger.info(s"Updating project id ${itemId} and status ${entry.status}") val newRecord = entry.id match { case Some(id)=>entry case None=>entry.copy(id=Some(itemId)) } dbConfig.db.run(TableQuery[ProjectEntryRow].filter(_.id===itemId).update(newRecord).asTry) .map(rows => { sendToRabbitMq(UpdateOperation(), itemId, rabbitMqPropagator) rows }) } override def notifyRequested[T](requestedId: Int, username: String, request: Request[T]): Unit = { request.headers.get("User-Agent") match { case None=> case Some(userAgent)=> if(userAgent.contains("Mozilla")) { //we are only interested in logging requests that came from a browser, otherwise the log would fill with the automated requests auditor ! Auditor.LogEvent( username, AuditAction.ViewProjectPage, requestedId, ZonedDateTime.now(), Some(userAgent) ) } } } /** * Fully generic container method to process an update request * @param requestedId an ID to identify what should be updated, this is passed to `selector` * @param selector a function that takes `requestedId` and returns a Future, containing a Try, containing a sequence of ProjectEntries * that correspond to the provided ID * @param f a function to perform the actual update. This is only called if selector returns a valid sequence of at least one ProjectEntry, * and is called for each ProjectEntry in the sequence that `selector` returns. * It should return a Future containing a Try containing the number of rows updated. * @tparam T the data type of `requestedId` * @return A Future containing a sequnce of results for each invokation of f. with either a Failure indicating why * `f` was not called, or a Success with the result of `f` */ def doUpdateGenericSelector[T](requestedId:T, selector:T=>Future[Try[Seq[ProjectEntry]]])(f: ProjectEntry=>Future[Try[Int]]):Future[Seq[Try[Int]]] = selector(requestedId).flatMap({ case Success(someSeq)=> if(someSeq.isEmpty) Future(Seq(Failure(new RecordNotFoundException(s"No records found for id $requestedId")))) else Future.sequence(someSeq.map(f)) case Failure(error)=>Future(Seq(Failure(error))) }) /** * Most updates are done with the primary key, this is a convenience method to call [[doUpdateGenericSelector]] * with the appropriate selector and data type for the primary key * @param requestedId integer primary key value identifying what should be updated * @param f a function to perform the actual update. See [[doUpdateGenericSelector]] for details * @return see [[doUpdateGenericSelector]] */ def doUpdateGeneric(requestedId:Int)(f: ProjectEntry=>Future[Try[Int]]) = doUpdateGenericSelector[Int](requestedId,selectid)(f) /** * Update the vidisipineId on a data record * @param requestedId primary key of the record to update * @param newVsid new vidispine ID. Note that this is an Option[String] as the id can be null * @return a Future containing a Try containing an Int describing the number of records updated */ def doUpdateVsid(requestedId:Int, newVsid:Option[String]):Future[Seq[Try[Int]]] = doUpdateGeneric(requestedId){ record=> val updatedProjectEntry = record.copy (vidispineProjectId = newVsid) dbConfig.db.run ( TableQuery[ProjectEntryRow].filter (_.id === requestedId).update (updatedProjectEntry).asTry ) .map(rows => { sendToRabbitMq(UpdateOperation(), requestedId, rabbitMqPropagator) rows }) } /** * generic code for an endpoint to update the title * @param requestedId identifier of the record to update * @param updater function to perform the actual update. This is passed requestedId and a string to change the title to * @tparam T type of @reqestedId * @return a Future[Response] */ def genericUpdateTitleEndpoint[T](requestedId:T)(updater:(T,String)=>Future[Seq[Try[Int]]]) = IsAuthenticatedAsync(parse.json) {uid=>{request=> request.body.validate[UpdateTitleRequest].fold( errors=> Future(BadRequest(Json.obj("status"->"error", "detail"->JsError.toJson(errors)))), updateTitleRequest=> { val results = updater(requestedId, updateTitleRequest.newTitle).map(_.partition(_.isSuccess)) results.map(resultTuple => { val failures = resultTuple._2 val successes = resultTuple._1 if (failures.isEmpty) Ok(Json.obj("status" -> "ok", "detail" -> s"${successes.length} record(s) updated")) else genericHandleFailures(failures, requestedId) }) } ) }} /** * endpoint to update project title field of record based on primary key * @param requestedId * @return */ def updateTitle(requestedId:Int) = genericUpdateTitleEndpoint[Int](requestedId) { (requestedId,newTitle)=> doUpdateGeneric(requestedId) {record=> val updatedProjectEntry = record.copy (projectTitle = newTitle) dbConfig.db.run ( TableQuery[ProjectEntryRow].filter (_.id === requestedId).update (updatedProjectEntry).asTry ) .map(rows => { sendToRabbitMq(UpdateOperation(), requestedId, rabbitMqPropagator) rows }) } } /** * endoint to update project title field of record based on vidispine id * @param vsid * @return */ def updateTitleByVsid(vsid:String) = genericUpdateTitleEndpoint[String](vsid) { (vsid,newTitle)=> doUpdateGenericSelector[String](vsid,selectVsid) { record=> //this lambda function is called once for each record val updatedProjectEntry = record.copy(projectTitle = newTitle) dbConfig.db.run( TableQuery[ProjectEntryRow].filter(_.id === record.id.get).update(updatedProjectEntry).asTry ) .map(rows => { sendToRabbitMq(UpdateOperation(), record, rabbitMqPropagator) rows }) } } def genericHandleFailures[T](failures:Seq[Try[Int]], requestedId:T) = { val notFoundFailures = failures.filter(_.failed.get.getClass==classOf[RecordNotFoundException]) if(notFoundFailures.length==failures.length) { NotFound(Json.obj("status" -> "error", "detail" -> s"no records found for $requestedId")) } else { InternalServerError(Json.obj("status" -> "error", "detail" -> failures.map(_.failed.get.toString))) } } def filesList(requestedId: Int, allVersions: Boolean) = IsAuthenticatedAsync {uid=>{request=> implicit val db = dbConfig.db selectid(requestedId).flatMap({ case Failure(error)=> logger.error(s"could not list files from project ${requestedId}",error) Future(InternalServerError(Json.obj("status"->"error","detail"->error.toString))) case Success(someSeq)=> someSeq.headOption match { //matching on pk, so can only be one result case Some(projectEntry)=> projectEntry.associatedFiles(allVersions).map(fileList=>Ok(Json.obj("status"->"ok","files"->fileList))) case None=> Future(NotFound(Json.obj("status"->"error","detail"->s"project $requestedId not found"))) } }) }} def withRequiredSort(query: =>Query[ProjectEntryRow, ProjectEntry, Seq], sort:String, sortDirection:SortDirection.Value):Query[ProjectEntryRow, ProjectEntry, Seq] = { import EntryStatusMapper._ (sort, sortDirection) match { case ("created", SortDirection.desc) => query.sortBy(_.created.desc) case ("created", SortDirection.asc) => query.sortBy(_.created.asc) case ("title", SortDirection.desc) => query.sortBy(_.projectTitle.desc) case ("title", SortDirection.asc) => query.sortBy(_.projectTitle.asc) case ("workingGroupId", SortDirection.desc) => query.sortBy(_.workingGroup.desc) case ("workingGroupId", SortDirection.asc) => query.sortBy(_.workingGroup.asc) case ("status", SortDirection.desc) => query.sortBy(_.status.desc) case ("status", SortDirection.asc) => query.sortBy(_.status.asc) case ("user", SortDirection.desc) => query.sortBy(_.user.desc) case ("user", SortDirection.asc) => query.sortBy(_.user.asc) case ("commissionId", SortDirection.desc) => query.sortBy(_.commission.desc) case ("commissionId", SortDirection.asc) => query.sortBy(_.commission.asc) case _ => logger.warn(s"Sort field $sort was not recognised, ignoring") query } } def listFilteredAndSorted(startAt:Int, limit:Int, sort: String, sortDirection: String) = IsAuthenticatedAsync(parse.json) {uid=>{request=> this.validateFilterParams(request).fold( errors => { logger.error(s"Errors parsing content: $errors") Future(BadRequest(Json.obj("status"->"error","detail"->JsError.toJson(errors)))) }, filterTerms => { this.selectFilteredAndSorted(startAt, limit, filterTerms, sort, getSortDirection(sortDirection).getOrElse(SortDirection.desc)).map({ case Success((count,result))=>Ok(Json.obj("status" -> "ok","count"->count,"result"->this.jstranslate(result))) case Failure(error)=> logger.error(error.toString) InternalServerError(Json.obj("status"->"error", "detail"->error.toString)) } ) } ) }} def selectall(startAt:Int, limit:Int) = dbConfig.db.run( TableQuery[ProjectEntryRow].length.result.zip( TableQuery[ProjectEntryRow].sortBy(_.created.desc).drop(startAt).take(limit).result ) ).map(Success(_)).recover(Failure(_)) override def selectFiltered(startAt: Int, limit: Int, terms: ProjectEntryFilterTerms): Future[Try[(Int, Seq[ProjectEntry])]] = { val basequery = terms.addFilterTerms { TableQuery[ProjectEntryRow] } dbConfig.db.run( basequery.length.result.zip( basequery.sortBy(_.created.desc).drop(startAt).take(limit).result ) ).map(Success(_)).recover(Failure(_)) } def selectFilteredAndSorted(startAt: Int, limit: Int, terms: ProjectEntryFilterTerms, sort: String, sortDirection: SortDirection.Value): Future[Try[(Int, Seq[ProjectEntry])]] = { val basequery = terms.addFilterTerms { TableQuery[ProjectEntryRow] } dbConfig.db.run( basequery.length.result.zip( withRequiredSort(basequery, sort, sortDirection).drop(startAt).take(limit).result ) ).map(Success(_)).recover(Failure(_)) } override def jstranslate(result: Seq[ProjectEntry]):Json.JsValueWrapper = result override def jstranslate(result: ProjectEntry):Json.JsValueWrapper = result //implicit translation should handle this /*this is pointless because of the override of [[create]] below, so it should not get called, but is needed to conform to the [[GenericDatabaseObjectController]] protocol*/ override def insert(entry: ProjectEntry,uid:String) = Future(Failure(new RuntimeException("ProjectEntryController::insert should not have been called"))) override def validate(request:Request[JsValue]) = request.body.validate[ProjectEntry] override def validateFilterParams(request: Request[JsValue]): JsResult[ProjectEntryFilterTerms] = request.body.validate[ProjectEntryFilterTerms] private val vsidValidator = "^\\w{2}-\\d+$".r def getByVsid(vsid:String) = IsAuthenticatedAsync { uid=> request=> if(vsidValidator.matches(vsid)) { dbConfig.db.run { TableQuery[ProjectEntryRow].filter(_.vidispineProjectId===vsid).sortBy(_.created.desc).result }.map(_.headOption match { case Some(projectRecord)=> Ok(Json.obj("status"->"ok","result"->projectRecord)) case None=> NotFound(Json.obj("status"->"notfound","detail"->"No project with that VSID")) }).recover({ case err:Throwable=> logger.error(s"Could not look up VSID $vsid: ", err) InternalServerError(Json.obj("status"->"error","detail"->"Database error looking up record, see server logs")) }) } else { Future(BadRequest(Json.obj("status"->"bad_request","detail"->"Malformed vidispine ID"))) } } def createFromFullRequest(rq:ProjectRequestFull) = { implicit val timeout:akka.util.Timeout = 60.seconds val initialData = ProjectCreateTransientData(None, None, None) val msg = NewProjectRequest(rq,None,initialData) (projectCreationActor ? msg).mapTo[CreationMessage].map({ case GenericCreationActor.ProjectCreateSucceeded(succeededRequest, projectEntry)=> logger.info(s"Created new project: $projectEntry") sendToRabbitMq(CreateOperation(), projectEntry, rabbitMqPropagator) if (projectEntry.sensitive == Some(true)) { logger.info(s"Sensitive project created.") try { val email = Email( config.get[String]("mail.subject"), s"${config.get[String]("mail.sender_name")} <${config.get[String]("mail.sender_address")}>", Seq(s"${config.get[String]("mail.recipient_name")} <${config.get[String]("mail.recipient_address")}>"), bodyText = Some(s"A sensitive project has been created by ${projectEntry.user}. It has the identity number ${projectEntry.id.get} and the tile '${projectEntry.projectTitle}'.")) mailerClient.send(email) } catch { case e: Exception => logger.error(s"Sending e-mail failed with error: $e") } } Ok(Json.obj("status"->"ok","detail"->"created project", "projectId"->projectEntry.id.get)) case GenericCreationActor.ProjectCreateFailed(failedRequest, error)=> logger.error("Could not create new project", error) InternalServerError(Json.obj("status"->"error","detail"->error.toString)) }) } override def create = IsAuthenticatedAsync(parse.json) {uid=>{ request => implicit val db = dbConfig.db request.body.validate[ProjectRequest].fold( errors=> Future(BadRequest(Json.obj("status"->"error","detail"->JsError.toJson(errors)))), projectRequest=> { val fullRequestFuture=projectRequest.hydrate fullRequestFuture.flatMap({ case None=> Future(BadRequest(Json.obj("status"->"error","detail"->"Invalid template or storage ID"))) case Some(rq)=> createFromFullRequest(rq) }) }) }} def getDistinctOwnersList:Future[Try[Seq[String]]] = { //work around distinctOn bug - https://github.com/slick/slick/issues/1712 dbConfig.db.run(sql"""select distinct(s_user) from "ProjectEntry" where s_user not like '%|%'""".as[String].asTry) } def distinctOwners = IsAuthenticatedAsync {uid=>{request=> getDistinctOwnersList.map({ case Success(ownerList)=> Ok(Json.obj("status"->"ok","result"->ownerList)) case Failure(error)=> logger.error("Could not look up distinct project owners: ", error) InternalServerError(Json.obj("status"->"error","detail"->error.toString)) }) }} /** * respond to CORS options requests for login from vaultdoor * see https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request * @return */ def searchOptions = Action { request=> AllowCORSFunctions.checkCorsOrigins(config, request) match { case Right(allowedOrigin) => val returnHeaders = Map( "Access-Control-Allow-Methods" -> "PUT, OPTIONS", "Access-Control-Allow-Origin" -> allowedOrigin, "Access-Control-Allow-Headers" -> "content-type", ) Result( ResponseHeader(204, returnHeaders), HttpEntity.NoEntity ) case Left(other) => logger.warn(s"Invalid CORS preflight request for authentication: $other") Forbidden("") } } def projectWasOpened(id: Int): EssentialAction = IsAuthenticatedAsync { uid=> request => import models.EntryStatusMapper._ def updateProject() = TableQuery[ProjectEntryRow] .filter(_.id === id) .filter(_.status === EntryStatus.New) .map(_.status) .update(EntryStatus.InProduction) .map(rows => { if (rows > 0) { sendToRabbitMq(UpdateOperation(), id, rabbitMqPropagator) } }) def updateCommission(commissionId: Option[Int]) = TableQuery[PlutoCommissionRow] .filter(_.id === commissionId) .filter(_.status === EntryStatus.New) .map(_.status) .update(EntryStatus.InProduction).flatMap(rows => { if (rows > 0) { TableQuery[PlutoCommissionRow].filter(_.id === commissionId).result.map({ case Seq() => logger.error(s"Failed to update commission, commission not updated: $commissionId") throw new IllegalStateException(s"Failed to update commission, commission not updated: $commissionId") case Seq(commission) => val commissionsSerializer = new PlutoCommissionSerializer {} implicit val commissionsWrites: Writes[PlutoCommission] = commissionsSerializer.plutoCommissionWrites rabbitMqPropagator ! ChangeEvent(Seq(commissionsWrites.writes(commission)), getItemType(commission), UpdateOperation()) case _ => logger.error(s"Failed to update commission, multiple commissions updated: $commissionId") throw new IllegalStateException(s"Failed to update commission, multiple commissions updated: $commissionId") }) } else { DBIOAction.successful(()) } }) auditor ! Auditor.LogEvent(uid, AuditAction.OpenProject, id, ZonedDateTime.now(), request.headers.get("User-Agent")) dbConfig.db.run( TableQuery[ProjectEntryRow] .filter(_.id === id) .result .flatMap(result => { val acts = result match { case Seq() => DBIOAction.successful(NotFound) case Seq(project: ProjectEntry) => DBIO.seq(updateProject(), updateCommission(project.commissionId)).map(_ => Ok) case _ => logger.error(s"Database inconsistency, multiple projects found for id=$id") DBIOAction.successful(InternalServerError) } acts }) ).recover({ case err: Throwable => logger.error("Failed to mark project as opened", err) InternalServerError(Json.obj("status" -> "error", "detail" -> "Failed to mark project as opened")) }) } private def updateStatusColumn(projectId:Int, newValue:EntryStatus.Value) = { import EntryStatusMapper._ dbConfig.db.run { val q = for {c <- TableQuery[ProjectEntryRow] if c.id === projectId} yield c.status q.update(newValue) } } def updateStatus(projectId: Int) = IsAuthenticatedAsync(parse.json) {uid=> request=> import PlutoCommissionStatusUpdateRequestSerializer._ request.body.validate[PlutoCommissionStatusUpdateRequest].fold( invalidErrs=> Future(BadRequest(Json.obj("status"->"bad_request","detail"->JsError.toJson(invalidErrs)))), requiredUpdate=> updateStatusColumn(projectId, requiredUpdate.status).map(rowsUpdated=>{ if(rowsUpdated==0){ NotFound(Json.obj("status"->"not_found","detail"->s"No project with id $projectId")) } else { if(rowsUpdated>1) logger.error(s"Status update request for project $projectId returned $rowsUpdated rows updated, expected 1! This indicates a database problem") auditor ! Auditor.LogEvent(uid, AuditAction.ChangeProjectStatus, projectId, ZonedDateTime.now, request.headers.get("User-Agent")) sendToRabbitMq(UpdateOperation(), projectId, rabbitMqPropagator).foreach(_ => ()) Ok(Json.obj("status"->"ok","detail"->"Project status updated")) } }).recover({ case err:Throwable=> logger.error(s"Could not update status of project $projectId to ${requiredUpdate.status}: ", err) InternalServerError(Json.obj("status"->"db_error","detail"->"Database error, see logs for details")) }) ) } def queryUsersForAutocomplete(prefix:String, limit:Option[Int]) = IsAuthenticatedAsync { uid=> request=> implicit val db = dbConfig.db implicit val ordering = Ordering.String ProjectEntry.listUsers(prefix, limit.getOrElse(10)) .map(results=>{ Ok(Json.obj("status"->"ok","users"->results.sorted)) }) .recover({ case err:Throwable=> logger.error(s"Could not look up users with prefix $prefix and limit ${limit.getOrElse(10)}: ${err.getMessage}", err) InternalServerError(Json.obj("status"->"db_error", "detail"->"Database error, see logs for details")) }) } def isUserKnown(uname:String) = IsAuthenticatedAsync { uid=> request=> implicit val db = dbConfig.db ProjectEntry.isUserKnown(uname) .map(result=>Ok(Json.obj("status"->"ok", "known"->result))) .recover(err=>{ logger.error(s"Could not check if '$uname' is known: ${err.getMessage}", err) InternalServerError(Json.obj("status"->"error", "detail"->"Database error, see logs for details")) }) } object SortDirection extends Enumeration { val desc, asc = Value } private def getSortDirection(directionString:String):Option[SortDirection.Value] = Try { SortDirection.withName(directionString) }.toOption def obitsListSorted(name:Option[String], startAt:Int, limit:Int, sort: String, sortDirection: String) = IsAuthenticatedAsync { uid => request => implicit val db = dbConfig.db val baseQuery = name match { case None=> TableQuery[ProjectEntryRow].filter(_.isObitProject.nonEmpty) case Some(obitName)=> TableQuery[ProjectEntryRow].filter(_.isObitProject.toLowerCase like s"%$obitName%") } val sortedQuery = (sort, getSortDirection(sortDirection).getOrElse(SortDirection.asc)) match { case ("created", SortDirection.desc) => baseQuery.sortBy(_.created.desc) case ("created", SortDirection.asc) => baseQuery.sortBy(_.created.asc) case ("title", SortDirection.desc) => baseQuery.sortBy(_.projectTitle.desc) case ("title", SortDirection.asc) => baseQuery.sortBy(_.projectTitle.asc) case ("isObitProject", SortDirection.desc) => baseQuery.sortBy(_.isObitProject.desc) case ("isObitProject", SortDirection.asc) => baseQuery.sortBy(_.isObitProject.asc) case _ => logger.warn(s"Sort field $sort was not recognised, ignoring.") baseQuery } db.run( for { content <- sortedQuery.drop(startAt).take(limit).result count <- sortedQuery.length.result } yield (content, count) ) .map(results=>Ok(Json.obj("status"->"ok","count"->results._2,"result"->jstranslate(results._1)))) .recover({ case err:Throwable=> logger.error(s"Could not query database for obituaries: ${err.getMessage}", err) InternalServerError(Json.obj("status"->"error", "detail"->"Database error, see logs for details")) }) } /** * Returns a JSON object containing a list of strings for names of valid obituaries startig with the given prefix. * If no prefix is supplied, then everything is returned (up to the given limit) * @param prefix optional prefix to limit the search to * @param limit don't return more than this number of results * @return */ def findAvailableObits(prefix:Option[String], limit:Int) = IsAuthenticatedAsync { uid=> request=> implicit val db = dbConfig.db implicit val ordering = Ordering.String ProjectEntry.listObits(prefix.getOrElse(""), limit) .map(results=>{ Ok(Json.obj("status"->"ok","obitNames"->results.sorted)) }) .recover({ case err:Throwable=> logger.error(s"Could not look up obituaries with prefix $prefix and limit ${limit}: ${err.getMessage}", err) InternalServerError(Json.obj("status"->"db_error", "detail"->"Database error, see logs for details")) }) } def assetFolderForProject(projectId:Int) = { implicit val db = dbConfig.db db.run( TableQuery[ProjectMetadataRow] .filter(_.key===ProjectMetadata.ASSET_FOLDER_KEY) .filter(_.projectRef===projectId) .result ).map(results=>{ val resultCount = results.length if(resultCount==0){ logger.error("No asset folder registered under that project id.") } else if(resultCount>1){ logger.warn(s"Multiple asset folders found for project $projectId: $results") } else { results.head.value.getOrElse("") } }).recover({ case err: Throwable => logger.error(s"Could not look up asset folder for project id $projectId: ", err) }) } def fixPermissions(projectId: Int) = IsAuthenticatedAsync {uid=> request=> val assetFolderString = Await.result(assetFolderForProject(projectId), Duration.Inf).toString val fileName = Paths.get(assetFolderString).getFileName.toString val parentDir = Paths.get(assetFolderString).getParent.toString rabbitMqSend ! FixEvent(true,false,fileName,parentDir) Future(Ok(Json.obj("status"->"ok","detail"->"Fix permissions run."))) } def deleteDataRunner(projectId: Int, delay: Int, pluto: Boolean, file: Boolean, backups: Boolean, pTR: Boolean, deliverables: Boolean, sAN: Boolean, matrix: Boolean, s3: Boolean, buckets: Array[String], bucketBooleans: Array[Boolean]): Unit = { def deleteFileJob() = Future { if (file) { implicit val db = dbConfig.db ProjectEntry.entryForId(projectId).map({ case Success(projectEntry: ProjectEntry) => projectEntry.associatedFiles(false).map(fileList => { fileList.map(entry => { logger.info(s"Attempting to delete the file at: ${entry.filepath}") fileEntryDAO .deleteFromDisk(entry) .andThen(_ => fileEntryDAO.deleteRecord(entry)) if(entry.filepath.endsWith(".cpr")) { db.run( TableQuery[ProjectMetadataRow] .filter(_.key===ProjectMetadata.ASSET_FOLDER_KEY) .filter(_.projectRef===projectId) .result ).map(results=>{ val resultCount = results.length if(resultCount==0){ logger.info(s"No asset folder registered for that project id.") } else { logger.info(s"Found the asset folder at: ${results.head.value.get} Attempting to delete any Cubase files present." ) for { files <- Option(new File(results.head.value.get).listFiles) file <- files if file.getName.endsWith(".cpr") } file.delete() } }).recover({ case err: Throwable => logger.error(s"Could not look up asset folder for project id $projectId: ", err) }) } }) } ) case Failure(error) => logger.error(s"Could not look up project entry for ${projectId}: ", error) }) } } def deleteBackupsJob() = Future { if (backups) { implicit val db = dbConfig.db ProjectEntry.entryForId(projectId).map({ case Success(projectEntry: ProjectEntry) => logger.info(s"About to attempt to delete any backups present for project ${projectId}") projectEntry.associatedFiles(true).map(fileList => { fileList.map(entry => { entry.backupOf match { case Some(value) => logger.info(s"Attempting to delete the file at: ${entry.filepath}") fileEntryDAO .deleteFromDisk(entry) .andThen(_ => fileEntryDAO.deleteRecord(entry)) case None => logger.info(s"Ignoring non-backup file at ${entry.filepath}") } }) } ) projectEntry.associatedAssetFolderFiles(true, implicitConfig).map(fileList => { fileList.map(entry => { if (entry.storageId == config.get[Int]("asset_folder_backup_storage")) { logger.info(s"Attempting to delete the file at: ${entry.filepath}") assetFolderFileEntryDAO .deleteFromDisk(entry) .andThen(_ => assetFolderFileEntryDAO.deleteRecord(entry)) } else { logger.info(s"Ignoring non-backup file at ${entry.filepath}") } }) } ) case Failure(error) => logger.error(s"Could not look up project entry for ${projectId}: ", error) }) } } val xtensionXtractor="^(.*)\\.([^.]+)$".r def removeProjectFileExtension(projectFileName:String) = projectFileName match { case xtensionXtractor(barePath,_)=>barePath case _=> logger.warn(s"The project file '$projectFileName' does not appear to have a file extension") projectFileName } def deletePTRJob() = Future { if (pTR) { implicit val db = dbConfig.db ProjectEntry.entryForId(projectId).map({ case Success(projectEntry: ProjectEntry) => projectEntry.associatedFiles(false).map(fileList => { fileList.map(entry => { fileEntryDAO .storage(entry) .andThen({ case Success(storageTry) => storageTry match { case Some(storage) => val targetFilePath = storage.rootpath.get + "/" + removeProjectFileExtension(entry.filepath) + ".ptr" logger.info(s"Attempting to delete a possible file at: ${targetFilePath}") new File(targetFilePath).delete() case None => logger.info(s"Attempt at loading storage data failed.") } case Failure(err)=> logger.error(s"Attempt at loading storage data failed.", err) }) }) } ) case Failure(error) => logger.error(s"Could not look up project entry for ${projectId}: ", error) }) } } def deleteDeliverables() = Future { if (deliverables) { rabbitMqDeliverable ! DeliverableEvent(projectId) } } def deleteS3() = Future { if (s3) { for((bucket,i) <- buckets.view.zipWithIndex) { if (bucketBooleans(i)) { val assetFolderString = Await.result(assetFolderForProject(projectId), Duration.Inf).toString logger.info(s"Asset folder for project: $assetFolderString") if (assetFolderString == "") { logger.warn(s"No asset folder found for project. Can not attempt to delete data from S3.") } else { implicit lazy val s3helper: S3Helper = helpers.S3Helper.createFromBucketName(bucket).toOption.get val assetFolderBasePath = config.get[String]("postrun.assetFolder.basePath") val keyForSearch = assetFolderString.replace(s"$assetFolderBasePath/", "") if (!keyForSearch.matches(".*?\\/.*?\\/.*?\\_.*?")) { logger.warn(s"Key for search does not match the expected format. Can not attempt to delete data from S3.") } else { logger.info(s"About to attempt to delete any data in the S3 bucket: ${bucket}") val bucketObjectData = s3helper.listBucketObjects(keyForSearch) for (s3Object <- bucketObjectData) { if (s"$keyForSearch/" != s3Object.key) { logger.info(s"Found S3 key: ${s3Object.key}") val objectVersions = s3helper.listObjectsVersions(s3Object) for (version <- objectVersions) { logger.info(s"Found version: ${version.versionId()} for key: ${version.key()}") val deleteOutcome = s3helper.deleteObject(s3Object, version.versionId()) logger.info(s"Delete response was: $deleteOutcome") } } } val bucketObjectDataFolder = s3helper.listBucketObjects(keyForSearch) for (s3Object <- bucketObjectDataFolder) { if (s"$keyForSearch/" == s3Object.key) { logger.info(s"Found S3 key: ${s3Object.key}") val objectVersionsFolder = s3helper.listObjectsVersions(s3Object) for (version <- objectVersionsFolder) { logger.info(s"Found version: ${version.versionId()} for key: ${version.key()}") val deleteOutcomeFolder = s3helper.deleteObject(s3Object, version.versionId()) logger.info(s"Delete response was: $deleteOutcomeFolder") } } } } } } } } } def onlineFilesByProject(vidispineCommunicator: VidispineCommunicator, projectId: Int): Future[Seq[OnlineOutputMessage]] = { vidispineCommunicator.getFilesOfProject(projectId) .map(_.filterNot(isBranding).map(InternalOnlineOutputMessage.toOnlineOutputMessage)) } def isBranding(item: VSOnlineOutputMessage): Boolean = item.mediaCategory.toLowerCase match { case "branding" => true // Case insensitive case _ => false } def deleteSAN() = Future { if (sAN) { Thread.sleep(delay) logger.info(s"About to attempt to delete any SAN data present for project ${projectId}") implicit val db = dbConfig.db DeleteJobDAO.getOrCreate(projectId, "Started") lazy val vidispineConfig = VidispineConfig.fromEnvironment.toOption.get implicit lazy val executionContext = new MdcExecutionContext( ExecutionContext.fromExecutor( Executors.newWorkStealingPool(10) ) ) implicit lazy val actorSystem:ActorSystem = ActorSystem("pluto-core-delete", defaultExecutionContext=Some(executionContext)) implicit lazy val mat:Materializer = Materializer(actorSystem) implicit lazy val vidispineCommunicator = new VidispineCommunicator(vidispineConfig) val vidispineMethodOut = Await.result(onlineFilesByProject(vidispineCommunicator, projectId), 120.seconds) vidispineMethodOut.map(onlineOutputMessage => { if (onlineOutputMessage.projectIds.length > 2) { logger.info(s"Refusing to attempt to delete Vidispine item ${onlineOutputMessage.vidispineItemId.get} as it is referenced by more than one project.") ItemDeleteDataDAO.getOrCreate(projectId, onlineOutputMessage.vidispineItemId.get) } else { logger.info(s"About to attempt to send a message to delete Vidispine item ${onlineOutputMessage.vidispineItemId.get}") rabbitMqSAN ! SANEvent(onlineOutputMessage) } }) Thread.sleep(1000) DeleteJobDAO.getOrCreate(projectId, "Finished") } } def nearlineFilesByProject(vault: Vault, projectId: String): Future[Seq[OnlineOutputMessage]] = { val sinkFactory = Sink.seq[OnlineOutputMessage] Source.fromGraph(new OMFastContentSearchSource(vault, s"""GNM_PROJECT_ID:\"$projectId\"""", Array("MXFS_PATH", "MXFS_FILENAME", "GNM_PROJECT_ID", "GNM_TYPE", "__mxs__length") ) ).filterNot(isBrandingMatrix) .map(InternalOnlineOutputMessage.toOnlineOutputMessage) .toMat(sinkFactory)(Keep.right) .run() } def isBrandingMatrix(entry: ObjectMatrixEntry): Boolean = entry.stringAttribute("GNM_TYPE") match { case Some(gnmType) => gnmType.toLowerCase match { case "branding" => true // Case insensitive case _ => false } case _ => false } def searchAssociatedNearlineMedia(projectId: Int, vault: Vault): Future[Seq[OnlineOutputMessage]] = { nearlineFilesByProject(vault, projectId.toString) } def getNearlineResults(projectId: Int, nearlineVaultId: String, matrixStore: MXSConnectionBuilderImpl): Future[Either[String, Seq[OnlineOutputMessage]]] = matrixStore.withVaultFuture(nearlineVaultId) { vault => searchAssociatedNearlineMedia(projectId, vault).map(Right.apply) } def deleteMatrix() = Future { if (matrix) { Thread.sleep(delay) logger.info(s"About to attempt to delete any Object Matrix data present for project ${projectId}") implicit val db = dbConfig.db MatrixDeleteJobDAO.getOrCreate(projectId, "Started") lazy val matrixStoreConfig = new MatrixStoreEnvironmentConfigProvider().get() match { case Left(err)=> logger.error(s"Could not initialise due to incorrect matrix-store config: $err") sys.exit(1) case Right(config)=>config } implicit lazy val executionContext = new MdcExecutionContext( ExecutionContext.fromExecutor( Executors.newWorkStealingPool(10) ) ) implicit lazy val actorSystem:ActorSystem = ActorSystem("pluto-core-delete-matrix", defaultExecutionContext=Some(executionContext)) implicit lazy val mat:Materializer = Materializer(actorSystem) val connectionIdleTime = sys.env.getOrElse("CONNECTION_MAX_IDLE", "750").toInt implicit val matrixStore = new MXSConnectionBuilderImpl( hosts = matrixStoreConfig.hosts, accessKeyId = matrixStoreConfig.accessKeyId, accessKeySecret = matrixStoreConfig.accessKeySecret, clusterId = matrixStoreConfig.clusterId, maxIdleSeconds = connectionIdleTime ) val matrixMethodOut = Await.result(getNearlineResults(projectId, matrixStoreConfig.nearlineVaultId, matrixStore), 120.seconds) matrixMethodOut match { case Right(nearlineResults) => nearlineResults.map(onlineOutputMessage => { if (onlineOutputMessage.projectIds.length > 2) { logger.info(s"Refusing to attempt to delete Object Matrix data for object ${onlineOutputMessage.nearlineId.get} as it is referenced by more than one project.") MatrixDeleteDataDAO.getOrCreate(projectId, onlineOutputMessage.nearlineId.get) } else { logger.info(s"About to attempt to send a message to delete Object Matrix data for object ${onlineOutputMessage.nearlineId.get}") rabbitMqMatrix ! MatrixEvent(onlineOutputMessage) } }) case Left(something) => logger.info(s"No Object Matrix data was found to process.") } MatrixDeleteJobDAO.getOrCreate(projectId, "Finished") } } def makeDeletionRecord() = Future { implicit val db = dbConfig.db var user = "" val currentDate = new Date() val timestampOfNow = new Timestamp(currentDate.getTime) var created = timestampOfNow var workingGroupName = "" ProjectEntry.entryForId(projectId).map({ case Success(projectEntry: ProjectEntry) => user = projectEntry.user created = projectEntry.created projectEntry.getWorkingGroup.map({ case Some(workingGroup: PlutoWorkingGroup) => workingGroupName = workingGroup.name DeletionRecordDAO.getOrCreate(projectId, user, timestampOfNow, created, workingGroupName) case None => logger.error(s"Could not get working group name for project ${projectId}") DeletionRecordDAO.getOrCreate(projectId, user, timestampOfNow, created, "Unknown") }) case Failure(error) => logger.error(s"Could not look up project entry for ${projectId}: ", error) Left(error.toString) }) } val f = for { f1 <- makeDeletionRecord() f2 <- deletePTRJob() f3 <- deleteFileJob() f4 <- deleteBackupsJob() f5 <- deleteDeliverables() f6 <- deleteS3() f7 <- deleteMatrix() f8 <- deleteSAN() } yield List(f1, f2, f3, f4, f5, f6, f7, f8) if (pluto) { Thread.sleep(800) implicit val db = dbConfig.db ProjectMetadata.deleteAllMetadataFor(projectId).map({ case Success(rows) => logger.info(s"Attempt at removing project metadata worked.") case Failure(err) => logger.error(s"Could not delete metadata", err) }) ProjectEntry.entryForId(projectId).map({ case Success(projectEntry: ProjectEntry) => projectEntry.removeFromDatabase.map({ case Success(_) => logger.info(s"Attempt at removing project record worked.") case Failure(error) => logger.error(s"Attempt at removing project record failed with error: ${error}") }) case Failure(error) => logger.error(s"Could not look up project entry for ${projectId}: ", error) Left(error.toString) }) } } def deleteData(projectId: Int) = IsAdmin { uid => request => logger.info(s"Got a delete data request for project ${projectId}.") logger.info(s"Pluto value is: ${request.body.asJson.get("pluto")}") logger.info(s"File value is: ${request.body.asJson.get("file")}") logger.info(s"Backups value is: ${request.body.asJson.get("backups")}") logger.info(s"PTR value is: ${request.body.asJson.get("PTR")}") logger.info(s"Deliverables value is: ${request.body.asJson.get("deliverables")}") logger.info(s"SAN value is: ${request.body.asJson.get("SAN")}") logger.info(s"Matrix value is: ${request.body.asJson.get("matrix")}") logger.info(s"S3 value is: ${request.body.asJson.get("S3")}") logger.info(s"Buckets value is: ${request.body.asJson.get("buckets")}") logger.info(s"Bucket Booleans value is: ${request.body.asJson.get("bucketBooleans")}") deleteDataRunner(projectId, 0, request.body.asJson.get("pluto").toString().toBoolean, request.body.asJson.get("file").toString().toBoolean, request.body.asJson.get("backups").toString().toBoolean, request.body.asJson.get("PTR").toString().toBoolean, request.body.asJson.get("deliverables").toString().toBoolean, request.body.asJson.get("SAN").toString().toBoolean, request.body.asJson.get("matrix").toString().toBoolean, request.body.asJson.get("S3").toString().toBoolean, request.body.asJson.get("buckets").validate[Array[String]].get, request.body.asJson.get("bucketBooleans").validate[Array[Boolean]].get) Ok(Json.obj("status"->"ok","detail"->"Delete data run.")) } def deleteJob(projectId: Int) = IsAdminAsync { uid => request => dbConfig.db.run( TableQuery[DeleteJob].filter(_.projectEntry===projectId).result ).map(_.headOption match { case Some(jobRecord)=> Ok(Json.obj("status"->"ok","job_status"->jobRecord.status)) case None=> NotFound(Json.obj("status"->"notfound","detail"->s"No job with project id: $projectId")) }).recover({ case err:Throwable=> logger.error(s"Could not look up project $projectId: ", err) InternalServerError(Json.obj("status"->"error","detail"->"Database error looking up job, see server logs")) }) } def matrixDeleteJob(projectId: Int) = IsAdminAsync { uid => request => dbConfig.db.run( TableQuery[MatrixDeleteJob].filter(_.projectEntry===projectId).result ).map(_.headOption match { case Some(jobRecord)=> Ok(Json.obj("status"->"ok","job_status"->jobRecord.status)) case None=> NotFound(Json.obj("status"->"notfound","detail"->s"No job with project id: $projectId")) }).recover({ case err:Throwable=> logger.error(s"Could not look up project $projectId: ", err) InternalServerError(Json.obj("status"->"error","detail"->"Database error looking up job, see server logs")) }) } def getProjectsForCommission(commission: Int) = dbConfig.db.run( TableQuery[ProjectEntryRow].filter(_.commission===commission).sortBy(_.created.desc).result ).map(Success(_)).recover(Failure(_)) def deleteCommissionData(commissionId: Int) = IsAdmin { uid => request => logger.info(s"Got a delete data request for commission ${commissionId}.") logger.info(s"Commission value is: ${request.body.asJson.get("commission")}") logger.info(s"Pluto value is: ${request.body.asJson.get("pluto")}") logger.info(s"File value is: ${request.body.asJson.get("file")}") logger.info(s"Backups value is: ${request.body.asJson.get("backups")}") logger.info(s"PTR value is: ${request.body.asJson.get("PTR")}") logger.info(s"Deliverables value is: ${request.body.asJson.get("deliverables")}") logger.info(s"SAN value is: ${request.body.asJson.get("SAN")}") logger.info(s"Matrix value is: ${request.body.asJson.get("matrix")}") logger.info(s"S3 value is: ${request.body.asJson.get("S3")}") logger.info(s"Buckets value is: ${request.body.asJson.get("buckets")}") logger.info(s"Bucket Booleans value is: ${request.body.asJson.get("bucketBooleans")}") implicit val db = dbConfig.db getProjectsForCommission(commissionId).map({ case Success(result)=> result.map((project) => { logger.info(s"Found project ${project.id.get}.") deleteDataRunner(project.id.get, 400, request.body.asJson.get("pluto").toString().toBoolean, request.body.asJson.get("file").toString().toBoolean, request.body.asJson.get("backups").toString().toBoolean, request.body.asJson.get("PTR").toString().toBoolean, request.body.asJson.get("deliverables").toString().toBoolean, request.body.asJson.get("SAN").toString().toBoolean, request.body.asJson.get("matrix").toString().toBoolean, request.body.asJson.get("S3").toString().toBoolean, request.body.asJson.get("buckets").validate[Array[String]].get, request.body.asJson.get("bucketBooleans").validate[Array[Boolean]].get) }) if (request.body.asJson.get("commission").toString().toBoolean) { Thread.sleep(1400) PlutoCommission.forId(commissionId).map({ case Some(plutoCommission: PlutoCommission) => plutoCommission.removeFromDatabase.map({ case Success(_) => logger.info(s"Attempt at removing commission record worked.") case Failure(error) => logger.error(s"Attempt at removing commission record failed with error: ${error}") }) case None => logger.error(s"Could not look up commission entry for ${commissionId}: ") }) } case Failure(error)=> logger.error(error.toString) }) Ok(Json.obj("status"->"ok","detail"->"Delete data run.")) } def assetFolderFilesList(requestedId: Int, allVersions: Boolean) = IsAuthenticatedAsync {uid=>{request=> implicit val db = dbConfig.db selectid(requestedId).flatMap({ case Failure(error)=> logger.error(s"Could not list files from project ${requestedId}",error) Future(InternalServerError(Json.obj("status"->"error","detail"->error.toString))) case Success(someSeq)=> someSeq.headOption match { //matching on pk, so can only be one result case Some(projectEntry)=> projectEntry.associatedAssetFolderFiles(allVersions, implicitConfig).map(fileList=>Ok(Json.obj("status"->"ok","files"->fileList))) case None=> Future(NotFound(Json.obj("status"->"error","detail"->s"project $requestedId not found"))) } }) }} def fileDownload(requestedId: Int) = IsAuthenticatedAsync {uid=>{request=> implicit val db = dbConfig.db selectid(requestedId).flatMap({ case Failure(error)=> logger.error(s"Could not download file for project ${requestedId}",error) Future(InternalServerError(Json.obj("status"->"error","detail"->error.toString))) case Success(someSeq)=> someSeq.headOption match { case Some(projectEntry)=> val fileData = for { f1 <- projectEntry.associatedFiles(false).map(fileList=>fileList(0)) f2 <- f1.getFullPath } yield (f1, f2) val (fileEntry, fullPath) = (fileData.map(_._1), fileData.map(_._2)) val fileEntryData = Await.result(fileEntry, Duration(10, TimeUnit.SECONDS)) val fullPathData = Await.result(fullPath, Duration(10, TimeUnit.SECONDS)) Future(Ok.sendFile( content = new java.io.File(fullPathData), fileName = _ => Some(fileEntryData.filepath) )) case None=> Future(NotFound(Json.obj("status"->"error","detail"->s"Project $requestedId not found"))) } }) }} def restoreBackup(requestedId: Int, requestedVersion: Int) = IsAuthenticatedAsync {uid=>{request=> implicit val db = dbConfig.db selectid(requestedId).flatMap({ case Failure(error)=> logger.error(s"Could not restore file for project ${requestedId}",error) Future(InternalServerError(Json.obj("status"->"error","detail"->error.toString))) case Success(someSeq)=> someSeq.headOption match { case Some(projectEntry)=> val fileData = for { f1 <- projectEntry.associatedFiles(false).map(fileList=>fileList(0)) } yield (f1) val fileToSaveOver = Await.result(fileData, Duration(10, TimeUnit.SECONDS)) val fileDataTwo = for { f2 <- projectEntry.associatedFiles(true).map(fileList=>fileList) } yield (f2) val fileEntryDataTwo = Await.result(fileDataTwo, Duration(10, TimeUnit.SECONDS)) var versionFound = 0 var filePlace = 0 val timestamp = dateTimeToTimestamp(ZonedDateTime.now()) var fileToLoad = FileEntry(None, "", 1, "", 1, timestamp, timestamp, timestamp, false, false, None, None) while (versionFound == 0) { if ((!fileEntryDataTwo(filePlace).backupOf.isEmpty) && (fileEntryDataTwo(filePlace).version == requestedVersion)) { fileToLoad = fileEntryDataTwo(filePlace) versionFound = 1 } filePlace = filePlace + 1 } storageHelper.copyFile(fileToLoad, fileToSaveOver) Future(Ok(Json.obj("status"->"okay","detail"->s"Restored file for project $requestedId from version $requestedVersion"))) case None=> Future(NotFound(Json.obj("status"->"error","detail"->s"Project $requestedId not found"))) } }) }} def deleteRecursively(file: File): Unit = { if (file.isDirectory) { file.listFiles.foreach(deleteRecursively) } if (file.exists && !file.delete) { throw new Exception(s"Unable to delete ${file.getAbsolutePath}") } } def restoreAssetFolderBackup(requestedId: Int) = IsAuthenticatedAsync {uid=>{request=> implicit val db = dbConfig.db selectid(requestedId).flatMap({ case Failure(error)=> logger.error(s"Could not restore files for project ${requestedId}",error) Future(InternalServerError(Json.obj("status"->"error","detail"->error.toString))) case Success(someSeq)=> someSeq.headOption match { case Some(projectEntry)=> db.run( TableQuery[ProjectMetadataRow] .filter(_.key===ProjectMetadata.ASSET_FOLDER_KEY) .filter(_.projectRef===requestedId) .result ).map(results=>{ val resultCount = results.length if(resultCount==0){ logger.warn(s"No asset folder registered under project id $requestedId") } else if(resultCount>1){ logger.warn(s"Multiple asset folders found for project $requestedId: $results") } else { logger.debug(s"Found this data: ${results.head}") logger.debug(s"Found this asset folder: ${results.head.value.get}") deleteRecursively(new File(s"${results.head.value.get}/RestoredProjectFiles")) new File(s"${results.head.value.get}/RestoredProjectFiles").mkdirs() val fileData = for { f2 <- projectEntry.associatedAssetFolderFiles(false, implicitConfig).map(fileList=>fileList) } yield (f2) val fileEntryData = Await.result(fileData, Duration(10, TimeUnit.SECONDS)) logger.debug(s"File data found: $fileEntryData") val splitterRegex = "^(?:[^\\/]*\\/){4}".r val filenameRegex = "([^\\/]+$)".r fileEntryData.map(fileData => { Thread.sleep(100) new File(s"${config.get[String]("postrun.assetFolder.basePath")}/${splitterRegex.findFirstIn(fileData.filepath).get}RestoredProjectFiles/${filenameRegex.replaceFirstIn(splitterRegex.replaceFirstIn(fileData.filepath,""),"")}").mkdirs() val timestamp = dateTimeToTimestamp(ZonedDateTime.now()) if (new File(s"${config.get[String]("postrun.assetFolder.basePath")}/${splitterRegex.findFirstIn(fileData.filepath).get}RestoredProjectFiles/${splitterRegex.replaceFirstIn(fileData.filepath,"")}").exists()) { var space_not_found = true var number_to_try = 1 while (space_not_found) { val pathToWorkOn = splitterRegex.replaceFirstIn(fileData.filepath,"") val indexOfPoint = pathToWorkOn.lastIndexOf(".") val readyPath = s"${pathToWorkOn.substring(0, indexOfPoint)}_$number_to_try${pathToWorkOn.substring(indexOfPoint)}" if (new File(s"${config.get[String]("postrun.assetFolder.basePath")}/${splitterRegex.findFirstIn(fileData.filepath).get}RestoredProjectFiles/$readyPath").exists()) { number_to_try = number_to_try + 1 } else { space_not_found = false val fileToSave = AssetFolderFileEntry(None, s"${splitterRegex.findFirstIn(fileData.filepath).get}RestoredProjectFiles/$readyPath", config.get[Int]("asset_folder_storage"), 1, timestamp, timestamp, timestamp, None, None) storageHelper.copyAssetFolderFile(fileData, fileToSave) } } } else { val fileToSave = AssetFolderFileEntry(None, s"${splitterRegex.findFirstIn(fileData.filepath).get}RestoredProjectFiles/${splitterRegex.replaceFirstIn(fileData.filepath, "")}", config.get[Int]("asset_folder_storage"), 1, timestamp, timestamp, timestamp, None, None) storageHelper.copyAssetFolderFile(fileData, fileToSave) } } ) } }).recover({ case err: Throwable => logger.error(s"Could not look up asset folder for project id $requestedId: ", err) }) Future(Ok(Json.obj("status"->"okay","detail"->s"Restored files for project $requestedId"))) case None=> Future(NotFound(Json.obj("status"->"error","detail"->s"Project $requestedId not found"))) } }) }} }