app/controllers/PremiereVersionConverter.scala (216 lines of code) (raw):
package controllers
import akka.stream.Materializer
import akka.stream.scaladsl.{Keep, Sink}
import auth.{BearerTokenAuth, Security}
import helpers.PostrunDataCache
import models.PremiereVersionTranslationCodec._
import models._
import play.api.Configuration
import play.api.cache.SyncCacheApi
import play.api.db.slick.DatabaseConfigProvider
import play.api.libs.json.Json
import play.api.mvc.{AbstractController, ControllerComponents}
import postrun.{AdobeXml, ExtractPremiereVersion}
import slick.jdbc.PostgresProfile
import javax.inject.{Inject, Singleton}
import scala.annotation.switch
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
@Singleton
class PremiereVersionConverter @Inject()(override val controllerComponents:ControllerComponents,
override val bearerTokenAuth:BearerTokenAuth,
override implicit val config: Configuration,
override val cache:SyncCacheApi,
fileEntryDAO:FileEntryDAO,
converter:services.PremiereVersionConverter,
premiereVersionTranslationDAO: PremiereVersionTranslationDAO,
dbConfigProvider:DatabaseConfigProvider)
(implicit ec:ExecutionContext, mat:Materializer) extends AbstractController(controllerComponents) with Security with AdobeXml with FileEntrySerializer
{
private implicit val db = dbConfigProvider.get[PostgresProfile].db
def changeVersion(projectFileId:Int, requiredDisplayVersion:String) = IsAuthenticatedAsync { uid=> request=>
DisplayedVersion(requiredDisplayVersion) match {
case None=>
Future(BadRequest(Json.obj("status"->"bad_request","detail"->"invalid version number")))
case Some(newVersion)=>
val resultFut = for {
targetVersion <- premiereVersionTranslationDAO
.findDisplayedVersionByMajor(newVersion.major)
.flatMap(results=>{
if(results.isEmpty) {
Future.failed(new RuntimeException("Version number is not recognised"))
} else {
logger.info(s"Target version is ${results.head}")
Future(results.head)
}
})
projectFile <- fileEntryDAO.entryFor(projectFileId).flatMap({
case None=>Future.failed(new RuntimeException(s"project id $projectFileId does not exist"))
case Some(f)=>Future(f)
})
projectFileJavaIO <- fileEntryDAO.getJavaPath(projectFile)
_ <- converter.checkExistingVersion(projectFile, targetVersion) //breaks the chain if the versions are both the same
backupFile <- converter.backupFile(projectFile)
//.get is safe here because if it is None then `checkExistingVersion` will have failed the job
_ <- converter
.tweakProjectVersionStreaming(projectFileJavaIO, backupFile, projectFile.maybePremiereVersion.get, targetVersion)
.recoverWith({
case err:Throwable=>
logger.error(s"Could not update $projectFile, restoring backup: ${err.getMessage}", err)
converter
.restoreFromBackup(backupFile, projectFile)
.flatMap(_=>{
logger.error(s"Restore of backup completed")
Future.failed(err)
})
})
updatedProjectFile <- fileEntryDAO.saveSimple(projectFile.copy(maybePremiereVersion = Some(targetVersion.internalVersionNumber)))
} yield (updatedProjectFile, projectFile)
resultFut
.map(afterAndBefore=>{
logger.info(s"Project file ${afterAndBefore._1.filepath} has been updated to internal version number ${afterAndBefore._1.maybePremiereVersion} from ${afterAndBefore._2.maybePremiereVersion}")
Ok(Json.obj("status"->"ok", "detail"->"Project updated", "entry"->afterAndBefore._1))
})
.recover({
case err:Throwable=>
logger.error(s"Could not update project file to version $newVersion: ${err.getClass.getCanonicalName} ${err.getMessage}", err)
BadRequest(Json.obj("status"->"error", "detail"->err.getMessage))
})
}
}
def lookupClientVersion(clientVersionString:String) = IsAuthenticatedAsync { uid => request =>
DisplayedVersion(clientVersionString) match {
case Some(clientVersion) =>
premiereVersionTranslationDAO
.findDisplayedVersionByMajor(clientVersion.major)
.map(results => {
if (results.isEmpty) {
BadRequest(Json.obj("status" -> "error", "detail" -> s"We did not recognise your installed version of Premiere, $clientVersionString. Please report this to Multimedia tech."))
} else {
Ok(Json.obj("status" -> "ok", "count" -> results.length, "result" -> results))
}
})
case None =>
Future(BadRequest(Json.obj("status" -> "error", "detail" -> "Invalid version string, should be of the form x.y.z")))
}
}
def lookupInternalVersion(internalVersion:Int) = IsAuthenticatedAsync { uid=> request=>
premiereVersionTranslationDAO
.findInternalVersion(internalVersion)
.map({
case Some(v)=>Ok(Json.obj("status"->"ok","version"->v))
case None=>NotFound(Json.obj("status"->"error","detail"->"unknown version number"))
})
.recover({
case err:Throwable=>
logger.error(s"Could not look up internal premiere version number $internalVersion: ${err.getClass.getCanonicalName} ${err.getMessage}", err)
InternalServerError(Json.obj("status"->"db_error", "detail"->"Database error, see server logs"))
})
}
def runVersionScanner(projectTypeId:Int) = {
val xtractor = new ExtractPremiereVersion
ProjectEntry
.scanProjectsForType(projectTypeId) //step 1 - stream out projects
.mapAsync(2)(p=>p.associatedFiles(allVersions=false).map(files=>(p, files))) //step 2 - now pick up the file associations for each of them. allVersions = false => only default storage.
.mapAsync(1)(content=>{ //step 3 - get the full filepath for each of the listed files and add it into the data stream
val project = content._1
val files = content._2
Future
.sequence(files.map(f=>fileEntryDAO.getFullPath(f)))
.map(filePaths=>(project, filePaths zip files))
})
.mapAsync(1)(content=>{ //step 4 - now comes the real action....
val project = content._1
val filePaths = content._2
for {
//run the ExtractPremiereVersion postrun across all of the listed files.
//map the output into a tuple list consisting of the postrun output (success/failure) and the incoming data
extractedInfo <- Future.sequence(filePaths.map(filePair =>
xtractor.postrun(filePair._1, project, null, PostrunDataCache(), None, None)
)).map(_ zip filePaths)
//now iterate through those results.
// If the postrun ran successfully, pick up the version and output and updated version of the FileEntry.
// If the postrun ran successfully but did not output a version, abort; this should not happen and indicates a bug.
// If it did not run successfully then log a warning but continue
results <- Future(extractedInfo.map(info=>info._1 match {
case Success(updatedCache)=>
updatedCache.get("premiere_version") match {
case Some(updatedVersion)=>
logger.info(s"${info._2._1}: Internal premiere version number is $updatedVersion")
Some(info._2._2.copy(maybePremiereVersion = Some(updatedVersion.toInt)))
case None=>
throw new RuntimeException(s"${info._2._1}: ExtractPremiereVersion completed successfully but did not return any info, this should not happen")
}
case Failure(err)=>
logger.warn(s"Could not get premiere version from ${info._2._1}: ${err.getClass.getCanonicalName} ${err.getMessage}", err)
None
}))
} yield results
})
.map(_.collect({case Some(fileEntry)=>fileEntry})) //step 5 - filter out unsuccessful attempts
.mapAsync(4)(files=>{ //step 6 - write the modified records back to the database
Future.sequence(
files.map(f=>{
logger.info(s"Saving updated record for ${f.id} ${f.filepath}")
fileEntryDAO.saveSimple(f)
})
)
})
.toMat(Sink.ignore)(Keep.right)
.run()
.map(_=>{
logger.info("Premiere version scan is now complete")
})
.recover({
case err:Throwable=>
logger.error(s"Premiere version scan failed: ${err.getClass.getCanonicalName} ${err.getMessage}", err)
})
}
def scanAllVersions(projectTypeId:Int) = IsAdmin { uid=> request=>
runVersionScanner(projectTypeId)
Ok(Json.obj("status"->"ok","detail"->"run started"))
}
def createOrUpdate = IsAdminAsync(parse.json[PremiereVersionTranslation]) { uid => request=>
premiereVersionTranslationDAO
.save(request.body)
.map(count=>{
(count: @switch) match {
case 0=>
logger.error(s"Could not create db record for version translation ${request.body}, success but no rows returned")
InternalServerError(Json.obj("status"->"db_error","detail"->"nothing was written"))
case 1=>
Ok(Json.obj("status"->"ok"))
case _=>
logger.error(s"Got unexpected return code $count from the database, expected 1 row")
Ok(Json.obj("status"->"warning"))
}
})
.recover({
case err:Throwable=>
logger.error(s"Could not create db record for version translation ${request.body}: ${err.getClass.getCanonicalName} ${err.getMessage}", err)
InternalServerError(Json.obj("status"->"error", "detail"->"db error, see server logs"))
})
}
def delete(internalVersion:Int) = IsAdminAsync { uid=> request=>
logger.info(s"Request from $uid to delete premiere version mapping for $internalVersion")
premiereVersionTranslationDAO
.remove(internalVersion)
.map(count=>{
if(count==0) {
NotFound(Json.obj("status"->"not_found", "recordId"->internalVersion))
} else {
Ok(Json.obj("status"->"ok"))
}
})
.recover({
case err:Throwable=>
logger.error(s"Could not delete db record for version translation ${request.body}: ${err.getClass.getCanonicalName} ${err.getMessage}", err)
InternalServerError(Json.obj("status"->"error", "detail"->"db error, see server logs"))
})
}
def allVersions = IsAdminAsync { uid=> request=>
premiereVersionTranslationDAO
.listAll
.map(results=>{
Ok(Json.obj("status"->"ok", "count"->results.length, "result"->results))
})
.recover({
case err:Throwable=>
logger.error(s"Could not list all premiere version translations: ${err.getClass.getCanonicalName} ${err.getMessage}", err)
InternalServerError(Json.obj("status"->"error", "detail"->"db error, see server logs"))
})
}
}