Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename media files corresponding to their order value #5701

Merged
merged 3 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
package org.kitodo.api.dataformat;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.List;
Expand Down Expand Up @@ -187,9 +188,20 @@ public List<LogicalDivision> getAllLogicalDivisions() {
* @return all physical divisions with type "page", sorted by their {@code order}
*/
public List<PhysicalDivision> getAllPhysicalDivisionChildrenFilteredByTypePageAndSorted() {
return getAllPhysicalDivisionChildrenFilteredByTypes(Collections.singletonList(PhysicalDivision.TYPE_PAGE));
}

/**
* Returns all child physical divisions of the physical division of the workpiece with any of the types in the
* provided list "types".
*
* @param types list of types
* @return child physical division of given types
*/
public List<PhysicalDivision> getAllPhysicalDivisionChildrenFilteredByTypes(List<String> types) {
return physicalStructure.getChildren().stream()
.flatMap(Workpiece::treeStream)
.filter(division -> Objects.equals(division.getType(), PhysicalDivision.TYPE_PAGE))
.filter(physicalDivisionToCheck -> types.contains(physicalDivisionToCheck.getType()))
.sorted(Comparator.comparing(PhysicalDivision::getOrder)).collect(Collectors.toUnmodifiableList());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ public class Project extends BaseIndexedBean implements Comparable<Project> {
@JoinColumn(name = "mediaView_video_folder_id", foreignKey = @ForeignKey(name = "FK_project_mediaView_video_folder_id"))
private Folder videoMediaView;

/**
* Filename length for renaming media files of processes in this project.
*/
@Column(name = "filename_length")
private Integer filenameLength;

/**
* Constructor.
*/
Expand Down Expand Up @@ -621,6 +627,25 @@ public void setDefaultChildProcessImportConfiguration(ImportConfiguration defaul
this.defaultChildProcessImportConfiguration = defaultChildProcessImportConfiguration;
}

/**
* Get filename length.
* @return filename length
*/
public Integer getFilenameLength() {
if (Objects.isNull(filenameLength)) {
filenameLength = 8;
}
return filenameLength;
}

/**
* Set filename length.
* @param filenameLength as Integer
*/
public void setFilenameLength(Integer filenameLength) {
this.filenameLength = filenameLength;
}

@Override
public int compareTo(Project project) {
return this.getTitle().compareTo(project.getTitle());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--
-- (c) Kitodo. Key to digital objects e. V. <contact@kitodo.org>
--
-- This file is part of the Kitodo project.
--
-- It is licensed under GNU General Public License version 3 or later.
--
-- For the full copyright and license information, please read the
-- GPL3-License.txt file that was distributed with this source code.
--

SET SQL_SAFE_UPDATES = 0;

-- add authorities/permission for renaming media files
INSERT IGNORE INTO authority (title) VALUES ('renameMedia_clientAssignable');
INSERT IGNORE INTO authority (title) VALUES ('renameMedia_globalAssignable');

-- add "filenameLength" column to project table
ALTER TABLE project ADD filename_length INT DEFAULT 8;

SET SQL_SAFE_UPDATES = 1;
Original file line number Diff line number Diff line change
Expand Up @@ -1081,4 +1081,13 @@ public boolean hasAuthorityToViewDatabaseStatistics() {
public boolean hasAuthorityToRunKitodoScripts() {
return securityAccessService.hasAuthorityToRunKitodoScripts();
}

/**
* Check if the current user has the permission to rename media files.
*
* @return true if the current user has the permission to rename media files.
*/
public boolean hasAuthorityToRenameMediaFiles() {
return securityAccessService.hasAuthorityToRenameMediaFiles();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
Expand All @@ -34,6 +35,7 @@
import javax.inject.Inject;
import javax.inject.Named;

import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
Expand Down Expand Up @@ -202,6 +204,12 @@ public class DataEditorForm implements MetadataTreeTableInterface, RulesetSetupI
private MediaProvider mediaProvider;
private boolean mediaUpdated = false;

private DualHashBidiMap<URI, URI> filenameMapping = new DualHashBidiMap<>();

private int numberOfNewMappings = 0;

private String renamingError = "";

/**
* Public constructor.
*/
Expand Down Expand Up @@ -382,6 +390,7 @@ public String closeAndReturn() {
public void close() {
deleteNotSavedUploadedMedia();
unsavedDeletedMedia.clear();
ServiceManager.getFileService().revertRenaming(filenameMapping.inverseBidiMap(), workpiece);
metadataPanel.clear();
structurePanel.clear();
workpiece = null;
Expand Down Expand Up @@ -431,6 +440,8 @@ private void deleteNotSavedUploadedMedia() {
ServiceManager.getProcessService().getProcessDataDirectory(this.process).getPath()).toUri();
for (PhysicalDivision mediaUnit : this.unsavedUploadedMedia) {
for (URI fileURI : mediaUnit.getMediaFiles().values()) {
this.filenameMapping = ServiceManager.getFileService()
.removeUnsavedUploadMediaUriFromFileMapping(fileURI, this.filenameMapping);
try {
ServiceManager.getFileService().delete(uri.resolve(fileURI));
} catch (IOException e) {
Expand Down Expand Up @@ -488,6 +499,8 @@ private String save(boolean close) {
try {
metadataPanel.preserve();
structurePanel.preserve();
// reset "image filename renaming map" so nothing is reverted after saving!
filenameMapping = new DualHashBidiMap<>();
ServiceManager.getProcessService().updateChildrenFromLogicalStructure(process, workpiece.getLogicalStructure());
ServiceManager.getFileService().createBackupFile(process);
try (OutputStream out = ServiceManager.getFileService().write(mainFileUri)) {
Expand Down Expand Up @@ -1100,4 +1113,45 @@ public boolean isMediaUpdated() {
public void setMediaUpdated(boolean mediaUpdated) {
this.mediaUpdated = mediaUpdated;
}

/**
* Rename media files of current process according to their corresponding physical divisions ORDER attribute.
*/
public void renameMediaFiles() {
renamingError = "";
try {
numberOfNewMappings = ServiceManager.getFileService().renameMediaFiles(process, workpiece, filenameMapping);
} catch (IOException | URISyntaxException e) {
renamingError = e.getMessage();
}
showPanels();
if (StringUtils.isBlank(renamingError)) {
PrimeFaces.current().executeScript("PF('renamingMediaSuccessDialog').show();");
} else {
PrimeFaces.current().executeScript("PF('renamingMediaErrorDialog').show();");
}
}

/**
* Get renamingError.
* @return renamingError
*/
public String getRenamingError() {
return renamingError;
}

/**
* Return renaming success message containing number of renamed media files and configured sub-folders.
* @return renaming success message
*/
public String getRenamingSuccessMessage() {
return Helper.getTranslation("dataEditor.renamingMediaText", String.valueOf(numberOfNewMappings),
String.valueOf(process.getProject().getFolders().size()));
}

private void showPanels() {
galleryPanel.show();
paginationPanel.show();
structurePanel.show();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
Expand Down Expand Up @@ -92,6 +94,9 @@ public class FileService {
private static final String ARABIC_DEFAULT_VALUE = "1";
private static final String ROMAN_DEFAULT_VALUE = "I";
private static final String UNCOUNTED_DEFAULT_VALUE = " - ";
private static final String TEMP_EXTENSION = ".tmp";

private static final String SLASH = "/";


/**
Expand Down Expand Up @@ -1432,9 +1437,8 @@ public URI deleteFirstSlashFromPath(URI uri) {
* @param process Process
* @param generatorSource Folder
* @return whether given URI points to empty directory or not
* @throws IOException thrown if listing contents of given URI is not possible
*/
public static boolean hasImages(Process process, Folder generatorSource) throws IOException, DAOException {
public static boolean hasImages(Process process, Folder generatorSource) {
if (Objects.nonNull(generatorSource)) {
Subfolder sourceFolder = new Subfolder(process, generatorSource);
return !sourceFolder.listContents().isEmpty();
Expand All @@ -1450,4 +1454,114 @@ public static boolean hasImages(Process process, Folder generatorSource) throws
public MetadataImageComparator getMetadataImageComparator() {
return metadataImageComparator;
}

/**
* Rename media files of current process according to their corresponding media units order attribute. Given Map
* "filenameMapping" is altered via side effect and does not need to be returned. Instead, the number of acutally
* changed filenames is returned to the calling method.
*
* @param process Process object for which media files are renamed.
* @param workpiece Workpiece object of process
* @param filenameMapping Bidirectional map containing current filename mapping; empty until first renaming
* @return number of renamed media files
* @throws IOException when renaming files fails
* @throws URISyntaxException when creating URI for new filenames fails
*/
public int renameMediaFiles(Process process, Workpiece workpiece, DualHashBidiMap<URI, URI> filenameMapping)
throws IOException, URISyntaxException {
int filenameLength = process.getProject().getFilenameLength();
URI processDataUri = ServiceManager.getProcessService().getProcessDataDirectory(process);

if (!processDataUri.toString().endsWith(SLASH)) {
processDataUri = URI.create(processDataUri + SLASH);
}

int numberOfRenamedMedia = 0;

// first, rename all files to new filenames plus "tmp" extension to avoid filename collisions
for (PhysicalDivision page : workpiece.getAllPhysicalDivisionChildrenFilteredByTypes(PhysicalDivision.TYPES)) {
String newFilename = StringUtils.leftPad(String.valueOf(page.getOrder()), filenameLength, '0');
for (Entry<MediaVariant, URI> variantURIEntry : page.getMediaFiles().entrySet()) {
URI fileUri = processDataUri.resolve(variantURIEntry.getValue());
String newFilepath = newFilename + "." + FilenameUtils.getExtension(fileUri.getPath()) + TEMP_EXTENSION;
// skip files that already have the correct target name
if (!newFilename.equals(FilenameUtils.getBaseName(variantURIEntry.getValue().toString()))) {
URI tmpUri = fileManagementModule.rename(fileUri, processDataUri + newFilepath);
if (filenameMapping.containsValue(fileUri)) {
// update existing mapping of files that are renamed multiple times
filenameMapping.replace(filenameMapping.getKey(fileUri), tmpUri);
} else {
// add new mapping otherwise
filenameMapping.put(fileUri, tmpUri);
}
URI targetUri = new URI(StringUtils.removeStart(StringUtils.removeEnd(tmpUri.toString(),
TEMP_EXTENSION), process.getId() + SLASH));
page.getMediaFiles().put(variantURIEntry.getKey(), targetUri);
numberOfRenamedMedia++;
}
}
}

// then remove "tmp" extension from all filenames
for (Entry<URI, URI> renamingEntry : filenameMapping.entrySet()) {
URI tempFilename = renamingEntry.getValue();
String tempFilenameString = tempFilename.toString();
// skip filename mappings from last renaming round that have not been renamed again
if (tempFilenameString.endsWith(TEMP_EXTENSION)) {
String newFilepath = StringUtils.removeEnd(tempFilename.toString(), TEMP_EXTENSION);
filenameMapping.put(renamingEntry.getKey(), fileManagementModule.rename(tempFilename, newFilepath));
}
}
return numberOfRenamedMedia;
}

/**
* Revert renaming of media files when the user leaves the metadata editor without saving. This method uses a
* provided map object to rename media files identified by the map entries values to the corresponding map entries
* keys.
*
* @param filenameMappings Bidirectional map containing original filenames as keys and new filenames as values.
* @param workpiece Workpiece of current process
*/
public void revertRenaming(BidiMap<URI, URI> filenameMappings, Workpiece workpiece) {
// revert media variant URIs for all media files in workpiece to previous, original values
for (PhysicalDivision physicalDivision : workpiece
.getAllPhysicalDivisionChildrenFilteredByTypes(PhysicalDivision.TYPES)) {
for (Entry<MediaVariant, URI> mediaVariantURIEntry : physicalDivision.getMediaFiles().entrySet()) {
physicalDivision.getMediaFiles().put(mediaVariantURIEntry.getKey(),
filenameMappings.get(mediaVariantURIEntry.getValue()));
}
}
// revert filenames of media files to previous, original values
try {
List<URI> tempUris = new LinkedList<>();
for (Entry<URI, URI> mapping : filenameMappings.entrySet()) {
tempUris.add(fileManagementModule.rename(mapping.getKey(), mapping.getValue().toString()
+ TEMP_EXTENSION));
}
for (URI tempUri : tempUris) {
fileManagementModule.rename(tempUri, StringUtils.removeEnd(tempUri.toString(), TEMP_EXTENSION));
}
} catch (IOException e) {
logger.error(e);
}
}

/**
* Remove given map entry with whose value URI ends with given URI "unsavedMediaUri" from map
* and return updated map.
* @param unsavedMediaUri URI for which corresponding map entry is removed
* @param mappingMap bidirectional map from URIs to URIs
* @return updated bidirectional map
*/
public DualHashBidiMap<URI, URI> removeUnsavedUploadMediaUriFromFileMapping(URI unsavedMediaUri,
DualHashBidiMap<URI, URI> mappingMap) {
DualHashBidiMap<URI, URI> updatedMap = new DualHashBidiMap<>();
for (Map.Entry<URI, URI> mapEntry : mappingMap.entrySet()) {
if (!mapEntry.getValue().toString().endsWith(unsavedMediaUri.toString())) {
updatedMap.put(mapEntry.getKey(), mapEntry.getValue());
}
}
return updatedMap;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1068,4 +1068,13 @@ public boolean hasAuthorityToDeleteMedia() {
public boolean hasAuthorityToRunKitodoScripts() {
return hasAnyAuthorityForClient("runKitodoScript");
}

/**
* Check if the current user has the permission to rename media files.
*
* @return true if the current user has the permission to rename media files.
*/
public boolean hasAuthorityToRenameMediaFiles() {
return hasAnyAuthorityForClient("renameMedia");
}
}
6 changes: 6 additions & 0 deletions Kitodo/src/main/resources/messages/messages_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,10 @@ dataEditor.removeElement.noConsecutivePagesSelected=Strukturelemente k\u00F6nnen
dataEditor.selectMetadataTask=Aufgabe w\u00E4hlen
dataEditor.layoutSavedSuccessfullyTitle=Aktuelle Spaltenaufteilung erfolgreich gespeichert
dataEditor.layoutSavedSuccessfullyText=Ihre aktuellen Metadaten-Editor-Einstellungen wurden erfolgreich gespeichert! Sie werden f\u00FCr alle zuk\u00FCnftigen Aufgaben dieses Typs wiederverwendet.
dataEditor.renameMedia=Medien umbenennen
dataEditor.renamingMediaComplete=Das Umbenennen der Medien ist abgeschlossen
dataEditor.renamingMediaError=Beim Umbenennen der Medien ist ein Fehler aufgetreten
dataEditor.renamingMediaText={0} Mediendateien in {1} Ordnern wurden umbenannt. Bitte Speichern Sie den Vorgang. Andernfalls werden die Dateiumbennungen beim Schlie\u00DFen des Metadateneditors verworfen.
dataEditor.structure.customizeDisplay=Anzeige anpassen
dataEditor.structureTree.collapseAll=Strukturbaum komplett einklappen
dataEditor.structureTree.expandAll=Strukturbaum komplett ausklappen
Expand Down Expand Up @@ -449,6 +453,7 @@ fileGroupDelete=Dateigruppe l\u00F6schen
fileMassImport=Datei Massenimport
filename=Dateiname
files=Dateien
filenameLengthForRenaming=Dateinamenl\u00e4nge f\u00FCr Umbenennung von Medien
filter=Filter
filterAdjust=Filter anpassen
filterApply=Filter anwenden
Expand Down Expand Up @@ -1271,6 +1276,7 @@ performTask=Aufgabe durchf\u00FChren
assignTask=Aufgabe zuweisen
overrideTask=Aufgabe \u00FCbernehmen
superviseTask=Aufgabe beobachten
renameMedia=Medien umbenennen
resetWorkflow=Workflow zur\u00FCcksetzen
runKitodoScript=KitodoScript ausf\u00FChren

Expand Down
Loading