1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.vfs2.provider.ftp;
18
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.OutputStream;
22 import java.time.Instant;
23 import java.util.Calendar;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Objects;
28 import java.util.TimeZone;
29 import java.util.TreeMap;
30 import java.util.concurrent.atomic.AtomicBoolean;
31
32 import org.apache.commons.io.function.Uncheck;
33 import org.apache.commons.lang3.ArrayUtils;
34 import org.apache.commons.logging.Log;
35 import org.apache.commons.logging.LogFactory;
36 import org.apache.commons.net.ftp.FTPFile;
37 import org.apache.commons.vfs2.FileName;
38 import org.apache.commons.vfs2.FileNotFolderException;
39 import org.apache.commons.vfs2.FileNotFoundException;
40 import org.apache.commons.vfs2.FileObject;
41 import org.apache.commons.vfs2.FileSystemException;
42 import org.apache.commons.vfs2.FileType;
43 import org.apache.commons.vfs2.RandomAccessContent;
44 import org.apache.commons.vfs2.provider.AbstractFileName;
45 import org.apache.commons.vfs2.provider.AbstractFileObject;
46 import org.apache.commons.vfs2.provider.UriParser;
47 import org.apache.commons.vfs2.util.FileObjectUtils;
48 import org.apache.commons.vfs2.util.Messages;
49 import org.apache.commons.vfs2.util.MonitorInputStream;
50 import org.apache.commons.vfs2.util.MonitorOutputStream;
51 import org.apache.commons.vfs2.util.RandomAccessMode;
52
53
54
55
56 public class FtpFileObject extends AbstractFileObject<FtpFileSystem> {
57
58
59
60
61 final class FtpInputStream extends MonitorInputStream {
62 private final FtpClient client;
63
64 FtpInputStream(final FtpClient client, final InputStream in) {
65 super(in);
66 this.client = client;
67 }
68
69 FtpInputStream(final FtpClient client, final InputStream in, final int bufferSize) {
70 super(in, bufferSize);
71 this.client = client;
72 }
73
74 void abort() throws IOException {
75 client.abort();
76 close();
77 }
78
79 private boolean isTransferAbortedOkReplyCode() throws IOException {
80 final List<Integer> transferAbortedOkReplyCodes = FtpFileSystemConfigBuilder
81 .getInstance()
82 .getTransferAbortedOkReplyCodes(getAbstractFileSystem().getFileSystemOptions());
83 return transferAbortedOkReplyCodes != null && transferAbortedOkReplyCodes.contains(client.getReplyCode());
84 }
85
86
87
88
89 @Override
90 protected void onClose() throws IOException {
91 final boolean ok;
92 try {
93 ok = client.completePendingCommand() || isTransferAbortedOkReplyCode();
94 } finally {
95 getAbstractFileSystem().putClient(client);
96 }
97 if (!ok) {
98 throw new FileSystemException("vfs.provider.ftp/finish-get.error", getName());
99 }
100 }
101 }
102
103
104
105 private final class FtpOutputStream extends MonitorOutputStream {
106 private final FtpClient client;
107
108 FtpOutputStream(final FtpClient client, final OutputStream outstr) {
109 super(outstr);
110 this.client = client;
111 }
112
113
114
115
116 @Override
117 protected void onClose() throws IOException {
118 final boolean ok;
119 try {
120 ok = client.completePendingCommand();
121 } finally {
122 getAbstractFileSystem().putClient(client);
123 }
124 if (!ok) {
125 throw new FileSystemException("vfs.provider.ftp/finish-put.error", getName());
126 }
127 }
128 }
129
130 private static final long DEFAULT_TIMESTAMP = 0L;
131 private static final Map<String, FTPFile> EMPTY_FTP_FILE_MAP = Collections.unmodifiableMap(new TreeMap<>());
132 private static final FTPFile UNKNOWN = new FTPFile();
133
134 private static final Log log = LogFactory.getLog(FtpFileObject.class);
135 private volatile boolean mdtmSet;
136 private final String relPath;
137
138 private volatile FTPFile ftpFile;
139 private volatile Map<String, FTPFile> childMap;
140
141 private volatile FileObject linkDestination;
142
143 private final AtomicBoolean inRefresh = new AtomicBoolean();
144
145
146
147
148
149
150
151
152
153 protected FtpFileObject(final AbstractFileName fileName, final FtpFileSystem fileSystem, final FileName rootName)
154 throws FileSystemException {
155 super(fileName, fileSystem);
156 final String relPath = UriParser.decode(rootName.getRelativeName(fileName));
157 if (".".equals(relPath)) {
158
159
160
161
162 this.relPath = null;
163 } else {
164 this.relPath = relPath;
165 }
166 }
167
168
169
170
171 @Override
172 protected void doAttach() throws IOException {
173
174
175 }
176
177
178
179
180 @Override
181 protected void doCreateFolder() throws Exception {
182 final boolean ok;
183 final FtpClient client = getAbstractFileSystem().getClient();
184 try {
185 ok = client.makeDirectory(relPath);
186 } finally {
187 getAbstractFileSystem().putClient(client);
188 }
189 if (!ok) {
190 throw new FileSystemException("vfs.provider.ftp/create-folder.error", getName());
191 }
192 }
193
194
195
196
197 @Override
198 protected void doDelete() throws Exception {
199 synchronized (getFileSystem()) {
200 if (ftpFile != null) {
201 final boolean ok;
202 final FtpClient ftpClient = getAbstractFileSystem().getClient();
203 try {
204 if (ftpFile.isDirectory()) {
205 ok = ftpClient.removeDirectory(relPath);
206 } else {
207 ok = ftpClient.deleteFile(relPath);
208 }
209 } finally {
210 getAbstractFileSystem().putClient(ftpClient);
211 }
212 if (!ok) {
213 throw new FileSystemException("vfs.provider.ftp/delete-file.error", getName());
214 }
215 ftpFile = null;
216 }
217 childMap = EMPTY_FTP_FILE_MAP;
218 }
219 }
220
221
222
223
224 @Override
225 protected void doDetach() {
226 synchronized (getFileSystem()) {
227 ftpFile = null;
228 childMap = null;
229 mdtmSet = false;
230 }
231 }
232
233
234
235
236 private void doGetChildren() throws IOException {
237 if (childMap != null) {
238 return;
239 }
240 final FtpClient client = getAbstractFileSystem().getClient();
241 try {
242 final String path = ftpFile != null && ftpFile.isSymbolicLink()
243 ? getFileSystem().getFileSystemManager().resolveName(getParent().getName(), ftpFile.getLink()).getPath()
244 : relPath;
245 final FTPFile[] tmpChildren = client.listFiles(path);
246 if (ArrayUtils.isEmpty(tmpChildren)) {
247 childMap = EMPTY_FTP_FILE_MAP;
248 } else {
249 childMap = new TreeMap<>();
250
251 for (int i = 0; i < tmpChildren.length; i++) {
252 final FTPFile child = tmpChildren[i];
253 if (child == null) {
254 if (log.isDebugEnabled()) {
255 log.debug(Messages.getString("vfs.provider.ftp/invalid-directory-entry.debug", Integer.valueOf(i), relPath));
256 }
257 continue;
258 }
259 if (!".".equals(child.getName()) && !"..".equals(child.getName())) {
260 childMap.put(child.getName(), child);
261 }
262 }
263 }
264 } finally {
265 getAbstractFileSystem().putClient(client);
266 }
267 }
268
269
270
271
272 @Override
273 protected long doGetContentSize() throws Exception {
274 synchronized (getFileSystem()) {
275 if (ftpFile == null) {
276 return 0;
277 }
278 if (ftpFile.isSymbolicLink()) {
279 final FileObject linkDest = getLinkDestination();
280
281 if (isCircular(linkDest)) {
282 return ftpFile.getSize();
283 }
284 return linkDest.getContent().getSize();
285 }
286 return ftpFile.getSize();
287 }
288 }
289
290
291
292
293 @Override
294 protected InputStream doGetInputStream(final int bufferSize) throws Exception {
295 final FtpClient client = getAbstractFileSystem().getClient();
296 try {
297 final InputStream inputStream = client.retrieveFileStream(relPath, 0);
298
299 if (inputStream == null) {
300 throw new FileNotFoundException(getName().toString());
301 }
302 return new FtpInputStream(client, inputStream, bufferSize);
303 } catch (final Exception e) {
304 getAbstractFileSystem().putClient(client);
305 throw e;
306 }
307 }
308
309
310
311
312
313
314 @Override
315 protected long doGetLastModifiedTime() throws Exception {
316 synchronized (getFileSystem()) {
317 if (ftpFile == null) {
318 return DEFAULT_TIMESTAMP;
319 }
320 if (ftpFile.isSymbolicLink()) {
321 final FileObject linkDest = getLinkDestination();
322
323 if (isCircular(linkDest)) {
324 return getTimestampMillis();
325 }
326 return linkDest.getContent().getLastModifiedTime();
327 }
328 return getTimestampMillis();
329 }
330 }
331
332
333
334
335 @Override
336 protected OutputStream doGetOutputStream(final boolean bAppend) throws Exception {
337 final FtpClient client = getAbstractFileSystem().getClient();
338 try {
339 final OutputStream out;
340 if (bAppend) {
341 out = client.appendFileStream(relPath);
342 } else {
343 out = client.storeFileStream(relPath);
344 }
345
346 FileSystemException.requireNonNull(out, "vfs.provider.ftp/output-error.debug", getName(),
347 client.getReplyString());
348
349 return new FtpOutputStream(client, out);
350 } catch (final Exception e) {
351 getAbstractFileSystem().putClient(client);
352 throw e;
353 }
354 }
355
356 @Override
357 protected RandomAccessContent doGetRandomAccessContent(final RandomAccessMode mode) throws Exception {
358 return new FtpRandomAccessContent(this, mode);
359 }
360
361
362
363
364 @Override
365 protected FileType doGetType() throws Exception {
366
367 synchronized (getFileSystem()) {
368 if (ftpFile == null) {
369 setFTPFile(false);
370 }
371
372 if (ftpFile == UNKNOWN) {
373 return FileType.IMAGINARY;
374 }
375 if (ftpFile.isDirectory()) {
376 return FileType.FOLDER;
377 }
378 if (ftpFile.isFile()) {
379 return FileType.FILE;
380 }
381 if (ftpFile.isSymbolicLink()) {
382 final FileObject linkDest = getLinkDestination();
383
384 if (isCircular(linkDest)) {
385
386
387
388
389 return FileType.IMAGINARY;
390 }
391 return linkDest.getType();
392
393 }
394 }
395 throw new FileSystemException("vfs.provider.ftp/get-type.error", getName());
396 }
397
398
399
400
401 @Override
402 protected String[] doListChildren() throws Exception {
403
404 doGetChildren();
405
406
407 if (childMap == null) {
408 return null;
409 }
410
411
412 final String[] childNames = childMap.values().stream().filter(Objects::nonNull).map(FTPFile::getName).toArray(String[]::new);
413
414 return UriParser.encode(childNames);
415 }
416
417 @Override
418 protected FileObject[] doListChildrenResolved() throws Exception {
419 synchronized (getFileSystem()) {
420 if (ftpFile != null && ftpFile.isSymbolicLink()) {
421 final FileObject linkDest = getLinkDestination();
422
423 if (isCircular(linkDest)) {
424 return null;
425 }
426 return linkDest.getChildren();
427 }
428 }
429 return null;
430 }
431
432
433
434
435 @Override
436 protected void doRename(final FileObject newFile) throws Exception {
437 synchronized (getFileSystem()) {
438 final boolean ok;
439 final FtpClient ftpClient = getAbstractFileSystem().getClient();
440 try {
441 final String newName = ((FtpFileObject) FileObjectUtils.getAbstractFileObject(newFile)).getRelPath();
442 ok = ftpClient.rename(relPath, newName);
443 } finally {
444 getAbstractFileSystem().putClient(ftpClient);
445 }
446
447 if (!ok) {
448 throw new FileSystemException("vfs.provider.ftp/rename-file.error", getName().toString(), newFile);
449 }
450 ftpFile = null;
451 childMap = EMPTY_FTP_FILE_MAP;
452 }
453 }
454
455
456
457
458
459
460
461 private FTPFile getChildFile(final String name, final boolean flush) throws IOException {
462
463
464
465
466
467 if (flush && !inRefresh.get()) {
468 childMap = null;
469 }
470
471
472 doGetChildren();
473
474
475
476 return childMap != null ? childMap.get(name) : null;
477 }
478
479
480
481
482
483
484
485
486
487 @Override
488 public FileObject[] getChildren() throws FileSystemException {
489 try {
490 if (doGetType() != FileType.FOLDER) {
491 throw new FileNotFolderException(getName());
492 }
493 } catch (final Exception ex) {
494 throw new FileNotFolderException(getName(), ex);
495 }
496 try {
497
498
499
500
501
502
503 inRefresh.set(true);
504 return super.getChildren();
505 } finally {
506 inRefresh.set(false);
507 }
508 }
509
510 FtpInputStream getInputStream(final long filePointer) throws IOException {
511 final FtpClient client = getAbstractFileSystem().getClient();
512 try {
513 final InputStream instr = client.retrieveFileStream(relPath, filePointer);
514 FileSystemException.requireNonNull(instr, "vfs.provider.ftp/input-error.debug", getName(),
515 client.getReplyString());
516 return new FtpInputStream(client, instr);
517 } catch (final IOException e) {
518 getAbstractFileSystem().putClient(client);
519 throw e;
520 }
521 }
522
523 private FileObject getLinkDestination() throws FileSystemException {
524 if (linkDestination == null) {
525 final String path;
526 synchronized (getFileSystem()) {
527 path = ftpFile == null ? null : ftpFile.getLink();
528 }
529 final FileName parent = getName().getParent();
530 final FileName relativeTo = parent == null ? getName() : parent;
531 final FileName linkDestinationName = getFileSystem().getFileSystemManager().resolveName(relativeTo, path);
532 linkDestination = getFileSystem().resolveFile(linkDestinationName);
533 }
534 return linkDestination;
535 }
536
537 String getRelPath() {
538 return relPath;
539 }
540
541
542
543
544 @SuppressWarnings("resource")
545 private long getTimestampMillis() throws IOException {
546 final FtpFileSystem abstractFileSystem = getAbstractFileSystem();
547 final Boolean mdtmLastModifiedTime = FtpFileSystemConfigBuilder.getInstance()
548 .getMdtmLastModifiedTime(abstractFileSystem.getFileSystemOptions());
549 if (mdtmLastModifiedTime != null && mdtmLastModifiedTime.booleanValue()) {
550 final FtpClient client = abstractFileSystem.getClient();
551 if (!mdtmSet && client.hasFeature("MDTM")) {
552 final Instant mdtmInstant = client.mdtmInstant(relPath);
553 final Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
554 final long epochMilli = mdtmInstant.toEpochMilli();
555 calendar.setTimeInMillis(epochMilli);
556 ftpFile.setTimestamp(calendar);
557 mdtmSet = true;
558 }
559 }
560 return ftpFile.getTimestamp().getTime().getTime();
561 }
562
563
564
565
566 private boolean isCircular(final FileObject linkDest) throws FileSystemException {
567 return linkDest.getName().getPathDecoded().equals(getName().getPathDecoded());
568 }
569
570
571
572
573 @Override
574 protected void onChange() throws IOException {
575 childMap = null;
576
577 if (getType().equals(FileType.IMAGINARY)) {
578
579 synchronized (getFileSystem()) {
580 ftpFile = UNKNOWN;
581 }
582 return;
583 }
584
585 setFTPFile(true);
586 }
587
588
589
590
591 @Override
592 protected void onChildrenChanged(final FileName child, final FileType newType) {
593 if (childMap != null && newType.equals(FileType.IMAGINARY)) {
594 Uncheck.run(() -> childMap.remove(UriParser.decode(child.getBaseName())));
595 } else {
596
597
598 childMap = null;
599 }
600 }
601
602
603
604
605 @Override
606 public void refresh() throws FileSystemException {
607 if (inRefresh.compareAndSet(false, true)) {
608 try {
609 super.refresh();
610 synchronized (getFileSystem()) {
611 ftpFile = null;
612 }
613
614
615
616
617 } finally {
618 inRefresh.set(false);
619 }
620 }
621 }
622
623
624
625
626 private void setFTPFile(final boolean flush) throws IOException {
627 synchronized (getFileSystem()) {
628 final FtpFileObject parent = (FtpFileObject) FileObjectUtils.getAbstractFileObject(getParent());
629 final FTPFile newFileInfo;
630 if (parent != null) {
631 newFileInfo = parent.getChildFile(UriParser.decode(getName().getBaseName()), flush);
632 } else {
633
634 newFileInfo = new FTPFile();
635 newFileInfo.setType(FTPFile.DIRECTORY_TYPE);
636 }
637 ftpFile = newFileInfo == null ? UNKNOWN : newFileInfo;
638 }
639 }
640 }