package net.filebot.ui.sfv;
import static java.util.Arrays.*;
import static java.util.Collections.*;
import static net.filebot.Logging.*;
import static net.filebot.MediaTypes.*;
import static net.filebot.Settings.*;
import static net.filebot.hash.VerificationUtilities.*;
import static net.filebot.util.FileUtilities.*;
import static net.filebot.util.ui.SwingUI.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutorService;
import java.util.logging.Level;
import net.filebot.hash.HashType;
import net.filebot.hash.VerificationFileReader;
import net.filebot.platform.mac.MacAppUtilities;
import net.filebot.ui.transfer.BackgroundFileTransferablePolicy;
import net.filebot.util.ExceptionUtilities;
import net.filebot.util.FileSet;
class ChecksumTableTransferablePolicy extends BackgroundFileTransferablePolicy<ChecksumCell> {
private final ChecksumTable table;
private final ChecksumTableModel model;
private final ChecksumComputationService computationService;
public ChecksumTableTransferablePolicy(ChecksumTable table, ChecksumComputationService checksumComputationService) {
this.table = table;
this.model = table.getModel();
this.computationService = checksumComputationService;
}
@Override
protected boolean accept(List<File> files) {
return true;
}
@Override
protected void clear() {
super.clear();
computationService.reset();
model.clear();
}
@Override
protected void handleInBackground(List<File> files, TransferAction action) {
if (files.size() == 1 && getHashType(files.get(0)) != null) {
model.setHashType(getHashType(files.get(0)));
}
super.handleInBackground(files, action);
}
@Override
protected void process(List<ChecksumCell> chunks) {
model.addAll(chunks);
}
@Override
protected void process(Exception e) {
log.log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), e);
}
private final ThreadLocal<ExecutorService> executor = new ThreadLocal<ExecutorService>();
private final ThreadLocal<VerificationTracker> verificationTracker = new ThreadLocal<VerificationTracker>();
@Override
protected void load(List<File> files, TransferAction action) throws IOException {
// make sure we have access to the parent folder structure, not just the dropped file
if (isMacSandbox()) {
MacAppUtilities.askUnlockFolders(getWindow(table), files);
}
// initialize drop parameters
executor.set(computationService.newExecutor());
verificationTracker.set(new VerificationTracker(5));
try {
// handle single verification file drop
if (containsOnly(files, VERIFICATION_FILES)) {
for (File file : files) {
loadVerificationFile(file, getHashType(file));
}
return;
}
// handle single folder drop
if (files.size() == 1 && containsOnly(files, FOLDERS)) {
for (File folder : files) {
for (File file : getChildren(folder, NOT_HIDDEN, HUMAN_NAME_ORDER)) {
load(file, null, folder);
}
}
return;
}
// handle files and folders dropped from the same parent folder
if (mapByFolder(files).size() == 1) {
for (File file : files) {
load(file, null, file.getParentFile());
}
return;
}
// handle all other drops and auto-detect common root folder from dropped fileset
FileSet fileset = new FileSet();
files.forEach(fileset::add);
for (Entry<Path, List<Path>> it : fileset.getRoots().entrySet()) {
File root = it.getKey().toFile();
for (Path path : it.getValue()) {
File relativeFile = path.toFile().getParentFile();
File absoluteFile = new File(root, path.toString());
load(absoluteFile, relativeFile, root);
}
}
} catch (InterruptedException e) {
// supposed to happen if background execution is aborted
} finally {
// shutdown executor after all tasks have been completed
executor.get().shutdown();
// remove drop parameters
executor.remove();
verificationTracker.remove();
}
}
protected void loadVerificationFile(File file, HashType type) throws IOException, InterruptedException {
VerificationFileReader parser = new VerificationFileReader(createTextReader(file), type.getFormat());
try {
// root for relative file paths in verification file
File baseFolder = file.getParentFile();
while (parser.hasNext()) {
// make this possibly long-running operation interruptible
if (Thread.interrupted()) {
throw new InterruptedException();
}
Entry<File, String> entry = parser.next();
String name = normalizePathSeparators(entry.getKey().getPath());
String hash = new String(entry.getValue());
ChecksumCell correct = new ChecksumCell(name, file, singletonMap(type, hash));
ChecksumCell current = createComputationCell(name, baseFolder, type);
ChecksumCell[] columns = { correct, current };
publish(columns);
}
} finally {
parser.close();
}
}
protected void load(File absoluteFile, File relativeFile, File root) throws IOException, InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
// ignore hidden files/folders
if (absoluteFile.isHidden()) {
return;
}
// add next name to relative path
relativeFile = new File(relativeFile, absoluteFile.getName());
if (absoluteFile.isDirectory()) {
// load all files in the file tree
for (File child : getChildren(absoluteFile, NOT_HIDDEN, HUMAN_NAME_ORDER)) {
load(child, relativeFile, root);
}
} else {
String name = normalizePathSeparators(relativeFile.getPath());
// publish computation cell first
ChecksumCell[] computeCell = { createComputationCell(name, root, model.getHashType()) };
publish(computeCell);
// publish verification cell, if we can
Map<File, String> hashByVerificationFile = verificationTracker.get().getHashByVerificationFile(absoluteFile);
for (Entry<File, String> entry : hashByVerificationFile.entrySet()) {
HashType hashType = verificationTracker.get().getVerificationFileType(entry.getKey());
ChecksumCell[] verifyCell = { new ChecksumCell(name, entry.getKey(), singletonMap(hashType, entry.getValue())) };
publish(verifyCell);
}
}
}
protected ChecksumCell createComputationCell(String name, File root, HashType hash) {
ChecksumCell cell = new ChecksumCell(name, root, new ChecksumComputationTask(new File(root, name), hash));
// start computation task
executor.get().execute(cell.getTask());
return cell;
}
@Override
public String getFileFilterDescription() {
return "Folders and SFV Files";
}
@Override
public List<String> getFileFilterExtensions() {
return asList(VERIFICATION_FILES.extensions());
}
private static class VerificationTracker {
private final Map<File, Integer> seen = new HashMap<File, Integer>();
private final Map<File, Map<File, String>> cache = new HashMap<File, Map<File, String>>();
private final Map<File, HashType> types = new HashMap<File, HashType>();
private final int maxDepth;
public VerificationTracker(int maxDepth) {
this.maxDepth = maxDepth;
}
public Map<File, String> getHashByVerificationFile(File file) throws IOException {
// cache all verification files
File folder = file.getParentFile();
int depth = 0;
while (folder != null && depth <= maxDepth) {
Integer seenLevel = seen.get(folder);
if (seenLevel != null && seenLevel <= depth) {
// we have completely seen this parent tree before
break;
}
if (seenLevel == null) {
// folder we have never encountered before
for (File verificationFile : getChildren(folder, VERIFICATION_FILES)) {
HashType hashType = getHashType(verificationFile);
cache.put(verificationFile, importVerificationFile(verificationFile, hashType, verificationFile.getParentFile()));
types.put(verificationFile, hashType);
}
}
// update
seen.put(folder, depth);
// step down
folder = folder.getParentFile();
depth++;
}
// just return if we know we won't find anything
if (cache.isEmpty()) {
return emptyMap();
}
// search all cached verification files
Map<File, String> result = new HashMap<File, String>(2);
for (Entry<File, Map<File, String>> entry : cache.entrySet()) {
String hash = entry.getValue().get(file);
if (hash != null) {
result.put(entry.getKey(), hash);
}
}
return result;
}
public HashType getVerificationFileType(File verificationFile) {
return types.get(verificationFile);
}
/**
* Completely read a verification file and resolve all relative file paths against a given base folder
*/
private Map<File, String> importVerificationFile(File verificationFile, HashType hashType, File baseFolder) throws IOException {
VerificationFileReader parser = new VerificationFileReader(createTextReader(verificationFile), hashType.getFormat());
Map<File, String> result = new HashMap<File, String>();
try {
while (parser.hasNext()) {
Entry<File, String> entry = parser.next();
// resolve relative path, the hash is probably a substring, so we compact it, for memory reasons
result.put(new File(baseFolder, entry.getKey().getPath()), new String(entry.getValue()));
}
} finally {
parser.close();
}
return result;
}
}
}