app/services/PremiereVersionConverter.scala (180 lines of code) (raw):
package services
import akka.stream.{IOResult, Materializer}
import akka.stream.alpakka.xml.scaladsl.{XmlParsing, XmlWriting}
import akka.stream.alpakka.xml.{Attribute, StartElement}
import akka.stream.scaladsl.{Compression, FileIO, Keep}
import models._
import org.apache.commons.io.FileUtils
import org.slf4j.LoggerFactory
import play.api.db.slick.DatabaseConfigProvider
import postrun.{AdobeXml, RunXmlLint}
import slick.jdbc.PostgresProfile
import java.io.File
import java.nio.charset.StandardCharsets
import java.nio.file.{Files, Path, Paths}
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
import scala.xml.transform.{RewriteRule, RuleTransformer}
import scala.xml.{Elem, MetaData, Node, UnprefixedAttribute}
class PremiereVersionConverter @Inject() (backupService:NewProjectBackup)(implicit fileEntryDAO:FileEntryDAO, ec:ExecutionContext, dbConfigProvider:DatabaseConfigProvider, mat:Materializer) extends AdobeXml {
private final val logger = LoggerFactory.getLogger(getClass)
def backupFile(fileEntry:FileEntry) = {
logger.warn("starting backupFile")
Future.sequence(Seq(
backupFileToTemp(fileEntry),
backupFileToStorage(fileEntry)
))
.map(results=>{
logger.warn(s"backupFile completed, results were $results")
results
})
.map(_.head.asInstanceOf[Path])
}
def restoreFromBackup(backupFile:Path, fileEntry:FileEntry) = {
logger.info(s"restoring $fileEntry file from backup")
for {
mainPath <- fileEntryDAO.getJavaPath(fileEntry)
result <- newCopyFile(backupFile, mainPath)
_ <- Future.fromTry({
logger.info(s"Copied ${result.count} bytes from $backupFile to $mainPath")
result.status
})
} yield result
}
def backupFileToStorage(fileEntry: FileEntry) = {
implicit val db = dbConfigProvider.get[PostgresProfile].db
logger.warn("in backupFileToStorage")
for {
projectStorage <- StorageEntryHelper.entryFor(fileEntry.storageId)
backupStorage <- projectStorage.flatMap(_.backsUpTo).map(StorageEntryHelper.entryFor).getOrElse(Future(None))
result <- backupStorage match {
case Some(actualBackupStorage)=>
logger.info(s"Creating an incremental backup for ${fileEntry.filepath} on storage ${actualBackupStorage.storageType} ${actualBackupStorage.id}")
for {
maybeProjectEntry <- ProjectEntry.projectForFileEntry(fileEntry)
mostRecentBackup <- if (maybeProjectEntry.isDefined) maybeProjectEntry.get.mostRecentBackup(db, mat, actualBackupStorage.id) else Future(None)
result <- backupService.performBackup(fileEntry, mostRecentBackup, actualBackupStorage).map(Some.apply)
} yield result
case None=>
logger.warn(s"Project for ${fileEntry.filepath} is on a storage which has no backup configured. Cannot make an incremental backup for it.")
Future(None)
}
} yield result
}.map(result=>{
logger.warn(s"completed backupFileToStorage, result was $result")
result
})
/**
* Makes a backup copy of the file pointed to by the given FileEntry in the system default temporary location
* @param fileEntry FileEntry to back up
* @return a Future, containing a File pointing to the backed-up location
*/
def backupFileToTemp(fileEntry:FileEntry):Future[Path] = {
logger.warn("in backupFileToTemp")
for {
inPath <- fileEntryDAO.getJavaPath(fileEntry)
outPath <- Future({
val tempFile = File.createTempFile(fileEntry.filepath, ".bak")
Paths.get(tempFile.getPath)
})
result <- {
logger.warn(s"about to run newCopyFile from $inPath to $outPath")
newCopyFile(inPath, outPath)
.map(result=>{
logger.warn(s"newCopyFile completed with result $result")
result
})
}
_ <- Future.fromTry(result.status)
rtn <- if(Files.size(inPath) != Files.size(outPath)) Future.failed(new RuntimeException(s"Local cache copy failed, expected length ${Files.size(inPath)} but got ${Files.size(outPath)}")) else Future(outPath)
} yield rtn
}
/**
* Returns a failed future indicating that no update is necessary if the current file and the target version are equal
* @param file
* @param targetVersion
* @return
*/
def checkExistingVersion(file:FileEntry, targetVersion: PremiereVersionTranslation):Future[Unit] =
file.maybePremiereVersion match {
case None=>Future.failed(new RuntimeException("The target file is not recognised as having a premiere version"))
case Some(version)=>
if(version==targetVersion.internalVersionNumber) {
Future.failed(new RuntimeException("The target file is already at the requested version"))
} else {
Future( () )
}
}
def tweakProjectVersionStreaming(targetFile:Path, backupFile:Path, currentVersion:Int, newVersion:PremiereVersionTranslation) = {
def canFindAttribute(attribs:List[akka.stream.alpakka.xml.Attribute], key:String): Option[String] = {
attribs.find(_.name == key).map(_.value)
}
logger.warn("in tweakProjectVersionStreaming")
FileIO.fromPath(backupFile)
.via(Compression.gunzip())
.via(XmlParsing.parser)
.map({
case elem@StartElement("Project", attributesList, prefix, namespace, namespaceCtx)=>
logger.debug(s"Found projectNode with attributes $attributesList")
canFindAttribute(attributesList, "Version") match {
case Some(oldVersion)=>
if(oldVersion!=currentVersion.toString) {
logger.warn(s"${targetFile.toString}: Expected current version of $currentVersion but got $oldVersion")
}
logger.info(s"Changing version from $oldVersion to ${newVersion.internalVersionNumber} in ${targetFile.toString}")
val newAttributes = attributesList.filter(_.name!="Version") :+ Attribute("Version", newVersion.internalVersionNumber.toString)
elem.copy(attributesList=newAttributes)
case None=>
elem
}
case other@_ => other
})
.via(XmlWriting.writer(StandardCharsets.UTF_8))
.via(Compression.gzip)
.toMat(FileIO.toPath(targetFile))(Keep.right)
.run()
.flatMap(result=>{
logger.info(s"Output to ${targetFile.toString} completed. Wrote ${result.count} bytes")
Future.fromTry(result.status)
})
.flatMap(_=>Future.fromTry(RunXmlLint.runXmlLint(targetFile.toAbsolutePath.toString)))
}
/**
* Changes the internal version number for the given Premiere project `targetFile` to the new value given in `newVersion`.
* Returns a failed future on error, or an empty Future on success.
*/
def tweakProjectVersion(targetFile:File, backupFile:File, currentVersion:Int, newVersion:PremiereVersionTranslation) = Future.fromTry({
for {
xmlContent <- getXmlFromGzippedFile(targetFile.getAbsolutePath)
updatedXml <- Try { new RuleTransformer(new PremiereVersionConverter.ProjectVersionTweaker(newVersion, currentVersion)).transform(xmlContent).head }
_ <- putXmlToGzippedFile(targetFile.getAbsolutePath,Elem.apply(updatedXml.prefix, updatedXml.label, updatedXml.attributes, updatedXml.scope, false, updatedXml.child :_*))
formattedResult <- RunXmlLint.runXmlLint(targetFile.getAbsolutePath)
} yield formattedResult
}).recoverWith({
case err:Throwable=>
logger.error(s"Could not update project version for ${targetFile.getAbsolutePath} to $newVersion: ${err.getClass.getCanonicalName} ${err.getMessage}", err)
logger.info(s"Restoring backup from $backupFile...")
Try { FileUtils.copyFile(backupFile, targetFile) } match {
case Success(_) => Future.failed(err) //if the copy-back succeeds pass on the original error
case Failure(copyErr)=> Future.failed(copyErr) //if the copy-back fails pass on that error
}
})
def newCopyFile(fromFile:Path, toFile:Path)(implicit mat:Materializer) : Future[IOResult] = {
import java.nio.file.StandardOpenOption._
FileIO.fromPath(fromFile).runWith(FileIO.toPath(toFile, Set(WRITE, TRUNCATE_EXISTING, SYNC)))
}
}
object PremiereVersionConverter {
private final val logger = LoggerFactory.getLogger(getClass)
class ProjectVersionTweaker(newVersion:PremiereVersionTranslation, oldVersion:Int) extends RewriteRule {
def canFindAttribute(attribs: MetaData, key: String): Boolean ={
if(attribs.key==key){
true
} else {
if(attribs.next!=null){
canFindAttribute(attribs.next, key)
} else {
false
}
}
}
override def transform(n: Node): collection.Seq[Node] = n match {
case Elem(prefix, "Project", attributes,scope,children@_*)=>
logger.debug(s"Found projectNode with attributes $attributes")
val updatedAttributes:MetaData =
if(canFindAttribute(attributes, "Version")) {
new UnprefixedAttribute("Version", newVersion.internalVersionNumber.toString, attributes.remove("Version"))
} else {
attributes
}
Elem.apply(prefix,"Project", updatedAttributes, scope, true,children:_*)
case other=>other
}
}
}