001/* 002 * Syncany, www.syncany.org 003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation, either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018package org.syncany.operations.down; 019 020import java.util.ArrayList; 021import java.util.Collections; 022import java.util.Comparator; 023import java.util.List; 024import java.util.logging.Level; 025import java.util.logging.Logger; 026 027import org.syncany.database.FileVersion.FileType; 028import org.syncany.operations.down.actions.DeleteFileSystemAction; 029import org.syncany.operations.down.actions.FileSystemAction; 030import org.syncany.operations.down.actions.NewFileSystemAction; 031import org.syncany.operations.down.actions.RenameFileSystemAction; 032 033/** 034 * Sorts file system actions according to their natural order to prevent scenarios 035 * in which a non-empty directory is deleted, ... 036 * 037 * Sorting necessary: 038 * 1. Delete file/symlink actions must happen first 039 * 2. New folder actions must happen after delete file/symlink actions, and must be sorted by path, shortest first (within new actions) 040 * 3. New file/symlink actions must happen after new folder actions 041 * 4. Rename file/symlink actions must happen after new folder actions 042 * 5. Delete folder actions must happen last, sorted by path, longest first (within delete actions) 043 * a. except if the deleted folder has a name clash with another action (rename/new) 044 * in that case, it must happen before the other action 045 * 046 * No sorting necessary: 047 * - Change file/symlink actions can happen anytime 048 * - Set attributes actions can happen anytime 049 * 050 * Cannot happen: 051 * - Rename folder 052 * - Change folder 053 * 054 * 055 * TODO [medium] NewSymlinkFileSystemAction → NewFile (has no content) 056 */ 057public class FileSystemActionComparator { 058 private static final Logger logger = Logger.getLogger(FileSystemActionComparator.class.getSimpleName()); 059 060 private static final Object[][] TARGET_ORDER = new Object[][] { 061 new Object[] { DeleteFileSystemAction.class, FileType.FILE }, 062 new Object[] { DeleteFileSystemAction.class, FileType.SYMLINK }, 063 new Object[] { NewFileSystemAction.class, FileType.FOLDER }, 064 new Object[] { NewFileSystemAction.class, FileType.FILE }, 065 new Object[] { NewFileSystemAction.class, FileType.SYMLINK }, 066 new Object[] { RenameFileSystemAction.class, FileType.FILE }, 067 new Object[] { RenameFileSystemAction.class, FileType.SYMLINK }, 068 new Object[] { DeleteFileSystemAction.class, FileType.FOLDER } 069 }; 070 071 private InternalFileSystemActionComparator internalComparator = new InternalFileSystemActionComparator(); 072 073 public void sort(List<FileSystemAction> actions) { 074 Collections.sort(actions, internalComparator); 075 postCompareSort(actions); 076 077 if (logger.isLoggable(Level.INFO)) { 078 logger.log(Level.INFO, " Sorted actions:"); 079 080 for (FileSystemAction action : actions) { 081 logger.log(Level.INFO, " + "+action); 082 } 083 } 084 } 085 086 /** 087 * Fixes the case in which a folder has been swapped with a file (case 5a, see above) 088 * 089 * New/Renamed file system actions must happen *after* the file was deleted. This method 090 * moves new/renamed actions below deleted folder actions. 091 * 092 * DEL FILE file1.jpg 093 * NEW FOLDER folder1 094 * NEW FILE folder1/fileinfolder1.jpg ← No Issue, but needs to be checked 095 * NEW FILE folder2 ←←← Issue, because folder2 is deleted below! 096 * NEW SYMLINK folder1/symlinkinfolder1.jpg ← No Issue, but needs to be checked 097 * REN FILE folder2/fileinfolder2.jpg → folder1/x2.jpg 098 * REN SYMLINK folder2/symlinkinfolder2.jpg → folder1/sym2 099 * DEL FOLDER folder2 100 * ←←← New position of "NEW FILE folder2"! 101 */ 102 public void postCompareSort(List<FileSystemAction> actions) { 103 List<FileSystemAction> fixedActions = new ArrayList<FileSystemAction>(actions); 104 105 int i = 0; 106 107 while (i < fixedActions.size()) { 108 FileSystemAction currentAction = fixedActions.get(i); 109 110 if (currentAction instanceof DeleteFileSystemAction && currentAction.getType() == FileType.FOLDER) { 111 break; 112 } 113 114 //System.out.println("testing "+currentAction); 115 if ((currentAction instanceof NewFileSystemAction || currentAction instanceof RenameFileSystemAction) 116 && (currentAction.getType() == FileType.FILE || currentAction.getType() == FileType.SYMLINK)) { 117 118 int conflictingDeleteActionIndex = getPathConflictingWithDeleteActionIndex(currentAction, fixedActions); 119 120 if (conflictingDeleteActionIndex >= 0) { 121 logger.log(Level.INFO, " --> match, conflict ["+i+"]: "+currentAction); 122 logger.log(Level.INFO, " with ["+conflictingDeleteActionIndex+"]: "+fixedActions.get(conflictingDeleteActionIndex)); 123 124 fixedActions.remove(i); 125 fixedActions.add(conflictingDeleteActionIndex, currentAction); 126 127 i--; // fix counter! 128 } 129 } 130 131 i++; 132 } 133 134 // Replace 135 actions.clear(); 136 actions.addAll(fixedActions); 137 } 138 139 private int getPathConflictingWithDeleteActionIndex(FileSystemAction currentAction, List<FileSystemAction> deleteFolderActions) { 140 for (int i=0; i<deleteFolderActions.size(); i++) { 141 FileSystemAction action = deleteFolderActions.get(i); 142 143 if (action instanceof DeleteFileSystemAction && action.getType() == FileType.FOLDER) { 144 if (action.getFile2().getPath().equals(currentAction.getFile2().getPath())) { 145 return i; 146 } 147 } 148 } 149 150 return -1; 151 } 152 153 private class InternalFileSystemActionComparator implements Comparator<FileSystemAction> { 154 @Override 155 public int compare(FileSystemAction a1, FileSystemAction a2) { 156 int a1Position = determinePosition(a1); 157 int a2Position = determinePosition(a2); 158 159 if (a1Position > a2Position) { 160 return 1; 161 } 162 else if (a1Position < a2Position) { 163 return -1; 164 } 165 166 return compareByFullName(a1, a2); 167 } 168 169 private int compareByFullName(FileSystemAction a1, FileSystemAction a2) { 170 // For deleted, do the longest path first 171 if (a1.getClass().equals(DeleteFileSystemAction.class)) { 172 return -1 * a1.getFile2().getPath().compareTo(a2.getFile2().getPath()); 173 } 174 175 // For new folder actions, do the shortest path first 176 else if (a1.getClass().equals(NewFileSystemAction.class)) { 177 return a1.getFile2().getPath().compareTo(a2.getFile2().getPath()); 178 } 179 180 return 0; 181 } 182 183 private int determinePosition(FileSystemAction a) { 184 for (int i=0; i<TARGET_ORDER.length; i++) { 185 Class<?> targetClass = (Class<?>) TARGET_ORDER[i][0]; 186 FileType targetFileType = (FileType) TARGET_ORDER[i][1]; 187 188 if (a.getClass().equals(targetClass) && a.getType() == targetFileType) { 189 return i; 190 } 191 } 192 193 return -1; 194 } 195 } 196 197}