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

Find Unlinked files should ignore Thumbs.db, etc #8800

Merged
merged 29 commits into from
Jun 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3f85f67
added ui component to ignore unlinked files
prashant1712 May 2, 2022
05ae9ef
ignore the files in ".gitignore" when searching for unlinked local files
zhaoqingying123 May 11, 2022
849bb4f
ignore the name of files and the extention in ".gitignore" when searc…
zhaoqingying123 May 11, 2022
c654ca5
Merge branch 'Ziheng' of https://github.com/prashant1712/jabref into …
prashant1712 May 12, 2022
c0926a5
linked ui to the logic & refactored the code
prashant1712 May 12, 2022
595e203
resolved conflicts
prashant1712 May 14, 2022
4be3ce4
adding localization key
prashant1712 May 14, 2022
8f3c80e
Merge branch 'unlinkedFiles#373' of https://github.com/prashant1712/j…
prashant1712 May 14, 2022
165c4a7
Merge branch 'JabRef:main' into unlinkedFiles#373
prashant1712 May 15, 2022
713b954
changelog updated
prashant1712 May 15, 2022
c6cb911
Merge branch 'unlinkedFiles#373' of https://github.com/prashant1712/j…
prashant1712 May 15, 2022
77cf185
Update .gitignore
calixtus May 16, 2022
65c1de5
changes after review
prashant1712 May 17, 2022
3fe6eab
Merge branch 'unlinkedFiles#373' of https://github.com/prashant1712/j…
prashant1712 May 17, 2022
68bdca8
Update CHANGELOG.md
prashant1712 May 17, 2022
0d88c52
Merge branch 'JabRef:main' into unlinkedFiles#373
prashant1712 May 17, 2022
79be853
remove any modification from JabRef_Main.xml
prashant1712 May 17, 2022
a10916e
Rrefactor and move
Siedlerchr May 19, 2022
2dae7de
checkstyle
Siedlerchr May 19, 2022
663f1b4
Update .gitignore
calixtus May 19, 2022
dfefd35
changing to read .gitignore from source directory of JabRef
prashant1712 May 21, 2022
a78b739
using Path.of instead Paths
prashant1712 May 21, 2022
81e8d9d
adding test for filterForUnlinkedFiles
prashant1712 May 22, 2022
1db7b6e
Merge remote-tracking branch 'origin/main' into unlinkedFiles#373
koppor Jun 20, 2022
0a066ea
Rewrote .gitignore logic
koppor Jun 20, 2022
463d3d8
Merge remote-tracking branch 'upstream/main' into unlinkedFiles#373
Siedlerchr Jun 27, 2022
27d021f
Fix tests by adding hashcode and equals
Siedlerchr Jun 27, 2022
9ede81d
Update FileNodeViewModel.java
koppor Jun 27, 2022
17ef36f
Fix indent
koppor Jun 27, 2022
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve

