/* * Copyright 2004 - 2008 Christian Sprajc. All rights reserved. * * This file is part of PowerFolder. * * PowerFolder is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation. * * PowerFolder is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PowerFolder. If not, see <http://www.gnu.org/licenses/>. * * $Id$ */ package de.dal33t.powerfolder.disk; import static de.dal33t.powerfolder.disk.FolderSettings.FOLDER_SETTINGS_PREFIX_V4; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.EOFException; import java.io.Externalizable; import java.io.File; import java.io.FileInputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TimerTask; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import de.dal33t.powerfolder.ConfigurationEntry; import de.dal33t.powerfolder.Constants; import de.dal33t.powerfolder.Controller; import de.dal33t.powerfolder.Feature; import de.dal33t.powerfolder.Member; import de.dal33t.powerfolder.PFComponent; import de.dal33t.powerfolder.PreferencesEntry; import de.dal33t.powerfolder.disk.dao.FileInfoCriteria; import de.dal33t.powerfolder.disk.dao.FileInfoDAO; import de.dal33t.powerfolder.disk.dao.FileInfoDAOHashMapImpl; import de.dal33t.powerfolder.disk.problem.DeviceDisconnectedProblem; import de.dal33t.powerfolder.disk.problem.FileConflictProblem; import de.dal33t.powerfolder.disk.problem.FilenameProblemHelper; import de.dal33t.powerfolder.disk.problem.FolderDatabaseProblem; import de.dal33t.powerfolder.disk.problem.Problem; import de.dal33t.powerfolder.disk.problem.ProblemListener; import de.dal33t.powerfolder.disk.problem.UnsynchronizedFolderProblem; import de.dal33t.powerfolder.event.FolderEvent; import de.dal33t.powerfolder.event.FolderListener; import de.dal33t.powerfolder.event.FolderMembershipEvent; import de.dal33t.powerfolder.event.FolderMembershipListener; import de.dal33t.powerfolder.event.ListenerSupportFactory; import de.dal33t.powerfolder.event.LocalMassDeletionEvent; import de.dal33t.powerfolder.event.RemoteMassDeletionEvent; import de.dal33t.powerfolder.light.DirectoryInfo; import de.dal33t.powerfolder.light.FileInfo; import de.dal33t.powerfolder.light.FileInfoFactory; import de.dal33t.powerfolder.light.FolderInfo; import de.dal33t.powerfolder.light.MemberInfo; import de.dal33t.powerfolder.message.FileList; import de.dal33t.powerfolder.message.FileRequestCommand; import de.dal33t.powerfolder.message.FolderFilesChanged; import de.dal33t.powerfolder.message.Invitation; import de.dal33t.powerfolder.message.Message; import de.dal33t.powerfolder.message.MessageProducer; import de.dal33t.powerfolder.message.ScanCommand; import de.dal33t.powerfolder.security.FolderPermission; import de.dal33t.powerfolder.transfer.TransferPriorities; import de.dal33t.powerfolder.transfer.TransferPriorities.TransferPriority; import de.dal33t.powerfolder.util.Convert; import de.dal33t.powerfolder.util.DateUtil; import de.dal33t.powerfolder.util.Debug; import de.dal33t.powerfolder.util.FileUtils; import de.dal33t.powerfolder.util.LoginUtil; import de.dal33t.powerfolder.util.Reject; import de.dal33t.powerfolder.util.StringUtils; import de.dal33t.powerfolder.util.Translation; import de.dal33t.powerfolder.util.UserDirectories; import de.dal33t.powerfolder.util.Util; import de.dal33t.powerfolder.util.Visitor; import de.dal33t.powerfolder.util.compare.FileInfoComparator; import de.dal33t.powerfolder.util.compare.ReverseComparator; import de.dal33t.powerfolder.util.logging.LoggingManager; import de.dal33t.powerfolder.util.os.OSUtil; import de.dal33t.powerfolder.util.os.Win32.WinUtils; import de.dal33t.powerfolder.util.pattern.DefaultExcludes; import de.schlichtherle.truezip.file.TFile; import de.schlichtherle.truezip.file.TFileInputStream; import de.schlichtherle.truezip.file.TFileOutputStream; /** * The main class representing a folder. Scans for new files automatically. * * @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc </a> * @version $Revision: 1.114 $ */ public class Folder extends PFComponent { private static final String LAST_SYNC_INFO_FILENAME = "Last_sync"; public static final String METAFOLDER_MEMBERS = "Members"; public static final String FOLDER_STATISTIC = "FolderStatistic"; private static final int FIVE_MINUTES = 60 * 5; //private static final int THIRTY_SECONDS = 30; /** The base location of the folder. */ private final TFile localBase; /** * #2056: The directory to commit/mirror the whole folder to when in reaches * 100% sync. */ private File commitDir; /** * TRAC #1422: The DAO to store the FileInfos in. */ private FileInfoDAO dao; /** * Date of the last directory scan */ private Date lastScan; /** * Date of the folder last went to 100% synchronized with another member(s). */ private Date lastSyncDate; /** * The result state of the last scan */ private ScanResult.ResultState lastScanResultState; /** * The last time the db was cleaned up. */ private Date lastDBMaintenance; /** * Access lock to the DB/DAO. */ private final Object dbAccessLock = new Object(); /** files that should(not) be downloaded in auto download */ private final DiskItemFilter diskItemFilter; /** * Stores the priorities for downloading of the files in this folder. */ private final TransferPriorities transferPriorities; /** * TODO THIS IS A HACK. Make it finer. Lock for scan / accessing the actual * files */ private final Object scanLock = new Object(); /** All members of this folder. Key == Value. Use Map for concurrency. */ private final Map<Member, Member> members; /** * the folder info, contains important information about * id/hash/name/filescount */ private final FolderInfo currentInfo; /** * Folders sync profile Always access using getSyncProfile (#76 - preview * mode) */ private SyncProfile syncProfile; /** * The entry id in config file. Usually MD5(random ID, folderID or a * meaningful unique name, e.g. Desktop). */ private String configEntryId; /** * Flag indicating that folder has a set of own know files will be true * after first scan ever */ private volatile boolean hasOwnDatabase; /** Flag indicating */ private volatile boolean shutdown; /** * Indicates, that the scan of the local filesystem was forced */ private boolean scanForced; /** * Flag indicating that the setting (e.g. folder db) have changed but not * been persisted. */ private volatile boolean dirty; /** * The FileInfos that have problems inlcuding the desciptions of the * problems. DISABLED */ // private Map<FileInfo, List<FilenameProblem>> problemFiles; /** The statistic for this folder */ private final FolderStatistic statistic; private volatile FileArchiver archiver; /** * TRAC #711: Automatic change detection by watching the filesystem. */ private FolderWatcher watcher; private final FolderListener folderListenerSupport; private final FolderMembershipListener folderMembershipListenerSupport; /** * If the folder is only preview then the files do not actually download and * the folder displays in the Available Folders group. */ private boolean previewOnly; /** * True if the base dir is inaccessible. */ private boolean deviceDisconnected; private boolean encrypted; /** * Encryption key when a client with valid credentials is connected. */ private char[] encryptionKeyOBF; /** * #1538: Script that gets executed after a download has been completed * successfully. */ private String downloadScript; private final ProblemListener problemListenerSupport; private final List<Problem> problems; /** True if patterns should be synchronized with others */ private boolean syncPatterns; /** * The number of seconds the folder is allowed to be out of sync. If it is * longer out of sync it produces an {@link UnsynchronizedFolderProblem}. If * not set (0) the default global will be assumed * {@link ConfigurationEntry#FOLDER_SYNC_WARN_DAYS}. */ private int syncWarnSeconds; private Persister persister; /** * Constructor for folder. * * @param controller * @param fInfo * @param folderSettings * @throws FolderException */ Folder(Controller controller, FolderInfo fInfo, FolderSettings folderSettings) { super(controller); Reject.ifNull(folderSettings.getSyncProfile(), "Sync profile is null"); currentInfo = new FolderInfo(fInfo.name, fInfo.id).intern(); // Create listener support folderListenerSupport = ListenerSupportFactory .createListenerSupport(FolderListener.class); folderMembershipListenerSupport = ListenerSupportFactory .createListenerSupport(FolderMembershipListener.class); problemListenerSupport = ListenerSupportFactory .createListenerSupport(ProblemListener.class); // Not until first scan or db load hasOwnDatabase = false; dirty = false; encrypted = folderSettings.getLocalBaseDir().getName() .endsWith(".pfzip"); problems = new CopyOnWriteArrayList<Problem>(); if (encrypted) { localBase = new TFile(folderSettings.getLocalBaseDir()); localBase.mkdirs(); try { new TFile(localBase, "dummy.txt").createNewFile(); new TFile(localBase, "dummy.txt").rm(); } catch (IOException e) { logWarning("Unable to initialize encrypted storage at " + localBase); } if (!localBase.isArchive()) { throw new IllegalStateException( "Unable to open encrypted container for folder " + getName() + " at " + localBase); } } else if (folderSettings.getLocalBaseDir().isAbsolute()) { localBase = new TFile(folderSettings.getLocalBaseDir()); } else { localBase = new TFile(getController().getFolderRepository() .getFoldersBasedir(), folderSettings.getLocalBaseDir() .getPath()); logWarning("Original path: " + folderSettings.getLocalBaseDir() + ". Choosen relative path: " + localBase); if (folderSettings.getLocalBaseDir().exists() && !localBase.exists()) { localBase.mkdirs(); } } // Support for meta folder. if (fInfo.isMetaFolder() && folderSettings.getLocalBaseDir().getAbsolutePath() .contains(".pfzip")) { encrypted = true; } Reject.ifTrue(localBase.equals(getController().getFolderRepository() .getFoldersBasedir()), "Folder cannot be located at base directory for all folders"); if (folderSettings.getCommitDir() != null) { if (folderSettings.getCommitDir().isAbsolute()) { commitDir = folderSettings.getCommitDir(); } else { commitDir = new File(getController().getFolderRepository() .getFoldersBasedir(), folderSettings.getCommitDir() .getPath()); } } syncProfile = folderSettings.getSyncProfile(); downloadScript = folderSettings.getDownloadScript(); syncPatterns = folderSettings.isSyncPatterns(); syncWarnSeconds = folderSettings.getSyncWarnSeconds(); previewOnly = folderSettings.isPreviewOnly(); configEntryId = folderSettings.getConfigEntryId(); // Check base dir try { checkBaseDir(false); logFine("Opened " + toString() + " at '" + localBase.getAbsolutePath() + '\''); } catch (FolderException e) { logWarning("Unable to open " + toString() + " at '" + localBase.getAbsolutePath() + "'. Local base directory is inaccessable."); deviceDisconnected = true; } FilenameFilter allExceptSystemDirFilter = new FilenameFilter() { public boolean accept(File dir, String name) { return !name .equalsIgnoreCase(Constants.POWERFOLDER_SYSTEM_SUBDIR); } }; String[] filenames = localBase.list(allExceptSystemDirFilter); if (filenames != null && filenames.length == 0) { // Empty folder... no scan required for database hasOwnDatabase = true; } transferPriorities = new TransferPriorities(); diskItemFilter = new DiskItemFilter(); // Initialize the DAO initFileInfoDAO(); checkIfDeviceDisconnected(); members = new ConcurrentHashMap<Member, Member>(); // Load folder database, ignore patterns and other metadata stuff. loadMetadata(); // put myself in membership // join0(controller.getMySelf()); members.put(controller.getMySelf(), controller.getMySelf()); // Now calc. statistic = new FolderStatistic(this); // Check desktop ini in Windows environments if (!currentInfo.isMetaFolder()) { FileUtils.maintainDesktopIni(getController(), localBase); } // Force the next time scan. recommendScanOnNextMaintenance(); // // maintain desktop shortcut if wanted // setDesktopShortcut(); if (isInfo()) { if (hasOwnDatabase) { logFiner("Has own database (" + getName() + ")? " + hasOwnDatabase); } else { logFine("Has own database (" + getName() + ")? " + hasOwnDatabase); } } if (hasOwnDatabase) { // Write filelist writeFilelist(getController().getMySelf()); } persister = new Persister(); getController().scheduleAndRepeat( persister, 1000L, 1000L * ConfigurationEntry.FOLDER_DB_PERSIST_TIME .getValueInt(getController())); File archive = new TFile(getSystemSubDir(), "archive"); if (!checkIfDeviceDisconnected() && !archive.exists() && !archive.mkdirs()) { logWarning("Failed to create archive directory in system subdirectory: " + archive); } archiver = new FileArchiver(archive, getController().getMySelf().getInfo()); archiver.setVersionsPerFile(folderSettings.getVersions()); // Create invitation // if (folderSettings.isCreateInvitationFile()) { // try { // Invitation inv = createInvitation(); // File invFile = new TFile(localBase, // FileUtils.removeInvalidFilenameChars(inv.folder.name) // + ".invitation"); // InvitationUtil.save(inv, invFile); // scanChangedFile(FileInfoFactory.lookupInstance(this, invFile)); // } catch (Exception e) { // // Failure to send invite is not fatal to folder create. // // Log it and move on. // logInfo(e); // } // } // Remove desktop.ini. Was accidentally created in 4.3.0 release. if (currentInfo.isMetaFolder()) { File desktopIni = new TFile(localBase, FileUtils.DESKTOP_INI_FILENAME); if (desktopIni.exists() && desktopIni.delete()) { scanChangedFile(FileInfoFactory .lookupInstance(this, desktopIni)); } } watcher = new FolderWatcher(this); // PFC-2318: Workaround if (diskItemFilter.getPatterns().isEmpty() && !isDeviceDisconnected()) { addDefaultExcludes(); } } public void addProblemListener(ProblemListener l) { Reject.ifNull(l, "Listener is null"); ListenerSupportFactory.addListener(problemListenerSupport, l); } public void removeProblemListener(ProblemListener l) { Reject.ifNull(l, "Listener is null"); ListenerSupportFactory.removeListener(problemListenerSupport, l); } /** * Add a problem to the list of problems. * * @param problem */ public void addProblem(Problem problem) { problems.add(problem); problemListenerSupport.problemAdded(problem); logFiner("Added problem"); } /** * Remove a problem from the list of known problems. * * @param problem */ public void removeProblem(Problem problem) { boolean removed = problems.remove(problem); if (removed) { problemListenerSupport.problemRemoved(problem); } else { logWarning("Failed to remove problem"); } } /** * Remove all problems from the list of known problems. */ public void removeAllProblems() { List<Problem> list = new ArrayList<Problem>(); list.addAll(problems); problems.clear(); for (Problem problem : list) { problemListenerSupport.problemRemoved(problem); } } /** * @return Count problems in folder? */ public int countProblems() { return problems.size(); } /** * @return unmodifyable list of problems. */ public List<Problem> getProblems() { return Collections.unmodifiableList(problems); } public void setArchiveVersions(int versions) { int oldVersions = archiver.getVersionsPerFile(); if (oldVersions == versions) { return; } archiver.setVersionsPerFile(versions); String syncProfKey = FOLDER_SETTINGS_PREFIX_V4 + configEntryId + FolderSettings.FOLDER_SETTINGS_VERSIONS; getController().getConfig().put(syncProfKey, String.valueOf(versions)); getController().saveConfig(); fireArchiveSettingsChanged(); } /** * @return the FileArchiver used */ public FileArchiver getFileArchiver() { return archiver; } /** * @return the watcher of this folder. */ public FolderWatcher getFolderWatcher() { return watcher; } /** * Commits the scan results into the internal file database. Changes get * broadcasted to other members if necessary. * * @param scanResult * the scanresult to commit. * @param ignoreLocalMassDeletions * bypass the local mass delete checks. */ private void commitScanResult(ScanResult scanResult, boolean ignoreLocalMassDeletions) { // See if everything has been deleted. if (!ignoreLocalMassDeletions && getKnownItemCount() > 0 && !scanResult.getDeletedFiles().isEmpty() && scanResult.getTotalFilesCount() == 0 && PreferencesEntry.EXPERT_MODE.getValueBoolean(getController()) && ConfigurationEntry.MASS_DELETE_PROTECTION .getValueBoolean(getController())) { // Advise controller of the carnage. getController().localMassDeletionDetected( new LocalMassDeletionEvent(this)); return; } synchronized (scanLock) { synchronized (dbAccessLock) { // new files if (isFiner()) { logFiner("Adding " + scanResult.getNewFiles().size() + " to directory"); } // New files store(getController().getMySelf(), scanResult.newFiles); // deleted files store(getController().getMySelf(), scanResult.deletedFiles); // restored files store(getController().getMySelf(), scanResult.restoredFiles); // changed files store(getController().getMySelf(), scanResult.changedFiles); } } hasOwnDatabase = true; if (isFine()) { logFine("Scanned " + scanResult.getTotalFilesCount() + " total, " + scanResult.getChangedFiles().size() + " changed, " + scanResult.getNewFiles().size() + " new, " + scanResult.getRestoredFiles().size() + " restored, " + scanResult.getDeletedFiles().size() + " removed, " + scanResult.getProblemFiles().size() + " problems"); } // Fire scan result fireScanResultCommited(scanResult); if (scanResult.isChangeDetected()) { // Check for identical files findSameFilesOnRemote(); setDBDirty(); // broadcast changes on folder broadcastFolderChanges(scanResult); } if (isFiner()) { logFiner("commitScanResult DONE"); } } public boolean hasOwnDatabase() { return hasOwnDatabase; } public DiskItemFilter getDiskItemFilter() { return diskItemFilter; } /** * Convenience method to add a pattern if it does not exist. * * @param pattern */ public void addPattern(String pattern) { diskItemFilter.addPattern(pattern); triggerPersist(); } /** * Convenience method to remove a pattern. * * @param pattern */ public void removePattern(String pattern) { diskItemFilter.removePattern(pattern); triggerPersist(); } /** * Retrieves the transferpriorities for file in this folder. * * @return the associated TransferPriorities object */ public TransferPriorities getTransferPriorities() { return transferPriorities; } public ScanResult.ResultState getLastScanResultState() { return lastScanResultState; } /** * Checks the basedir is valid * * @throws FolderException * if base dir is not ok */ private void checkBaseDir(boolean quite) throws FolderException { // Basic checks if (!localBase.exists()) { // TRAC #1249 if ((OSUtil.isMacOS() || OSUtil.isLinux()) && localBase.getAbsolutePath().toLowerCase() .startsWith("/volumes")) { throw new FolderException(currentInfo, "Unmounted volume not available at " + localBase.getAbsolutePath()); } // #2329 throw new FolderException(currentInfo, "Local base dir not available " + localBase.getAbsolutePath()); // Old code: // if (!localBase.mkdirs()) { // if (!quite) { // logSevere(" not able to create folder(" + getName() // + "), (sub) dir (" + localBase + ") creation failed"); // } // throw new FolderException(currentInfo, // "Unable to create folder at " + localBase.getAbsolutePath()); // } else { // // logWarning("Created base dir at " + localBase, new // RuntimeException("here")); // } } else if (!localBase.isDirectory()) { if (!quite) { logSevere(" not able to create folder(" + getName() + "), (sub) dir (" + localBase + ") is no dir"); } throw new FolderException(currentInfo, Translation.getTranslation( "foldercreate.error.unable_to_open", localBase.getAbsolutePath())); } // Complex checks FolderRepository repo = getController().getFolderRepository(); if (repo.getFoldersBasedir().equals(localBase)) { throw new FolderException(currentInfo, Translation.getTranslation( "foldercreate.error.it_is_base_dir", localBase.getAbsolutePath())); } } /* * Local disk/folder management */ /** * Scans a downloaded file, renames tempfile to real name moves possible * existing file to file archive. * * @param fInfo * @param tempFile * @return true if the download could be completed and the file got scanned. * false if any problem happend. */ public boolean scanDownloadFile(FileInfo fInfo, File tempFile) { try { watcher.addIgnoreFile(fInfo); return scanDownloadFile0(fInfo, tempFile); } finally { watcher.removeIgnoreFile(fInfo); } } private boolean scanDownloadFile0(FileInfo fInfo, File tempFile) { // FIXME What happens if the file was locally modified before the // download finished? There should be a check here if the current local // version differs from the version when the download began. In that // case a conflict has to be raised! // rename file File targetFile = fInfo.getDiskFile(getController() .getFolderRepository()); if (!targetFile.getParentFile().exists()) { targetFile.getParentFile().mkdirs(); } if (!targetFile.getParentFile().isDirectory()) { boolean ok = false; // Hack to solve the rarely occurring 0 byte directory files if (targetFile.getParentFile().isFile() && targetFile.getParentFile().length() == 0) { if (targetFile.getParentFile().delete()) { ok = targetFile.getParentFile().mkdirs(); } } if (!ok) { logWarning("Unable to scan downloaded file. Parent dir is not a directory: " + targetFile + ". " + fInfo.toDetailString()); return false; } } synchronized (scanLock) { // Prepare last modification date of tempfile. if (!tempFile.setLastModified(fInfo.getModifiedDate().getTime())) { logSevere("Failed to set modified date on " + tempFile + " for " + fInfo.getModifiedDate().getTime()); return false; } if (targetFile.exists()) { // if file was a "newer file" the file already exists here // Using local var because of possible race condition!! FileArchiver arch = archiver; if (arch != null) { try { FileInfo oldLocalFileInfo = fInfo .getLocalFileInfo(getController() .getFolderRepository()); if (oldLocalFileInfo != null) { if (!currentInfo.isMetaFolder() && ConfigurationEntry.CONFLICT_DETECTION .getValueBoolean(getController())) { try { doSimpleConflictDetection(fInfo, targetFile, oldLocalFileInfo); } catch (Exception e) { logSevere("Problem withe conflict detection. " + e); } } arch.archive(oldLocalFileInfo, targetFile, false); } } catch (IOException e) { // Same behavior as below, on failure drop out // TODO Maybe raise folder-problem.... logWarning("Unable to archive old file. " + e); return false; } } if (targetFile.exists() && !targetFile.delete()) { logWarning("Unable to scan downloaded file. Was not able to move old file to file archive " + targetFile.getAbsolutePath() + ". " + fInfo.toDetailString()); return false; } } if (!tempFile.renameTo(targetFile)) { logWarning("Was not able to rename tempfile, copiing " + tempFile.getAbsolutePath() + " to " + targetFile.getAbsolutePath() + ". " + fInfo.toDetailString()); try { FileUtils.copyFile(tempFile, targetFile); } catch (IOException e) { // TODO give a diskfull warning? logSevere("Unable to store completed download " + targetFile.getAbsolutePath() + ". " + e.getMessage() + ". " + fInfo.toDetailString()); logFiner(e); return false; } // Set modified date of remote // TODO: Set last modified only if required if (!targetFile.setLastModified(fInfo.getModifiedDate() .getTime())) { logSevere("Failed to set modified date on " + targetFile + " to " + fInfo.getModifiedDate().getTime()); return false; } if (tempFile.exists() && !tempFile.delete()) { logSevere("Unable to remove temp file: " + tempFile); } } synchronized (dbAccessLock) { // Update internal database store(getController().getMySelf(), correctFolderInfo(fInfo)); fileChanged(fInfo); } } return true; } private FileInfo doSimpleConflictDetection(FileInfo fInfo, File targetFile, FileInfo oldLocalFileInfo) { boolean conflict = oldLocalFileInfo.getVersion() == fInfo.getVersion() && fInfo.isNewerThan(oldLocalFileInfo) && (oldLocalFileInfo.getVersion() + fInfo.getVersion() != 0); conflict |= oldLocalFileInfo.getVersion() <= fInfo.getVersion() && DateUtil.isNewerFileDateCrossPlattform( oldLocalFileInfo.getModifiedDate(), fInfo.getModifiedDate()); if (conflict) { logWarning("Conflict detected on file " + fInfo.toDetailString() + ". old: " + oldLocalFileInfo.toDetailString()); // Really basic raw conflict detection. addProblem(new FileConflictProblem(fInfo)); // String fn = fInfo.getFilenameOnly(); // String extraInfo = "_"; // extraInfo += oldLocalFileInfo.getModifiedBy().getNick(); // extraInfo += "_"; // extraInfo += oldLocalFileInfo.getVersion(); // if (fn.contains(".")) { // int i = fn.lastIndexOf('.'); // fn = fn.substring(0, i) + extraInfo // + fn.substring(i, fn.length()); // } else { // fn += extraInfo; // } // File oldCopy = new File(targetFile.getParentFile(), // FileUtils.removeInvalidFilenameChars(fn)); // FileInfo oldCopyFInfo = FileInfoFactory.lookupInstance(this, // oldCopy); // watcher.addIgnoreFile(oldCopyFInfo); // try { // FileUtils.copyFile(targetFile, oldCopy); // logInfo("Saved copy of conflicting file to " + oldCopy); // return scanChangedFile(oldCopyFInfo); // } catch (Exception e) { // logWarning("Unable to save old copy on conflict file to " // + oldCopy + ": " + e); // } finally { // watcher.removeIgnoreFile(oldCopyFInfo); // // } } return null; } /** * Scans the local directory for new files. Be carefull! This method is not * Thread safe. In most cases you want to use * recommendScanOnNextMaintenance() followed by maintain(). * * @return if the local files where scanned */ public boolean scanLocalFiles() { return scanLocalFiles(false); } /** * Scans the local directory for new files. Be careful! This method is not * Thread safe. In most cases you want to use * recommendScanOnNextMaintenance() followed by maintain(). * * @param ignoreLocalMassDeletion * bypass the local mass delete checks. * @return if the local files where scanned */ public boolean scanLocalFiles(boolean ignoreLocalMassDeletion) { checkIfDeviceDisconnected(); ScanResult result; FolderScanner scanner = getController().getFolderRepository() .getFolderScanner(); // Acquire the folder wait boolean scannerBusy; do { synchronized (scanLock) { result = scanner.scanFolder(this); } scannerBusy = ScanResult.ResultState.BUSY == result .getResultState(); if (scannerBusy) { logFine("Folder scanner is busy, waiting..."); try { Thread.sleep(50); } catch (InterruptedException e) { logFiner(e); return false; } } } while (scannerBusy); if (checkIfDeviceDisconnected()) { if (isFiner()) { logFiner("Device disconnected while scanning folder: " + localBase); } return false; } try { if (result.getResultState() == ScanResult.ResultState.SCANNED) { // Push any file problems into the Folder's problems. Map<FileInfo, List<Problem>> filenameProblems = result .getProblemFiles(); for (Map.Entry<FileInfo, List<Problem>> fileInfoListEntry : filenameProblems .entrySet()) { for (Problem problem : fileInfoListEntry.getValue()) { addProblem(problem); } } commitScanResult(result, ignoreLocalMassDeletion); lastScan = new Date(); return true; } // scan aborted, hardware broken, mass local delete? return false; } finally { lastScanResultState = result.getResultState(); checkLastSyncDate(); } } /** * @return true if a scan in the background is required of the folder */ private boolean autoScanRequired() { if (syncProfile.isManualSync()) { return false; } Date wasLastScan = lastScan; if (wasLastScan == null) { return true; } if (syncProfile.isInstantSync()) { long secondsSinceLastSync = (System.currentTimeMillis() - wasLastScan .getTime()) / 1000; int items = getKnownItemCount(); // Dynamically adapt fallback scan time. // The less files we have, the faster we scan. // 0 files = every minute (MIN) // 10.000 files = every minute // 50.000 files = every 5 minutes (MAX) // 130.000 files = every 5 minutes (MAX) // If folder watch is not supported filesystem is scanned every minute int frequency = (int) (6L * items / 1000L); int setFrequency = syncProfile.getSecondsBetweenScans(); if (setFrequency < 0) { // No scanning supported return false; } // Min if (frequency < setFrequency) { frequency = setFrequency; } // Max if (watcher.isSupported()) { if (!syncProfile.isCustom() && frequency > FIVE_MINUTES) { frequency = FIVE_MINUTES; } } else { // Fallback for not supported watcher frequency = setFrequency; } if (secondsSinceLastSync < frequency) { if (isFiner()) { logFiner("Skipping regular scan"); } return false; } } else if (syncProfile.isDailySync()) { if (!shouldDoDailySync()) { if (isFiner()) { logFiner("Skipping daily scan"); } return false; } } else if (syncProfile.isPeriodicSync()) { long secondsSinceLastSync = (System.currentTimeMillis() - wasLastScan .getTime()) / 1000; if (secondsSinceLastSync < syncProfile.getSecondsBetweenScans()) { if (isFiner()) { logFiner("Skipping regular scan"); } return false; } } else { logSevere("Do not know what sort of sync to do!!! Folder = " + getName() + ", instant = " + syncProfile.getConfiguration().isInstantSync() + ", daily = " + syncProfile.getConfiguration().isDailySync() + ", periodic = " + syncProfile.getConfiguration().isDailySync()); } return true; } /** * @return true if a daily scan is required. */ private boolean shouldDoDailySync() { Calendar lastScannedCalendar = new GregorianCalendar(); lastScannedCalendar.setTime(lastScan); int lastScannedDay = lastScannedCalendar.get(Calendar.DAY_OF_YEAR); if (isFiner()) { logFiner("Last scanned " + lastScannedCalendar.getTime()); } Calendar todayCalendar = new GregorianCalendar(); todayCalendar.setTime(new Date()); int currentDayOfYear = todayCalendar.get(Calendar.DAY_OF_YEAR); if (lastScannedDay == currentDayOfYear && lastScannedCalendar.get(Calendar.YEAR) == todayCalendar .get(Calendar.YEAR)) { // Scanned today, so skip. if (isFiner()) { logFiner("Skipping daily scan (already scanned today)"); } return false; } int requiredSyncHour = syncProfile.getConfiguration().getDailyHour(); int currentHour = todayCalendar.get(Calendar.HOUR_OF_DAY); if (requiredSyncHour > currentHour) { // Not correct time, so skip. if (isFiner()) { logFiner("Skipping daily scan (not correct time) " + requiredSyncHour + " > " + currentHour); } return false; } int requiredSyncDay = syncProfile.getConfiguration().getDailyDay(); int currentDay = todayCalendar.get(Calendar.DAY_OF_WEEK); // Check daily synchronization day of week. if (requiredSyncDay != SyncProfileConfiguration.DAILY_DAY_EVERY_DAY) { if (requiredSyncDay == SyncProfileConfiguration.DAILY_DAY_WEEKDAYS) { if (currentDay == Calendar.SATURDAY || currentDay == Calendar.SUNDAY) { if (isFiner()) { logFiner("Skipping daily scan (not weekday)"); } return false; } } else if (requiredSyncDay == SyncProfileConfiguration.DAILY_DAY_WEEKENDS) { if (currentDay != Calendar.SATURDAY && currentDay != Calendar.SUNDAY) { if (isFiner()) { logFiner("Skipping daily scan (not weekend)"); } return false; } } else { if (currentDay != requiredSyncDay) { if (isFiner()) { logFiner("Skipping daily scan (not correct day)"); } return false; } } } return true; } /** * Scans a new, deleted or restored File. * * @param fileInfo * the file to scan * @return the new {@link FileInfo} or null if file was not actually changed */ public FileInfo scanChangedFile(FileInfo fileInfo) { Reject.ifNull(fileInfo, "FileInfo is null"); FileInfo localFileInfo = scanChangedFile0(fileInfo); if (localFileInfo != null) { FileInfo existinfFInfo = findSameFile(localFileInfo); if (existinfFInfo != null) { localFileInfo = existinfFInfo; } fileChanged(localFileInfo); } return localFileInfo; } /** * Scans all parent directories of a file. Useful after restoring single * files in deleted subdirs. * * @param fileInfo */ public void scanAllParentDirectories(FileInfo fileInfo) { FileInfo dirInfo = fileInfo.getDirectory(); dirInfo = getFile(dirInfo); if (dirInfo == null || !dirInfo.isDeleted()) { // No need. return; } DirectoryInfo baseDir = getBaseDirectoryInfo(); int i = 0; while (!dirInfo.equals(baseDir)) { if (isFiner()) { logFiner("Scanning parent dir: " + dirInfo); } scanChangedFile(dirInfo); dirInfo = dirInfo.getDirectory(); if (i++ > 10000) { break; } } } /** * Scans multiple new, deleted or restored File callback for * {@link #getFolderWatcher()} only. * * @param fileInfos * the files to scan. ATTENTION: Does modify the {@link List} */ void scanChangedFiles(final List<FileInfo> fileInfos) { Reject.ifNull(fileInfos, "FileInfo collection is null"); boolean checkRevert = isRevertLocalChanges(); int i = 0; for (Iterator<FileInfo> it = fileInfos.iterator(); it.hasNext();) { FileInfo fileInfo = (FileInfo) it.next(); FileInfo localFileInfo = scanChangedFile0(fileInfo); if (localFileInfo == null) { // No change it.remove(); } else { if (checkRevert && checkRevertLocalChanges(localFileInfo)) { // No change it.remove(); } else { // Allowed to change files FileInfo existinfFInfo = findSameFile(localFileInfo); if (existinfFInfo != null) { localFileInfo = existinfFInfo; } fileInfos.set(i, localFileInfo); i++; } } } if (!fileInfos.isEmpty()) { fireFilesChanged(fileInfos); setDBDirty(); broadcastMessages(new MessageProducer() { public Message[] getMessages(boolean useExt) { return FolderFilesChanged.create(currentInfo, fileInfos, diskItemFilter, useExt); } }); } } /** * Scans one file and updates the internal db if required. * <p> * * @param fInfo * the file to be scanned * @return null, if the file hasn't changed, the new FileInfo otherwise */ private FileInfo scanChangedFile0(FileInfo fInfo) { if (isFiner()) { logFiner("Scanning file: " + fInfo + ", folderId: " + fInfo); } File file = getDiskFile(fInfo); // ignore our database file if (file.getName().equals(Constants.DB_FILENAME) || file.getName().equals(Constants.DB_BACKUP_FILENAME)) { logFiner("Ignoring folder database file: " + file); return null; } checkFileName(fInfo); // First relink modified by memberinfo to // actual instance if available on nodemanager long start = System.currentTimeMillis(); try { synchronized (scanLock) { synchronized (dbAccessLock) { FileInfo localFile = getFile(fInfo); if (localFile == null) { if (isFiner()) { logFiner("Scan new file: " + fInfo.toDetailString()); } // Update last - modified data MemberInfo modifiedBy = fInfo.getModifiedBy(); if (modifiedBy == null) { modifiedBy = getController().getMySelf().getInfo(); } Member from = modifiedBy.getNode(getController(), true); Date modDate; long size; boolean deleted; if (fInfo.isLookupInstance()) { size = 0; modDate = new Date(); deleted = !file.exists(); } else { size = fInfo.getSize(); modDate = fInfo.getModifiedDate(); deleted = fInfo.isDeleted(); } if (from != null) { modifiedBy = from.getInfo(); } if (file.exists()) { modDate = new Date(file.lastModified()); size = file.length(); } if (deleted) { fInfo = FileInfoFactory.unmarshallDeletedFile( currentInfo, fInfo.getRelativeName(), modifiedBy, modDate, fInfo.getVersion(), file.isDirectory()); } else { fInfo = FileInfoFactory.unmarshallExistingFile( currentInfo, fInfo.getRelativeName(), size, modifiedBy, modDate, fInfo.getVersion(), file.isDirectory()); } store(getController().getMySelf(), fInfo); // get folder icon info and set it if (FileUtils.isDesktopIni(file)) { makeFolderIcon(file); } // Fire folder change event // fireEvent(new FolderChanged()); if (isFiner()) { logFiner(toString() + ": Local file scanned: " + fInfo.toDetailString()); } return fInfo; } FileInfo syncFile = localFile.syncFromDiskIfRequired(this, file); if (syncFile != null) { store(getController().getMySelf(), syncFile); if (isFiner()) { logFiner("Scan file changed: " + syncFile.toDetailString()); } } else { if (isFiner()) { logFiner("Scan file unchanged: " + localFile.toDetailString()); } } return syncFile; } } } finally { long took = System.currentTimeMillis() - start; if (isWarning() && took > 60 * 1000L) { logWarning("Scanning file took " + (took / 1000) + "s: " + fInfo.toDetailString()); } } } /** * Creates/Deletes and scans one directory. * * @param dirInfo * the dir to be scanned. * @param dir * the directory */ public void scanDirectory(FileInfo dirInfo, File dir) { Reject.ifNull(dirInfo, "DirInfo is null"); if (isFiner()) { logFiner("Scanning dir: " + dirInfo.toDetailString()); } if (!dirInfo.getFolderInfo().equals(currentInfo)) { logSevere("Unable to scan of directory. not on folder: " + dirInfo.toDetailString()); return; } if (dir.equals(getSystemSubDir0())) { logWarning("Ignoring system subdirectory: " + dir); return; } watcher.addIgnoreFile(dirInfo); try { synchronized (scanLock) { if (dirInfo.isDeleted()) { if (!dir.delete()) { logSevere("Unable to deleted directory: " + dir + ". " + dirInfo.toDetailString()); return; } } else { // #2627 / ASR-771-79727 if (dir.exists() && dir.isFile() && dir.length() == 0) { dir.delete(); } dir.mkdirs(); dir.setLastModified(dirInfo.getModifiedDate().getTime()); } } } finally { watcher.removeIgnoreFile(dirInfo); } store(getController().getMySelf(), correctFolderInfo(dirInfo)); setDBDirty(); } /** * Checks a single filename if there are problems with the name * * @param fileInfo */ private void checkFileName(FileInfo fileInfo) { List<Problem> problemList = FilenameProblemHelper.getProblems( getController(), fileInfo); for (Problem problem : problemList) { addProblem(problem); } } /** * Corrects the folder info * * @param theFInfo */ private FileInfo correctFolderInfo(FileInfo theFInfo) { // Add to this folder FileInfo fInfo = FileInfoFactory.changedFolderInfo(theFInfo, currentInfo); TransferPriority prio = transferPriorities.getPriority(fInfo); transferPriorities.setPriority(fInfo, prio); return fInfo; } /** * @param fi * @return if this file is known to the internal db */ public boolean isKnown(FileInfo fi) { return hasFile(fi); } /** * Removes a file on local folder, diskfile will be removed and file tagged * as deleted * * @param fInfo * @return The new deleted FileInfo, null if unchanged. */ private FileInfo removeFileLocal(FileInfo fInfo) { if (isFiner()) { logFiner("Remove file local: " + fInfo + ", Folder equal ? " + Util.equals(fInfo.getFolderInfo(), currentInfo)); } if (!isKnown(fInfo)) { if (isWarning()) { logWarning("Tried to remove a unknown file: " + fInfo.toDetailString()); } return null; } // Abort transfers files if (fInfo.isFile()) { getController().getTransferManager().breakTransfers(fInfo); } File diskFile = getDiskFile(fInfo); boolean folderChanged = false; synchronized (scanLock) { if (diskFile != null && diskFile.exists()) { if (!deleteFile(fInfo, diskFile)) { logWarning("Unable to remove local file. Was not able to move old file to file archive " + diskFile.getAbsolutePath() + ". " + fInfo.toDetailString()); // Failure. return null; } FileInfo localFile = getFile(fInfo); FileInfo synced = localFile.syncFromDiskIfRequired(this, diskFile); folderChanged = synced != null; if (folderChanged) { store(getController().getMySelf(), synced); return synced; } } } return null; } /** * Removes files from the local disk * * @param fInfos */ public void removeFilesLocal(FileInfo... fInfos) { removeFilesLocal(Arrays.asList(fInfos)); } /** * Removes files from the local disk * * @param fInfos */ public void removeFilesLocal(Collection<FileInfo> fInfos) { Reject.ifNull(fInfos, "Files null"); if (fInfos.isEmpty()) { return; } final List<FileInfo> removedFiles = new ArrayList<FileInfo>(); Comparator<FileInfo> comparator = new ReverseComparator<FileInfo>( FileInfoComparator .getComparator(FileInfoComparator.BY_RELATIVE_NAME)); Set<FileInfo> dirs = new TreeSet<FileInfo>(comparator); synchronized (scanLock) { for (FileInfo fileInfo : fInfos) { if (fileInfo.isDiretory()) { dirs.add(fileInfo); continue; } FileInfo deletedFileInfo = removeFileLocal(fileInfo); if (deletedFileInfo != null) { removedFiles.add(deletedFileInfo); } } for (FileInfo dirInfo : dirs) { FileInfoCriteria c = new FileInfoCriteria(); c.addMySelf(this); c.setPath((DirectoryInfo) dirInfo); logInfo("Deleting directory: " + dirInfo); removeFilesLocal(dao.findFiles(c)); FileInfo deletedDirInfo = removeFileLocal(dirInfo); if (deletedDirInfo != null) { removedFiles.add(deletedDirInfo); } } } if (!removedFiles.isEmpty()) { fireFilesDeleted(removedFiles); setDBDirty(); broadcastMessages(new MessageProducer() { public Message[] getMessages(boolean useExt) { return FolderFilesChanged.create(currentInfo, removedFiles, diskItemFilter, useExt); } }); } } private void initFileInfoDAO() { if (dao != null) { // Stop old DAO dao.stop(); } dao = new FileInfoDAOHashMapImpl(getController().getMySelf().getId(), diskItemFilter); // File daoDir = new File(getSystemSubDir(), "db/h2"); // try { // FileUtils.recursiveDelete(daoDir.getParentFile()); // } catch (IOException e) { // // TODO Auto-generated catch block // e.printStackTrace(); // } // dao = new FileInfoDAOSQLImpl(getController(), "jdbc:h2:" + daoDir, // "sa", "", null); } /** * Loads the folder database from disk * * @param dbFile * the file to load as db file * @return true if succeeded */ // @SuppressWarnings("unchecked") @SuppressWarnings({"unchecked"}) private boolean loadFolderDB(File dbFile) { synchronized (scanLock) { if (!dbFile.exists()) { logFine(this + ": Database file not found: " + dbFile.getAbsolutePath()); return false; } InputStream fIn = null; ObjectInputStream in = null; try { // load files and scan in fIn = new BufferedInputStream(new TFileInputStream(dbFile)); in = new ObjectInputStream(fIn); FileInfo[] files = (FileInfo[]) in.readObject(); // Convert.cleanMemberInfos(getController().getNodeManager(), // files); synchronized (dbAccessLock) { for (int i = 0; i < files.length; i++) { FileInfo fInfo = files[i]; files[i] = correctFolderInfo(fInfo); if (fInfo != files[i]) { // Instance has changes. setDBDirty(); } } // Help with initial capacity info. dao.deleteDomain(null, files.length); dao.store(null, files); } // read them always .. MemberInfo[] members1 = (MemberInfo[]) in.readObject(); // Do not load members logFiner("Loading " + members1.length + " members"); for (MemberInfo memberInfo : members1) { Member member = memberInfo.getNode(getController(), true); if (member.isMySelf()) { continue; } join0(member, !getController().isStarted()); } // Send filelist to connected members for (Member member : getConnectedMembers()) { if (hasReadPermission(member)) { member.sendMessagesAsynchron(FileList.create(this, supportExternalizable(member))); } else { member.sendMessagesAsynchron(FileList.createEmpty( currentInfo, supportExternalizable(member))); } } // Old blacklist explicit items. // Now disused, but maintained for backward compatability. try { Object object = in.readObject(); Collection<FileInfo> infos = (Collection<FileInfo>) object; for (FileInfo info : infos) { diskItemFilter.addPattern(info.getRelativeName()); if (isFiner()) { logFiner("ignore@" + info.getRelativeName()); } } } catch (EOFException e) { logFiner("No ignore list"); } catch (Exception e) { logSevere("read ignore error: " + this + e.getMessage(), e); } catch (OutOfMemoryError e) { logWarning("Read ignore error: " + this + " on " + dbFile + ": " + e.getMessage()); } try { Object object = in.readObject(); if (object instanceof Date) { lastScan = (Date) object; if (isFiner()) { logFiner("lastScan " + lastScan); } } } catch (EOFException e) { // ignore nothing available for ignore logFine("No last scan date"); } catch (Exception e) { logSevere("read ignore error: " + this + e.getMessage(), e); } in.close(); fIn.close(); logFine("Loaded folder database (" + files.length + " files) from " + dbFile.getAbsolutePath()); } catch (Exception e) { logWarning(this + ": Unable to read database file: " + dbFile.getAbsolutePath() + ". " + e); logFiner(e); return false; } finally { if (in != null) { try { in.close(); } catch (IOException e) { } } if (fIn != null) { try { fIn.close(); } catch (IOException e) { } } } // Ok has own database hasOwnDatabase = true; } return true; } /** * Loads the metadata information of this folder. Folder database, ignore * patterns and last synchronized date. */ private void loadMetadata() { loadFolderDB(); loadLastSyncDate(); diskItemFilter.loadPatternsFrom(new TFile(getSystemSubDir0(), DiskItemFilter.PATTERNS_FILENAME), false); } /** * Loads the folder db from disk */ private void loadFolderDB() { if (loadFolderDB(new TFile(localBase, Constants.POWERFOLDER_SYSTEM_SUBDIR + '/' + Constants.DB_FILENAME))) { return; } if (loadFolderDB(new TFile(localBase, Constants.POWERFOLDER_SYSTEM_SUBDIR + '/' + Constants.DB_BACKUP_FILENAME))) { return; } logFine("Unable to read folder db, even from backup. Maybe new folder?"); } /** * Shuts down the folder */ public void shutdown() { if (isFine()) { logFine("Shutting down folder " + this); } shutdown = true; watcher.remove(); if (dirty) { persist(); } if (diskItemFilter.isDirty() && !checkIfDeviceDisconnected()) { diskItemFilter.savePatternsTo(new TFile(getSystemSubDir(), DiskItemFilter.PATTERNS_FILENAME), true); savePatternsToMetaFolder(); } dao.stop(); removeAllListeners(); ListenerSupportFactory.removeAllListeners(folderListenerSupport); ListenerSupportFactory .removeAllListeners(folderMembershipListenerSupport); diskItemFilter.removeAllListener(); if (encrypted && !currentInfo.isMetaFolder()) { try { TFile.umount(localBase); } catch (Throwable e) { logWarning("Problem unmounting " + localBase + ". " + e); } } } /** * This is the date that the folder last 100% synced with other members. It * may be null if never synchronized externally. * * @return the last sync date. */ public Date getLastSyncDate() { return lastSyncDate; } /** * Stores the current file-database to disk */ private boolean storeFolderDB() { File dbTempFile = new TFile(getSystemSubDir(), Constants.DB_FILENAME + FileUtils.removeInvalidFilenameChars(getController().getMySelf() .getId()) + ".writing"); File dbFile = new TFile(getSystemSubDir(), Constants.DB_FILENAME); File dbFileBackup = new TFile(getSystemSubDir(), Constants.DB_BACKUP_FILENAME); OutputStream fOut = null; ObjectOutputStream oOut = null; try { FileInfo[] diskItems; synchronized (dbAccessLock) { Collection<FileInfo> files = dao.findAllFiles(null); Collection<DirectoryInfo> dirs = dao.findAllDirectories(null); diskItems = new FileInfo[files.size() + dirs.size()]; int i = 0; for (FileInfo fileInfo : files) { diskItems[i] = fileInfo; i++; } for (DirectoryInfo dirInfo : dirs) { diskItems[i] = dirInfo; i++; } } if (dbTempFile.exists()) { if (!dbTempFile.delete()) { logSevere("Failed to delete temp database file: " + dbTempFile); return false; } } if (!dbTempFile.createNewFile()) { logSevere("Failed to create temp database file: " + dbTempFile); return false; } fOut = new BufferedOutputStream(new TFileOutputStream(dbTempFile)); oOut = new ObjectOutputStream(fOut); // Store files oOut.writeObject(diskItems); // Store members oOut.writeObject(Convert.asMemberInfos(getMembersAsCollection() .toArray(new Member[getMembersAsCollection().size()]))); // Old blacklist. Maintained for backward serialization // compatability. Do not remove. oOut.writeObject(Collections.emptyList()); if (lastScan == null) { if (isFiner()) { logFiner("write default time: " + new Date()); } oOut.writeObject(new Date()); } else { if (isFiner()) { logFiner("write lastScan: " + lastScan); } oOut.writeObject(lastScan); } oOut.close(); fOut.close(); if (isFine()) { logFine("Successfully wrote folder database file (" + diskItems.length + " disk items)"); } // Put in the right place: boolean copy = true; if (dbFile.exists()) { if (dbFile.delete()) { if (dbTempFile.renameTo(dbFile)) { copy = false; } } } if (copy) { FileUtils.copyFile(dbTempFile, dbFile); } if (dbFileBackup.exists()) { dbFileBackup.delete(); } if (copy && !dbTempFile.delete()) { logWarning("Failed to delete temp database file: " + dbTempFile); } // TODO Remove this in later version // Cleanup for older versions File oldDbFile = new TFile(localBase, Constants.DB_FILENAME); if (!oldDbFile.delete()) { logFiner("Failed to delete 'old' database file: " + oldDbFile); } File oldDbFileBackup = new TFile(localBase, Constants.DB_BACKUP_FILENAME); if (!oldDbFileBackup.delete()) { logFiner("Failed to delete backup of 'old' database file: " + oldDbFileBackup); } return true; } catch (IOException e) { logWarning(this + ": Unable to write database file " + dbFile.getAbsolutePath() + ". " + e); logFiner(e); return false; } finally { if (oOut != null) { try { oOut.close(); } catch (Exception e2) { } } if (fOut != null) { try { fOut.close(); } catch (IOException e) { } } } } private boolean maintainFolderDBrequired() { if (getKnownItemCount() == 0) { return false; } if (lastDBMaintenance == null) { return true; } return lastDBMaintenance.getTime() + ConfigurationEntry.DB_MAINTENANCE_SECONDS .getValueInt(getController()) * 1000L < System .currentTimeMillis(); } /** * Creates a list of fileinfos of deleted files that are old than the * configured max age. These files do not get written to the DB. So files * deleted long ago do not stay in DB for ever. * <p> * Also: #2759 Check sync consistency * * @param removeBefore */ public void maintainFolderDB(long removeBefore) { Date removeBeforeDate = new Date(removeBefore); int nFilesBefore = getKnownItemCount(); if (isFiner()) { logFiner("Maintaining folder db, known files: " + nFilesBefore + ". Expiring deleted files older than " + removeBeforeDate); } int expired = 0; int keepDeleted = 0; int total = 0; List<FileInfo> brokenExisting = new LinkedList<FileInfo>(); for (FileInfo file : dao.findAllFiles(null)) { total++; if (!file.isDeleted()) { // #2759 Check sync consistency for (Member member : members.keySet()) { FileInfo remoteFile = member.getFile(file); if (remoteFile == null || remoteFile.getVersion() != file.getVersion()) { continue; } // Same version if (remoteFile.isVersionDateAndSizeIdentical(file)) { // File is ok continue; } boolean sameDate = remoteFile.getModifiedDate().equals( file.getModifiedDate()); boolean remoteOlder = !sameDate && remoteFile.getModifiedDate().before( file.getModifiedDate()); boolean remoteSmaller = remoteFile.getSize() < file .getSize(); if (remoteOlder || (sameDate && remoteSmaller)) { // Our version is "better" newer or bigger. // Increase version to force re-sync if (!brokenExisting.contains(file)) { brokenExisting.add(file); if (isWarning() && !currentInfo.isMetaFolder()) { logWarning("Fixing file entry. Local: " + file.toDetailString() + ".\n@" + member.getNick() + ": " + remoteFile.toDetailString()); } } } } continue; } if (file.getModifiedDate().before(removeBeforeDate)) { // Don't remove. We have archived files. if (archiver.hasArchivedFileInfo(file)) { continue; } expired++; // Remove dao.delete(null, file); for (Member member : members.values()) { dao.delete(member.getId(), file); } if (isFiner()) { logFiner("FileInfo expired: " + file.toDetailString()); } } else { keepDeleted++; } } if (!brokenExisting.isEmpty()) { for (int i = 0; i < brokenExisting.size(); i++) { FileInfo fileInfo = brokenExisting.get(i); FileInfo newFileInfo = FileInfoFactory.unmarshallExistingFile( currentInfo, fileInfo.getRelativeName(), fileInfo.getSize(), fileInfo.getModifiedBy(), fileInfo.getModifiedDate(), fileInfo.getVersion() + 1, fileInfo.isDiretory()); brokenExisting.set(i, newFileInfo); } store(getController().getMySelf(), brokenExisting); filesChanged(brokenExisting); } if (expired > 0 || brokenExisting.size() > 0) { setDBDirty(); logFine("Maintained folder db, " + nFilesBefore + " known files, " + expired + " expired FileInfos, " + brokenExisting.size() + " fixed entries. Expiring deleted files older than " + removeBeforeDate); statistic.scheduleCalculate(); } else if (isFiner()) { logFiner("Maintained folder db, " + nFilesBefore + " known files, " + expired + " expired FileInfos. Expiring deleted files older than " + removeBeforeDate); } lastDBMaintenance = new Date(); long max = Runtime.getRuntime().maxMemory() / 4181; if (total > max && total - keepDeleted * 2 < 0) { Problem fdp = new FolderDatabaseProblem(currentInfo); if (!getProblems().contains(fdp)) { addProblem(new FolderDatabaseProblem(currentInfo)); } } // Also maintain meta folder Folder mFolder = getController().getFolderRepository() .getMetaFolderForParent(currentInfo); if (mFolder != null) { mFolder.maintainFolderDB(removeBefore); } } /** * #2311: Revert local changes. * * @return */ private boolean isRevertLocalChanges() { boolean mySelfReadOnly = hasReadPermission(getController().getMySelf()) && !hasWritePermission(getController().getMySelf()); return mySelfReadOnly || syncProfile.equals(SyncProfile.BACKUP_TARGET); } private void checkRevertLocalChanges() { if (!isRevertLocalChanges()) { return; } if (isFine()) { logFine("Checking revert on my files"); } boolean reverted = false; for (FileInfo fileInfo : dao.findAllFiles(null)) { if (checkRevertLocalChanges(fileInfo)) { reverted = true; } } if (reverted) { getController().getFolderRepository().getFileRequestor() .triggerFileRequesting(currentInfo); syncRemoteDeletedFiles(false); } } private boolean checkRevertLocalChanges(FileInfo fileInfo) { if (getConnectedMembersCount() == 0) { // Don't check anything. No other partner. Keep everything return false; } FileInfo newestVersion = fileInfo.getNewestVersion(getController() .getFolderRepository()); if (newestVersion != null && !fileInfo.isNewerThan(newestVersion)) { // Ok in sync return false; } if (diskItemFilter.isExcluded(fileInfo)) { // Is excluded from sync. Don't delete. Might be meta-data. return false; } getFolderWatcher().addIgnoreFile(fileInfo); try { if (newestVersion == null) { logWarning("Reverting local change: " + fileInfo.toDetailString() + ". File not in repository."); } else { logWarning("Reverting local change: " + fileInfo.toDetailString() + ". Found newer version: " + newestVersion.toDetailString()); } File file = fileInfo.getDiskFile(getController() .getFolderRepository()); synchronized (scanLock) { if (file.exists()) { try { archiver.archive(fileInfo, file, false); if (file.exists()) { if (!file.delete()) { throw new IOException("Unable to revert: " + file); } } } catch (IOException e) { logWarning("Unable to revert changes on file " + file + ". Cannot overwrite local change. " + e); return false; } } dao.delete(null, fileInfo); } return true; } finally { getFolderWatcher().removeIgnoreFile(fileInfo); } } /** * Set the needed folder/file attributes on windows systems, if we have a * desktop.ini * * @param desktopIni */ private void makeFolderIcon(File desktopIni) { if (desktopIni == null) { throw new NullPointerException("File (desktop.ini) is null"); } if (!OSUtil.isWindowsSystem()) { logFiner("Not a windows system, ignoring folder icon. " + desktopIni.getAbsolutePath()); return; } logFiner("Setting icon of " + desktopIni.getParentFile().getAbsolutePath()); FileUtils.setAttributesOnWindows(desktopIni, true, true); } /** * Creates or removes a desktop shortcut for this folder. currently only * available on windows systems. * * @param active * true if the desktop shortcut should be created. * @return true if succeeded */ public boolean setDesktopShortcut(boolean active) { String shortCutName = getName(); if (getController().isVerbose()) { shortCutName = '[' + getController().getMySelf().getNick() + "] " + shortCutName; } if (active) { return Util.createDesktopShortcut(shortCutName, localBase.getAbsoluteFile()); } // Remove shortcuts to folder if not wanted Util.removeDesktopShortcut(shortCutName); return false; } /** * Deletes the desktop shortcut of the folder if set in prefs. */ public void removeDesktopShortcut() { String shortCutName = getName(); if (getController().isVerbose()) { shortCutName = '[' + getController().getMySelf().getNick() + "] " + shortCutName; } // Remove shortcuts to folder Util.removeDesktopShortcut(shortCutName); } /** * @return the script to be executed after a successful download or null if * none set. */ public String getDownloadScript() { return downloadScript; } /** * @param downloadScript * the new */ public void setDownloadScript(String downloadScript) { if (Util.equals(this.downloadScript, downloadScript)) { // Not changed return; } this.downloadScript = downloadScript; String confKey = FOLDER_SETTINGS_PREFIX_V4 + configEntryId + FolderSettings.FOLDER_SETTINGS_DOWNLOAD_SCRIPT; String confVal = downloadScript != null ? downloadScript : ""; getController().getConfig().put(confKey, confVal); logInfo("Download script set to '" + confVal + '\''); getController().saveConfig(); } /** * Gets the sync profile. * * @return the syncprofile of this folder */ public SyncProfile getSyncProfile() { return syncProfile; } /** * Sets the synchronisation profile for this folder. * * @param aSyncProfile */ public void setSyncProfile(SyncProfile aSyncProfile) { Reject.ifNull(aSyncProfile, "Unable to set null sync profile"); if (syncProfile.equals(aSyncProfile)) { // Skip. return; } logFine("Setting " + aSyncProfile.getName()); Reject.ifTrue(previewOnly, "Can not change Sync Profile in Preview mode."); syncProfile = aSyncProfile; if (!currentInfo.isMetaFolder()) { String syncProfKey = FOLDER_SETTINGS_PREFIX_V4 + configEntryId + FolderSettings.FOLDER_SETTINGS_SYNC_PROFILE; getController().getConfig().put(syncProfKey, syncProfile.getFieldList()); getController().saveConfig(); } if (!syncProfile.isAutodownload()) { // Possibly changed from autodownload to manual, we need to abort // all automatic download getController().getTransferManager().abortAllAutodownloads(this); } if (syncProfile.isAutodownload()) { // Trigger request files getController().getFolderRepository().getFileRequestor() .triggerFileRequesting(currentInfo); } if (syncProfile.isSyncDeletion()) { triggerSyncRemoteDeletedFiles(members.keySet()); } watcher.reconfigure(syncProfile); recommendScanOnNextMaintenance(); fireSyncProfileChanged(); } /** * Recommends the scan of the local filesystem on the next maintenace run. * Useful when files are detected that have been changed. * <p> * ATTENTION: Does not force a scan if continuous auto-detection is disabled * or scheduled sync is setup. */ public void recommendScanOnNextMaintenance() { recommendScanOnNextMaintenance(false); } /** * Recommends the scan of the local filesystem on the next maintenace run. * Useful when files are detected that have been changed. * <p> * ATTENTION: Does not force a scan if continuous auto-detection is disabled * or scheduled sync is setup unless manual. 'force' should only be true if * the user actually requests a scan from the local UI, like clicks the scan * button. * * @param force * user actually requested scan, override scanAllowedNow */ public void recommendScanOnNextMaintenance(boolean force) { if (scanAllowedNow() || force) { if (isFiner()) { logFiner("recommendScanOnNextMaintenance"); } scanForced = true; lastScan = null; } } /** * @return true if auto scanning files on-the-fly is allowed now. */ public boolean scanAllowedNow() { return !syncProfile.isManualSync() && !syncProfile.isDailySync() && !getController().isPaused(); } /** * Runs the maintenance on this folder. This means the folder gets synced * with remotesides. */ void maintain() { if (isFiner()) { logFiner("Maintaining '" + getName() + "' (forced? " + scanForced + ')'); } // local files boolean forcedNow = scanForced; scanForced = false; if (forcedNow || autoScanRequired()) { if (scanLocalFiles()) { checkRevertLocalChanges(); } } if (maintainFolderDBrequired()) { long removeBefore = System.currentTimeMillis() - 1000L * ConfigurationEntry.MAX_FILEINFO_DELETED_AGE_SECONDS .getValueInt(getController()); maintainFolderDB(removeBefore); } } /** * @return true if this folder requires the maintenance to be run. */ public boolean isMaintenanceRequired() { return scanForced || autoScanRequired() || maintainFolderDBrequired(); } /* * Member managing methods */ /** * Join a Member from its MemberInfo * * @param memberInfo */ private boolean join0(MemberInfo memberInfo) { if (memberInfo.isInvalid(getController())) { return false; } Member member = memberInfo.getNode(getController(), true); if (member.isMySelf()) { return false; } Date deadLine = new Date(System.currentTimeMillis() - Constants.NODE_TIME_TO_REMOVE_MEMBER); boolean offline2Long = memberInfo.getLastConnectTime() == null || memberInfo.getLastConnectTime().before(deadLine); if (offline2Long) { logFine(member + " was offline too long. " + "Hiding in memberslist: " + member + " last seen online: " + memberInfo.getLastConnectTime()); return false; } // Ok let him join return join(member); } /** * Joins a member to the folder, * * @param member * @return true if actually joined the folder. */ public boolean join(Member member) { boolean memberRead = hasReadPermission(member); boolean mySelfRead = hasReadPermission(getController().getMySelf()); if (!memberRead || !mySelfRead) { if (memberRead) { if (isFine()) { String msg = "Not joining " + member + " / " + member.getAccountInfo() + ". Myself got no read permission"; if (getController().isStarted() && member.isCompletelyConnected() && getController().getOSClient().isConnected() && getController().getOSClient().isLoggedIn()) { logWarning(msg); } else { logFine(msg); } } } else { if (isFine()) { String msg = "Not joining " + member + " / " + member.getAccountInfo() + " no read permission"; if (getController().isStarted() && member.isCompletelyConnected() && getController().getOSClient().isConnected()) { logWarning(msg); } else { logFine(msg); } } } if (member.isCompletelyConnected()) { member.sendMessagesAsynchron(FileList.createEmpty(currentInfo, supportExternalizable(member))); } return false; } join0(member, false); return true; } /** * Joins a member to the folder. * * @param member */ private boolean join0(Member member, boolean init) { Reject.ifNull(member, "Member is null, unable to join"); // member will be joined, here on local boolean wasMember = members.put(member, member) != null; if (!wasMember && isInfo() && !init && !currentInfo.isMetaFolder()) { logInfo("Member " + member.getNick() + " joined (connected? " + member.isConnected() + ")"); } if (!init) { if (!wasMember && member.isCompletelyConnected()) { // FIX for #924 waitForScan(); Message[] filelistMsgs = FileList.create(this, supportExternalizable(member)); member.sendMessagesAsynchron(filelistMsgs); } if (!wasMember) { // Fire event if this member is new fireMemberJoined(member); updateMetaFolderMembers(); // Persist new members list setDBDirty(); } } return !wasMember; } /** * Merge members with metafolder Members file and write back if there was a * change. Also join any new members found in the file. */ public void updateMetaFolderMembers() { // Only do this for parent folders. if (currentInfo.isMetaFolder()) { return; } FolderRepository folderRepository = getController() .getFolderRepository(); Folder metaFolder = folderRepository .getMetaFolderForParent(currentInfo); if (metaFolder == null) { // May happen at startup logFine("Could not yet find metaFolder for " + currentInfo); return; } if (metaFolder.deviceDisconnected) { logFine("Not writing members. Meta folder disconnected."); return; } // Update in the meta directory. File file = new TFile(metaFolder.localBase, METAFOLDER_MEMBERS); FileInfo fileInfo = FileInfoFactory.lookupInstance(metaFolder, file); // Read in. Map<String, MemberInfo> membersMap = readMetaFolderMembers(fileInfo); Map<String, MemberInfo> originalMap = new HashMap<String, MemberInfo>(); originalMap.putAll(membersMap); // Update members with any new ones from this file. for (MemberInfo memberInfo : membersMap.values()) { Member memberCanidate = memberInfo.getNode(getController(), true); if (members.containsKey(memberCanidate)) { continue; } if (!memberInfo.isOnSameNetwork(getController())) { continue; } if (join0(memberInfo)) { logInfo("Discovered new Member " + memberInfo); } } // Update members map with my members. for (Member member : members.keySet()) { membersMap.put(member.getId(), member.getInfo()); } // See if there has been a change to the members map. boolean changed = false; if (originalMap.size() == membersMap.size()) { for (String s : membersMap.keySet()) { if (!originalMap.containsKey(s)) { changed = true; break; } } } else { changed = true; } if (changed && !checkIfDeviceDisconnected()) { // Write back and scan. writewMetaFolderMembers(membersMap, fileInfo); metaFolder.scanChangedFile(fileInfo); } } /** * Read the metafolder Members file from disk. It is a Map<String, * MemberInfo>. * * @param fileInfo * @return */ @SuppressWarnings({"unchecked"}) private Map<String, MemberInfo> readMetaFolderMembers(FileInfo fileInfo) { if (isFine()) { logFine("Loading metafolder members from " + fileInfo + '.'); } Map<String, MemberInfo> membersMap = new TreeMap<String, MemberInfo>(); InputStream is = null; ObjectInputStream ois = null; File f = fileInfo.getDiskFile(getController().getFolderRepository()); if (!f.exists()) { return membersMap; } try { is = new BufferedInputStream(new FileInputStream(f)); ois = new ObjectInputStream(is); membersMap.putAll((Map<String, MemberInfo>) ois.readObject()); } catch (IOException e) { logWarning("Unable to read members file " + fileInfo + ". " + e); } catch (ClassNotFoundException e) { logWarning("Unable to read members file " + fileInfo + ". " + e); } finally { if (ois != null) { try { ois.close(); } catch (IOException e) { // Don't care. } } if (is != null) { try { is.close(); } catch (IOException e) { // Don't care. } } } if (isFine()) { logFine("Loaded " + membersMap.size() + " metafolder members."); } return membersMap; } /** * Write the metafolder Members file with all known members. * * @param membersMap * @param fileInfo */ private void writewMetaFolderMembers(Map<String, MemberInfo> membersMap, FileInfo fileInfo) { if (isFine()) { logFine("Saving " + membersMap.size() + " metafolder member(s) to " + fileInfo + '.'); } if (isFiner()) { for (MemberInfo memberInfo : membersMap.values()) { logFiner("Saved " + memberInfo.getNick()); } } OutputStream os = null; ObjectOutputStream oos = null; try { os = new BufferedOutputStream(new TFileOutputStream( fileInfo.getDiskFile(getController().getFolderRepository()))); oos = new ObjectOutputStream(os); oos.writeObject(membersMap); } catch (IOException e) { logSevere(e); } finally { if (oos != null) { try { oos.flush(); oos.close(); } catch (IOException e) { // Don't care. } } if (os != null) { try { os.flush(); os.close(); } catch (IOException e) { // Don't care. } } } } public boolean waitForScan() { if (!isScanning()) { // folder OK! return true; } logFine("Waiting to complete scan"); ScanResult.ResultState resultState = lastScanResultState; while (isScanning() && resultState == lastScanResultState) { try { Thread.sleep(100); } catch (InterruptedException e) { return false; } } logFine("Scan completed. Continue with connect."); return true; } /** * Removes a member from this folder * * @param member */ public void remove(Member member) { if (members.remove(member) == null) { // Skip if not member return; } logFine("Member left " + member); // remove files of this member in our datastructure dao.deleteDomain(member.getId(), -1); // Fire event fireMemberLeft(member); // updateMetaFolderMembers(); // TODO: Trigger file requestor. Other folders may have files to // download. } /** * Delete a FileInfo that has been deleted. This is used to remove a deleted * file entry so that it can be restored from the first Member that has the * file available in the future. * * @param fileInfo */ public void removeDeletedFileInfo(FileInfo fileInfo) { Reject.ifFalse(fileInfo.isDeleted(), "Should only be removing deleted infos."); dao.delete(null, fileInfo); setDBDirty(); } /** * @return true if this folder has beend start. false if shut down */ public boolean isStarted() { return !shutdown; } /** * In sync = Folders is 100% synced and all syncing actions ( * {@link #isSyncing()}) have stopped. * * @return true if this folder is 100% in sync */ public boolean isInSync() { if (isSyncing()) { return false; } return statistic.getHarmonizedSyncPercentage() >= 100.0d; } /** * Checks if the folder is syncing. Means: local file scan running or active * transfers. * * @return if this folder is currently synchronizing. */ public boolean isSyncing() { return isScanning() || isTransferring() || getController().getFolderRepository() .getCurrentlyMaintainingFolder() == this; } /** * Checks if the folder is in Sync, called by FolderRepository * * @return if this folder is transferring files */ public boolean isTransferring() { return isDownloading() || isUploading(); } /** * @return true if the folder get currently scanned */ public boolean isScanning() { return getController().getFolderRepository().getFolderScanner() .getCurrentScanningFolder() == this; } /** * Checks if the folder is in Downloading, called by FolderRepository * * @return if this folder downloading */ public boolean isDownloading() { return getController().getTransferManager() .countNumberOfDownloads(this) > 0; } /** * Checks if the folder is in Uploading, called by FolderRepository * * @return if this folder uploading */ public boolean isUploading() { return getController().getTransferManager().countUploadsOn(this) > 0; } /** * Triggers the deletion sync in background. * <p> * * @param collection * selected members to sync deletions with */ public void triggerSyncRemoteDeletedFiles( final Collection<Member> collection) { getController().getIOProvider().startIO(new Runnable() { public void run() { syncRemoteDeletedFiles(collection, false); } }); } /** * Synchronizes the deleted files with local folder * * @param force * true if the sync is forced with ALL connected members of the * folder. otherwise it checks the modifier. */ public void syncRemoteDeletedFiles(boolean force) { syncRemoteDeletedFiles(members.keySet(), force); } /** * Synchronizes the deleted files with local folder * * @param members * the members to sync the deletions with. * @param force * true if the sync is forced with ALL connected members of the * folder. otherwise it checks the modifier. */ public void syncRemoteDeletedFiles(Collection<Member> collection, boolean force) { if (collection.isEmpty()) { // Skip. return; } if (isFine()) { logFine("Sync Remote file deltions with: " + collection); } final List<FileInfo> removedFiles = new ArrayList<FileInfo>(); // synchronized (scanLock) { for (Member member : collection) { if (!member.isCompletelyConnected()) { // disconnected go to next member continue; } if (!hasWritePermission(member)) { if (isFine()) { logFine("Not syncing deletions. " + member + " / " + member.getAccountInfo() + " no write permission"); } continue; } Collection<FileInfo> fileList = getFilesAsCollection(member); if (fileList != null) { if (isFiner()) { logFiner("RemoteFileDeletion sync. Member '" + member.getNick() + "' has " + fileList.size() + " possible files"); } for (FileInfo remoteFile : fileList) { handleFileDeletion(remoteFile, force, member, removedFiles, 0); } } Collection<DirectoryInfo> dirList = getDirectoriesAsCollection(member); if (dirList != null) { if (isFiner()) { logFiner("RemoteDirDeletion sync. Member '" + member.getNick() + "' has " + dirList.size() + " possible files"); } List<FileInfo> list = new ArrayList<FileInfo>(dirList); Collections.sort( list, new ReverseComparator<FileInfo>(FileInfoComparator .getComparator(FileInfoComparator.BY_RELATIVE_NAME))); synchronized (scanLock) { for (FileInfo remoteDir : list) { handleFileDeletion(remoteDir, force, member, removedFiles, 0); } } } } // } // Broadcast folder change if changes happend if (!removedFiles.isEmpty()) { fireFilesDeleted(removedFiles); setDBDirty(); broadcastMessages(new MessageProducer() { public Message[] getMessages(boolean useExt) { return FolderFilesChanged.create(currentInfo, removedFiles, diskItemFilter, useExt); } }); } } private void handleFileDeletion(FileInfo remoteFile, boolean force, Member member, List<FileInfo> removedFiles, int nTried) { if (!remoteFile.isDeleted()) { // Not interesting... return; } boolean syncFromMemberAllowed = syncProfile.isSyncDeletion() || force; if (!syncFromMemberAllowed) { // Not allowed to sync return; } FileInfo localFile = getFile(remoteFile); if (localFile != null && !remoteFile.isNewerThan(localFile)) { // Remote file is not newer = we are up to date. return; } // Ignored? Skip! if (diskItemFilter.isExcluded(remoteFile)) { return; } // Add to local file to database if was deleted on remote if (localFile == null) { long removeBefore = System.currentTimeMillis() - 1000L * ConfigurationEntry.MAX_FILEINFO_DELETED_AGE_SECONDS .getValueInt(getController()); if (remoteFile.getModifiedDate().getTime() > removeBefore) { if (isFine()) { logFine("Taking over deletion file info: " + remoteFile.toDetailString()); } // Take over info remoteFile = correctFolderInfo(remoteFile); store(getController().getMySelf(), remoteFile); localFile = getFile(remoteFile); // File has been marked as removed at our side removedFiles.add(localFile); } return; } if (localFile.isDeleted()) { if (remoteFile.isNewerThan(localFile)) { if (isFine()) { logFine("Taking over new deletion file info: " + remoteFile.toDetailString()); } // Take over modification infos remoteFile = correctFolderInfo(remoteFile); store(getController().getMySelf(), remoteFile); localFile = getFile(remoteFile); removedFiles.add(localFile); } return; } // Local file NOT deleted / still existing. So do a local delete File localCopy = localFile.getDiskFile(getController() .getFolderRepository()); if (!localFile.inSyncWithDisk(localCopy)) { if (isFine()) { logFine("Not deleting file from member " + member + ", local file not in sync with disk: " + localFile.toDetailString() + " at " + localCopy.getAbsolutePath()); } if (scanAllowedNow() && scanChangedFile(localFile) != null && nTried < 10) { // Scan an trigger a sync of deletions later (again). handleFileDeletion(remoteFile, force, member, removedFiles, ++nTried); } return; } if (isFine()) { logFine("File was deleted by " + remoteFile.getModifiedBy() + ", deleting local: " + localFile.toDetailString() + " at " + localCopy.getAbsolutePath()); } // Abort transfers on file. if (remoteFile.isFile()) { getController().getTransferManager().breakTransfers(remoteFile); } if (localCopy.exists()) { synchronized (scanLock) { if (localFile.isDiretory()) { if (isFine()) { logFine("Deleting directory from remote: " + localFile.toDetailString()); } watcher.addIgnoreFile(localFile); try { if (!localCopy.delete()) { // #1977 String[] remaining = localCopy.list(); if (remaining != null) { // Basic cleanup stuff. Simply remove any caches // or meta info. for (String path : remaining) { String pathL = path.toLowerCase(); if (pathL.endsWith("thumbs.db") || pathL.endsWith(".ds_store") || pathL.endsWith("desktop.ini")) { new TFile(path).delete(); } } // If structure is completely empty, just kill // it. // try { // removeEmptyDirectoryStructure(localCopy); // } catch (Exception e) { // logWarning("Unable remove empty directory structure at " // + localCopy + ". " + e.getMessage()); // } if (!localCopy.delete()) { if (isWarning()) { remaining = localCopy.list(); String contentStr = remaining != null ? Arrays.asList(remaining) .toString() : "(unable to access)"; logFine("Unable to delete directory locally: " + localCopy + ". Info: " + localFile.toDetailString() + ". contents: " + contentStr); } // Skip. Dir was not actually deleted / // could // not // sync return; } } } } finally { watcher.removeIgnoreFile(localFile); } } else if (localFile.isFile()) { if (!deleteFile(localFile, localCopy)) { logWarning("Unable to deleted. was not able to move old file to recycle bin " + localCopy.getAbsolutePath() + ". " + localFile.toDetailString()); return; } } else { logSevere("Unable to apply remote deletion: " + localFile.toDetailString()); } } } // File has been removed // Changed localFile -> remoteFile removedFiles.add(remoteFile); store(getController().getMySelf(), remoteFile); } private boolean removeEmptyDirectoryStructure(File dir) { if (dir.isFile()) { return false; } else if (dir.isDirectory()) { File[] list = dir.listFiles(); if (list.length == 0) { FileInfo fileInfo = FileInfoFactory.lookupInstance(this, dir); fileInfo = fileInfo.getNewestVersion(getController() .getFolderRepository()); if (fileInfo != null && !fileInfo.isDeleted()) { // Meta info not matching. Directory not deleted. return false; } // Remove empty directory return dir.delete(); } else { for (File file : list) { if (!removeEmptyDirectoryStructure(file)) { return false; } } return true; } } else { return false; } } /** * Broadcasts a message through the folder * * @param message */ public void broadcastMessages(Message... message) { for (Member member : getMembersAsCollection()) { // Connected? if (member.isCompletelyConnected()) { // sending all nodes my knows nodes member.sendMessagesAsynchron(message); } } } /** * Broadcasts a message through the folder. * <p> * Caches the built messages. * * @param msgProvider */ public void broadcastMessages(MessageProducer msgProvider) { Message[] msgs = null; Message[] msgsExt = null; for (Member member : getMembersAsCollection()) { // Connected? if (member.isCompletelyConnected()) { if (supportExternalizable(member)) { if (msgsExt == null) { msgsExt = msgProvider.getMessages(true); } if (msgsExt != null && msgsExt.length > 0) { member.sendMessagesAsynchron(msgsExt); } } else { if (msgs == null) { msgs = msgProvider.getMessages(false); } if (msgs != null && msgs.length > 0) { member.sendMessagesAsynchron(msgs); } } } } } /** * Updated sync patterns have been downloaded to the metaFolder. Update the * sync patterns in this (parent) folder. * * @param fileInfo * fileInfo of the new sync patterns */ public void handleMetaFolderSyncPatterns(FileInfo fileInfo) { if (!syncPatterns) { logFine("Not syncing patterns: " + getName()); return; } Folder metaFolder = getController().getFolderRepository() .getMetaFolderForParent(currentInfo); if (metaFolder == null) { logWarning("Could not find metaFolder for " + currentInfo); return; } File syncPatternsFile = metaFolder.getDiskFile(fileInfo); logInfo("Reading syncPatterns " + syncPatternsFile); diskItemFilter.loadPatternsFrom(syncPatternsFile, true); // Trigger resync getController().getTransferManager().checkActiveTranfersForExcludes(); getController().getFolderRepository().getFileRequestor() .triggerFileRequesting(currentInfo); } public DirectoryInfo getBaseDirectoryInfo() { return FileInfoFactory.createBaseDirectoryInfo(currentInfo); } /** * Broadcasts the remote command to scan the folder. */ public void broadcastScanCommand() { if (isFiner()) { logFiner("Broadcasting remote scan command"); } Message commando = new ScanCommand(currentInfo); broadcastMessages(commando); } /** * Broadcasts the remote command to requests files of the folder. */ public void broadcastFileRequestCommand() { if (isFiner()) { logFiner("Broadcasting remote file request command"); } Message commando = new FileRequestCommand(currentInfo); broadcastMessages(commando); } private void broadcastFolderChanges(final ScanResult scanResult) { if (getConnectedMembersCount() == 0) { return; } if (!scanResult.getNewFiles().isEmpty()) { broadcastMessages(new MessageProducer() { public Message[] getMessages(boolean useExt) { return FolderFilesChanged.create(currentInfo, scanResult.getNewFiles(), diskItemFilter, useExt); } }); } if (!scanResult.getChangedFiles().isEmpty()) { broadcastMessages(new MessageProducer() { public Message[] getMessages(boolean useExt) { return FolderFilesChanged.create(currentInfo, scanResult.getChangedFiles(), diskItemFilter, useExt); } }); } if (!scanResult.getDeletedFiles().isEmpty()) { broadcastMessages(new MessageProducer() { public Message[] getMessages(boolean useExt) { return FolderFilesChanged.create(currentInfo, scanResult.getDeletedFiles(), diskItemFilter, useExt); } }); } if (!scanResult.getRestoredFiles().isEmpty()) { broadcastMessages(new MessageProducer() { public Message[] getMessages(boolean useExt) { return FolderFilesChanged.create(currentInfo, scanResult.getRestoredFiles(), diskItemFilter, useExt); } }); } if (isFine()) { logFine("Broadcasted folder changes for: " + scanResult); } } /** * Callback method from member. Called when a new filelist was send * * @param from * @param newList */ public void fileListChanged(Member from, FileList newList) { if (shutdown) { return; } // Correct FolderInfo in case it differs. if (newList.files != null) { for (int i = 0; i < newList.files.length; i++) { FileInfo fInfo = newList.files[i]; newList.files[i] = FileInfoFactory.changedFolderInfo(fInfo, currentInfo); } } // #1022 - Mass delete detection. Switch to a safe profile if // a large percent of files would get deleted by another node. if (newList.files != null && syncProfile.isSyncDeletion() && PreferencesEntry.EXPERT_MODE.getValueBoolean(getController()) && ConfigurationEntry.MASS_DELETE_PROTECTION .getValueBoolean(getController())) { checkForMassDeletion(from, newList.files); } // Update DAO if (newList.isNull()) { // Delete files in domain and do nothing dao.deleteDomain(from.getId(), -1); return; } // Store but also deleted/clear domain before. int expectedItems = newList.nFollowingDeltas * newList.files.length; store(from, expectedItems, newList.files); // Try to find same files findSameFiles(from, Arrays.asList(newList.files)); if (syncProfile.isAutodownload() && from.isCompletelyConnected()) { // Trigger file requestor if (isFiner()) { logFiner("Triggering file requestor because of new remote file list from " + from); } getController().getFolderRepository().getFileRequestor() .triggerFileRequesting(newList.folder); } // Handle remote deleted files if (syncProfile.isSyncDeletion() && from.isCompletelyConnected()) { syncRemoteDeletedFiles(Collections.singleton(from), false); } // Logging writeFilelist(from); fireRemoteContentsChanged(from, newList); } /** * Callback method from member. Called when a filelist delta received * * @param from * @param changes */ public void fileListChanged(Member from, FolderFilesChanged changes) { if (shutdown) { return; } // Correct FolderInfo in case it differs. if (changes.getFiles() != null) { for (int i = 0; i < changes.getFiles().length; i++) { FileInfo fInfo = changes.getFiles()[i]; changes.getFiles()[i] = FileInfoFactory.changedFolderInfo( fInfo, currentInfo); } } if (changes.getRemoved() != null) { for (int i = 0; i < changes.getRemoved().length; i++) { FileInfo fInfo = changes.getRemoved()[i]; changes.getRemoved()[i] = FileInfoFactory.changedFolderInfo( fInfo, currentInfo); } } // #1022 - Mass delete detection. Switch to a safe profile if // a large percent of files would get deleted by another node. if (changes.getFiles() != null && syncProfile.isSyncDeletion() && PreferencesEntry.EXPERT_MODE.getValueBoolean(getController()) && ConfigurationEntry.MASS_DELETE_PROTECTION .getValueBoolean(getController())) { checkForMassDeletion(from, changes.getFiles()); } if (changes.getRemoved() != null && syncProfile.isSyncDeletion() && PreferencesEntry.EXPERT_MODE.getValueBoolean(getController()) && ConfigurationEntry.MASS_DELETE_PROTECTION .getValueBoolean(getController())) { checkForMassDeletion(from, changes.getRemoved()); } // Try to find same files if (changes.getFiles() != null) { store(from, changes.getFiles()); findSameFiles(from, Arrays.asList(changes.getFiles())); } if (changes.getRemoved() != null) { store(from, changes.getRemoved()); findSameFiles(from, Arrays.asList(changes.getRemoved())); } // Avoid hammering of sync remote deletion boolean singleExistingFileMsg = changes.getFiles() != null && changes.getFiles().length == 1 && !changes.getFiles()[0].isDeleted(); if (syncProfile.isAutodownload()) { // Check if we need to trigger the filerequestor boolean triggerFileRequestor = from.isCompletelyConnected(); if (triggerFileRequestor && singleExistingFileMsg) { // This was caused by a completed download // TODO Maybe check this also on bigger lists! FileInfo localfileInfo = getFile(changes.getFiles()[0]); FileInfo remoteFileInfo = changes.getFiles()[0]; if (localfileInfo != null && !remoteFileInfo.isNewerThan(localfileInfo) && !remoteFileInfo.isDeleted()) { // We have this or a newer version of the file. = Dont' // trigger filerequestor. triggerFileRequestor = false; } } if (triggerFileRequestor) { if (isFiner()) { logFiner("Triggering file requestor because of remote file list change " + changes + " from " + from); } getController().getFolderRepository().getFileRequestor() .triggerFileRequesting(changes.folder); } else if (isFiner()) { logFiner("Not triggering filerequestor, no new files in remote filelist" + changes + " from " + from); } } // Handle remote deleted files if (!singleExistingFileMsg && syncProfile.isSyncDeletion() && from.isCompletelyConnected()) { syncRemoteDeletedFiles(Collections.singleton(from), false); } // Fire event fireRemoteContentsChanged(from, changes); } private void store(Member member, FileInfo... fileInfos) { store(member, -1, fileInfos); } private void store(Member member, int newDomainSize, FileInfo... fileInfos) { store(member, newDomainSize, Arrays.asList(fileInfos)); } private void store(Member member, Collection<FileInfo> fileInfos) { store(member, -1, fileInfos); } private void store(Member member, int newDomainSize, Collection<FileInfo> fileInfos) { synchronized (dbAccessLock) { String domainID = member.isMySelf() ? null : member.getId(); if (newDomainSize > 0) { dao.deleteDomain(domainID, newDomainSize); } dao.store(domainID, fileInfos); } } private void checkForMassDeletion(Member from, FileInfo[] fileInfos) { int delsCount = 0; for (FileInfo remoteFile : fileInfos) { if (!remoteFile.isDeleted()) { continue; } // #1842: Actually check if these files have just been deleted. FileInfo localFile = getFile(remoteFile); if (localFile != null && !localFile.isDeleted() && remoteFile.isNewerThan(localFile)) { delsCount++; } } if (delsCount >= Constants.FILE_LIST_MAX_FILES_PER_MESSAGE) { // #1786 - If deletion >= max files per message, switch. switchToSafe(from, delsCount, false); } else { int knownFilesCount = getKnownItemCount(); if (knownFilesCount > 1) { int delPercentage = 100 * delsCount / knownFilesCount; if (isFiner()) { logFiner("FolderFilesChanged delete percentage " + delPercentage + '%'); } if (delPercentage >= ConfigurationEntry.MASS_DELETE_THRESHOLD .getValueInt(getController())) { switchToSafe(from, delPercentage, true); } } } } private void switchToSafe(Member from, int delsCount, boolean percentage) { logWarning("Received a FolderFilesChanged message from " + from.getInfo().nick + " which will delete " + delsCount + " files in folder " + currentInfo.name + ". The sync profile will now be switched from " + syncProfile.getName() + " to " + SyncProfile.HOST_FILES.getName() + " to protect the files."); SyncProfile original = syncProfile; // Emergency profile switch to something safe. setSyncProfile(SyncProfile.HOST_FILES); // Advise the controller of the problem. getController().remoteMassDeletionDetected( new RemoteMassDeletionEvent(currentInfo, from.getInfo(), delsCount, original, syncProfile, percentage)); logWarning("Switched to " + syncProfile.getName()); } /** * Tries to find same files in the list of remotefiles. This methods takes * over the file information from remote under following circumstances: * <p> * 1. When the last modified date and size matches our file. (=good guess * that this is the same file) * <p> * 2. Our file has version 0 (=Scanned by initial scan) * <p> * 3. The remote file version is > 0 and is not deleted * <p> * This if files moved from node to node without PowerFolder. e.g. just copy * over windows share. Helps to identifiy same files and prevents unessesary * downloads. * * @param remoteFileInfos */ private boolean findSameFiles(Member member, Collection<FileInfo> remoteFileInfos) { Reject.ifNull(remoteFileInfos, "Remote file info list is null"); if (isFiner()) { logFiner("Triing to find same files in remote list with " + remoteFileInfos.size() + " files from " + member); } Boolean hasWrite = null; List<FileInfo> found = new LinkedList<FileInfo>(); for (FileInfo remoteFileInfo : remoteFileInfos) { FileInfo localFileInfo = getFile(remoteFileInfo); if (localFileInfo == null) { continue; } if (localFileInfo.isDeleted() != remoteFileInfo.isDeleted()) { continue; } boolean fileSizeSame = localFileInfo.getSize() == remoteFileInfo .getSize(); boolean dateSame = DateUtil.equalsFileDateCrossPlattform( localFileInfo.getModifiedDate(), remoteFileInfo.getModifiedDate()); boolean fileCaseSame = localFileInfo.getRelativeName().equals( remoteFileInfo.getRelativeName()); if (localFileInfo.getVersion() < remoteFileInfo.getVersion() && remoteFileInfo.getVersion() > 0) { // boolean localFileNewer = Util.isNewerFileDateCrossPlattform( // localFileInfo.getModifiedDate(), remoteFileInfo // .getModifiedDate()); if (fileSizeSame && dateSame) { if (hasWrite == null) { hasWrite = hasWritePermission(member); } if (!hasWrite) { if (isFine()) { logFine("Not searching same files. " + member + " / " + member.getAccountInfo() + " no write permission"); } return false; } if (isFine()) { logFine("Found identical file remotely: local " + localFileInfo.toDetailString() + " remote: " + remoteFileInfo.toDetailString() + ". Taking over modification infos"); } // localFileInfo.copyFrom(remoteFileInfo); found.add(remoteFileInfo); } // Disabled because of TRAC #999. Causes strange behavior. // if (localFileNewer) { // if (isWarning()) { // logWarning( // "Found file remotely, but local is newer: local " // + localFileInfo.toDetailString() + " remote: " // + remoteFileInfo.toDetailString() // + ". Increasing local version to " // + (remoteFileInfo.getVersion() + 1)); // } // localFileInfo.setVersion(remoteFileInfo.getVersion() + 1); // // FIXME That might produce a LOT of traffic! Single update // // message per file! This also might intefere with FileList // // exchange at beginning of communication // fileChanged(localFileInfo); // } } else if (!fileCaseSame && dateSame && fileSizeSame) { if (localFileInfo.getRelativeName().compareTo( remoteFileInfo.getRelativeName()) <= 0) { if (hasWrite == null) { hasWrite = hasWritePermission(member); } if (!hasWrite) { if (isInfo()) { logInfo("Not searching same files. " + member + " / " + member.getAccountInfo() + " no write permission"); } return false; } // Skip this fileinfo. Compare by name is performed // to ensure that the FileInfo with the greatest // lexographic index is taken. This is a // deterministic rule to keep file db repos in sync // among peers. if (isFine()) { logFine("Found identical file remotely with diffrent name-case: local " + localFileInfo.toDetailString() + " remote: " + remoteFileInfo.toDetailString() + ". Taking over all infos"); } remoteFileInfo = correctFolderInfo(remoteFileInfo); found.add(remoteFileInfo); } } } if (!found.isEmpty()) { store(getController().getMySelf(), found); filesChanged(found); return true; } return false; } /** * Tries to find same files in the list of remotefiles of all members. This * methods takes over the file information from remote under following * circumstances: See #findSameFiles(FileInfo[]) * * @see #findSameFiles(FileInfo[]) */ private void findSameFilesOnRemote() { for (Member member : getConnectedMembers()) { Collection<FileInfo> lastFileList = getFilesAsCollection(member); if (lastFileList != null) { findSameFiles(member, lastFileList); } } } /** * @param fInfo * @return the remotely found better fileinfo. null if not found. */ private FileInfo findSameFile(FileInfo fInfo) { for (Member member : getConnectedMembers()) { FileInfo remoteFInfo = member.getFile(fInfo); if (remoteFInfo != null) { if (findSameFiles(member, Collections.singleton(remoteFInfo))) { return fInfo.getLocalFileInfo(getController() .getFolderRepository()); } } } return null; } /** * TRAC #2072 * * @param member * @return if this member supports the {@link Externalizable} versions of * {@link FileList} and {@link FolderFilesChanged} */ public boolean supportExternalizable(Member member) { return member.getProtocolVersion() >= 105; } /** * Sets the DB to a dirty state. e.g. through a change. gets stored on next * persisting run. */ private void setDBDirty() { dirty = true; } /** * Triggers the persisting. */ private void triggerPersist() { getController().schedule(persister, 1000L); } /** * Persists settings to disk. */ private void persist() { if (checkIfDeviceDisconnected()) { if (!currentInfo.isMetaFolder()) { logWarning("Unable to persist database. Device is disconnected: " + localBase); } return; } logFiner("Persisting settings"); if ((hasOwnDatabase || getKnownItemCount() > 0) && !getSystemSubDir0().exists()) { logWarning("Not storing folder database. Local system directory does not exists: " + getLocalBase()); return; } int tries = 1; boolean success = storeFolderDB(); while (!success && tries < 10) { try { // Wait a bit and try again. Thread.sleep(144L * tries); } catch (InterruptedException e) { break; } tries += 1; success = storeFolderDB(); } if (tries > 1) { if (success) { logWarning("Was able to write folder database, but only after " + tries + " trys."); } else { logSevere("Was NOT able to write folder database, even after " + tries + " trys."); } } // Write filelist if (LoggingManager.isLogToFile() && Feature.DEBUG_WRITE_FILELIST_CSV.isEnabled()) { // And members' filelists. for (Member member : members.keySet()) { writeFilelist(member); } } dirty = false; } private void writeFilelist(Member member) { if (!LoggingManager.isLogToFile() || Feature.DEBUG_WRITE_FILELIST_CSV.isDisabled()) { return; } // Write filelist to disk Debug.writeFileListCSV(getName(), member.getNick(), dao.findAllFiles(member.getId()), "FileList of folder " + getName() + ", member " + member + ':'); } /* * Simple getters/exposing methods */ /** * @return the local base directory */ public File getLocalBase() { return localBase; } /** * @return the dir to commit/mirror the whole folder contents to after * folder has been fully updated. null if no commit should be * performed */ public File getCommitDir() { return commitDir; } /** * @return the commit dir or the local base if commit dir is null. */ public File getCommitOrLocalDir() { if (commitDir != null) { return commitDir; } return localBase; } /** * @param commitDir * the dir to commit/mirror the whole folder contents to after * folder has been fully updated. null if no commit should be * performed */ public void setCommitDir(File commitDir) { this.commitDir = commitDir; String confKey = FOLDER_SETTINGS_PREFIX_V4 + configEntryId + FolderSettings.FOLDER_SETTINGS_COMMIT_DIR; String confVal = commitDir != null ? commitDir.getAbsolutePath() : ""; getController().getConfig().put(confKey, confVal); logInfo("Commit dir set to '" + confVal + '\''); getController().saveConfig(); } /** * @return the system subdir in the local base folder. subdir gets created * if not exists */ public File getSystemSubDir() { File systemSubDir = getSystemSubDir0(); if (!systemSubDir.exists()) { if (!checkIfDeviceDisconnected() && systemSubDir.mkdirs()) { // logWarning("Create local directory at: " + systemSubDir, // new RuntimeException("here")); FileUtils.setAttributesOnWindows(systemSubDir, true, true); } else if (!deviceDisconnected) { logSevere("Failed to create system subdir: " + systemSubDir); } else if (isFine()) { logFine("Failed to create system subdir: " + systemSubDir); } } return systemSubDir; } private File getSystemSubDir0() { return new TFile(localBase, Constants.POWERFOLDER_SYSTEM_SUBDIR); } /** * Is this directory the system subdirectory? * * @param aDir * @return */ public boolean isSystemSubDir(File aDir) { return aDir.isDirectory() && getSystemSubDir0().getAbsolutePath().equals( aDir.getAbsolutePath()); } /** * @return true if this folder is disconnected/not available at the moment. */ public boolean isDeviceDisconnected() { return deviceDisconnected; } /** * Actually checks if the device is disconnected or available. Also sets the * property "deviceDisconnected". * * @return true if the device is disconnected. false if everything is ok. */ public boolean checkIfDeviceDisconnected() { /** * Check that we still have a good local base. */ try { checkBaseDir(true); } catch (FolderException e) { logFiner("invalid local base: " + e); return setDeviceDisconnected(true); } // #1249 if (getKnownItemCount() > 0 && (OSUtil.isMacOS() || OSUtil.isLinux())) { boolean inaccessible = localBase.list() == null || localBase.list().length == 0 || !localBase.exists(); if (inaccessible) { logWarning("Local base empty on linux file system, but has known files. " + localBase); return setDeviceDisconnected(true); } } // Is OK! return setDeviceDisconnected(false); } private boolean setDeviceDisconnected(boolean disconnected) { boolean wasDeviceDisconnected = deviceDisconnected; deviceDisconnected = disconnected; boolean addProblem = disconnected; for (Problem problem : problems) { if (problem instanceof DeviceDisconnectedProblem) { if (deviceDisconnected) { addProblem = false; } else { logFine("Device reconnected"); removeProblem(problem); } } } if (addProblem) { logInfo("Device disconnected. Folder disappeared from " + getLocalBase()); String bd = getController().getFolderRepository() .getFoldersBasedirString(); boolean inBaseDir = false; if (bd != null) { inBaseDir = getLocalBase().getAbsolutePath().startsWith(bd); } if (inBaseDir && !currentInfo.isMetaFolder() && !getController().getMySelf().isServer()) { // Schedule for removal getController().schedule(new Runnable() { public void run() { getController().getFolderRepository().removeFolder( Folder.this, false); } }, 5000L); } else { addProblem(new DeviceDisconnectedProblem(currentInfo)); } } if (wasDeviceDisconnected && !deviceDisconnected) { if (!currentInfo.isMetaFolder()) { logInfo("Device reconnected @ " + localBase); } // Try to load db from connected device now. loadMetadata(); if (!getSystemSubDir().exists()) { getSystemSubDir().mkdirs(); } // Re-attach folder watcher watcher.reconfigure(syncProfile); // Pull new files getController().getFolderRepository().getFileRequestor() .triggerFileRequesting(currentInfo); if (!currentInfo.isMetaFolder()) { Folder metaFolder = getController().getFolderRepository() .getMetaFolderForParent(currentInfo); if (metaFolder != null) { metaFolder.checkIfDeviceDisconnected(); } } // TODO Correctly init Archive } return deviceDisconnected; } public String getName() { return currentInfo.name; } public String getConfigEntryId() { return configEntryId; } public int getKnownItemCount() { // All! Also excluded items return dao.count(null, true, false); } public boolean isPreviewOnly() { return previewOnly; } public void setPreviewOnly(boolean previewOnly) { this.previewOnly = previewOnly; } /** * WARNING: Contents may change after getting the collection. * * @return a unmodifiable collection referecing the internal file database * hashmap (keySet). */ public Collection<FileInfo> getKnownFiles() { return dao.findAllFiles(null); } /** * WARNING: Contents may change after getting the collection. * * @return a unmodifiable collection referecing the internal directory * database hashmap (keySet). */ public Collection<DirectoryInfo> getKnownDirectories() { return dao.findAllDirectories(null); } /** * Common file delete method. Either deletes the file or moves it to the * recycle bin. * * @param newFileInfo * @param file */ private boolean deleteFile(FileInfo newFileInfo, File file) { Reject.ifNull(newFileInfo, "FileInfo is null"); FileInfo fileInfo = getFile(newFileInfo); if (isFine()) { logFine("Deleting file " + fileInfo.toDetailString() + " moving to archive"); } try { watcher.addIgnoreFile(newFileInfo); synchronized (scanLock) { if (fileInfo != null && fileInfo.isFile() && file.exists()) { try { archiver.archive(fileInfo, file, false); } catch (IOException e) { logSevere("Unable to move file to archive: " + file + ". " + e, e); } } if (file.exists() && !file.delete()) { logSevere("Unable to delete file " + file); return false; } } return true; } finally { watcher.removeIgnoreFile(newFileInfo); } } /** * Gets all the incoming files. That means files that exist on the remote * side with a higher version. * * @return the list of files that are incoming/newer available on remote * side as unmodifiable collection. */ public Collection<FileInfo> getIncomingFiles() { return getIncomingFiles(true, -1); } /** * Gets all the incoming files. That means files that exist on the remote * side with a higher version. * * @param includeDeleted * true if also deleted files should be considered. * @return the list of files that are incoming/newer available on remote * side as unmodifiable collection. */ public Collection<FileInfo> getIncomingFiles(boolean includeDeleted) { return getIncomingFiles(includeDeleted, -1); } /** * Gets all the incoming files. That means files that exist on the remote * side with a higher version. * * @param includeDeleted * true if also deleted files should be considered. * @param maxPerMember * the aproximately maximum number of incoming files (not * directories) to be included per member. This prevents the * resulting Collection from abnormal growth. * @return the list of files that are incoming/newer available on remote * side as unmodifiable collection. */ public Collection<FileInfo> getIncomingFiles(boolean includeDeleted, int maxPerMember) { // build a temp list // Map<FileInfo, FileInfo> incomingFiles = new HashMap<FileInfo, // FileInfo>(); SortedMap<FileInfo, FileInfo> incomingFiles = new TreeMap<FileInfo, FileInfo>( new FileInfoComparator(FileInfoComparator.BY_RELATIVE_NAME)); // add0 expeced files Map<Member, Integer> incomingCount = maxPerMember > 0 ? new HashMap<Member, Integer>(getMembersCount()) : null; boolean revert = isRevertLocalChanges(); for (Member member : getMembersAsCollection()) { if (!member.isCompletelyConnected()) { // disconnected or myself (=skip) continue; } if (!member.hasCompleteFileListFor(currentInfo)) { if (isFine()) { logFine("Skipping " + member + " no complete filelist from him"); } continue; } if (!hasWritePermission(member)) { if (isFine()) { logFine("Not downloading files. " + member + " / " + member.getAccountInfo() + " no write permission"); } continue; } Collection<FileInfo> memberFiles = getFilesAsCollection(member); if (incomingCount != null) { incomingCount.put(member, 0); } if (memberFiles != null) { for (FileInfo remoteFile : memberFiles) { if (incomingCount != null && incomingCount.get(member) > maxPerMember) { continue; } if (remoteFile.isDeleted() && !includeDeleted) { continue; } // Check if remote file is newer FileInfo localFile = getFile(remoteFile); if (revert && localFile != null) { FileInfo newestFileInfo = remoteFile .getNewestVersion(getController() .getFolderRepository()); if (localFile.isNewerThan(newestFileInfo)) { // Ignore/Rever local files logWarning("Local change detected, but has no write permission: " + localFile.toDetailString()); localFile = null; } } FileInfo alreadyIncoming = incomingFiles.get(remoteFile); boolean notLocal = localFile == null; boolean newerThanLocal = localFile != null && remoteFile.isNewerThan(localFile); // Check if this remote file is newer than one we may // already have. boolean newestRemote = alreadyIncoming == null || remoteFile.isNewerThan(alreadyIncoming); if (notLocal && remoteFile.isDeleted()) { // A remote deleted file is not incoming! // TODO Maby download deleted files from archive of // remote? // and put it directly into own recycle bin. continue; } if (notLocal || newerThanLocal && newestRemote) { // Okay this one is expected if (!diskItemFilter.isExcluded(remoteFile)) { incomingFiles.put(remoteFile, remoteFile); if (incomingCount != null) { Integer i = incomingCount.get(member); incomingCount.put(member, ++i); } } } } } Collection<DirectoryInfo> memberDirs = dao .findAllDirectories(member.getId()); if (memberDirs != null) { for (DirectoryInfo remoteDir : memberDirs) { if (remoteDir.isDeleted() && !includeDeleted) { continue; } // Check if remote file is newer FileInfo localFile = getFile(remoteDir); FileInfo alreadyIncoming = incomingFiles.get(remoteDir); boolean notLocal = localFile == null; boolean newerThanLocal = localFile != null && remoteDir.isNewerThan(localFile); // Check if this remote file is newer than one we may // already have. boolean newestRemote = alreadyIncoming == null || remoteDir.isNewerThan(alreadyIncoming); if (notLocal && remoteDir.isDeleted()) { // A remote deleted file is not incoming! // TODO Maby download deleted files from archive of // remote? // and put it directly into own recycle bin. continue; } if (notLocal || newerThanLocal && newestRemote) { // Okay this one is expected if (!diskItemFilter.isExcluded(remoteDir)) { incomingFiles.put(remoteDir, remoteDir); } } } } } if (incomingFiles.isEmpty()) { logFiner("No Incoming files"); } else { if (isFine()) { logFine((incomingCount != null ? "" : "Aprox. ") + incomingFiles.size() + " incoming files"); } } return Collections.unmodifiableCollection(incomingFiles.keySet()); } /** * Visits all remote {@link FileInfo}s and {@link DirectoryInfo}s, that * <p> * 1) Do not exist locally or * <p> * 2) Are newer than the local version. * * @param vistor * the {@link Visitor} to pass the incoming files to. */ public void visitIncomingFiles(Visitor<FileInfo> vistor) { // add0 expeced files for (Member member : getMembersAsCollection()) { if (!member.isCompletelyConnected()) { // disconnected or myself (=skip) continue; } if (!member.hasCompleteFileListFor(currentInfo)) { if (isFine()) { logFine("Skipping " + member + " no complete filelist from him"); } continue; } if (!hasWritePermission(member)) { if (isFine()) { logFine("Not downloading files. " + member + " / " + member.getAccountInfo() + " no write permission"); } continue; } Collection<FileInfo> memberFiles = getFilesAsCollection(member); if (memberFiles != null) { for (FileInfo fileInfo : memberFiles) { if (!visitFileIfNewer(fileInfo, vistor)) { // Stop visiting. return; } } } Collection<DirectoryInfo> memberDirs = dao .findAllDirectories(member.getId()); if (memberDirs != null) { for (FileInfo fileInfo : memberDirs) { if (!visitFileIfNewer(fileInfo, vistor)) { // Stop visiting. return; } } } } } private boolean visitFileIfNewer(FileInfo fileInfo, Visitor<FileInfo> vistor) { // Check if remote file is newer FileInfo localFile = getFile(fileInfo); boolean notLocal = localFile == null; boolean remoteNewerThanLocal = localFile != null && fileInfo.isNewerThan(localFile); if (notLocal && fileInfo.isDeleted()) { return true; } if (notLocal || remoteNewerThanLocal) { // Okay this one is expected if (!diskItemFilter.isExcluded(fileInfo)) { try { return vistor.visit(fileInfo); } catch (Exception e) { logSevere("Error while visiting incoming files. " + e, e); } } } return true; } /** * @param member * @return the list of files from a member as unmodifiable collection */ public Collection<FileInfo> getFilesAsCollection(Member member) { if (member == null) { throw new NullPointerException("Member is null"); } return dao.findAllFiles(member.getId()); } /** * @param member * @return the list of directories from a member as unmodifiable collection */ public Collection<DirectoryInfo> getDirectoriesAsCollection(Member member) { if (member == null) { throw new NullPointerException("Member is null"); } return dao.findAllDirectories(member.getId()); } /** * Visits all {@link Member}s of this folder * * @param visitor */ public void visitMembers(Visitor<Member> visitor) { for (Member member : members.keySet()) { if (!visitor.visit(member)) { return; } } } /** * Visits all fully connected {@link Member}s of this folder. * * @param visitor */ public void visitMembersConnected(Visitor<Member> visitor) { for (Member member : members.keySet()) { if (!member.isCompletelyConnected()) { continue; } if (!visitor.visit(member)) { return; } } } /** * This list also includes myself! * * @return all members in a collection. The collection is a unmodifiable * referece to the internal member storage. May change after has * been returned! */ public Collection<Member> getMembersAsCollection() { return Collections.unmodifiableCollection(members.values()); } /** * @return the number of members */ public int getMembersCount() { return members.size(); } /** * @return the number of connected members EXCLUDING myself. */ public int getConnectedMembersCount() { int nConnected = 0; for (Member member : members.values()) { if (member.isCompletelyConnected()) { nConnected++; } } return nConnected; } /** * @return the connected members EXCLUDING myself. */ public Member[] getConnectedMembers() { List<Member> connected = new ArrayList<Member>(members.size()); for (Member member : getMembersAsCollection()) { if (member.isCompletelyConnected()) { if (member.isMySelf()) { continue; } connected.add(member); } } return connected.toArray(new Member[connected.size()]); } /** * @param member * @return true if that member is on this folder */ public boolean hasMember(Member member) { if (members == null) { // FIX a rare NPE at startup return false; } if (member == null) { return false; } return members.keySet().contains(member); } /** * @param fInfo * @return if folder has this file */ public boolean hasFile(FileInfo fInfo) { return getFile(fInfo) != null; } /** * @param fInfo * @return the local fileinfo instance */ public FileInfo getFile(FileInfo fInfo) { Reject.ifNull(fInfo, "FileInfo is null"); FileInfo localInfo = dao.find(fInfo, null); if (localInfo != null) { return localInfo; } if (fInfo.isBaseDirectory()) { return getBaseDirectoryInfo(); } return null; } /** * @param fInfo * @return the local file from a file info Never returns null, file MAY NOT * exist!! check before use */ public File getDiskFile(FileInfo fInfo) { return new TFile(localBase, FileInfoFactory.encodeIllegalChars(fInfo .getRelativeName())); } /** * @return true if members are there, were files can be downloaded from. * Remote nodes have to have free upload capacity. */ public boolean hasUploadCapacity() { for (Member member : members.values()) { if (getController().getTransferManager().hasUploadCapacity(member)) { return true; } } return false; } /** * @return the globally unique folder ID, generate once at folder creation */ public String getId() { return currentInfo.id; } /** * @return Date of the last file change for this folder. */ public Date getLastFileChangeDate() { return statistic.getLastFileChangeDate(); } /** * @return the date of the last maintenance. */ public Date getLastDBMaintenanceDate() { return lastDBMaintenance; } /** * @return the info object of this folder */ public FolderInfo getInfo() { return currentInfo; } /** * @return the statistic for this folder */ public FolderStatistic getStatistic() { return statistic; } /** * @return the {@link FileInfoDAO}. TRAC #1422 */ public FileInfoDAO getDAO() { return dao; } /** * @return an Invitation to this folder. Includes a intelligent opposite * sync profile. */ public Invitation createInvitation() { Invitation inv = new Invitation(currentInfo, getController() .getMySelf().getInfo()); inv.setFilesCount(statistic.getLocalFilesCount()); inv.setSize(statistic.getLocalSize()); inv.setSuggestedSyncProfile(syncProfile); if (syncProfile.equals(SyncProfile.BACKUP_SOURCE)) { inv.setSuggestedSyncProfile(SyncProfile.BACKUP_TARGET); } else if (syncProfile.equals(SyncProfile.BACKUP_TARGET)) { inv.setSuggestedSyncProfile(SyncProfile.BACKUP_SOURCE); } else if (syncProfile.equals(SyncProfile.HOST_FILES)) { inv.setSuggestedSyncProfile(SyncProfile.AUTOMATIC_DOWNLOAD); } inv.setSuggestedLocalBase(getController(), localBase); String username = getController().getOSClient().getUsername(); if (StringUtils.isNotBlank(username)) { inv.setUsername(username); } return inv; } /** * Ensures that default ignore patterns are set. */ public void addDefaultExcludes() { File pFile = new TFile(getSystemSubDir(), DiskItemFilter.PATTERNS_FILENAME); boolean init = !pFile.exists(); for (DefaultExcludes pattern : DefaultExcludes.values()) { addPattern(pattern.getPattern()); } if (WinUtils.getAppDataCurrentUser() != null && localBase.getAbsolutePath().equals( WinUtils.getAppDataCurrentUser())) { addPattern("PowerFolder/logs/*"); } // #2083 if (UserDirectories.getDocumentsReported() != null && localBase.getAbsolutePath().equals( UserDirectories.getDocumentsReported())) { logFine("My documents @ " + UserDirectories.getDocumentsReported()); logFine("Folder @ " + localBase.getAbsolutePath()); logWarning("Adding transition ignore patterns for My documents folder"); // Ignore My Pictures, My Music, My Videos, PowerFolders (basedir) File baseDir = getController().getFolderRepository() .getFoldersBasedir(); addPattern(baseDir.getName() + '*'); if (UserDirectories.getDocumentsReported() != null) { int i = UserDirectories.getDocumentsReported().length(); if (UserDirectories.getMusicReported() != null && UserDirectories.getMusicReported().startsWith( UserDirectories.getDocumentsReported())) { addPattern(UserDirectories.getMusicReported().substring( i + 1) + '*'); } if (UserDirectories.getPicturesReported() != null && UserDirectories.getPicturesReported().startsWith( UserDirectories.getDocumentsReported())) { addPattern(UserDirectories.getPicturesReported().substring( i + 1) + '*'); } if (UserDirectories.getVideosReported() != null && UserDirectories.getVideosReported().startsWith( UserDirectories.getDocumentsReported())) { addPattern(UserDirectories.getVideosReported().substring( i + 1) + '*'); } } } if (init) { diskItemFilter.savePatternsTo(pFile, false); // Defaults have 0 pFile.setLastModified(0); } } /** * Watch for harmonized sync is 100%. If so, set a new lastSync date. */ private void checkLastSyncDate() { double percentage = statistic.getHarmonizedSyncPercentage(); boolean newInSync = Double.compare(percentage, 100.0d) == 0; if (newInSync) { lastSyncDate = new Date(); storeLastSyncDate(); } if (isFiner()) { logFiner("Harmonized percentage: " + percentage + ". In sync? " + newInSync + ". last sync date: " + lastSyncDate + " . connected: " + getConnectedMembersCount()); } } private void storeLastSyncDate() { File lastSyncFile = new TFile(getSystemSubDir0(), LAST_SYNC_INFO_FILENAME); if (!getSystemSubDir0().exists()) { return; } try { lastSyncFile.createNewFile(); } catch (IOException e) { // Ignore. } try { lastSyncFile.setLastModified(lastSyncDate.getTime()); } catch (Exception e) { logSevere("Unable to update last synced date to " + lastSyncFile); } } private void loadLastSyncDate() { File lastSyncFile = new TFile(getSystemSubDir0(), LAST_SYNC_INFO_FILENAME); if (lastSyncFile.exists()) { lastSyncDate = new Date(lastSyncFile.lastModified()); } else { lastSyncDate = null; } } // Security methods ******************************************************* public boolean hasReadPermission(Member member) { return hasFolderPermission(member, FolderPermission.read(getParentFolderInfo())); } // PFS-638 private Map<Member, Date> hasWriteCache = Util.createConcurrentHashMap(); private static final long HAS_WRITE_CACHE_TIMEOUT = 987L; private static volatile int CACHE_HITS; public boolean hasWritePermission(Member member) { Date lastTimeHadWrite = hasWriteCache.get(member); if (lastTimeHadWrite != null) { long lastHasWriteAgo = System.currentTimeMillis() - lastTimeHadWrite.getTime(); if (lastHasWriteAgo < HAS_WRITE_CACHE_TIMEOUT) { CACHE_HITS++; if (CACHE_HITS % 100000 == 0 && isFine()) { logFine("Permission write cache hit count: " + CACHE_HITS + " (last: " + lastHasWriteAgo + "ms ago)"); } return true; } } boolean hasWrite = hasFolderPermission(member, FolderPermission.readWrite(getParentFolderInfo())); if (hasWrite) { hasWriteCache.put(member, new Date()); } else { hasWriteCache.remove(member); } return hasWrite; } public boolean hasAdminPermission(Member member) { return hasFolderPermission(member, FolderPermission.admin(getParentFolderInfo())); } public boolean hasOwnerPermission(Member member) { return hasFolderPermission(member, FolderPermission.owner(getParentFolderInfo())); } private boolean hasFolderPermission(Member member, FolderPermission permission) { if (getController().getOSClient().isClusterServer(member)) { return true; } return getController().getSecurityManager().hasPermission( member.getInfo(), permission); } private FolderInfo getParentFolderInfo() { if (!currentInfo.isMetaFolder()) { return currentInfo; } return currentInfo.getParentFolderInfo(); } // General stuff ********************************************************** @Override public String toString() { return currentInfo.toString(); } // Logger methods ********************************************************* @Override public String getLoggerName() { return super.getLoggerName() + " '" + getName() + '\''; } // *************** Event support public void addMembershipListener(FolderMembershipListener listener) { ListenerSupportFactory.addListener(folderMembershipListenerSupport, listener); } public void removeMembershipListener(FolderMembershipListener listener) { ListenerSupportFactory.removeListener(folderMembershipListenerSupport, listener); } public void addFolderListener(FolderListener listener) { ListenerSupportFactory.addListener(folderListenerSupport, listener); } public void removeFolderListener(FolderListener listener) { ListenerSupportFactory.removeListener(folderListenerSupport, listener); } private void fireMemberJoined(Member member) { FolderMembershipEvent folderMembershipEvent = new FolderMembershipEvent( this, member); folderMembershipListenerSupport.memberJoined(folderMembershipEvent); } private void fireMemberLeft(Member member) { FolderMembershipEvent folderMembershipEvent = new FolderMembershipEvent( this, member); folderMembershipListenerSupport.memberLeft(folderMembershipEvent); } private void fileChanged(FileInfo... fileInfos) { filesChanged(Arrays.asList(fileInfos)); } private void filesChanged(final List<FileInfo> fileInfosList) { Reject.ifNull(fileInfosList, "FileInfo is null"); for (int i = 0; i < fileInfosList.size(); i++) { FileInfo fileInfo = fileInfosList.get(i); // TODO Bulk fire event fireFileChanged(fileInfo); final FileInfo localInfo = getFile(fileInfo); fileInfosList.set(i, localInfo); } setDBDirty(); if (fileInfosList.size() >= 1 || diskItemFilter.isRetained(fileInfosList.get(0))) { broadcastMessages(new MessageProducer() { public Message[] getMessages(boolean useExt) { return FolderFilesChanged.create(getInfo(), fileInfosList, diskItemFilter, useExt); } }); } } private void fireFileChanged(FileInfo fileInfo) { if (isFiner()) { logFiner("fireFileChanged: " + this); } FolderEvent folderEvent = new FolderEvent(this, fileInfo); folderListenerSupport.fileChanged(folderEvent); } private void fireFilesChanged(List<FileInfo> fileInfos) { if (isFiner()) { logFiner("fireFileChanged: " + this); } FolderEvent folderEvent = new FolderEvent(this, fileInfos, true); folderListenerSupport.fileChanged(folderEvent); } private void fireFilesDeleted(Collection<FileInfo> fileInfos) { if (isFiner()) { logFiner("fireFilesDeleted: " + this); } FolderEvent folderEvent = new FolderEvent(this, fileInfos); folderListenerSupport.filesDeleted(folderEvent); } private void fireRemoteContentsChanged(Member from, FileList list) { if (isFiner()) { logFiner("fireRemoteContentsChanged: " + this); } FolderEvent folderEvent = new FolderEvent(this, list, from); folderListenerSupport.remoteContentsChanged(folderEvent); } private void fireRemoteContentsChanged(Member from, FolderFilesChanged list) { if (isFiner()) { logFiner("fireRemoteContentsChanged: " + this); } FolderEvent folderEvent = new FolderEvent(this, list, from); folderListenerSupport.remoteContentsChanged(folderEvent); } private void fireSyncProfileChanged() { FolderEvent folderEvent = new FolderEvent(this, syncProfile); folderListenerSupport.syncProfileChanged(folderEvent); } private void fireArchiveSettingsChanged() { FolderEvent folderEvent = new FolderEvent(this); folderListenerSupport.archiveSettingsChanged(folderEvent); } private void fireScanResultCommited(ScanResult scanResult) { if (isFiner()) { logFiner("fireScanResultCommited: " + this); } FolderEvent folderEvent = new FolderEvent(this, scanResult); folderListenerSupport.scanResultCommited(folderEvent); } /** package protected because fired by FolderStatistics */ void notifyStatisticsCalculated() { checkLastSyncDate(); checkSync(); FolderEvent folderEvent = new FolderEvent(this); folderListenerSupport.statisticsCalculated(folderEvent); } /** * Call when a folder is being removed to clear any references. */ public void clearAllProblemListeners() { ListenerSupportFactory.removeAllListeners(problemListenerSupport); } /** * This creates / removes a warning if the folder has not been synchronized * in a long time. */ public void checkSync() { if (!ConfigurationEntry.FOLDER_SYNC_USE .getValueBoolean(getController())) { removeUnsyncedProblem(); return; } if (previewOnly) { return; } // Calculate the date that folders should be synced by. int warnSeconds = syncWarnSeconds; if (warnSeconds == 0) { warnSeconds = ConfigurationEntry.FOLDER_SYNC_WARN_SECONDS .getValueInt(getController()); } if (warnSeconds <= 0) { removeUnsyncedProblem(); return; } Calendar cal = new GregorianCalendar(); cal.add(Calendar.SECOND, -warnSeconds); Date warningDate = cal.getTime(); // If others are in sync, do not warn because I can sync up with them. boolean othersInSync = false; Member me = getController().getMySelf(); for (Member member : members.values()) { double memberSync = statistic.getSyncPercentage(member); if (!member.equals(me) && Double.compare(memberSync, 100.0) == 0) { othersInSync = true; break; } } Date myLastSyncDate = lastSyncDate; if (myLastSyncDate != null && myLastSyncDate.before(warningDate)) { if (!(othersInSync && isSyncing())) { // Only need one of these. UnsynchronizedFolderProblem ufp = null; for (Problem problem : problems) { if (problem instanceof UnsynchronizedFolderProblem) { ufp = (UnsynchronizedFolderProblem) problem; break; } } if (ufp == null && PreferencesEntry.EXPERT_MODE .getValueBoolean(getController())) { if (new Date().before(lastSyncDate)) { logWarning("Last sync date in future: " + lastSyncDate); } Problem problem = new UnsynchronizedFolderProblem( currentInfo, myLastSyncDate); addProblem(problem); } } } else { removeUnsyncedProblem(); } } private void removeUnsyncedProblem() { if (problems.isEmpty()) { return; } // Perhaps now need to remove it? UnsynchronizedFolderProblem ufp = null; for (Problem problem : problems) { if (problem instanceof UnsynchronizedFolderProblem) { ufp = (UnsynchronizedFolderProblem) problem; break; } } if (ufp != null) { removeProblem(ufp); } } public boolean isSyncPatterns() { return syncPatterns; } public void setSyncPatterns(boolean syncPatterns) { this.syncPatterns = syncPatterns; String syncProfKey = FOLDER_SETTINGS_PREFIX_V4 + configEntryId + FolderSettings.FOLDER_SETTINGS_SYNC_PATTERNS; getController().getConfig().put(syncProfKey, String.valueOf(syncPatterns)); getController().saveConfig(); } public int getSyncWarnSeconds() { return syncWarnSeconds; } /** * @param syncWarnSeconds * The number of seconds after the folder will raise a * {@link UnsynchronizedFolderProblem} if not 100% sync. 0 or * lower means use default. */ public void setSyncWarnSeconds(int syncWarnSeconds) { if (syncWarnSeconds == this.syncWarnSeconds) { return; } this.syncWarnSeconds = syncWarnSeconds; if (syncWarnSeconds != 0) { getController().getConfig().setProperty( FOLDER_SETTINGS_PREFIX_V4 + configEntryId + FolderSettings.FOLDER_SETTINGS_SYNC_WARN_SECONDS, String.valueOf(syncWarnSeconds)); } else { getController().getConfig().remove( FOLDER_SETTINGS_PREFIX_V4 + configEntryId + FolderSettings.FOLDER_SETTINGS_SYNC_WARN_SECONDS); } getController().saveConfig(); checkSync(); } /** * Save patterns to metaFolder for transfer to other computers. */ private void savePatternsToMetaFolder() { // Should the patterns be synchronized? if (!syncPatterns) { return; } if (currentInfo.isMetaFolder()) { return; } // Only do this for parent folders. FolderRepository folderRepository = getController() .getFolderRepository(); Folder metaFolder = folderRepository .getMetaFolderForParent(currentInfo); if (metaFolder == null) { logWarning("Could not find metaFolder for " + currentInfo); return; } if (metaFolder.deviceDisconnected) { logFiner("Not writing synced ignored patterns. Meta folder disconnected"); return; } // Write the patterns in the meta directory. File file = new TFile(metaFolder.localBase, DiskItemFilter.PATTERNS_FILENAME); FileInfo fInfo = FileInfoFactory.lookupInstance(metaFolder, file); diskItemFilter.savePatternsTo(file, false); if (isFine()) { logFine("Saving ignore patterns to Meta folder: " + file); } metaFolder.scanChangedFile(fInfo); } /** * Delete any file archives over a specified age. */ public void cleanupOldArchiveFiles(Date cleanupDate) { archiver.cleanupOldArchiveFiles(cleanupDate); } // Encryption logic ******************************************************* public boolean isEncrypted() { return encrypted; } public char[] getEncryptionKeyOBF() { return encryptionKeyOBF; } public void setEncryptionKey(char[] encryptionKeyPlain) { char[] newKeyOBF = Util.toCharArray(LoginUtil .obfuscate(encryptionKeyPlain)); if (encryptionKeyOBF == null || Arrays.equals(newKeyOBF, encryptionKeyOBF)) { this.encryptionKeyOBF = newKeyOBF; } } // Inner classes ********************************************************** /** * Persister task, persists settings from time to time. */ private class Persister extends TimerTask { @Override public synchronized void run() { if (shutdown) { return; } if (dirty) { persist(); } if (diskItemFilter.isDirty() && !checkIfDeviceDisconnected()) { diskItemFilter.savePatternsTo(new TFile(getSystemSubDir(), DiskItemFilter.PATTERNS_FILENAME), true); if (!shutdown) { savePatternsToMetaFolder(); } } } @Override public String toString() { return "FolderPersister for '" + Folder.this; } } }