2026-02-03 09:59:03 +01:00

1237 lines
59 KiB
Java

package cz.kamma.kfmanager.service;
import cz.kamma.kfmanager.model.FileItem;
import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.zip.*;
/**
* Service for file operations - copy, move, delete, etc.
*/
public class FileOperations {
/**
* Copy files/directories to target directory
*/
public static void copy(List<FileItem> items, File targetDirectory, ProgressCallback callback) throws IOException {
if (targetDirectory == null || !targetDirectory.isDirectory()) {
throw new IOException("Target directory does not exist");
}
List<FileItem> cleanedItems = cleanDuplicateItems(items);
// Sort items so symlinks are last
cleanedItems.sort((a, b) -> {
boolean aSym = Files.isSymbolicLink(a.getFile().toPath());
boolean bSym = Files.isSymbolicLink(b.getFile().toPath());
if (aSym && !bSym) return 1;
if (!aSym && bSym) return -1;
return 0;
});
long totalItems = calculateTotalItems(cleanedItems);
final long[] currentItem = {0};
final OverwriteResponse[] globalResponse = {null};
final SymlinkResponse[] globalSymlinkResponse = {null};
for (FileItem item : cleanedItems) {
if (callback != null && callback.isCancelled()) break;
File source = item.getFile();
File target = new File(targetDirectory, source.getName());
// If target is redicrected to subfolder of source, skip to avoid infinite loop
if (isSubfolder(source, targetDirectory)) {
continue;
}
// If target is the same as source (copying to the same directory), rename target
if (source.getAbsolutePath().equals(target.getAbsolutePath())) {
target = new File(targetDirectory, "copy-of-" + source.getName());
} else if (target.exists()) {
if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) continue;
if (globalResponse[0] != OverwriteResponse.YES_TO_ALL) {
OverwriteResponse res = callback.confirmOverwrite(source, target);
if (res == OverwriteResponse.CANCEL) break;
if (res == OverwriteResponse.NO_TO_ALL) {
globalResponse[0] = OverwriteResponse.NO_TO_ALL;
continue;
}
if (res == OverwriteResponse.NO) continue;
if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL;
}
// Handle directory vs file clash
if (target.isDirectory() && !source.isDirectory()) {
deleteDirectoryInternal(target.toPath());
} else if (!target.isDirectory() && source.isDirectory()) {
Files.delete(target.toPath());
}
}
while (true) {
try {
Path sourcePath = source.toPath();
if (Files.isSymbolicLink(sourcePath)) {
SymlinkResponse sRes = getSymlinkResponse(source, callback, globalSymlinkResponse);
if (sRes == SymlinkResponse.CANCEL) {
return;
}
if (sRes == SymlinkResponse.IGNORE || sRes == SymlinkResponse.IGNORE_ALL) {
currentItem[0] += countItems(sourcePath);
break;
}
// If FOLLOW, sRes will be FOLLOW or FOLLOW_ALL
if (Files.exists(sourcePath)) {
if (Files.isDirectory(sourcePath)) {
copyDirectory(sourcePath, target.toPath(), totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, null);
// Count the symlink itself as processed to match countItems prediction
currentItem[0]++;
} else {
copyFileWithProgress(sourcePath, target.toPath(), totalItems, currentItem, callback);
}
} else {
// Link is broken, follow is impossible, copy as link
copySymlink(sourcePath, target.toPath(), totalItems, currentItem, callback);
}
break;
} else if (source.isDirectory()) {
copyDirectory(sourcePath, target.toPath(), totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, null);
// Directory itself counted in countItems but copyDirectory skips increment for root.
// So we increment here.
currentItem[0]++;
} else {
copyFileWithProgress(sourcePath, target.toPath(), totalItems, currentItem, callback);
}
break;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(source, e);
if (res == ErrorResponse.ABORT) throw e;
if (res == ErrorResponse.RETRY) continue;
break;
} else {
throw e;
}
}
}
}
}
private static SymlinkResponse getSymlinkResponse(File source, ProgressCallback callback, SymlinkResponse[] globalSymlinkResponse) {
if (globalSymlinkResponse[0] != null) return globalSymlinkResponse[0];
if (callback == null) return SymlinkResponse.IGNORE;
SymlinkResponse res = callback.confirmSymlink(source);
if (res == SymlinkResponse.FOLLOW_ALL) globalSymlinkResponse[0] = SymlinkResponse.FOLLOW_ALL;
if (res == SymlinkResponse.IGNORE_ALL) globalSymlinkResponse[0] = SymlinkResponse.IGNORE_ALL;
return res;
}
private static long calculateTotalItems(List<FileItem> items) {
long total = 0;
for (FileItem item : items) {
total += countItems(item.getFile().toPath());
}
return total;
}
private static long countItems(Path path) {
if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) return 0;
if (Files.isSymbolicLink(path)) return 1;
if (!Files.isDirectory(path)) return 1;
final long[] count = {1}; // Start with 1 for the directory itself
try {
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (!dir.equals(path)) count[0]++;
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
count[0]++;
return FileVisitResult.CONTINUE;
}
});
} catch (IOException ignore) {}
return count[0];
}
private static void copyFileWithProgress(Path source, Path target, long totalItems, long[] currentItem, ProgressCallback callback) throws IOException {
long fileSize = Files.size(source);
long bytesCopied = 0;
try (InputStream in = Files.newInputStream(source);
OutputStream out = Files.newOutputStream(target)) {
byte[] buffer = new byte[24576];
int length;
while ((length = in.read(buffer)) > 0) {
if (callback != null && callback.isCancelled()) return;
out.write(buffer, 0, length);
bytesCopied += length;
if (callback != null) {
callback.onFileProgress(bytesCopied, fileSize);
}
}
}
currentItem[0]++;
if (callback != null) {
callback.onProgress(currentItem[0], totalItems, source.getFileName().toString());
callback.onFileProgress(fileSize, fileSize);
}
try {
Files.setLastModifiedTime(target, Files.getLastModifiedTime(source));
try {
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(source);
Files.setPosixFilePermissions(target, permissions);
} catch (UnsupportedOperationException ignore) {
// Not a POSIX filesystem
}
} catch (IOException e) {
// Ignore failure to set time or permissions on some filesystems
}
}
private static void copySymlink(Path source, Path target, long totalItems, long[] currentItem, ProgressCallback callback) throws IOException {
Path linkTarget = Files.readSymbolicLink(source);
Files.deleteIfExists(target);
Files.createSymbolicLink(target, linkTarget);
currentItem[0]++;
if (callback != null) {
callback.onProgress(currentItem[0], totalItems, source.getFileName().toString());
}
}
private static void copyDirectory(Path source, Path target, long totalItems, final long[] currentItem, ProgressCallback callback, final OverwriteResponse[] globalResponse, final SymlinkResponse[] globalSymlinkResponse, Set<Path> visitedPaths) throws IOException {
final Path effectiveSource = Files.isSymbolicLink(source) ? source.toRealPath() : source.toAbsolutePath().normalize();
final Path finalTarget = target.toAbsolutePath().normalize();
if (visitedPaths == null) visitedPaths = new HashSet<>();
if (!visitedPaths.add(effectiveSource)) {
// Cycle detected, skip this directory
return;
}
try {
final Set<Path> finalVisitedPaths = visitedPaths;
Files.walkFileTree(effectiveSource, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path targetDir = finalTarget.resolve(effectiveSource.relativize(dir.toAbsolutePath().normalize()));
if (Files.exists(targetDir, LinkOption.NOFOLLOW_LINKS) && !Files.isDirectory(targetDir, LinkOption.NOFOLLOW_LINKS)) {
Files.delete(targetDir);
}
while (true) {
try {
Files.createDirectories(targetDir);
try {
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(dir);
Files.setPosixFilePermissions(targetDir, permissions);
} catch (UnsupportedOperationException ignore) {
} catch (IOException e) {
// Ignore permission set failures for directories too
}
if (!dir.toAbsolutePath().normalize().equals(effectiveSource)) {
currentItem[0]++;
if (callback != null) {
callback.onProgress(currentItem[0], totalItems, dir.getFileName().toString());
}
}
return FileVisitResult.CONTINUE;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(dir.toFile(), e);
if (res == ErrorResponse.ABORT) return FileVisitResult.TERMINATE;
if (res == ErrorResponse.RETRY) continue;
return FileVisitResult.SKIP_SUBTREE;
}
throw e;
}
}
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (callback != null && callback.isCancelled()) return FileVisitResult.TERMINATE;
Path targetFile = finalTarget.resolve(effectiveSource.relativize(file.toAbsolutePath().normalize()));
if (Files.exists(targetFile, LinkOption.NOFOLLOW_LINKS)) {
if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) return FileVisitResult.CONTINUE;
if (globalResponse[0] != OverwriteResponse.YES_TO_ALL) {
OverwriteResponse res = callback.confirmOverwrite(file.toFile(), targetFile.toFile());
if (res == OverwriteResponse.CANCEL) return FileVisitResult.TERMINATE;
if (res == OverwriteResponse.NO_TO_ALL) {
globalResponse[0] = OverwriteResponse.NO_TO_ALL;
return FileVisitResult.CONTINUE;
}
if (res == OverwriteResponse.NO) return FileVisitResult.CONTINUE;
if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL;
}
// Handle directory vs file clash
if (Files.isDirectory(targetFile, LinkOption.NOFOLLOW_LINKS)) {
deleteDirectoryInternal(targetFile);
} else {
Files.delete(targetFile);
}
}
while (true) {
try {
if (attrs.isSymbolicLink()) {
SymlinkResponse sRes = getSymlinkResponse(file.toFile(), callback, globalSymlinkResponse);
if (sRes == SymlinkResponse.CANCEL) return FileVisitResult.TERMINATE;
if (sRes == SymlinkResponse.IGNORE || sRes == SymlinkResponse.IGNORE_ALL) {
currentItem[0]++;
return FileVisitResult.CONTINUE;
}
// FOLLOW or FOLLOW_ALL
if (Files.exists(file)) {
if (Files.isDirectory(file)) {
copyDirectory(file, targetFile, totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, finalVisitedPaths);
// Count the symlink itself as processed to match countItems prediction
currentItem[0]++;
} else {
copyFileWithProgress(file, targetFile, totalItems, currentItem, callback);
}
} else {
// Link is broken, follow is impossible, copy as link
copySymlink(file, targetFile, totalItems, currentItem, callback);
}
} else {
copyFileWithProgress(file, targetFile, totalItems, currentItem, callback);
}
return FileVisitResult.CONTINUE;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(file.toFile(), e);
if (res == ErrorResponse.ABORT) return FileVisitResult.TERMINATE;
if (res == ErrorResponse.RETRY) continue;
return FileVisitResult.CONTINUE;
}
throw e;
}
}
}
});
} finally {
visitedPaths.remove(effectiveSource);
}
}
private static void deleteDirectoryInternal(Path path) throws IOException {
if (Files.isSymbolicLink(path)) {
Files.delete(path);
return;
}
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
/**
* Move files/directories to target directory
*/
public static void move(List<FileItem> items, File targetDirectory, ProgressCallback callback) throws IOException {
if (targetDirectory == null || !targetDirectory.isDirectory()) {
throw new IOException("Target directory does not exist");
}
List<FileItem> cleanedItems = cleanDuplicateItems(items);
// Sort items so symlinks are last
cleanedItems.sort((a, b) -> {
boolean aSym = Files.isSymbolicLink(a.getFile().toPath());
boolean bSym = Files.isSymbolicLink(b.getFile().toPath());
if (aSym && !bSym) return 1;
if (!aSym && bSym) return -1;
return 0;
});
long totalItems = calculateTotalItems(cleanedItems);
long[] currentItem = {0};
final OverwriteResponse[] globalResponse = {null};
final SymlinkResponse[] globalSymlinkResponse = {null};
for (FileItem item : cleanedItems) {
if (callback != null && callback.isCancelled()) break;
File source = item.getFile();
File target = new File(targetDirectory, source.getName());
if (isSubfolder(source, targetDirectory)) {
continue;
}
// Use NOFOLLOW_LINKS for target existence check
if (Files.exists(target.toPath(), LinkOption.NOFOLLOW_LINKS) && !source.getAbsolutePath().equals(target.getAbsolutePath())) {
if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) continue;
if (globalResponse[0] != OverwriteResponse.YES_TO_ALL) {
OverwriteResponse res = callback.confirmOverwrite(source, target);
if (res == OverwriteResponse.CANCEL) break;
if (res == OverwriteResponse.NO_TO_ALL) {
globalResponse[0] = OverwriteResponse.NO_TO_ALL;
continue;
}
if (res == OverwriteResponse.NO) continue;
if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL;
}
// Handle directory vs file clash
if (target.isDirectory() && !source.isDirectory()) {
deleteDirectoryInternal(target.toPath());
} else if (!target.isDirectory() && source.isDirectory()) {
Files.delete(target.toPath());
}
}
long itemCount = countItems(source.toPath());
while (true) {
try {
Path sourcePath = source.toPath();
if (Files.isSymbolicLink(sourcePath)) {
SymlinkResponse sRes = getSymlinkResponse(source, callback, globalSymlinkResponse);
if (sRes == SymlinkResponse.CANCEL) return;
if (sRes == SymlinkResponse.IGNORE || sRes == SymlinkResponse.IGNORE_ALL) {
currentItem[0] += itemCount;
break;
}
if (sRes == SymlinkResponse.FOLLOW || sRes == SymlinkResponse.FOLLOW_ALL) {
if (Files.exists(sourcePath)) {
if (Files.isDirectory(sourcePath)) {
copyDirectory(sourcePath, target.toPath(), totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, null);
currentItem[0]++;
} else {
copyFileWithProgress(sourcePath, target.toPath(), totalItems, currentItem, callback);
}
} else {
// Link is broken, follow is impossible, copy as link
copySymlink(sourcePath, target.toPath(), totalItems, currentItem, callback);
}
Files.delete(sourcePath);
break;
}
}
Files.move(sourcePath, target.toPath(), StandardCopyOption.REPLACE_EXISTING);
currentItem[0] += itemCount;
if (callback != null) {
callback.onProgress(currentItem[0], totalItems, source.getName());
}
break;
} catch (IOException e) {
// Fallback for cross-device moves (e.g., to network/Samba mounts)
try {
if (source.isDirectory()) {
copyDirectory(source.toPath(), target.toPath(), totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, null);
currentItem[0]++; // For the directory itself which copyDirectory skips
deleteDirectoryInternal(source.toPath());
} else {
copyFileWithProgress(source.toPath(), target.toPath(), totalItems, currentItem, callback);
Files.delete(source.toPath());
}
break;
} catch (IOException ex) {
if (callback != null) {
ErrorResponse res = callback.onError(source, ex);
if (res == ErrorResponse.ABORT) throw ex;
if (res == ErrorResponse.RETRY) continue;
break;
} else {
throw ex;
}
}
}
}
}
}
/**
* Delete files/directories
*/
public static void delete(List<FileItem> items, ProgressCallback callback) throws IOException {
List<FileItem> cleanedItems = cleanDuplicateItems(items);
long totalItems = calculateTotalItems(cleanedItems);
long[] currentItem = {0};
for (FileItem item : cleanedItems) {
if (callback != null && callback.isCancelled()) break;
File file = item.getFile();
while (true) {
try {
if (Files.isSymbolicLink(file.toPath())) {
currentItem[0]++;
if (callback != null) {
callback.onProgress(currentItem[0], totalItems, file.getName());
}
Files.delete(file.toPath());
} else if (file.isDirectory()) {
deleteDirectory(file.toPath(), totalItems, currentItem, callback);
} else {
currentItem[0]++;
if (callback != null) {
callback.onProgress(currentItem[0], totalItems, file.getName());
}
Files.delete(file.toPath());
}
break;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(file, e);
if (res == ErrorResponse.ABORT) throw e;
if (res == ErrorResponse.RETRY) continue;
break;
} else {
throw e;
}
}
}
}
}
private static List<FileItem> cleanDuplicateItems(List<FileItem> items) {
if (items == null || items.size() <= 1) return items;
List<FileItem> sorted = new ArrayList<>(items);
sorted.sort((a, b) -> a.getFile().getAbsolutePath().compareTo(b.getFile().getAbsolutePath()));
List<FileItem> cleaned = new ArrayList<>();
for (int i = 0; i < sorted.size(); i++) {
FileItem current = sorted.get(i);
boolean isDuplicate = false;
for (int j = 0; j < i; j++) {
File parent = sorted.get(j).getFile();
if (isSubfolder(parent, current.getFile())) {
isDuplicate = true;
break;
}
}
if (!isDuplicate) {
cleaned.add(current);
}
}
return cleaned;
}
private static boolean isSubfolder(File parent, File child) {
if (parent == null || child == null) return false;
String parentPath = parent.getAbsolutePath();
String childPath = child.getAbsolutePath();
if (parentPath.equals(childPath)) return true;
if (!parentPath.endsWith(File.separator)) {
parentPath += File.separator;
}
return childPath.startsWith(parentPath);
}
/**
* Rename a file or directory
*/
public static void rename(File file, String newName) throws IOException {
File target = new File(file.getParentFile(), newName);
Files.move(file.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
/**
* Create a new directory
*/
public static void createDirectory(File parentDirectory, String name) throws IOException {
File newDir = new File(parentDirectory, name);
if (!newDir.mkdir()) {
throw new IOException("Failed to create directory");
}
}
/**
* Create a new empty file
*/
public static void createFile(File parentDirectory, String name) throws IOException {
File newFile = new File(parentDirectory, name);
if (!newFile.createNewFile()) {
throw new IOException("Failed to create file (maybe it already exists?)");
}
}
private static void setPermissionsFromMode(Path path, int mode) {
if (mode <= 0 || (cz.kamma.kfmanager.MainApp.CURRENT_OS != cz.kamma.kfmanager.MainApp.OS.LINUX &&
cz.kamma.kfmanager.MainApp.CURRENT_OS != cz.kamma.kfmanager.MainApp.OS.MACOS)) {
return;
}
try {
Set<PosixFilePermission> perms = new HashSet<>();
if ((mode & 0400) != 0) perms.add(PosixFilePermission.OWNER_READ);
if ((mode & 0200) != 0) perms.add(PosixFilePermission.OWNER_WRITE);
if ((mode & 0100) != 0) perms.add(PosixFilePermission.OWNER_EXECUTE);
if ((mode & 0040) != 0) perms.add(PosixFilePermission.GROUP_READ);
if ((mode & 0020) != 0) perms.add(PosixFilePermission.GROUP_WRITE);
if ((mode & 0010) != 0) perms.add(PosixFilePermission.GROUP_EXECUTE);
if ((mode & 0004) != 0) perms.add(PosixFilePermission.OTHERS_READ);
if ((mode & 0002) != 0) perms.add(PosixFilePermission.OTHERS_WRITE);
if ((mode & 0001) != 0) perms.add(PosixFilePermission.OTHERS_EXECUTE);
if (!perms.isEmpty()) {
Files.setPosixFilePermissions(path, perms);
}
} catch (Exception ignore) {}
}
/**
* Delete directory recursively
*/
private static void deleteDirectory(Path directory, long totalItems, long[] currentItem, ProgressCallback callback) throws IOException {
if (Files.isSymbolicLink(directory)) {
currentItem[0]++;
if (callback != null) {
callback.onProgress(currentItem[0], totalItems, directory.getFileName().toString());
}
Files.delete(directory);
return;
}
Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
while (true) {
try {
currentItem[0]++;
if (callback != null) {
callback.onProgress(currentItem[0], totalItems, file.getFileName().toString());
}
Files.delete(file);
return FileVisitResult.CONTINUE;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(file.toFile(), e);
if (res == ErrorResponse.ABORT) return FileVisitResult.TERMINATE;
if (res == ErrorResponse.RETRY) continue;
return FileVisitResult.CONTINUE;
}
throw e;
}
}
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (exc != null) throw exc;
while (true) {
try {
currentItem[0]++;
if (callback != null) {
callback.onProgress(currentItem[0], totalItems, dir.getFileName().toString());
}
Files.delete(dir);
return FileVisitResult.CONTINUE;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(dir.toFile(), e);
if (res == ErrorResponse.ABORT) return FileVisitResult.TERMINATE;
if (res == ErrorResponse.RETRY) continue;
return FileVisitResult.CONTINUE;
}
throw e;
}
}
}
});
}
/**
* Search files by filename pattern and/or content text.
* If both are provided, both must match.
*/
public static void search(File directory, String filenamePattern, String contentText, boolean recursive, boolean searchArchives, SearchCallback callback) throws IOException {
Pattern filenameRegex = null;
String filenameLower = null;
if (filenamePattern != null && !filenamePattern.isEmpty()) {
filenameLower = filenamePattern.toLowerCase();
if (filenamePattern.contains("*") || filenamePattern.contains("?")) {
String regex = filenamePattern
.replace(".", "\\.")
.replace("*", ".*")
.replace("?", ".");
filenameRegex = Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
}
}
Pattern contentPattern = null;
if (contentText != null && !contentText.isEmpty()) {
contentPattern = Pattern.compile(Pattern.quote(contentText), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
}
searchRecursive(directory.toPath(), filenameLower, filenameRegex, contentPattern, recursive, searchArchives, callback);
}
private static void searchRecursive(Path directory, String patternLower, Pattern filenameRegex, Pattern contentPattern, boolean recursive, boolean searchArchives, SearchCallback callback) throws IOException {
if (callback != null && callback.isCancelled()) return;
// Use absolute path for reliable prefix checking
Path absolutePath = directory.toAbsolutePath().normalize();
String pathStr = absolutePath.toString();
callback.onProgress(pathStr);
// Skip troublesome virtual filesystems and device nodes
if (pathStr.startsWith("/proc") || pathStr.startsWith("/sys") || pathStr.startsWith("/dev")) {
return;
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path entry : stream) {
if (callback != null && callback.isCancelled()) return;
try {
// Always skip symbolic links during search to prevent infinite loops and hangs on special files
if (Files.isSymbolicLink(entry)) {
continue;
}
if (Files.isDirectory(entry)) {
if (recursive) {
searchRecursive(entry, patternLower, filenameRegex, contentPattern, recursive, searchArchives, callback);
}
} else {
File file = entry.toFile();
boolean nameMatched = true;
if (patternLower != null && !patternLower.isEmpty()) {
nameMatched = matchName(file.getName(), patternLower, filenameRegex);
}
boolean contentMatched = true;
if (nameMatched && contentPattern != null) {
contentMatched = fileMatchesContent(entry, contentPattern);
}
if (nameMatched && contentMatched) {
callback.onFileFound(file, null);
}
// SEARCH IN ARCHIVES
if (searchArchives && isArchiveFile(file)) {
searchInArchiveCombined(file, patternLower, filenameRegex, contentPattern, callback);
}
}
} catch (AccessDeniedException e) {
// Silently skip
}
}
} catch (AccessDeniedException e) {
// Silently skip
}
}
private static boolean matchName(String name, String patternLower, Pattern filenameRegex) {
String nameLower = name.toLowerCase();
if (nameLower.contains(patternLower)) return true;
if (filenameRegex != null) {
return filenameRegex.matcher(name).matches();
}
return false;
}
private static boolean fileMatchesContent(Path entry, Pattern contentPattern) {
try (BufferedReader br = Files.newBufferedReader(entry)) {
String line;
while ((line = br.readLine()) != null) {
if (contentPattern.matcher(line).find()) {
return true;
}
}
} catch (IOException ex) {
// Silently skip
}
return false;
}
/**
* Reads the content of a file from an archive.
*/
public static byte[] readFileFromArchive(File archive, String entryPath) throws IOException {
String name = archive.getName().toLowerCase();
try {
if (name.endsWith(".zip") || name.endsWith(".jar")) {
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(archive))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.getName().equals(entryPath) || (archive.getAbsolutePath() + File.separator + entry.getName()).equals(entryPath)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[24576];
int len;
while ((len = zis.read(buffer)) > 0) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
}
}
}
} else if (name.endsWith(".tar.gz") || name.endsWith(".tgz") || name.endsWith(".tar")) {
InputStream is = new FileInputStream(archive);
if (name.endsWith(".gz") || name.endsWith(".tgz")) {
is = new org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream(is);
}
try (org.apache.commons.compress.archivers.tar.TarArchiveInputStream tais = new org.apache.commons.compress.archivers.tar.TarArchiveInputStream(is)) {
org.apache.commons.compress.archivers.tar.TarArchiveEntry entry;
while ((entry = tais.getNextTarEntry()) != null) {
if (entry.getName().equals(entryPath) || (archive.getAbsolutePath() + File.separator + entry.getName()).equals(entryPath)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[24576];
int len;
while ((len = tais.read(buffer)) > 0) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
}
}
}
} else if (name.endsWith(".7z")) {
try (org.apache.commons.compress.archivers.sevenz.SevenZFile sevenZFile = new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive)) {
org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry;
while ((entry = sevenZFile.getNextEntry()) != null) {
if (entry.getName().equals(entryPath) || (archive.getAbsolutePath() + File.separator + entry.getName()).equals(entryPath)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[24576];
int len;
while ((len = sevenZFile.read(buffer)) > 0) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
}
}
}
} else if (name.endsWith(".rar")) {
try (com.github.junrar.Archive rar = new com.github.junrar.Archive(archive)) {
for (com.github.junrar.rarfile.FileHeader fh : rar.getFileHeaders()) {
if (fh.getFileName().equals(entryPath) || (archive.getAbsolutePath() + File.separator + fh.getFileName()).equals(entryPath)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
rar.extractFile(fh, baos);
return baos.toByteArray();
}
}
}
}
} catch (Exception e) {
throw new IOException("Failed to read from archive: " + e.getMessage(), e);
}
throw new IOException("Entry not found in archive: " + entryPath);
}
public static boolean isArchiveFile(File f) {
if (f == null) return false;
return isArchiveFile(f.getName());
}
public static boolean isArchiveFile(String filename) {
if (filename == null) return false;
String n = filename.toLowerCase();
return n.endsWith(".war") || n.endsWith(".zip") || n.endsWith(".jar") || n.endsWith(".tar") || n.endsWith(".tar.gz") || n.endsWith(".tgz") || n.endsWith(".7z") || n.endsWith(".rar");
}
private static void searchInArchiveCombined(File archive, String patternLower, Pattern filenameRegex, Pattern contentPattern, SearchCallback callback) {
if (callback != null && callback.isCancelled()) return;
String name = archive.getName().toLowerCase();
try {
if (name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".war")) {
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(archive))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (callback.isCancelled()) return;
if (!entry.isDirectory()) {
boolean nameMatched = true;
if (patternLower != null && !patternLower.isEmpty()) {
nameMatched = matchEntry(entry.getName(), patternLower, filenameRegex);
}
boolean contentMatched = true;
if (nameMatched && contentPattern != null) {
contentMatched = searchInStream(zis, contentPattern);
}
if (nameMatched && contentMatched) {
callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + entry.getName());
}
}
}
}
} else if (name.endsWith(".tar.gz") || name.endsWith(".tgz") || name.endsWith(".tar")) {
InputStream is = new FileInputStream(archive);
if (name.endsWith(".gz") || name.endsWith(".tgz")) {
is = new org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream(is);
}
try (org.apache.commons.compress.archivers.tar.TarArchiveInputStream tais = new org.apache.commons.compress.archivers.tar.TarArchiveInputStream(is)) {
org.apache.commons.compress.archivers.tar.TarArchiveEntry entry;
while ((entry = tais.getNextTarEntry()) != null) {
if (callback.isCancelled()) return;
if (!entry.isDirectory()) {
boolean nameMatched = true;
if (patternLower != null && !patternLower.isEmpty()) {
nameMatched = matchEntry(entry.getName(), patternLower, filenameRegex);
}
boolean contentMatched = true;
if (nameMatched && contentPattern != null) {
contentMatched = searchInStream(tais, contentPattern);
}
if (nameMatched && contentMatched) {
callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + entry.getName());
}
}
}
}
} else if (name.endsWith(".7z")) {
try (org.apache.commons.compress.archivers.sevenz.SevenZFile sevenZFile = new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive)) {
org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry;
while ((entry = sevenZFile.getNextEntry()) != null) {
if (callback.isCancelled()) return;
if (!entry.isDirectory()) {
boolean nameMatched = true;
if (patternLower != null && !patternLower.isEmpty()) {
nameMatched = matchEntry(entry.getName(), patternLower, filenameRegex);
}
boolean contentMatched = true;
if (nameMatched && contentPattern != null) {
contentMatched = searchInStream(new InputStream() {
@Override
public int read() throws IOException {
return sevenZFile.read();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return sevenZFile.read(b, off, len);
}
}, contentPattern);
}
if (nameMatched && contentMatched) {
callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + entry.getName());
}
}
}
}
} else if (name.endsWith(".rar")) {
try (com.github.junrar.Archive rar = new com.github.junrar.Archive(archive)) {
for (com.github.junrar.rarfile.FileHeader fh : rar.getFileHeaders()) {
if (callback.isCancelled()) return;
if (!fh.isDirectory()) {
boolean nameMatched = true;
if (patternLower != null && !patternLower.isEmpty()) {
nameMatched = matchEntry(fh.getFileName(), patternLower, filenameRegex);
}
boolean contentMatched = true;
if (nameMatched && contentPattern != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
rar.extractFile(fh, baos);
contentMatched = contentPattern.matcher(new String(baos.toByteArray())).find();
}
if (nameMatched && contentMatched) {
callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + fh.getFileName());
}
}
}
}
}
} catch (Exception e) {
// Silently skip archive errors during search
}
}
private static boolean matchEntry(String entryName, String patternLower, Pattern filenameRegex) {
if (entryName == null) return false;
String nameShort = entryName;
int lastSlash = entryName.lastIndexOf('/');
if (lastSlash != -1) nameShort = entryName.substring(lastSlash + 1);
String nameLower = nameShort.toLowerCase();
if (nameLower.contains(patternLower)) return true;
if (filenameRegex != null && filenameRegex.matcher(nameShort).matches()) return true;
return false;
}
private static boolean searchInStream(InputStream is, Pattern contentPattern) {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = br.readLine()) != null) {
if (contentPattern.matcher(line).find()) {
return true;
}
}
} catch (Exception ignore) {}
return false;
}
/**
* Zip files/directories into a target zip file
*/
public static void zip(List<FileItem> items, File targetZipFile, ProgressCallback callback) throws IOException {
List<FileItem> cleanedItems = cleanDuplicateItems(items);
long totalItems = calculateTotalItems(cleanedItems);
long[] currentItem = {0};
try (org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream zos = new org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream(targetZipFile)) {
for (FileItem item : cleanedItems) {
if (callback != null && callback.isCancelled()) break;
addToZip(item.getFile(), item.getName(), zos, totalItems, currentItem, callback);
}
}
}
private static void addToZip(File fileToZip, String fileName, org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream zos, long totalItems, long[] currentItem, ProgressCallback callback) throws IOException {
if (fileToZip.isHidden()) {
return;
}
currentItem[0]++;
if (callback != null) {
callback.onProgress(currentItem[0], totalItems, fileName);
}
org.apache.commons.compress.archivers.zip.ZipArchiveEntry zipEntry = new org.apache.commons.compress.archivers.zip.ZipArchiveEntry(fileToZip, fileName);
// Try to set POSIX permissions
if (cz.kamma.kfmanager.MainApp.CURRENT_OS == cz.kamma.kfmanager.MainApp.OS.LINUX ||
cz.kamma.kfmanager.MainApp.CURRENT_OS == cz.kamma.kfmanager.MainApp.OS.MACOS) {
try {
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(fileToZip.toPath());
int mode = 0;
if (perms.contains(PosixFilePermission.OWNER_READ)) mode |= 0400;
if (perms.contains(PosixFilePermission.OWNER_WRITE)) mode |= 0200;
if (perms.contains(PosixFilePermission.OWNER_EXECUTE)) mode |= 0100;
if (perms.contains(PosixFilePermission.GROUP_READ)) mode |= 0040;
if (perms.contains(PosixFilePermission.GROUP_WRITE)) mode |= 0020;
if (perms.contains(PosixFilePermission.GROUP_EXECUTE)) mode |= 0010;
if (perms.contains(PosixFilePermission.OTHERS_READ)) mode |= 0004;
if (perms.contains(PosixFilePermission.OTHERS_WRITE)) mode |= 0002;
if (perms.contains(PosixFilePermission.OTHERS_EXECUTE)) mode |= 0001;
zipEntry.setUnixMode(mode);
} catch (Exception ignore) {}
}
if (fileToZip.isDirectory()) {
zos.putArchiveEntry(zipEntry);
zos.closeArchiveEntry();
File[] children = fileToZip.listFiles();
if (children != null) {
for (File childFile : children) {
addToZip(childFile, fileName + (fileName.endsWith("/") ? "" : "/") + childFile.getName(), zos, totalItems, currentItem, callback);
}
}
return;
}
try (InputStream is = Files.newInputStream(fileToZip.toPath())) {
zos.putArchiveEntry(zipEntry);
long fileSize = fileToZip.length();
long bytesCopied = 0;
byte[] buffer = new byte[24576];
int len;
while ((len = is.read(buffer)) >= 0) {
zos.write(buffer, 0, len);
bytesCopied += len;
if (callback != null) {
callback.onFileProgress(bytesCopied, fileSize);
}
}
if (callback != null) {
callback.onFileProgress(fileSize, fileSize);
}
zos.closeArchiveEntry();
}
}
/**
* Extract an archive into a target directory
*/
public static void extractArchive(File archiveFile, File targetDirectory, ProgressCallback callback) throws Exception {
if (!targetDirectory.exists()) {
Files.createDirectories(targetDirectory.toPath());
}
String name = archiveFile.getName().toLowerCase();
if (name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".war")) {
unzip(archiveFile, targetDirectory, callback);
} else if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) {
extractTarGz(archiveFile, targetDirectory, callback);
} else if (name.endsWith(".tar")) {
extractTar(archiveFile, targetDirectory, callback);
} else if (name.endsWith(".7z")) {
extractSevenZ(archiveFile, targetDirectory, callback);
} else if (name.endsWith(".rar")) {
extractRar(archiveFile, targetDirectory, callback);
} else {
throw new IOException("Unsupported archive format: " + archiveFile.getName());
}
}
private static void extractTarGz(File archive, File targetDir, ProgressCallback callback) throws IOException {
try (InputStream fis = Files.newInputStream(archive.toPath());
InputStream gzis = new org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream(fis);
org.apache.commons.compress.archivers.tar.TarArchiveInputStream tais = new org.apache.commons.compress.archivers.tar.TarArchiveInputStream(gzis)) {
extractTarInternal(tais, targetDir, callback);
}
}
private static void extractTar(File archive, File targetDir, ProgressCallback callback) throws IOException {
try (InputStream fis = Files.newInputStream(archive.toPath());
org.apache.commons.compress.archivers.tar.TarArchiveInputStream tais = new org.apache.commons.compress.archivers.tar.TarArchiveInputStream(fis)) {
extractTarInternal(tais, targetDir, callback);
}
}
private static void extractTarInternal(org.apache.commons.compress.archivers.tar.TarArchiveInputStream tais, File targetDir, ProgressCallback callback) throws IOException {
org.apache.commons.compress.archivers.tar.TarArchiveEntry entry;
long current = 0;
while ((entry = tais.getNextTarEntry()) != null) {
current++;
if (callback != null) callback.onProgress(current, -1, entry.getName());
File newFile = new File(targetDir, entry.getName());
if (entry.isDirectory()) {
newFile.mkdirs();
setPermissionsFromMode(newFile.toPath(), entry.getMode());
} else {
newFile.getParentFile().mkdirs();
Files.copy(tais, newFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
setPermissionsFromMode(newFile.toPath(), entry.getMode());
}
}
}
private static void extractSevenZ(File archive, File targetDir, ProgressCallback callback) throws IOException {
try (org.apache.commons.compress.archivers.sevenz.SevenZFile sevenZFile = new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive)) {
org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry;
long current = 0;
while ((entry = sevenZFile.getNextEntry()) != null) {
current++;
if (callback != null) callback.onProgress(current, -1, entry.getName());
File newFile = new File(targetDir, entry.getName());
if (entry.isDirectory()) {
newFile.mkdirs();
} else {
newFile.getParentFile().mkdirs();
try (OutputStream os = Files.newOutputStream(newFile.toPath())) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = sevenZFile.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
}
}
}
}
}
private static void extractRar(File archiveFile, File targetDir, ProgressCallback callback) throws Exception {
try (com.github.junrar.Archive archive = new com.github.junrar.Archive(archiveFile)) {
com.github.junrar.rarfile.FileHeader fh = archive.nextFileHeader();
long current = 0;
while (fh != null) {
current++;
String entryName = fh.getFileName().replace('\\', '/');
if (callback != null) callback.onProgress(current, -1, entryName);
File newFile = new File(targetDir, entryName);
if (fh.isDirectory()) {
newFile.mkdirs();
} else {
newFile.getParentFile().mkdirs();
try (OutputStream os = Files.newOutputStream(newFile.toPath())) {
archive.extractFile(fh, os);
}
}
fh = archive.nextFileHeader();
}
}
}
/**
* Unzip a zip file into a target directory
*/
public static void unzip(File zipFile, File targetDirectory, ProgressCallback callback) throws IOException {
if (!targetDirectory.exists()) {
Files.createDirectories(targetDirectory.toPath());
}
try (org.apache.commons.compress.archivers.zip.ZipFile zf = new org.apache.commons.compress.archivers.zip.ZipFile(zipFile)) {
java.util.List<org.apache.commons.compress.archivers.zip.ZipArchiveEntry> entries = java.util.Collections.list(zf.getEntries());
long totalItems = entries.size();
long currentItem = 0;
for (org.apache.commons.compress.archivers.zip.ZipArchiveEntry entry : entries) {
currentItem++;
File newFile = new File(targetDirectory, entry.getName());
if (callback != null) {
callback.onProgress(currentItem, totalItems, entry.getName());
}
if (entry.isDirectory()) {
if (!newFile.isDirectory() && !newFile.mkdirs()) {
throw new IOException("Failed to create directory " + newFile);
}
setPermissionsFromMode(newFile.toPath(), entry.getUnixMode());
} else {
// create parent directories if they don't exist
File parent = newFile.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
if (newFile.exists() && newFile.isDirectory()) {
deleteDirectoryInternal(newFile.toPath());
}
try (InputStream is = zf.getInputStream(entry)) {
Files.copy(is, newFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
setPermissionsFromMode(newFile.toPath(), entry.getUnixMode());
}
}
}
}
// legacy matchesPattern removed — filename wildcard handling is done via a precompiled Pattern
/**
* Callback for operation progress
*/
public enum OverwriteResponse {
YES, NO, YES_TO_ALL, NO_TO_ALL, CANCEL
}
public enum SymlinkResponse {
FOLLOW, IGNORE, FOLLOW_ALL, IGNORE_ALL, CANCEL
}
public enum ErrorResponse {
SKIP, RETRY, ABORT
}
public interface ProgressCallback {
void onProgress(long current, long total, String currentFile);
default void onFileProgress(long current, long total) {}
default boolean isCancelled() { return false; }
default OverwriteResponse confirmOverwrite(File source, File destination) { return OverwriteResponse.YES; }
default SymlinkResponse confirmSymlink(File symlink) { return SymlinkResponse.IGNORE; }
default ErrorResponse onError(File file, Exception e) { return ErrorResponse.ABORT; }
}
/**
* Callback for search
*/
public interface SearchCallback {
void onFileFound(File file, String virtualPath);
default boolean isCancelled() { return false; }
default void onProgress(String status) {}
}
}