- We added a fetcher for [Biodiversity Heritage Library](https://www.biodiversitylibrary.org/). [8539](https://github.com/JabRef/jabref/issues/8539)
- We added support for multiple messages in the snackbar. [#7340](https://github.com/JabRef/jabref/issues/7340)
- We added an extra option in the 'Find Unlinked Files' dialog view to ignore unnecessary files like Thumbs.db, DS_Store, etc. [koppor#373](https://github.com/koppor/jabref/issues/373)
- JabRef now writes log files. Linux: `$home/.cache/jabref/logs/version`, Windows: `%APPDATA%\..\Local\harawata\jabref\version\logs`, Mac: `Users/.../Library/Logs/jabref/version`
- We added an importer for Citavi backup files, support ".ctv5bak" and ".ctv6bak" file formats. [#8322](https://github.com/JabRef/jabref/issues/8322)

Expand Down
35 changes: 35 additions & 0 deletions src/main/java/org/jabref/gui/externalfiles/ChainedFilters.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.jabref.gui.externalfiles;

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Path;
import java.util.Arrays;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Chains the given filters - if ALL of them accept, the result is also accepted
*/
public class ChainedFilters implements DirectoryStream.Filter<Path> {

private static final Logger LOGGER = LoggerFactory.getLogger(ChainedFilters.class);

private DirectoryStream.Filter<Path>[] filters;

public ChainedFilters(DirectoryStream.Filter<Path>... filters) {
this.filters = filters;
}

@Override
public boolean accept(Path entry) throws IOException {
return Arrays.stream(filters).allMatch(filter -> {
try {
return filter.accept(entry);
} catch (IOException e) {
LOGGER.error("Could not apply filter", e);
return true;
}
});
}
}
30 changes: 18 additions & 12 deletions src/main/java/org/jabref/gui/externalfiles/FileFilterUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
public class FileFilterUtils {

private static final Logger LOGGER = LoggerFactory.getLogger(FileFilterUtils.class);
calixtus marked this conversation as resolved.
Show resolved Hide resolved

/* Returns the last edited time of a file as LocalDateTime. */
public static LocalDateTime getFileTime(Path path) {
FileTime lastEditedTime = null;
FileTime lastEditedTime;
try {
lastEditedTime = Files.getLastModifiedTime(path);
} catch (IOException e) {
Expand All @@ -33,28 +33,28 @@ public static LocalDateTime getFileTime(Path path) {
return localDateTime;
}

/* Returns true if a file with a specific path
/* Returns true if a file with a specific path
* was edited during the last 24 hours. */
public boolean isDuringLastDay(LocalDateTime fileEditTime) {
LocalDateTime NOW = LocalDateTime.now(ZoneId.systemDefault());
return fileEditTime.isAfter(NOW.minusHours(24));
}

/* Returns true if a file with a specific path
/* Returns true if a file with a specific path
* was edited during the last 7 days. */
public boolean isDuringLastWeek(LocalDateTime fileEditTime) {
LocalDateTime NOW = LocalDateTime.now(ZoneId.systemDefault());
return fileEditTime.isAfter(NOW.minusDays(7));
}

/* Returns true if a file with a specific path
/* Returns true if a file with a specific path
* was edited during the last 30 days. */
public boolean isDuringLastMonth(LocalDateTime fileEditTime) {
LocalDateTime NOW = LocalDateTime.now(ZoneId.systemDefault());
return fileEditTime.isAfter(NOW.minusDays(30));
}

/* Returns true if a file with a specific path
/* Returns true if a file with a specific path
* was edited during the last 365 days. */
public boolean isDuringLastYear(LocalDateTime fileEditTime) {
LocalDateTime NOW = LocalDateTime.now(ZoneId.systemDefault());
Expand All @@ -75,8 +75,10 @@ public static boolean filterByDate(Path path, DateRange filter) {
return isInDateRange;
}

/* Sorts a list of Path objects according to the last edited date
* of their corresponding files, from newest to oldest. */
/**
* Sorts a list of Path objects according to the last edited date
* of their corresponding files, from newest to oldest.
*/
public List<Path> sortByDateAscending(List<Path> files) {
return files.stream()
.sorted(Comparator.comparingLong(file -> FileFilterUtils.getFileTime(file)
Expand All @@ -86,8 +88,10 @@ public List<Path> sortByDateAscending(List<Path> files) {
.collect(Collectors.toList());
}

/* Sorts a list of Path objects according to the last edited date
* of their corresponding files, from oldest to newest. */
/**
* Sorts a list of Path objects according to the last edited date
* of their corresponding files, from oldest to newest.
*/
public List<Path> sortByDateDescending(List<Path> files) {
return files.stream()
.sorted(Comparator.comparingLong(file -> -FileFilterUtils.getFileTime(file)
Expand All @@ -97,8 +101,10 @@ public List<Path> sortByDateDescending(List<Path> files) {
.collect(Collectors.toList());
}

/* Sorts a list of Path objects according to the last edited date
* the order depends on the specified sorter type. */
/**
* Sorts a list of Path objects according to the last edited date
* the order depends on the specified sorter type.
*/
public static List<Path> sortByDate(List<Path> files, ExternalFileSorter sortType) {
FileFilterUtils fileFilter = new FileFilterUtils();
List<Path> sortedFiles = switch (sortType) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package org.jabref.gui.externalfiles;

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static java.util.function.Predicate.not;

public class GitIgnoreFileFilter implements DirectoryStream.Filter<Path> {

private static final Logger LOGGER = LoggerFactory.getLogger(GitIgnoreFileFilter.class);

private Set<PathMatcher> gitIgnorePatterns;

public GitIgnoreFileFilter(Path path) {
Path currentPath = path;
while ((currentPath != null) && !Files.exists(currentPath.resolve(".gitignore"))) {
currentPath = currentPath.getParent();
}
if (currentPath == null) {
// we did not find any gitignore, lets use the default
gitIgnorePatterns = Set.of(".git", ".DS_Store", "desktop.ini", "Thumbs.db").stream()
// duplicate code as below
.map(line -> "glob:" + line)
.map(matcherString -> FileSystems.getDefault().getPathMatcher(matcherString))
.collect(Collectors.toSet());
} else {
Path gitIgnore = currentPath.resolve(".gitignore");
try {
Set<PathMatcher> plainGitIgnorePatternsFromGitIgnoreFile = Files.readAllLines(gitIgnore).stream()
.map(line -> line.trim())
.filter(not(String::isEmpty))
.filter(line -> !line.startsWith("#"))
// convert to Java syntax for Glob patterns
.map(line -> "glob:" + line)
.map(matcherString -> FileSystems.getDefault().getPathMatcher(matcherString))
.collect(Collectors.toSet());
gitIgnorePatterns = new HashSet<>(plainGitIgnorePatternsFromGitIgnoreFile);
// we want to ignore ".gitignore" itself
gitIgnorePatterns.add(FileSystems.getDefault().getPathMatcher("glob:.gitignore"));
} catch (IOException e) {
LOGGER.info("Could not read .gitignore from {}", gitIgnore, e);
gitIgnorePatterns = Set.of();
}
}
}

@Override
public boolean accept(Path path) throws IOException {
// We assume that git does not stop at a patern, but tries all. We implement that behavior
return gitIgnorePatterns.stream().noneMatch(filter ->
// we need this one for "*.png"
filter.matches(path.getFileName()) ||
// we need this one for "**/*.png"
filter.matches(path));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,52 +66,76 @@ protected FileNodeViewModel call() throws IOException {
* 'state' must be set to 1, to keep the recursion running. When the states value changes, the method will resolve
* its recursion and return what it has saved so far.
* <br>
* The files are filtered according to the {@link DateRange} filter value
* The files are filtered according to the {@link DateRange} filter value
* and then sorted according to the {@link ExternalFileSorter} value.
*
* @param unlinkedPDFFileFilter contains a BibDatabaseContext which is used to determine whether the file is linked
*
* @return FileNodeViewModel containing the data of the current directory and all subdirectories
* @throws IOException if directory is not a directory or empty
*/
private FileNodeViewModel searchDirectory(Path directory, UnlinkedPDFFileFilter fileFilter) throws IOException {
FileNodeViewModel searchDirectory(Path directory, UnlinkedPDFFileFilter unlinkedPDFFileFilter) throws IOException {
// Return null if the directory is not valid.
if ((directory == null) || !Files.isDirectory(directory)) {
throw new IOException(String.format("Invalid directory for searching: %s", directory));
}

FileNodeViewModel parent = new FileNodeViewModel(directory);
Map<Boolean, List<Path>> fileListPartition;
FileNodeViewModel fileNodeViewModelForCurrentDirectory = new FileNodeViewModel(directory);

try (Stream<Path> filesStream = StreamSupport.stream(Files.newDirectoryStream(directory, fileFilter).spliterator(), false)) {
fileListPartition = filesStream.collect(Collectors.partitioningBy(Files::isDirectory));
// Map from isDirectory (true/false) to full path
// Result: Contains only files not matching the filter (i.e., PDFs not linked and files not ignored)
// Filters:
// 1. UnlinkedPDFFileFilter
// 2. GitIgnoreFilter
ChainedFilters filters = new ChainedFilters(unlinkedPDFFileFilter, new GitIgnoreFileFilter(directory));
Map<Boolean, List<Path>> directoryAndFilePartition;
try (Stream<Path> filesStream = StreamSupport.stream(Files.newDirectoryStream(directory, filters).spliterator(), false)) {
directoryAndFilePartition = filesStream.collect(Collectors.partitioningBy(Files::isDirectory));
} catch (IOException e) {
LOGGER.error(String.format("%s while searching files: %s", e.getClass().getName(), e.getMessage()));
return parent;
LOGGER.error("Error while searching files", e);
return fileNodeViewModelForCurrentDirectory;
}
List<Path> subDirectories = directoryAndFilePartition.get(true);
List<Path> files = directoryAndFilePartition.get(false);

List<Path> subDirectories = fileListPartition.get(true);
List<Path> files = new ArrayList<>(fileListPartition.get(false));
int fileCount = 0;
// at this point, only unlinked PDFs AND unignored files are contained

for (Path subDirectory : subDirectories) {
FileNodeViewModel subRoot = searchDirectory(subDirectory, fileFilter);
// initially, we find no files at all
int fileCountOfSubdirectories = 0;

// now we crawl into the found subdirectories first (!)
for (Path subDirectory : subDirectories) {
FileNodeViewModel subRoot = searchDirectory(subDirectory, unlinkedPDFFileFilter);
if (!subRoot.getChildren().isEmpty()) {
fileCount += subRoot.getFileCount();
parent.getChildren().add(subRoot);
fileCountOfSubdirectories += subRoot.getFileCount();
fileNodeViewModelForCurrentDirectory.getChildren().add(subRoot);
}
}
// now we have the data of all subdirectories
// it is stored in fileNodeViewModelForCurrentDirectory.getChildren()

// now we handle the files in the current directory

// filter files according to last edited date.
List<Path> filteredFiles = new ArrayList<Path>();
// Note that we do not use the "StreamSupport.stream" filtering functionality, because refactoring the code to that would lead to more code
List<Path> resultingFiles = new ArrayList<>();
for (Path path : files) {
if (FileFilterUtils.filterByDate(path, dateFilter)) {
filteredFiles.add(path);
resultingFiles.add(path);
}
}

// sort files according to last edited date.
filteredFiles = FileFilterUtils.sortByDate(filteredFiles, sorter);
parent.setFileCount(filteredFiles.size() + fileCount);
parent.getChildren().addAll(filteredFiles.stream()
resultingFiles = FileFilterUtils.sortByDate(resultingFiles, sorter);

// the count of all files is the count of the found files in current directory plus the count of all files in the subdirectories
fileNodeViewModelForCurrentDirectory.setFileCount(resultingFiles.size() + fileCountOfSubdirectories);

// create and add FileNodeViewModel to the FileNodeViewModel for the current directory
fileNodeViewModelForCurrentDirectory.getChildren().addAll(resultingFiles.stream()
.map(FileNodeViewModel::new)
.collect(Collectors.toList()));
return parent;

return fileNodeViewModelForCurrentDirectory;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public UnlinkedPDFFileFilter(DirectoryStream.Filter<Path> fileFilter, BibDatabas

@Override
public boolean accept(Path pathname) throws IOException {

if (Files.isDirectory(pathname)) {
return true;
} else {
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/org/jabref/gui/util/FileNodeViewModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Objects;

import javafx.beans.property.ReadOnlyListWrapper;
import javafx.collections.FXCollections;
Expand Down Expand Up @@ -94,4 +95,21 @@ public String toString() {
this.children,
this.fileCount);
}

@Override
public int hashCode() {
return Objects.hash(children, fileCount, path);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof FileNodeViewModel)) {
return false;
}
FileNodeViewModel other = (FileNodeViewModel) obj;
return Objects.equals(children, other.children) && (fileCount == other.fileCount) && Objects.equals(path, other.path);
}
}
Loading