-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Auto file name truncation if path length exceeding limit while renaming on Windows #15488
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
base: main
Are you sure you want to change the base?
Changes from all commits
3836e4f
ef31e65
232f677
7b8976d
8757e8f
1843b7e
7f47bfe
31b9e08
cfc7f1d
7b8ccc6
f098f05
b6aed17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package org.jabref.gui.fieldeditors; | ||
|
|
||
| import java.io.IOException; | ||
| import java.nio.file.FileSystemException; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.util.ArrayList; | ||
|
|
@@ -270,12 +271,14 @@ public void renameFileToName(String targetFileName) { | |
| private void performRenameWithConflictCheck(String targetFileName) { | ||
| // Check if a file with the same name already exists | ||
| Optional<Path> existingFile = linkedFileHandler.findExistingFile(linkedFile, entry, targetFileName); | ||
| LOGGER.debug("file already exists: {}", existingFile.isPresent()); | ||
| boolean overwriteFile = false; | ||
|
|
||
| if (existingFile.isPresent()) { | ||
| // Get existing file path and its directory | ||
| Path existingFilePath = existingFile.get(); | ||
| Path targetDirectory = existingFilePath.getParent(); | ||
| LOGGER.debug("existing file path: {} || target Directory: {}", existingFilePath, targetDirectory); | ||
|
|
||
| // Suggest a non-conflicting file name | ||
| String suggestedFileName = FileNameUniqueness.getNonOverWritingFileName(targetDirectory, targetFileName); | ||
|
|
@@ -316,10 +319,18 @@ private void performRenameWithConflictCheck(String targetFileName) { | |
| // Attempt the rename operation | ||
| linkedFileHandler.renameToName(targetFileName, overwriteFile); | ||
| } catch (IOException e) { | ||
| // Display an error dialog if file is locked or inaccessible | ||
| dialogService.showErrorDialogAndWait( | ||
| Localization.lang("Rename failed"), | ||
| Localization.lang("JabRef cannot access the file because it is being used by another process.")); | ||
| LOGGER.error("ERROR MESSAGE FOR RENAMING THE FILE: {}", e.getMessage()); | ||
| if (e instanceof FileSystemException fe) { | ||
| LOGGER.error(fe.getReason()); | ||
| dialogService.showErrorDialogAndWait( | ||
| Localization.lang("Rename failed"), | ||
| Localization.lang("JabRef could not rename the file. Please use a shorter filename or a shorter pattern or try changing the directory.")); | ||
| } else { | ||
| // Display an error dialog if file is locked or inaccessible | ||
| dialogService.showErrorDialogAndWait( | ||
| Localization.lang("Rename failed"), | ||
| Localization.lang("JabRef cannot access the file because it is being used by another process.")); | ||
| } | ||
|
Comment on lines
+322
to
+333
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1- Remove the LOGGER.error with ALL CAPS message development leftover 3-FileSystemException is too broad, it's the parent class of AccessDeniedException, FileAlreadyExistsException, NoSuchFileException, DirectoryNotEmptyException, etc.
Comment on lines
+323
to
+333
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think it is better to use two separate catch clauses instead of instanceof this is cleaner and matches JabRef style that is my thought, what do you think? |
||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| import java.util.stream.Stream; | ||
|
|
||
| import org.jabref.logic.FilePreferences; | ||
| import org.jabref.logic.os.OS; | ||
| import org.jabref.logic.util.io.FileNameUniqueness; | ||
| import org.jabref.logic.util.io.FileUtil; | ||
| import org.jabref.logic.util.strings.StringUtil; | ||
|
|
@@ -16,13 +17,17 @@ | |
| import org.jabref.model.entry.LinkedFile; | ||
|
|
||
| import org.jspecify.annotations.NonNull; | ||
| import org.jspecify.annotations.Nullable; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| public class LinkedFileHandler { | ||
|
|
||
| private static final Logger LOGGER = LoggerFactory.getLogger(LinkedFileHandler.class); | ||
|
|
||
| private static final int MAX_PATH_LENGTH_WINDOWS = 259; | ||
| private static final int SEPERATOR_WINDOWS = 1; | ||
|
Comment on lines
+28
to
+29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo: "SEPERATOR_WINDOWS " -> "SEPARATOR_WINDOWS" i think the it's also wrong location, MAX_PATH_LENGTH_WINDOWS is a platform fact, not a LinkedFileHandler concept, it belongs in org.jabref.logic.os.OS, next to OS.WINDOWS, that way copyOrMoveToDefaultDirectory (same file, same bug class) and anyone else can reuse it instead of redefining 259 late but i will say that the second point is optional, if you want |
||
|
|
||
| private final BibDatabaseContext databaseContext; | ||
| private final FilePreferences filePreferences; | ||
| private final BibEntry entry; | ||
|
|
@@ -111,7 +116,10 @@ public boolean copyOrMoveToDefaultDirectory(boolean shouldMove, boolean shouldRe | |
| /// If exists: the path already exists and has the same content as the given sourcePath | ||
| /// | ||
| /// @param renamed The original/suggested filename was adapted to fit it | ||
| private record GetTargetPathResult(boolean exists, boolean renamed, Path path) { | ||
| private record GetTargetPathResult( | ||
| boolean exists, | ||
| boolean renamed, | ||
| Path path) { | ||
| } | ||
|
|
||
| private GetTargetPathResult getTargetPath(Path sourcePath, Path targetDirectory, boolean useSuggestedName) throws IOException { | ||
|
|
@@ -193,14 +201,29 @@ public boolean renameToName(String targetFileName, boolean overwriteExistingFile | |
| } | ||
|
|
||
| final Path oldPath = oldFile.get(); | ||
| final int parentPathLength = oldPath.getParent() == null ? 0 : oldPath.getParent().toString().length(); | ||
|
|
||
| LOGGER.debug("PARENT: {}", oldPath.getParent()); | ||
| Optional<String> oldExtension = FileUtil.getFileExtension(oldPath); | ||
| Optional<String> newExtension = FileUtil.getFileExtension(targetFileName); | ||
|
|
||
| Path newPath; | ||
| if (newExtension.isPresent() || (oldExtension.isEmpty() && newExtension.isEmpty())) { | ||
| if (OS.WINDOWS && (parentPathLength + targetFileName.length() + SEPERATOR_WINDOWS) > MAX_PATH_LENGTH_WINDOWS) { | ||
| if (newExtension.isPresent()) { | ||
| targetFileName = truncateFileNameOnWindows(targetFileName, parentPathLength, newExtension.get(), null); | ||
| } else { | ||
| targetFileName = truncateFileNameOnWindows(targetFileName, parentPathLength, null, null); | ||
| } | ||
| } | ||
| newPath = oldPath.resolveSibling(targetFileName); | ||
|
|
||
| LOGGER.debug("NEW PATH WITH THE NEW FILENAME: {}", newPath); | ||
| } else { | ||
| assert oldExtension.isPresent() && newExtension.isEmpty(); | ||
| if (OS.WINDOWS && (parentPathLength + targetFileName.length() + SEPERATOR_WINDOWS) > MAX_PATH_LENGTH_WINDOWS) { | ||
| targetFileName = truncateFileNameOnWindows(targetFileName, parentPathLength, null, oldExtension.get()); | ||
| } | ||
| newPath = oldPath.resolveSibling(targetFileName + "." + oldExtension.get()); | ||
| } | ||
|
Comment on lines
+224
to
228
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The guard and the actual path length don't match the final path is Example: parentPathLength = 240 |
||
|
|
||
|
|
@@ -222,7 +245,9 @@ public boolean renameToName(String targetFileName, boolean overwriteExistingFile | |
| Files.move(oldPath, newPath, StandardCopyOption.REPLACE_EXISTING); | ||
| } else { | ||
| Files.createDirectories(newPath.getParent()); | ||
| LOGGER.debug("OVERWRITING FILENAME TO {}", newPath); | ||
| Files.move(oldPath, newPath); | ||
| LOGGER.debug("OVERWRITING SUCCESSFUL"); | ||
| } | ||
|
|
||
| // Update path | ||
|
|
@@ -235,15 +260,48 @@ public boolean renameToName(String targetFileName, boolean overwriteExistingFile | |
| return true; | ||
| } | ||
|
|
||
| /// Helper function which truncates a file name for Windows if length (path + filename) exceeds the max windows | ||
| /// path limit. | ||
| /// | ||
| /// @param targetFileName proposed file name (may include an extension; only the base name is truncated) | ||
| /// @param parentLength Length of the parent directory for the file being renamed | ||
| /// @param newExtension extension from the target name (no leading "."), or null if the target has none | ||
| /// @param oldExtension extension from the existing file when the target has no extension and the old extension is kept | ||
| /// @return the shortened file name; includes {@code .extension} when {@code newExtension} is not {@code null} | ||
| private String truncateFileNameOnWindows(String targetFileName, int parentLength, @Nullable String newExtension, @Nullable String oldExtension) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. StringIndexOutOfBoundsException crash, if parentLength + extensionLength + 2 >= 259 (deep dir tree, long extension think OneDrive synced folders), |
||
| String baseName = FileUtil.getBaseName(targetFileName); | ||
| LOGGER.debug("BASENAME: {}", baseName); | ||
| int extensionLength = 0; | ||
| int dot = 0; | ||
| String fileName; | ||
|
|
||
| if (newExtension != null) { | ||
| extensionLength = newExtension.length(); | ||
| dot = 1; | ||
| fileName = baseName.substring(0, (MAX_PATH_LENGTH_WINDOWS - parentLength - extensionLength - dot - SEPERATOR_WINDOWS)) + "." + newExtension; | ||
| } else if (oldExtension != null) { | ||
| extensionLength = oldExtension.length(); | ||
| dot = 1; | ||
| fileName = baseName.substring(0, (MAX_PATH_LENGTH_WINDOWS - parentLength - extensionLength - dot - SEPERATOR_WINDOWS)); | ||
| } else { | ||
| fileName = baseName.substring(0, (MAX_PATH_LENGTH_WINDOWS - parentLength - extensionLength - dot - SEPERATOR_WINDOWS)); | ||
| } | ||
| LOGGER.debug("NEW FILE NAME: {}", fileName); | ||
|
|
||
| return fileName; | ||
| } | ||
|
|
||
| /// Determines the suggested file name based on the pattern specified in the preferences and valid for the file system. | ||
| /// Uses file extension from original file. | ||
| /// | ||
| /// @return the suggested filename, including extension | ||
| public String getSuggestedFileName() { | ||
| LOGGER.debug("NORMAL getSuggestedFileName METHOD CALLED"); | ||
| String filename = linkedFile.getFileName().orElse("file"); | ||
| LOGGER.debug("FILENAME: {}", filename); | ||
| final String targetFileName = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, filePreferences.getFileNamePattern()) | ||
| .orElse(FileUtil.getBaseName(filename)); | ||
|
|
||
| LOGGER.debug("TARGET FILE NAME: {}", targetFileName); | ||
| return FileUtil.getValidFileName(FileUtil.getFileExtension(filename).map(ext -> targetFileName + "." + ext).orElse(targetFileName)); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove these development leftovers as you said, i think not useful for production logging.