diff --git a/CHANGELOG.md b/CHANGELOG.md index a7119a590bb..f04677a676e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added a new group icon column to the main table showing the icons of the entry's groups. [#10801](https://github.com/JabRef/jabref/pull/10801) - We added ability to jump to an entry in the command line using `-j CITATIONKEY`. [koppor#540](https://github.com/koppor/jabref/issues/540) - We added a new boolean to the style files for Openoffice/Libreoffice integration to switch between ZERO_WIDTH_SPACE (default) and no space. [#10843](https://github.com/JabRef/jabref/pull/10843) +- We added the possibility to redownload files that had been present but are no longer in the specified location. [#10848](https://github.com/JabRef/jabref/issues/10848) ### Changed diff --git a/src/main/java/org/jabref/gui/MainMenu.java b/src/main/java/org/jabref/gui/MainMenu.java index c898417b927..db5c69c7fd2 100644 --- a/src/main/java/org/jabref/gui/MainMenu.java +++ b/src/main/java/org/jabref/gui/MainMenu.java @@ -44,6 +44,7 @@ import org.jabref.gui.integrity.IntegrityCheckAction; import org.jabref.gui.journals.AbbreviateAction; import org.jabref.gui.libraryproperties.LibraryPropertiesAction; +import org.jabref.gui.linkedfile.RedownloadMissingFilesAction; import org.jabref.gui.mergeentries.MergeEntriesAction; import org.jabref.gui.preferences.ShowPreferencesAction; import org.jabref.gui.preview.CopyCitationAction; @@ -287,7 +288,11 @@ private void createMenu() { new SeparatorMenuItem(), - factory.createMenuItem(StandardActions.REBUILD_FULLTEXT_SEARCH_INDEX, new RebuildFulltextSearchIndexAction(stateManager, frame::getCurrentLibraryTab, dialogService, preferencesService.getFilePreferences(), taskExecutor)) + factory.createMenuItem(StandardActions.REBUILD_FULLTEXT_SEARCH_INDEX, new RebuildFulltextSearchIndexAction(stateManager, frame::getCurrentLibraryTab, dialogService, preferencesService.getFilePreferences(), taskExecutor)), + + new SeparatorMenuItem(), + + factory.createMenuItem(StandardActions.REDOWNLOAD_MISSING_FILES, new RedownloadMissingFilesAction(stateManager, dialogService, preferencesService.getFilePreferences(), taskExecutor)) ); SidePaneType webSearchPane = SidePaneType.WEB_SEARCH; SidePaneType groupsPane = SidePaneType.GROUPS; diff --git a/src/main/java/org/jabref/gui/actions/StandardActions.java b/src/main/java/org/jabref/gui/actions/StandardActions.java index 7c3429d2a4e..6f74e2caa1f 100644 --- a/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -30,6 +30,7 @@ public enum StandardActions implements Action { SEND_AS_EMAIL(Localization.lang("As Email")), SEND_TO_KINDLE(Localization.lang("To Kindle")), REBUILD_FULLTEXT_SEARCH_INDEX(Localization.lang("Rebuild fulltext search index"), IconTheme.JabRefIcons.FILE), + REDOWNLOAD_MISSING_FILES(Localization.lang("Redownload missing files"), IconTheme.JabRefIcons.DOWNLOAD), OPEN_EXTERNAL_FILE(Localization.lang("Open file"), IconTheme.JabRefIcons.FILE, KeyBinding.OPEN_FILE), OPEN_URL(Localization.lang("Open URL or DOI"), IconTheme.JabRefIcons.WWW, KeyBinding.OPEN_URL_OR_DOI), SEARCH_SHORTSCIENCE(Localization.lang("Search ShortScience")), @@ -149,6 +150,7 @@ public enum StandardActions implements Action { EDIT_FILE_LINK(Localization.lang("Edit"), IconTheme.JabRefIcons.EDIT, KeyBinding.EDIT_ENTRY), DOWNLOAD_FILE(Localization.lang("Download file"), IconTheme.JabRefIcons.DOWNLOAD_FILE), + REDOWNLOAD_FILE(Localization.lang("Redownload file"), IconTheme.JabRefIcons.DOWNLOAD_FILE), RENAME_FILE_TO_PATTERN(Localization.lang("Rename file to defined pattern"), IconTheme.JabRefIcons.AUTO_RENAME), RENAME_FILE_TO_NAME(Localization.lang("Rename file to a given name"), IconTheme.JabRefIcons.RENAME, KeyBinding.REPLACE_STRING), MOVE_FILE_TO_FOLDER(Localization.lang("Move file to file directory"), IconTheme.JabRefIcons.MOVE_TO_FOLDER), diff --git a/src/main/java/org/jabref/gui/externalfiles/FileDownloadTask.java b/src/main/java/org/jabref/gui/externalfiles/FileDownloadTask.java deleted file mode 100644 index cb49f8f7f11..00000000000 --- a/src/main/java/org/jabref/gui/externalfiles/FileDownloadTask.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.jabref.gui.externalfiles; - -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; - -import org.jabref.gui.util.BackgroundTask; -import org.jabref.logic.net.ProgressInputStream; -import org.jabref.logic.net.URLDownload; - -import com.tobiasdiez.easybind.EasyBind; - -public class FileDownloadTask extends BackgroundTask { - - private final URL source; - private final Path destination; - - public FileDownloadTask(URL source, Path destination) { - this.source = source; - this.destination = destination; - } - - @Override - protected Path call() throws Exception { - URLDownload download = new URLDownload(source); - try (ProgressInputStream inputStream = download.asInputStream()) { - EasyBind.subscribe( - inputStream.totalNumBytesReadProperty(), - bytesRead -> updateProgress(bytesRead.longValue(), inputStream.getMaxNumBytes())); - - // Make sure directory exists since otherwise copy fails - Files.createDirectories(destination.getParent()); - - Files.copy(inputStream, destination, StandardCopyOption.REPLACE_EXISTING); - } - - return destination; - } -} diff --git a/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java b/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java index 0b3ce97a36e..5f4d8ef8478 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java +++ b/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java @@ -1,7 +1,6 @@ package org.jabref.gui.fieldeditors; import java.io.IOException; -import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -11,11 +10,6 @@ import java.util.function.BiPredicate; import java.util.function.Supplier; -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLSocketFactory; - import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.binding.ObjectBinding; @@ -32,20 +26,16 @@ import org.jabref.gui.AbstractViewModel; import org.jabref.gui.DialogService; import org.jabref.gui.desktop.JabRefDesktop; -import org.jabref.gui.externalfiles.FileDownloadTask; import org.jabref.gui.externalfiletype.ExternalFileType; import org.jabref.gui.externalfiletype.ExternalFileTypes; -import org.jabref.gui.externalfiletype.StandardExternalFileType; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.icon.JabRefIcon; +import org.jabref.gui.linkedfile.DownloadLinkedFileAction; import org.jabref.gui.linkedfile.LinkedFileEditDialogView; import org.jabref.gui.mergeentries.MultiMergeEntriesView; -import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.ControlHelper; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.externalfiles.LinkedFileHandler; -import org.jabref.logic.importer.FetcherClientException; -import org.jabref.logic.importer.FetcherServerException; import org.jabref.logic.importer.Importer; import org.jabref.logic.importer.ParserResult; import org.jabref.logic.importer.fileformat.PdfContentImporter; @@ -54,8 +44,6 @@ import org.jabref.logic.importer.fileformat.PdfVerbatimBibTextImporter; import org.jabref.logic.importer.fileformat.PdfXmpImporter; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.net.URLDownload; -import org.jabref.logic.util.io.FileNameUniqueness; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -421,146 +409,47 @@ public void edit() { this.linkedFile.setLink(file.getLink()); this.linkedFile.setDescription(file.getDescription()); this.linkedFile.setFileType(file.getFileType()); + this.linkedFile.setSourceURL(file.getSourceUrl()); }); } - public void download() { - if (!linkedFile.isOnlineLink()) { - throw new UnsupportedOperationException("In order to download the file it has to be an online link"); - } - try { - Optional targetDirectory = databaseContext.getFirstExistingFileDir(preferencesService.getFilePreferences()); - if (targetDirectory.isEmpty()) { - dialogService.showErrorDialogAndWait(Localization.lang("Download file"), Localization.lang("File directory is not set or does not exist!")); - return; - } - - URLDownload urlDownload = new URLDownload(linkedFile.getLink()); - if (!checkSSLHandshake(urlDownload)) { - return; - } - - BackgroundTask downloadTask = prepareDownloadTask(targetDirectory.get(), urlDownload); - downloadTask.onSuccess(destination -> { - boolean isDuplicate; - try { - isDuplicate = FileNameUniqueness.isDuplicatedFile(targetDirectory.get(), destination.getFileName(), dialogService); - } catch (IOException e) { - LOGGER.error("FileNameUniqueness.isDuplicatedFile failed", e); - return; - } - - if (!isDuplicate) { - // we need to call LinkedFileViewModel#fromFile, because we need to make the path relative to the configured directories - LinkedFile newLinkedFile = LinkedFilesEditorViewModel.fromFile( - destination, - databaseContext.getFileDirectories(preferencesService.getFilePreferences()), - preferencesService.getFilePreferences()); - entry.replaceDownloadedFile(linkedFile.getLink(), newLinkedFile); - - // Notify in bar when the file type is HTML. - if (newLinkedFile.getFileType().equals(StandardExternalFileType.URL.getName())) { - dialogService.notify(Localization.lang("Downloaded website as an HTML file.")); - LOGGER.debug("Downloaded website {} as an HTML file at {}", linkedFile.getLink(), destination); - } - } - }); - downloadProgress.bind(downloadTask.workDonePercentageProperty()); - downloadTask.titleProperty().set(Localization.lang("Downloading")); - downloadTask.messageProperty().set( - Localization.lang("Fulltext for") + ": " + entry.getCitationKey().orElse(Localization.lang("New entry"))); - downloadTask.showToUser(true); - downloadTask.onFailure(ex -> { - LOGGER.error("Error downloading from URL: " + urlDownload, ex); - String fetcherExceptionMessage = ex.getMessage(); - int statusCode; - if (ex instanceof FetcherClientException clientException) { - statusCode = clientException.getStatusCode(); - if (statusCode == 401) { - dialogService.showInformationDialogAndWait(Localization.lang("Failed to download from URL"), Localization.lang("401 Unauthorized: Access Denied. You are not authorized to access this resource. Please check your credentials and try again. If you believe you should have access, please contact the administrator for assistance.\nURL: %0 \n %1", urlDownload.getSource(), fetcherExceptionMessage)); - } else if (statusCode == 403) { - dialogService.showInformationDialogAndWait(Localization.lang("Failed to download from URL"), Localization.lang("403 Forbidden: Access Denied. You do not have permission to access this resource. Please contact the administrator for assistance or try a different action.\nURL: %0 \n %1", urlDownload.getSource(), fetcherExceptionMessage)); - } else if (statusCode == 404) { - dialogService.showInformationDialogAndWait(Localization.lang("Failed to download from URL"), Localization.lang("404 Not Found Error: The requested resource could not be found. It seems that the file you are trying to download is not available or has been moved. Please verify the URL and try again. If you believe this is an error, please contact the administrator for further assistance.\nURL: %0 \n %1", urlDownload.getSource(), fetcherExceptionMessage)); - } - } else if (ex instanceof FetcherServerException serverException) { - statusCode = serverException.getStatusCode(); - dialogService.showInformationDialogAndWait(Localization.lang("Failed to download from URL"), - Localization.lang("Error downloading from URL. Cause is likely the server side. HTTP Error %0 \n %1 \nURL: %2 \nPlease try again later or contact the server administrator.", statusCode, fetcherExceptionMessage, urlDownload.getSource())); - } else { - dialogService.showErrorDialogAndWait(Localization.lang("Failed to download from URL"), Localization.lang("Error message: %0 \nURL: %1 \nPlease check the URL and try again.", fetcherExceptionMessage, urlDownload.getSource())); - } - }); - taskExecutor.execute(downloadTask); - } catch (MalformedURLException exception) { - dialogService.showErrorDialogAndWait(Localization.lang("Invalid URL"), exception); - } + public void redownload() { + LOGGER.info("Redownloading file from " + linkedFile.getSourceUrl()); + if (linkedFile.getSourceUrl().isEmpty() || !LinkedFile.isOnlineLink(linkedFile.getSourceUrl())) { + throw new UnsupportedOperationException("In order to download the file, the source url has to be an online link"); } - public boolean checkSSLHandshake(URLDownload urlDownload) { - try { - urlDownload.canBeReached(); - } catch (kong.unirest.UnirestException ex) { - if (ex.getCause() instanceof SSLHandshakeException) { - if (dialogService.showConfirmationDialogAndWait(Localization.lang("Download file"), - Localization.lang("Unable to find valid certification path to requested target(%0), download anyway?", - urlDownload.getSource().toString()))) { - return true; - } else { - dialogService.notify(Localization.lang("Download operation canceled.")); - return false; - } - } else { - LOGGER.error("Error while checking if the file can be downloaded", ex); - dialogService.notify(Localization.lang("Error downloading")); - return false; - } - } - return true; - } + String fileName = Path.of(linkedFile.getLink()).getFileName().toString(); - public BackgroundTask prepareDownloadTask(Path targetDirectory, URLDownload urlDownload) { - SSLSocketFactory defaultSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory(); - HostnameVerifier defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); - return BackgroundTask - .wrap(() -> { - Optional suggestedType = inferFileType(urlDownload); - ExternalFileType externalFileType = suggestedType.orElse(StandardExternalFileType.PDF); - - String suggestedName = linkedFileHandler.getSuggestedFileName(externalFileType.getExtension()); - String fulltextDir = FileUtil.createDirNameFromPattern(databaseContext.getDatabase(), entry, preferencesService.getFilePreferences().getFileDirectoryPattern()); - suggestedName = FileNameUniqueness.getNonOverWritingFileName(targetDirectory.resolve(fulltextDir), suggestedName); - return targetDirectory.resolve(fulltextDir).resolve(suggestedName); - }) - .then(destination -> new FileDownloadTask(urlDownload.getSource(), destination)) - .onFailure(ex -> LOGGER.error("Error in download", ex)) - .onFinished(() -> URLDownload.setSSLVerification(defaultSSLSocketFactory, defaultHostnameVerifier)); - } - - private Optional inferFileType(URLDownload urlDownload) { - Optional suggestedType = inferFileTypeFromMimeType(urlDownload); - - // If we did not find a file type from the MIME type, try based on extension: - if (suggestedType.isEmpty()) { - suggestedType = inferFileTypeFromURL(urlDownload.getSource().toExternalForm()); - } - return suggestedType; + DownloadLinkedFileAction downloadLinkedFileAction = new DownloadLinkedFileAction( + databaseContext, + entry, + linkedFile, + linkedFile.getSourceUrl(), + dialogService, + preferencesService.getFilePreferences(), + taskExecutor, + fileName); + downloadProgress.bind(downloadLinkedFileAction.downloadProgress()); + downloadLinkedFileAction.execute(); } - private Optional inferFileTypeFromMimeType(URLDownload urlDownload) { - String mimeType = urlDownload.getMimeType(); - - if (mimeType != null) { - LOGGER.debug("MIME Type suggested: " + mimeType); - return ExternalFileTypes.getExternalFileTypeByMimeType(mimeType, preferencesService.getFilePreferences()); - } else { - return Optional.empty(); + public void download() { + LOGGER.info("Downloading file from " + linkedFile.getSourceUrl()); + if (!linkedFile.isOnlineLink()) { + throw new UnsupportedOperationException("In order to download the file it has to be an online link"); } - } - private Optional inferFileTypeFromURL(String url) { - return URLUtil.getSuffix(url, preferencesService.getFilePreferences()) - .flatMap(extension -> ExternalFileTypes.getExternalFileTypeByExt(extension, preferencesService.getFilePreferences())); + DownloadLinkedFileAction downloadLinkedFileAction = new DownloadLinkedFileAction( + databaseContext, + entry, + linkedFile, + linkedFile.getLink(), + dialogService, + preferencesService.getFilePreferences(), + taskExecutor); + downloadProgress.bind(downloadLinkedFileAction.downloadProgress()); + downloadLinkedFileAction.execute(); } public LinkedFile getFile() { diff --git a/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java b/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java index d7d76eb957c..afec9677e52 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java +++ b/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java @@ -323,6 +323,7 @@ private ContextMenu createContextMenuForFile(LinkedFileViewModel linkedFile) { factory.createMenuItem(StandardActions.MOVE_FILE_TO_FOLDER, new ContextAction(StandardActions.MOVE_FILE_TO_FOLDER, linkedFile, preferencesService)), factory.createMenuItem(StandardActions.MOVE_FILE_TO_FOLDER_AND_RENAME, new ContextAction(StandardActions.MOVE_FILE_TO_FOLDER_AND_RENAME, linkedFile, preferencesService)), factory.createMenuItem(StandardActions.COPY_FILE_TO_FOLDER, new CopySingleFileAction(linkedFile.getFile(), dialogService, databaseContext, preferencesService.getFilePreferences())), + factory.createMenuItem(StandardActions.REDOWNLOAD_FILE, new ContextAction(StandardActions.REDOWNLOAD_FILE, linkedFile, preferencesService)), factory.createMenuItem(StandardActions.REMOVE_LINK, new ContextAction(StandardActions.REMOVE_LINK, linkedFile, preferencesService)), factory.createMenuItem(StandardActions.DELETE_FILE, new ContextAction(StandardActions.DELETE_FILE, linkedFile, preferencesService)) ); @@ -354,6 +355,9 @@ public ContextAction(StandardActions command, LinkedFileViewModel linkedFile, Pr case DOWNLOAD_FILE -> Bindings.createBooleanBinding( () -> linkedFile.getFile().isOnlineLink(), linkedFile.getFile().linkProperty(), bibEntry.getValue().map(BibEntry::getFieldsObservable).orElse(null)); + case REDOWNLOAD_FILE -> Bindings.createBooleanBinding( + () -> !linkedFile.getFile().getSourceUrl().isEmpty(), + linkedFile.getFile().sourceUrlProperty(), bibEntry.getValue().map(BibEntry::getFieldsObservable).orElse(null)); case OPEN_FILE, OPEN_FOLDER, RENAME_FILE_TO_NAME, DELETE_FILE -> Bindings.createBooleanBinding( () -> !linkedFile.getFile().isOnlineLink() && linkedFile.getFile().findIn(databaseContext, preferencesService.getFilePreferences()).isPresent(), @@ -369,6 +373,7 @@ public void execute() { case OPEN_FILE -> linkedFile.open(); case OPEN_FOLDER -> linkedFile.openFolder(); case DOWNLOAD_FILE -> linkedFile.download(); + case REDOWNLOAD_FILE -> linkedFile.redownload(); case RENAME_FILE_TO_PATTERN -> linkedFile.renameToSuggestion(); case RENAME_FILE_TO_NAME -> linkedFile.askForNameAndRename(); case MOVE_FILE_TO_FOLDER -> linkedFile.moveToDefaultDirectory(); diff --git a/src/main/java/org/jabref/gui/linkedfile/DownloadLinkedFileAction.java b/src/main/java/org/jabref/gui/linkedfile/DownloadLinkedFileAction.java new file mode 100644 index 00000000000..e5ac2660ce3 --- /dev/null +++ b/src/main/java/org/jabref/gui/linkedfile/DownloadLinkedFileAction.java @@ -0,0 +1,300 @@ +package org.jabref.gui.linkedfile; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Optional; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSocketFactory; + +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; + +import org.jabref.gui.DialogService; +import org.jabref.gui.LibraryTab; +import org.jabref.gui.actions.SimpleCommand; +import org.jabref.gui.externalfiletype.ExternalFileType; +import org.jabref.gui.externalfiletype.ExternalFileTypes; +import org.jabref.gui.externalfiletype.StandardExternalFileType; +import org.jabref.gui.fieldeditors.LinkedFilesEditorViewModel; +import org.jabref.gui.fieldeditors.URLUtil; +import org.jabref.gui.util.BackgroundTask; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.externalfiles.LinkedFileHandler; +import org.jabref.logic.importer.FetcherClientException; +import org.jabref.logic.importer.FetcherServerException; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.net.ProgressInputStream; +import org.jabref.logic.net.URLDownload; +import org.jabref.logic.util.io.FileNameUniqueness; +import org.jabref.logic.util.io.FileUtil; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; +import org.jabref.preferences.FilePreferences; + +import com.tobiasdiez.easybind.EasyBind; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DownloadLinkedFileAction extends SimpleCommand { + + private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); + + private final DialogService dialogService; + private final BibEntry entry; + private final LinkedFile linkedFile; + private final String suggestedName; + private final String downloadUrl; + private final FilePreferences filePreferences; + private final TaskExecutor taskExecutor; + + private final BibDatabaseContext databaseContext; + + private final DoubleProperty downloadProgress = new SimpleDoubleProperty(); + private final LinkedFileHandler linkedFileHandler; + + public DownloadLinkedFileAction(BibDatabaseContext databaseContext, + BibEntry entry, + LinkedFile linkedFile, + String downloadUrl, + DialogService dialogService, + FilePreferences filePreferences, + TaskExecutor taskExecutor, + String suggestedName) { + this.databaseContext = databaseContext; + this.entry = entry; + this.linkedFile = linkedFile; + this.suggestedName = suggestedName; + this.downloadUrl = downloadUrl; + this.dialogService = dialogService; + this.filePreferences = filePreferences; + this.taskExecutor = taskExecutor; + + this.linkedFileHandler = new LinkedFileHandler(linkedFile, entry, databaseContext, filePreferences); + } + + public DownloadLinkedFileAction(BibDatabaseContext databaseContext, + BibEntry entry, + LinkedFile linkedFile, + String downloadUrl, + DialogService dialogService, + FilePreferences filePreferences, + TaskExecutor taskExecutor) { + this(databaseContext, entry, linkedFile, downloadUrl, dialogService, filePreferences, taskExecutor, ""); + } + + @Override + public void execute() { + LOGGER.info("Downloading file from " + downloadUrl); + if (downloadUrl.isEmpty() || !LinkedFile.isOnlineLink(downloadUrl)) { + throw new UnsupportedOperationException("In order to download the file, the url has to be an online link"); + } + + Optional targetDirectory = databaseContext.getFirstExistingFileDir(filePreferences); + if (targetDirectory.isEmpty()) { + dialogService.showErrorDialogAndWait(Localization.lang("Download file"), Localization.lang("File directory is not set or does not exist!")); + return; + } + + try { + URLDownload urlDownload = new URLDownload(downloadUrl); + if (!checkSSLHandshake(urlDownload)) { + return; + } + + doDownload(targetDirectory.get(), urlDownload); + } catch (MalformedURLException exception) { + dialogService.showErrorDialogAndWait(Localization.lang("Invalid URL"), exception); + } + } + + private void doDownload(Path targetDirectory, URLDownload urlDownload) { + BackgroundTask downloadTask = prepareDownloadTask(targetDirectory, urlDownload); + downloadProgress.bind(downloadTask.workDonePercentageProperty()); + + downloadTask.titleProperty().set(Localization.lang("Downloading")); + entry.getCitationKey().ifPresentOrElse( + citationkey -> downloadTask.messageProperty().set(Localization.lang("Fulltext for %0", citationkey)), + () -> downloadTask.messageProperty().set(Localization.lang("Fulltext for a new entry"))); + downloadTask.showToUser(true); + + downloadTask.onFailure(ex -> onFailure(urlDownload, ex)); + downloadTask.onSuccess(destination -> onSuccess(targetDirectory, destination)); + + taskExecutor.execute(downloadTask); + } + + private void onSuccess(Path targetDirectory, Path destination) { + boolean isDuplicate; + boolean isHtml; + try { + isDuplicate = FileNameUniqueness.isDuplicatedFile(targetDirectory, destination.getFileName(), dialogService); + } catch ( + IOException e) { + LOGGER.error("FileNameUniqueness.isDuplicatedFile failed", e); + return; + } + + if (isDuplicate) { + destination = targetDirectory.resolve( + FileNameUniqueness.eraseDuplicateMarks(destination.getFileName())); + + linkedFile.setLink(FileUtil.relativize(destination, + databaseContext.getFileDirectories(filePreferences)).toString()); + + isHtml = linkedFile.getFileType().equals(StandardExternalFileType.URL.getName()); + } else { + // we need to call LinkedFileViewModel#fromFile, because we need to make the path relative to the configured directories + LinkedFile newLinkedFile = LinkedFilesEditorViewModel.fromFile( + destination, + databaseContext.getFileDirectories(filePreferences), + filePreferences); + if (newLinkedFile.getDescription().isEmpty() && !linkedFile.getDescription().isEmpty()) { + newLinkedFile.setDescription((linkedFile.getDescription())); + } + newLinkedFile.setSourceURL(linkedFile.getLink()); + entry.replaceDownloadedFile(linkedFile.getLink(), newLinkedFile); + isHtml = newLinkedFile.getFileType().equals(StandardExternalFileType.URL.getName()); + } + + // Notify in bar when the file type is HTML. + if (isHtml) { + dialogService.notify(Localization.lang("Downloaded website as an HTML file.")); + LOGGER.debug("Downloaded website {} as an HTML file at {}", linkedFile.getLink(), destination); + } + } + + private void onFailure(URLDownload urlDownload, Exception ex) { + LOGGER.error("Error downloading from URL: " + urlDownload, ex); + String fetcherExceptionMessage = ex.getMessage(); + String failedTitle = Localization.lang("Failed to download from URL"); + int statusCode; + if (ex instanceof FetcherClientException clientException) { + statusCode = clientException.getStatusCode(); + if (statusCode == 401) { + dialogService.showInformationDialogAndWait(failedTitle, Localization.lang("401 Unauthorized: Access Denied. You are not authorized to access this resource. Please check your credentials and try again. If you believe you should have access, please contact the administrator for assistance.\nURL: %0 \n %1", urlDownload.getSource(), fetcherExceptionMessage)); + } else if (statusCode == 403) { + dialogService.showInformationDialogAndWait(failedTitle, Localization.lang("403 Forbidden: Access Denied. You do not have permission to access this resource. Please contact the administrator for assistance or try a different action.\nURL: %0 \n %1", urlDownload.getSource(), fetcherExceptionMessage)); + } else if (statusCode == 404) { + dialogService.showInformationDialogAndWait(failedTitle, Localization.lang("404 Not Found Error: The requested resource could not be found. It seems that the file you are trying to download is not available or has been moved. Please verify the URL and try again. If you believe this is an error, please contact the administrator for further assistance.\nURL: %0 \n %1", urlDownload.getSource(), fetcherExceptionMessage)); + } + } else if (ex instanceof FetcherServerException serverException) { + statusCode = serverException.getStatusCode(); + dialogService.showInformationDialogAndWait(failedTitle, + Localization.lang("Error downloading from URL. Cause is likely the server side. HTTP Error %0 \n %1 \nURL: %2 \nPlease try again later or contact the server administrator.", statusCode, fetcherExceptionMessage, urlDownload.getSource())); + } else { + dialogService.showErrorDialogAndWait(failedTitle, Localization.lang("Error message: %0 \nURL: %1 \nPlease check the URL and try again.", fetcherExceptionMessage, urlDownload.getSource())); + } + } + + private boolean checkSSLHandshake(URLDownload urlDownload) { + try { + urlDownload.canBeReached(); + } catch (kong.unirest.UnirestException ex) { + if (ex.getCause() instanceof SSLHandshakeException) { + if (dialogService.showConfirmationDialogAndWait(Localization.lang("Download file"), + Localization.lang("Unable to find valid certification path to requested target(%0), download anyway?", + urlDownload.getSource().toString()))) { + return true; + } else { + dialogService.notify(Localization.lang("Download operation canceled.")); + return false; + } + } else { + LOGGER.error("Error while checking if the file can be downloaded", ex); + dialogService.notify(Localization.lang("Error downloading")); + return false; + } + } + return true; + } + + private BackgroundTask prepareDownloadTask(Path targetDirectory, URLDownload urlDownload) { + SSLSocketFactory defaultSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory(); + HostnameVerifier defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); + return BackgroundTask + .wrap(() -> { + String suggestedName; + if (this.suggestedName.isEmpty()) { + Optional suggestedType = inferFileType(urlDownload); + ExternalFileType externalFileType = suggestedType.orElse(StandardExternalFileType.PDF); + suggestedName = linkedFileHandler.getSuggestedFileName(externalFileType.getExtension()); + } else { + suggestedName = this.suggestedName; + } + String fulltextDir = FileUtil.createDirNameFromPattern(databaseContext.getDatabase(), entry, filePreferences.getFileDirectoryPattern()); + suggestedName = FileNameUniqueness.getNonOverWritingFileName(targetDirectory.resolve(fulltextDir), suggestedName); + + return targetDirectory.resolve(fulltextDir).resolve(suggestedName); + }) + .then(destination -> new FileDownloadTask(urlDownload.getSource(), destination)) + .onFailure(ex -> LOGGER.error("Error in download", ex)) + .onFinished(() -> { + URLDownload.setSSLVerification(defaultSSLSocketFactory, defaultHostnameVerifier); + downloadProgress.unbind(); + downloadProgress.set(1); + }); + } + + private Optional inferFileType(URLDownload urlDownload) { + Optional suggestedType = inferFileTypeFromMimeType(urlDownload); + + // If we did not find a file type from the MIME type, try based on extension: + if (suggestedType.isEmpty()) { + suggestedType = inferFileTypeFromURL(urlDownload.getSource().toExternalForm()); + } + return suggestedType; + } + + private Optional inferFileTypeFromMimeType(URLDownload urlDownload) { + String mimeType = urlDownload.getMimeType(); + + if (mimeType != null) { + LOGGER.debug("MIME Type suggested: " + mimeType); + return ExternalFileTypes.getExternalFileTypeByMimeType(mimeType, filePreferences); + } else { + return Optional.empty(); + } + } + + private Optional inferFileTypeFromURL(String url) { + return URLUtil.getSuffix(url, filePreferences) + .flatMap(extension -> ExternalFileTypes.getExternalFileTypeByExt(extension, filePreferences)); + } + + public ReadOnlyDoubleProperty downloadProgress() { + return downloadProgress; + } + + private static class FileDownloadTask extends BackgroundTask { + private final URL source; + private final Path destination; + + public FileDownloadTask(URL source, Path destination) { + this.source = source; + this.destination = destination; + } + + @Override + protected Path call() throws Exception { + URLDownload download = new URLDownload(source); + try (ProgressInputStream inputStream = download.asInputStream()) { + EasyBind.subscribe( + inputStream.totalNumBytesReadProperty(), + bytesRead -> updateProgress(bytesRead.longValue(), inputStream.getMaxNumBytes())); + // Make sure directory exists since otherwise copy fails + Files.createDirectories(destination.getParent()); + Files.copy(inputStream, destination, StandardCopyOption.REPLACE_EXISTING); + } + return destination; + } + } +} diff --git a/src/main/java/org/jabref/gui/linkedfile/LinkedFileEditDialog.fxml b/src/main/java/org/jabref/gui/linkedfile/LinkedFileEditDialog.fxml index 364f3da9214..d644991f741 100644 --- a/src/main/java/org/jabref/gui/linkedfile/LinkedFileEditDialog.fxml +++ b/src/main/java/org/jabref/gui/linkedfile/LinkedFileEditDialog.fxml @@ -47,6 +47,10 @@