diff --git a/app/controllers/ProjectEntryController.scala b/app/controllers/ProjectEntryController.scala index 5c3811b8..cedf83bb 100644 --- a/app/controllers/ProjectEntryController.scala +++ b/app/controllers/ProjectEntryController.scala @@ -1184,4 +1184,83 @@ class ProjectEntryController @Inject() (@Named("project-creation-actor") project } }) }} + + 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"))) + } + }) + }} } diff --git a/conf/routes b/conf/routes index 6ef3916d..09434224 100644 --- a/conf/routes +++ b/conf/routes @@ -69,6 +69,7 @@ GET /api/project/:id/missingFiles @controllers.MissingFilesController. GET /api/project/:id/removeWarning @controllers.MissingFilesController.removeWarning(id:Int) GET /api/project/:id/fileDownload @controllers.ProjectEntryController.fileDownload(id:Int) PUT /api/project/:id/restore/:version @controllers.ProjectEntryController.restoreBackup(id:Int, version:Int) +PUT /api/project/:id/restoreForAssetFolder @controllers.ProjectEntryController.restoreAssetFolderBackup(id:Int) GET /api/valid-users @controllers.ProjectEntryController.queryUsersForAutocomplete(prefix:String ?= "", limit:Option[Int]) GET /api/known-user @controllers.ProjectEntryController.isUserKnown(uname:String ?= "") diff --git a/frontend/app/ProjectEntryList/AssetFolderProjectBackups.tsx b/frontend/app/ProjectEntryList/AssetFolderProjectBackups.tsx index 127d3158..e577809f 100644 --- a/frontend/app/ProjectEntryList/AssetFolderProjectBackups.tsx +++ b/frontend/app/ProjectEntryList/AssetFolderProjectBackups.tsx @@ -13,6 +13,7 @@ import { Paper, Tooltip, Typography, + DialogContent, } from "@material-ui/core"; import { Breadcrumb } from "@guardian/pluto-headers"; import { ArrowBack, PermMedia, WarningRounded } from "@material-ui/icons"; @@ -20,6 +21,11 @@ import { getProject, getAssetFolderProjectFiles } from "./helpers"; import clsx from "clsx"; import AssetFolderBackupEntry from "./AssetFolderBackupEntry"; import { useGuardianStyles } from "~/misc/utils"; +import axios from "axios"; +import { + SystemNotification, + SystemNotifcationKind, +} from "@guardian/pluto-headers"; declare var deploymentRootPath: string; @@ -30,7 +36,7 @@ const AssetFolderProjectBackups: React.FC( undefined ); - + const [openDialog, setOpenDialog] = useState(false); const [backupFiles, setBackupFiles] = useState([]); const history = useHistory(); const classes = useGuardianStyles(); @@ -61,6 +67,42 @@ const AssetFolderProjectBackups: React.FC { + setOpenDialog(true); + }; + + const handleCloseDialog = () => { + setOpenDialog(false); + }; + + const handleConfirmUpload = () => { + handleCloseDialog(); + handleRestore(); + }; + + const handleRestore = async () => { + try { + const request = + "/api/project/" + props.match.params.itemid + "/restoreForAssetFolder"; + const response = await axios.put(request, null, { + headers: { + "Content-Type": "application/json", + }, + }); + console.log(response.data); + SystemNotification.open( + SystemNotifcationKind.Success, + `${response.data.detail}` + ); + } catch (error) { + console.error("Error restoring file:", error); + SystemNotification.open( + SystemNotifcationKind.Error, + `Failed to restore project: ${error}` + ); + } + }; + return ( <> {project ? ( @@ -81,6 +123,47 @@ const AssetFolderProjectBackups: React.FC ) : undefined} + + + {/* Confirmation Dialog */} + + + Confirm Restoration of Backed up Project Files: + + + + You are about to restore all the backed up project files shown + on this page. This will result in a folder being created in the + project's asset folder named "RestoredProjectFiles", and within + that a list of all Cubase/Audition files we have backed up. Once + you have identified which project file you need, please move it + out of this folder and into the root of project’s asset folder, + before you carry on working with it. +
+
+
+
+ + + + +
+