diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bd9eb302..4a00a7ba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Log an event each time that a user archives or unarchives a file. - Endpoint `/api/datasets/createfrombag` to ingest datasets in BagIt format. Includes basic dataset metadata, files, folders and technical metadata. Downloading datasets now includes extra Datacite and Clowder metadata. +- Endpoint /api/files/bulkRemove to delete multiple files in one call. [#12](https://github.com/clowder-framework/clowder/issues/12) ## 1.16.0 - 2021-03-31 diff --git a/app/api/Files.scala b/app/api/Files.scala index e16c41979..1c8406e0d 100644 --- a/app/api/Files.scala +++ b/app/api/Files.scala @@ -1,14 +1,8 @@ package api -import scala.annotation.tailrec -import java.io.FileInputStream -import java.net.{URL, URLEncoder} - -import javax.inject.Inject -import javax.mail.internet.MimeUtility -import _root_.util.{FileUtils, JSONLD, Parsers, RequestUtils, SearchUtils} +import _root_.util._ import com.mongodb.casbah.Imports._ -import controllers.Previewers +import controllers.{Previewers, Utils} import jsonutils.JsonUtil import models._ import play.api.Logger @@ -18,16 +12,16 @@ import play.api.libs.concurrent.Execution.Implicits._ import play.api.libs.iteratee.Enumerator import play.api.libs.json.Json._ import play.api.libs.json._ -import play.api.mvc.{Action, ResponseHeader, Result, SimpleResult} +import play.api.mvc.{ResponseHeader, SimpleResult} import services._ +import services.s3.S3ByteStorageService -import scala.collection.mutable.ListBuffer -import scala.util.parsing.json.JSONArray -import java.text.SimpleDateFormat +import java.io.FileInputStream +import java.net.URL import java.util.Date - -import controllers.Utils -import services.s3.S3ByteStorageService +import javax.inject.Inject +import scala.annotation.tailrec +import scala.collection.mutable.ListBuffer /** * Json API for files. @@ -1616,6 +1610,32 @@ class Files @Inject()( } } + def bulkDeleteFiles() = PrivateServerAction (parse.json) {implicit request=> + request.user match { + case Some(user) => { + val fileIds = request.body.\("fileIds").asOpt[List[String]].getOrElse(List.empty[String]) + if (fileIds.isEmpty){ + BadRequest("No file ids supplied") + } else { + var resourceRefList: ListBuffer[ResourceRef] = ListBuffer.empty[ResourceRef] + for (fileId <- fileIds) { + if (UUID.isValid(fileId)) { + val current_resource_ref = ResourceRef(ResourceRef.file, UUID(fileId)) + resourceRefList += current_resource_ref + } + } + val filesIdsCanDelete = Permission.checkPermissions(request.user, Permission.DeleteFile, resourceRefList.toList).approved.map(_.id) + for (id <- filesIdsCanDelete) { + files.removeFile(id,Utils.baseUrl(request), request.apiKey, request.user) + } + Ok(toJson(Map("status" -> "success"))) + } + } + case None => { + BadRequest("No user supplied") + } + } + } def removeFile(id: UUID) = PermissionAction(Permission.DeleteFile, Some(ResourceRef(ResourceRef.file, id))) { implicit request => files.get(id) match { diff --git a/app/controllers/Application.scala b/app/controllers/Application.scala index 1952006e4..138f572f8 100644 --- a/app/controllers/Application.scala +++ b/app/controllers/Application.scala @@ -1,20 +1,14 @@ package controllers -import java.net.URL - -import javax.inject.{Inject, Singleton} -import api.Permission -import api.Permission._ -import play.api.{Logger, Play, Routes} +import models.{Event, UUID, UserStatus} +import play.api.Play.current import play.api.mvc.Action +import play.api.{Logger, Play, Routes} import services._ -import models.{Event, UUID, User, UserStatus} -import org.owasp.html.Sanitizers -import play.api.Logger -import play.api.libs.concurrent.Execution.Implicits._ -import play.api.Play.current import util.Formatters.sanitizeHTML +import java.net.URL +import javax.inject.{Inject, Singleton} import scala.collection.immutable.List import scala.collection.mutable.ListBuffer @@ -368,6 +362,7 @@ class Application @Inject() (files: FileService, collections: CollectionService, api.routes.javascript.Files.updateFileName, api.routes.javascript.Files.updateDescription, api.routes.javascript.Files.extract, + api.routes.javascript.Files.bulkDeleteFiles, api.routes.javascript.Files.removeFile, api.routes.javascript.Files.follow, api.routes.javascript.Files.unfollow, diff --git a/app/services/mongodb/MongoDBFileService.scala b/app/services/mongodb/MongoDBFileService.scala index 74b7cc539..a526c769b 100644 --- a/app/services/mongodb/MongoDBFileService.scala +++ b/app/services/mongodb/MongoDBFileService.scala @@ -806,67 +806,67 @@ class MongoDBFileService @Inject() ( def removeFile(id: UUID, host: String, apiKey: Option[String], user: Option[User]){ get(id) match{ case Some(file) => { - if(!file.isIntermediate){ - val fileDatasets = datasets.findByFileIdDirectlyContain(file.id) - for(fileDataset <- fileDatasets){ - datasets.removeFile(fileDataset.id, id) - if(!file.xmlMetadata.isEmpty){ - datasets.index(fileDataset.id) - } + if(!file.isIntermediate){ + val fileDatasets = datasets.findByFileIdDirectlyContain(file.id) + for(fileDataset <- fileDatasets){ + datasets.removeFile(fileDataset.id, id) + if(!file.xmlMetadata.isEmpty){ + datasets.index(fileDataset.id) + } - if(!file.thumbnail_id.isEmpty && !fileDataset.thumbnail_id.isEmpty){ - if(file.thumbnail_id.get.equals(fileDataset.thumbnail_id.get)){ - datasets.newThumbnail(fileDataset.id) - collections.get(fileDataset.collections).found.foreach(collection => { - if(!collection.thumbnail_id.isEmpty){ - if(collection.thumbnail_id.get.equals(fileDataset.thumbnail_id.get)){ - collections.createThumbnail(collection.id) + if(!file.thumbnail_id.isEmpty && !fileDataset.thumbnail_id.isEmpty){ + if(file.thumbnail_id.get.equals(fileDataset.thumbnail_id.get)){ + datasets.newThumbnail(fileDataset.id) + collections.get(fileDataset.collections).found.foreach(collection => { + if(!collection.thumbnail_id.isEmpty){ + if(collection.thumbnail_id.get.equals(fileDataset.thumbnail_id.get)){ + collections.createThumbnail(collection.id) + } } - } - }) - } + }) + } + } + } - - } - val fileFolders = folders.findByFileId(file.id) - for(fileFolder <- fileFolders) { - folders.removeFile(fileFolder.id, file.id) - } - for(section <- sections.findByFileId(file.id)){ - sections.removeSection(section) - } - for(comment <- comments.findCommentsByFileId(id)){ - comments.removeComment(comment) - } - for(texture <- threeD.findTexturesByFileId(file.id)){ - ThreeDTextureDAO.removeById(new ObjectId(texture.id.stringify)) - } - for (follower <- file.followers) { - userService.unfollowFile(follower, id) + val fileFolders = folders.findByFileId(file.id) + for(fileFolder <- fileFolders) { + folders.removeFile(fileFolder.id, file.id) + } + for(section <- sections.findByFileId(file.id)){ + sections.removeSection(section) + } + for(comment <- comments.findCommentsByFileId(id)){ + comments.removeComment(comment) + } + for(texture <- threeD.findTexturesByFileId(file.id)){ + ThreeDTextureDAO.removeById(new ObjectId(texture.id.stringify)) + } + for (follower <- file.followers) { + userService.unfollowFile(follower, id) + } } - } - // delete the actual file - if(isLastPointingToLoader(file.loader, file.loader_id)) { - for(preview <- previews.findByFileId(file.id)){ - previews.removePreview(preview) + // delete the actual file + if(isLastPointingToLoader(file.loader, file.loader_id)) { + for(preview <- previews.findByFileId(file.id)){ + previews.removePreview(preview) + } + if(!file.thumbnail_id.isEmpty) + thumbnails.remove(UUID(file.thumbnail_id.get)) + ByteStorageService.delete(file.loader, file.loader_id, FileDAO.COLLECTION) } - if(!file.thumbnail_id.isEmpty) - thumbnails.remove(UUID(file.thumbnail_id.get)) - ByteStorageService.delete(file.loader, file.loader_id, FileDAO.COLLECTION) - } - import UUIDConversions._ - FileDAO.removeById(file.id) - appConfig.incrementCount('files, -1) - appConfig.incrementCount('bytes, -file.length) - current.plugin[ElasticsearchPlugin].foreach { - _.delete(id.stringify) - } + import UUIDConversions._ + FileDAO.removeById(file.id) + appConfig.incrementCount('files, -1) + appConfig.incrementCount('bytes, -file.length) + current.plugin[ElasticsearchPlugin].foreach { + _.delete(id.stringify) + } - // finally remove metadata - if done before file is deleted, document metadataCounts won't match - metadatas.removeMetadataByAttachTo(ResourceRef(ResourceRef.file, id), host, apiKey, user) + // finally remove metadata - if done before file is deleted, document metadataCounts won't match + metadatas.removeMetadataByAttachTo(ResourceRef(ResourceRef.file, id), host, apiKey, user) } case None => Logger.debug("File not found") } diff --git a/conf/routes b/conf/routes index 2f2da84ef..21e6fe83c 100644 --- a/conf/routes +++ b/conf/routes @@ -388,6 +388,7 @@ GET /api/files/:id/blob # deprecrated use DELETE POST /api/files/:id/remove @api.Files.removeFile(id: UUID) +POST /api/files/bulkRemove @api.Files.bulkDeleteFiles() GET /api/files/:id/metadata @api.Files.get(id: UUID) POST /api/files/:id/metadata @api.Files.addMetadata(id: UUID) GET /api/files/:id/metadataDefinitions @api.Files.getMetadataDefinitions(id: UUID, space: Option[String] ?= None) diff --git a/public/swagger.yml b/public/swagger.yml index b110554ec..4eb326759 100644 --- a/public/swagger.yml +++ b/public/swagger.yml @@ -189,6 +189,24 @@ paths: '401': $ref: '#/components/responses/Disabled' + /files/bulkRemove: + post: + tags: + - files + summary: Deletes files + descriptions: Deletes a list of files by fileIds + responses: + '200': + description: Returns Status Success + content: + application/json: + schema: + type: array + items: + $ref: '#components/parameters/fileIds' + '401': + $ref: '#components/responses/Disabled' + /files/{id}: parameters: - $ref: '#/components/parameters/fileId'