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.cli;
019
020import static java.util.Arrays.asList;
021
022import java.text.DateFormat;
023import java.text.SimpleDateFormat;
024import java.util.Date;
025import java.util.HashSet;
026import java.util.Iterator;
027import java.util.List;
028import java.util.logging.Level;
029import java.util.logging.Logger;
030import java.util.regex.Matcher;
031import java.util.regex.Pattern;
032
033import joptsimple.OptionParser;
034import joptsimple.OptionSet;
035import joptsimple.OptionSpec;
036
037import org.syncany.cli.util.CommandLineUtil;
038import org.syncany.database.FileVersion;
039import org.syncany.database.FileVersion.FileStatus;
040import org.syncany.database.FileVersion.FileType;
041import org.syncany.database.ObjectId;
042import org.syncany.database.PartialFileHistory;
043import org.syncany.operations.OperationResult;
044import org.syncany.operations.ls.LsOperation;
045import org.syncany.operations.ls.LsOperationOptions;
046import org.syncany.operations.ls.LsOperationResult;
047
048import com.google.common.base.Function;
049
050public class LsCommand extends Command {        
051        protected static final Logger logger = Logger.getLogger(LsCommand.class.getSimpleName());
052        
053        private static final int CHECKSUM_LENGTH_LONG = 40;
054        private static final int CHECKSUM_LENGTH_SHORT = 10;    
055        private static final String DATE_FORMAT_PATTERN = "yy-MM-dd HH:mm:ss";
056        private static final DateFormat DATE_FORMAT = new SimpleDateFormat(DATE_FORMAT_PATTERN);
057        
058        private int checksumLength;
059        private boolean groupedVersions;
060        private boolean fetchHistories;
061        
062        @Override
063        public CommandScope getRequiredCommandScope() { 
064                return CommandScope.INITIALIZED_LOCALDIR;
065        }
066
067        @Override
068        public boolean canExecuteInDaemonScope() {
069                return true;
070        }
071
072        @Override
073        public int execute(String[] operationArgs) throws Exception {
074                LsOperationOptions operationOptions = parseOptions(operationArgs);
075                LsOperationResult operationResult = new LsOperation(config, operationOptions).execute();
076
077                printResults(operationResult);
078
079                return 0;
080        }       
081
082        @Override
083        public LsOperationOptions parseOptions(String[] operationArgs) throws Exception {
084                LsOperationOptions operationOptions = new LsOperationOptions();
085
086                OptionParser parser = new OptionParser();
087                parser.allowsUnrecognizedOptions();
088                
089                OptionSpec<String> optionDateStr = parser.acceptsAll(asList("D", "date")).withRequiredArg();
090                OptionSpec<Void> optionRecursive = parser.acceptsAll(asList("r", "recursive"));
091                OptionSpec<String> optionFileTypes = parser.acceptsAll(asList("t", "types")).withRequiredArg();
092                OptionSpec<Void> optionLongChecksums = parser.acceptsAll(asList("f", "full-checksums"));
093                OptionSpec<Void> optionWithVersions = parser.acceptsAll(asList("V", "versions"));
094                OptionSpec<Void> optionGroupedVersions = parser.acceptsAll(asList("g", "group"));
095                OptionSpec<Void> optionFileHistoryId = parser.acceptsAll(asList("H", "file-history"));
096                OptionSpec<Void> optionDeleted = parser.acceptsAll(asList("q", "deleted"));
097
098                OptionSet options = parser.parse(operationArgs);
099
100                // --date=..
101                if (options.has(optionDateStr)) {                       
102                        Date logViewDate = parseDateOption(options.valueOf(optionDateStr));
103                        operationOptions.setDate(logViewDate);
104                }
105                
106                // --recursive
107                operationOptions.setRecursive(options.has(optionRecursive));
108                
109                // --types=[tds]
110                if (options.has(optionFileTypes)) {
111                        String fileTypesStr = options.valueOf(optionFileTypes).toLowerCase();
112                        HashSet<FileType> fileTypes = new HashSet<>();
113                        
114                        if (fileTypesStr.contains("f")) {
115                                fileTypes.add(FileType.FILE);
116                        }
117                        
118                        if (fileTypesStr.contains("d")) {
119                                fileTypes.add(FileType.FOLDER);
120                        }
121                        
122                        if (fileTypesStr.contains("s")) {
123                                fileTypes.add(FileType.SYMLINK);
124                        }
125                        
126                        operationOptions.setFileTypes(fileTypes);
127                }
128                                
129                // --versions
130                fetchHistories = options.has(optionWithVersions) || options.has(optionFileHistoryId);
131                operationOptions.setFetchHistories(fetchHistories);
132
133                // --file-history
134                operationOptions.setFileHistoryId(options.has(optionFileHistoryId));
135                
136                // --long-checksums (display option)
137                checksumLength = (options.has(optionLongChecksums)) ? CHECKSUM_LENGTH_LONG : CHECKSUM_LENGTH_SHORT;
138
139                // --group (display option)
140                groupedVersions = options.has(optionGroupedVersions);
141                
142                // --deleted
143                operationOptions.setDeleted(options.has(optionDeleted));
144                
145                // <path-expr>
146                List<?> nonOptionArgs = options.nonOptionArguments();
147                
148                if (nonOptionArgs.size() > 0) {
149                        operationOptions.setPathExpression(nonOptionArgs.get(0).toString());
150                }
151
152                return operationOptions;
153        }
154
155        @Override
156        public void printResults(OperationResult operationResult) {
157                LsOperationResult concreteOperationResult = (LsOperationResult) operationResult;
158                
159                int longestSize = calculateLongestSize(concreteOperationResult.getFileList());
160                int longestVersion = calculateLongestVersion(concreteOperationResult.getFileList());
161
162                if (fetchHistories) {
163                        printHistories(concreteOperationResult, longestSize, longestVersion);                           
164                }
165                else {
166                        printTree(concreteOperationResult, longestSize, longestVersion);                        
167                }
168        }
169        
170        private void printTree(LsOperationResult operationResult, int longestSize, int longestVersion) {
171                for (FileVersion fileVersion : operationResult.getFileList()) {                 
172                        printOneVersion(fileVersion, longestVersion, longestSize);                              
173                }
174        }
175
176        private void printHistories(LsOperationResult operationResult, int longestSize, int longestVersion) {
177                if (groupedVersions) {
178                        printGroupedHistories(operationResult, longestSize, longestVersion);                    
179                }
180                else {
181                        printNonGroupedHistories(operationResult, longestSize, longestVersion);                 
182                }
183        }
184
185        private void printNonGroupedHistories(LsOperationResult operationResult, int longestSize, int longestVersion) {
186                for (FileVersion fileVersion : operationResult.getFileList()) {
187                        PartialFileHistory fileHistory = operationResult.getFileVersions().get(fileVersion.getFileHistoryId());
188                        
189                        for (FileVersion fileVersionInHistory : fileHistory.getFileVersions().values()) {
190                                printOneVersion(fileVersionInHistory, longestVersion, longestSize);                                             
191                        }                                       
192                }       
193        }
194
195        private void printGroupedHistories(LsOperationResult operationResult, int longestSize, int longestVersion) {
196                Iterator<FileVersion> fileVersionIterator = operationResult.getFileList().iterator();
197                
198                while (fileVersionIterator.hasNext()) {
199                        FileVersion fileVersion = fileVersionIterator.next();
200                        PartialFileHistory fileHistory = operationResult.getFileVersions().get(fileVersion.getFileHistoryId());
201                        
202                        out.printf("File %s, %s\n", formatObjectId(fileHistory.getFileHistoryId()), fileVersion.getPath());
203                        
204                        for (FileVersion fileVersionInHistory : fileHistory.getFileVersions().values()) {
205                                if (fileVersionInHistory.equals(fileVersion)) {
206                                        out.print(" * ");
207                                }
208                                else {
209                                        out.print("   ");       
210                                }
211                                
212                                printOneVersion(fileVersionInHistory, longestVersion, longestSize);                                             
213                        }       
214                        
215                        if (fileVersionIterator.hasNext()) {
216                                out.println();
217                        }
218                }               
219        }
220
221        private void printOneVersion(FileVersion fileVersion, int longestVersion, int longestSize) {
222                String fileStatus = formatFileStatusShortStr(fileVersion.getStatus());
223                String fileType = formatFileTypeShortStr(fileVersion.getType());
224                String posixPermissions = (fileVersion.getPosixPermissions() != null) ? fileVersion.getPosixPermissions() : "";
225                String dosAttributes = (fileVersion.getDosAttributes() != null) ? fileVersion.getDosAttributes() : "";
226                String fileChecksum = formatObjectId(fileVersion.getChecksum());
227                String fileHistoryId = formatObjectId(fileVersion.getFileHistoryId());
228                String path = (fileVersion.getType() == FileType.SYMLINK) ? fileVersion.getPath() + " -> " + fileVersion.getLinkTarget() : fileVersion.getPath();
229
230                out.printf("%s %s %s %9s %4s %" + longestSize + "d %" + checksumLength + "s %" + checksumLength + "s %"+longestVersion+"d %s\n", 
231                                DATE_FORMAT.format(fileVersion.getUpdated()), fileStatus, fileType, posixPermissions, dosAttributes, fileVersion.getSize(), 
232                                fileChecksum, fileHistoryId, fileVersion.getVersion(), path);
233        }
234        
235        private String formatFileStatusShortStr(FileStatus status) {
236                switch (status) {
237                case NEW:
238                        return "A";
239                        
240                case CHANGED:
241                case RENAMED:
242                        return "M";
243                        
244                case DELETED:
245                        return "D";
246                        
247                default:
248                        return "?";
249                }
250        }
251
252        private String formatFileTypeShortStr(FileType type) {
253                switch (type) {
254                case FILE:
255                        return "-";
256                        
257                case FOLDER: 
258                        return "d";
259                        
260                case SYMLINK:
261                        return "s";
262                        
263                default:
264                        return "?";                             
265                }
266        }
267
268        private String formatObjectId(ObjectId checksum) {
269                if (checksum == null || "".equals(checksum)) {
270                        return "";
271                }
272                else {
273                        return checksum.toString().substring(0, checksumLength);
274                }
275        }
276
277        private int calculateLongestVersion(List<FileVersion> fileVersions) {
278                return calculateLongestValue(fileVersions, new Function<FileVersion, Integer>() {
279                        public Integer apply(FileVersion fileVersion) {
280                                return (""+fileVersion.getVersion()).length();
281                        }
282                });     
283        }
284        
285        private int calculateLongestSize(List<FileVersion> fileVersions) {
286                return calculateLongestValue(fileVersions, new Function<FileVersion, Integer>() {
287                        public Integer apply(FileVersion fileVersion) {
288                                return (""+fileVersion.getSize()).length();
289                        }
290                });     
291        }
292        
293        private int calculateLongestValue(List<FileVersion> fileVersions, Function<FileVersion, Integer> callbackFunction) {
294                int result = 0;
295                
296                for (FileVersion fileVersion : fileVersions) {
297                        result = Math.max(result, callbackFunction.apply(fileVersion));
298                }
299                
300                return result;  
301        }
302        
303        protected Date parseDateOption(String dateStr) throws Exception {
304                Pattern relativeDatePattern = Pattern.compile("(\\d+(?:[.,]\\d+)?)(mo|[smhdwy])");              
305                Matcher relativeDateMatcher = relativeDatePattern.matcher(dateStr);             
306                
307                if (relativeDateMatcher.find()) {
308                        long restoreDateMillies = CommandLineUtil.parseTimePeriod(dateStr)*1000;
309                        
310                        Date restoreDate = new Date(System.currentTimeMillis()-restoreDateMillies);
311                        
312                        logger.log(Level.FINE, "Restore date: "+restoreDate);
313                        return restoreDate;
314                }
315                else {
316                        try {
317                                Date restoreDate = DATE_FORMAT.parse(dateStr);
318                                
319                                logger.log(Level.FINE, "Restore date: "+restoreDate);
320                                return restoreDate;
321                        }
322                        catch (Exception e) {
323                                throw new Exception("Invalid '--date' argument: " + dateStr + ", use relative date or absolute format: " + DATE_FORMAT_PATTERN);
324                        }
325                }               
326        }
327}