1 package ch
.cyberduck
.core
.ftp
;
4 * Copyright (c) 2002-2013 David Kocher. All rights reserved.
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
;
40 import java
.util
.regex
.Matcher
;
41 import java
.util
.regex
.Pattern
;
46 public class FTPMlsdListResponseReader
implements FTPDataResponseReader
{
47 private static final Logger log
= Logger
.getLogger(FTPMlsdListResponseReader
.class);
49 public FTPMlsdListResponseReader() {
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
);
62 log
.error(String
.format("Error parsing line %s", line
));
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
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
));
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
)));
98 parsed
.setSymlinkTarget(new Path(String
.format("%s/%s", directory
.getAbsolute(), target
), EnumSet
.of(Path
.Type
.file
)));
102 log
.warn(String
.format("Missing symbolic link target for type %s in line %s", facts
.get("type"), line
));
107 log
.warn(String
.format("Ignored type %s in line %s", facts
.get("type"), line
));
111 if(parsed
.isDirectory() && directory
.getName().equals(name
)) {
112 log
.warn(String
.format("Possibly bogus response line %s", line
));
118 if(name
.equals(".") || name
.equals("..")) {
119 if(log
.isDebugEnabled()) {
120 log
.debug(String
.format("Skip %s", name
));
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
);
155 throw new FTPInvalidListException(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
) {
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());
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
));
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.
197 * A sample of a typical series of facts would be: (spread over two
198 * lines for presentation here only)
200 * size=4161;lang=en-US;modify=19970214165800;create=19961001124534;
201 * type=file;x.myfact=foo,bar;
203 * This document defines a standard set of facts as follows:
205 * size -- Size in octets
206 * modify -- Last modification time
207 * create -- Creation time
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
)) {
231 String value
= StringUtils
.substringAfter(fact
, "=");
232 if(StringUtils
.isBlank(value
)) {
235 facts
.put(key
.toLowerCase(Locale
.ROOT
), value
);
237 file
.put(filename
, facts
);
240 log
.warn(String
.format("No match for %s", line
));