1237 lines
59 KiB
Java
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) {}
|
|
}
|
|
}
|