Add session to cache with both hostname and address.
[cyberduck.git] / source / ch / cyberduck / core / ftp / FTPMlsdListResponseReader.java
blobd6f54efbdcfe00ba8007378effce3d45d3aba25e
1 package ch.cyberduck.core.ftp;
3 /*
4 * Copyright (c) 2002-2013 David Kocher. All rights reserved.
5 * http://cyberduck.ch/
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * Bug fixes, suggestions and comments should be sent to feedback@cyberduck.ch
20 import ch.cyberduck.core.AttributedList;
21 import ch.cyberduck.core.ListProgressListener;
22 import ch.cyberduck.core.Path;
23 import ch.cyberduck.core.PathNormalizer;
24 import ch.cyberduck.core.Permission;
25 import ch.cyberduck.core.date.InvalidDateException;
26 import ch.cyberduck.core.date.MDTMMillisecondsDateFormatter;
27 import ch.cyberduck.core.date.MDTMSecondsDateFormatter;
28 import ch.cyberduck.core.exception.ConnectionCanceledException;
30 import org.apache.commons.lang3.StringUtils;
31 import org.apache.log4j.Logger;
33 import java.io.IOException;
34 import java.util.Date;
35 import java.util.EnumSet;
36 import java.util.HashMap;
37 import java.util.List;
38 import java.util.Locale;
39 import java.util.Map;
40 import java.util.regex.Matcher;
41 import java.util.regex.Pattern;
43 /**
44 * @version $Id$
46 public class FTPMlsdListResponseReader implements FTPDataResponseReader {
47 private static final Logger log = Logger.getLogger(FTPMlsdListResponseReader.class);
49 public FTPMlsdListResponseReader() {
53 @Override
54 public AttributedList<Path> read(final Path directory, final List<String> replies, final ListProgressListener listener)
55 throws IOException, FTPInvalidListException, ConnectionCanceledException {
56 final AttributedList<Path> children = new AttributedList<Path>();
57 // At least one entry successfully parsed
58 boolean success = false;
59 for(String line : replies) {
60 final Map<String, Map<String, String>> file = this.parseFacts(line);
61 if(null == file) {
62 log.error(String.format("Error parsing line %s", line));
63 continue;
65 for(Map.Entry<String, Map<String, String>> f : file.entrySet()) {
66 final String name = f.getKey();
67 // size -- Size in octets
68 // modify -- Last modification time
69 // create -- Creation time
70 // type -- Entry type
71 // unique -- Unique id of file/directory
72 // perm -- File permissions, whether read, write, execute is allowed for the login id.
73 // lang -- Language of the file name per IANA [11] registry.
74 // media-type -- MIME media-type of file contents per IANA registry.
75 // charset -- Character set per IANA registry (if not UTF-8)
76 final Map<String, String> facts = f.getValue();
77 if(!facts.containsKey("type")) {
78 log.error(String.format("No type fact in line %s", line));
79 continue;
81 final Path parsed;
82 if("dir".equals(facts.get("type").toLowerCase(Locale.ROOT))) {
83 parsed = new Path(directory, PathNormalizer.name(f.getKey()), EnumSet.of(Path.Type.directory));
85 else if("file".equals(facts.get("type").toLowerCase(Locale.ROOT))) {
86 parsed = new Path(directory, PathNormalizer.name(f.getKey()), EnumSet.of(Path.Type.file));
88 else if(facts.get("type").toLowerCase(Locale.ROOT).matches("os\\.unix=slink:.*")) {
89 parsed = new Path(directory, PathNormalizer.name(f.getKey()), EnumSet.of(Path.Type.file, Path.Type.symboliclink));
90 // Parse symbolic link target in Type=OS.unix=slink:/foobar;Perm=;Unique=keVO1+4G4; foobar
91 final String[] type = facts.get("type").split(":");
92 if(type.length == 2) {
93 final String target = type[1];
94 if(target.startsWith(String.valueOf(Path.DELIMITER))) {
95 parsed.setSymlinkTarget(new Path(target, EnumSet.of(Path.Type.file)));
97 else {
98 parsed.setSymlinkTarget(new Path(String.format("%s/%s", directory.getAbsolute(), target), EnumSet.of(Path.Type.file)));
101 else {
102 log.warn(String.format("Missing symbolic link target for type %s in line %s", facts.get("type"), line));
103 continue;
106 else {
107 log.warn(String.format("Ignored type %s in line %s", facts.get("type"), line));
108 continue;
110 if(!success) {
111 if(parsed.isDirectory() && directory.getName().equals(name)) {
112 log.warn(String.format("Possibly bogus response line %s", line));
114 else {
115 success = true;
118 if(name.equals(".") || name.equals("..")) {
119 if(log.isDebugEnabled()) {
120 log.debug(String.format("Skip %s", name));
122 continue;
124 if(facts.containsKey("size")) {
125 parsed.attributes().setSize(Long.parseLong(facts.get("size")));
127 if(facts.containsKey("unix.uid")) {
128 parsed.attributes().setOwner(facts.get("unix.uid"));
130 if(facts.containsKey("unix.owner")) {
131 parsed.attributes().setOwner(facts.get("unix.owner"));
133 if(facts.containsKey("unix.gid")) {
134 parsed.attributes().setGroup(facts.get("unix.gid"));
136 if(facts.containsKey("unix.group")) {
137 parsed.attributes().setGroup(facts.get("unix.group"));
139 if(facts.containsKey("unix.mode")) {
140 parsed.attributes().setPermission(new Permission(facts.get("unix.mode")));
142 if(facts.containsKey("modify")) {
143 // Time values are always represented in UTC
144 parsed.attributes().setModificationDate(this.parseTimestamp(facts.get("modify")));
146 if(facts.containsKey("create")) {
147 // Time values are always represented in UTC
148 parsed.attributes().setCreationDate(this.parseTimestamp(facts.get("create")));
150 children.add(parsed);
151 listener.chunk(directory, children);
154 if(!success) {
155 throw new FTPInvalidListException(children);
157 return children;
161 * Parse the timestamp using the MTDM format
163 * @param timestamp Date string
164 * @return Milliseconds
166 protected long parseTimestamp(final String timestamp) {
167 if(null == timestamp) {
168 return -1;
170 try {
171 final Date parsed = new MDTMSecondsDateFormatter().parse(timestamp);
172 return parsed.getTime();
174 catch(InvalidDateException e) {
175 log.warn("Failed to parse timestamp:" + e.getMessage());
176 try {
177 final Date parsed = new MDTMMillisecondsDateFormatter().parse(timestamp);
178 return parsed.getTime();
180 catch(InvalidDateException f) {
181 log.warn("Failed to parse timestamp:" + f.getMessage());
184 log.error(String.format("Failed to parse timestamp %s", timestamp));
185 return -1;
189 * The "facts" for a file in a reply to a MLSx command consist of
190 * information about that file. The facts are a series of keyword=value
191 * pairs each followed by semi-colon (";") characters. An individual
192 * fact may not contain a semi-colon in its name or value. The complete
193 * series of facts may not contain the space character. See the
194 * definition or "RCHAR" in section 2.1 for a list of the characters
195 * that can occur in a fact value. Not all are applicable to all facts.
196 * <p/>
197 * A sample of a typical series of facts would be: (spread over two
198 * lines for presentation here only)
199 * <p/>
200 * size=4161;lang=en-US;modify=19970214165800;create=19961001124534;
201 * type=file;x.myfact=foo,bar;
202 * <p/>
203 * This document defines a standard set of facts as follows:
204 * <p/>
205 * size -- Size in octets
206 * modify -- Last modification time
207 * create -- Creation time
208 * type -- Entry type
209 * unique -- Unique id of file/directory
210 * perm -- File permissions, whether read, write, execute is
211 * allowed for the login id.
212 * lang -- Language of the file name per IANA [11] registry.
213 * media-type -- MIME media-type of file contents per IANA registry.
214 * charset -- Character set per IANA registry (if not UTF-8)
216 * @param line The "facts" for a file in a reply to a MLSx command
217 * @return Parsed keys and values
219 protected Map<String, Map<String, String>> parseFacts(final String line) {
220 final Pattern p = Pattern.compile("\\s?(\\S+\\=\\S+;)*\\s(.*)");
221 final Matcher result = p.matcher(line);
222 final Map<String, Map<String, String>> file = new HashMap<String, Map<String, String>>();
223 if(result.matches()) {
224 final String filename = result.group(2);
225 final Map<String, String> facts = new HashMap<String, String>();
226 for(String fact : result.group(1).split(";")) {
227 String key = StringUtils.substringBefore(fact, "=");
228 if(StringUtils.isBlank(key)) {
229 continue;
231 String value = StringUtils.substringAfter(fact, "=");
232 if(StringUtils.isBlank(value)) {
233 continue;
235 facts.put(key.toLowerCase(Locale.ROOT), value);
237 file.put(filename, facts);
238 return file;
240 log.warn(String.format("No match for %s", line));
241 return null;