001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.net.ftp;
019
020import java.io.Serializable;
021import java.time.Instant;
022import java.util.Calendar;
023import java.util.Date;
024import java.util.Formatter;
025import java.util.TimeZone;
026
027/**
028 * The FTPFile class is used to represent information about files stored on an FTP server.
029 *
030 * @see FTPFileEntryParser
031 * @see FTPClient#listFiles
032 */
033public class FTPFile implements Serializable {
034
035    private static final long serialVersionUID = 9010790363003271996L;
036
037    /** A constant indicating an FTPFile is a file. */
038    public static final int FILE_TYPE = 0;
039
040    /** A constant indicating an FTPFile is a directory. */
041    public static final int DIRECTORY_TYPE = 1;
042
043    /** A constant indicating an FTPFile is a symbolic link. */
044    public static final int SYMBOLIC_LINK_TYPE = 2;
045
046    /** A constant indicating an FTPFile is of unknown type. */
047    public static final int UNKNOWN_TYPE = 3;
048
049    /** A constant indicating user access permissions. */
050    public static final int USER_ACCESS = 0;
051
052    /** A constant indicating group access permissions. */
053    public static final int GROUP_ACCESS = 1;
054
055    /** A constant indicating world access permissions. */
056    public static final int WORLD_ACCESS = 2;
057
058    /** A constant indicating file/directory read permission. */
059    public static final int READ_PERMISSION = 0;
060
061    /** A constant indicating file/directory write permission. */
062    public static final int WRITE_PERMISSION = 1;
063
064    /** A constant indicating file execute permission or directory listing permission. */
065    public static final int EXECUTE_PERMISSION = 2;
066
067    private int type = UNKNOWN_TYPE;
068
069    /** 0 is invalid as a link count. */
070    private int hardLinkCount;
071
072    /** 0 is valid, so use -1. */
073    private long size = -1;
074    private String rawListing;
075    private String user = "";
076    private String group = "";
077    private String name;
078    private String link;
079
080    // TODO Consider changing internal representation to java.time.
081    private Calendar calendar;
082
083    /** If this is null, then list entry parsing failed. */
084    private final boolean[][] permissions; // e.g. _permissions[USER_ACCESS][READ_PERMISSION]
085
086    /** Creates an empty FTPFile. */
087    public FTPFile() {
088        permissions = new boolean[3][3];
089    }
090
091    /**
092     * Constructor for use by {@link FTPListParseEngine} only. Used to create FTPFile entries for failed parses
093     *
094     * @param rawListing line that could not be parsed.
095     * @since 3.4
096     */
097    FTPFile(final String rawListing) {
098        this.permissions = null; // flag that entry is invalid
099        this.rawListing = rawListing;
100    }
101
102    private char formatType() {
103        switch (type) {
104        case FILE_TYPE:
105            return '-';
106        case DIRECTORY_TYPE:
107            return 'd';
108        case SYMBOLIC_LINK_TYPE:
109            return 'l';
110        default:
111            return '?';
112        }
113    }
114
115    /**
116     * Gets the name of the group owning the file. Sometimes this will be a string representation of the group number.
117     *
118     * @return The name of the group owning the file.
119     */
120    public String getGroup() {
121        return group;
122    }
123
124    /**
125     * Gets the number of hard links to this file. This is not to be confused with symbolic links.
126     *
127     * @return The number of hard links to this file.
128     */
129    public int getHardLinkCount() {
130        return hardLinkCount;
131    }
132
133    /**
134     * If the FTPFile is a symbolic link, this method returns the name of the file being pointed to by the symbolic link.
135     * Otherwise, it returns {@code null}.
136     *
137     * @return The file pointed to by the symbolic link ({@code null} if the FTPFile is not a symbolic link).
138     */
139    public String getLink() {
140        return link;
141    }
142
143    /**
144     * Gets the name of the file.
145     *
146     * @return The name of the file.
147     */
148    public String getName() {
149        return name;
150    }
151
152    /**
153     * Gets the original FTP server raw listing used to initialize the FTPFile.
154     *
155     * @return The original FTP server raw listing used to initialize the FTPFile.
156     */
157    public String getRawListing() {
158        return rawListing;
159    }
160
161    /**
162     * Gets the file size in bytes.
163     *
164     * @return The file size in bytes.
165     */
166    public long getSize() {
167        return size;
168    }
169
170    /**
171     * Gets the file timestamp. This usually the last modification time.
172     *
173     * @return A Calendar instance representing the file timestamp.
174     */
175    public Calendar getTimestamp() {
176        return calendar;
177    }
178
179    /**
180     * Gets the file timestamp. This usually the last modification time.
181     *
182     * @return A Calendar instance representing the file timestamp.
183     * @since 3.9.0
184     */
185    public Instant getTimestampInstant() {
186        return calendar == null ? null : calendar.toInstant();
187    }
188
189    /**
190     * Gets the type of the file (one of the {@code _TYPE} constants), e.g., if it is a directory, a regular file, or a symbolic link.
191     *
192     * @return The type of the file.
193     */
194    public int getType() {
195        return type;
196    }
197
198    /**
199     * Gets the name of the user owning the file. Sometimes this will be a string representation of the user number.
200     *
201     * @return The name of the user owning the file.
202     */
203    public String getUser() {
204        return user;
205    }
206
207    /**
208     * Tests if the given access group (one of the {@code _ACCESS} constants) has the given access permission (one of the {@code _PERMISSION}
209     * constants) to the file.
210     *
211     * @param access     The access group (one of the {@code _ACCESS} constants)
212     * @param permission The access permission (one of the {@code _PERMISSION} constants)
213     * @throws ArrayIndexOutOfBoundsException if either of the parameters is out of range
214     * @return {@code true} if {@link #isValid()} is {@code true} and the associated permission is set; {@code false} otherwise.
215     */
216    public boolean hasPermission(final int access, final int permission) {
217        if (permissions == null) {
218            return false;
219        }
220        return permissions[access][permission];
221    }
222
223    /**
224     * Tests if the file is a directory.
225     *
226     * @return {@code true} if the file is of type {@code DIRECTORY_TYPE}, {@code false} if not.
227     */
228    public boolean isDirectory() {
229        return type == DIRECTORY_TYPE;
230    }
231
232    /**
233     * Tests if the file is a regular file.
234     *
235     * @return {@code true} if the file is of type {@code FILE_TYPE}, {@code false} if not.
236     */
237    public boolean isFile() {
238        return type == FILE_TYPE;
239    }
240
241    /**
242     * Tests if the file is a symbolic link.
243     *
244     * @return {@code true} if the file is of type {@code SYMBOLIC_LINK_TYPE}, {@code false} if not.
245     */
246    public boolean isSymbolicLink() {
247        return type == SYMBOLIC_LINK_TYPE;
248    }
249
250    /**
251     * Tests if the type of the file is unknown.
252     *
253     * @return {@code true} if the file is of type {@code UNKNOWN_TYPE}, {@code false} if not.
254     */
255    public boolean isUnknown() {
256        return type == UNKNOWN_TYPE;
257    }
258
259    /**
260     * Tests whether an entry is valid or not. If the entry is invalid, only the {@link #getRawListing()} method will be useful. Other methods may fail.
261     *
262     * Used in conjunction with list parsing that preserves entries that failed to parse.
263     *
264     * @see FTPClientConfig#setUnparseableEntries(boolean)
265     * @return {@code true} if the entry is valid; {@code false} otherwise
266     * @since 3.4
267     */
268    public boolean isValid() {
269        return permissions != null;
270    }
271
272    private String permissionToString(final int access) {
273        final StringBuilder sb = new StringBuilder();
274        if (hasPermission(access, READ_PERMISSION)) {
275            sb.append('r');
276        } else {
277            sb.append('-');
278        }
279        if (hasPermission(access, WRITE_PERMISSION)) {
280            sb.append('w');
281        } else {
282            sb.append('-');
283        }
284        if (hasPermission(access, EXECUTE_PERMISSION)) {
285            sb.append('x');
286        } else {
287            sb.append('-');
288        }
289        return sb.toString();
290    }
291
292    private void readObject(final java.io.ObjectInputStream in) {
293        throw new UnsupportedOperationException("Serialization is not supported");
294    }
295
296    /**
297     * Sets the name of the group owning the file. This may be a string representation of the group number.
298     *
299     * @param group The name of the group owning the file.
300     */
301    public void setGroup(final String group) {
302        this.group = group;
303    }
304
305    /**
306     * Sets the number of hard links to this file. This is not to be confused with symbolic links.
307     *
308     * @param links The number of hard links to this file.
309     */
310    public void setHardLinkCount(final int links) {
311        this.hardLinkCount = links;
312    }
313
314    /**
315     * If the FTPFile is a symbolic link, use this method to set the name of the file being pointed to by the symbolic link.
316     *
317     * @param link The file pointed to by the symbolic link.
318     */
319    public void setLink(final String link) {
320        this.link = link;
321    }
322
323    /**
324     * Sets the name of the file.
325     *
326     * @param name The name of the file.
327     */
328    public void setName(final String name) {
329        this.name = name;
330    }
331
332    /**
333     * Sets if the given access group (one of the {@code _ACCESS} constants) has the given access permission (one of the {@code _PERMISSION}
334     * constants) to the file.
335     *
336     * @param access     The access group (one of the {@code _ACCESS} constants)
337     * @param permission The access permission (one of the {@code _PERMISSION} constants)
338     * @param value      {@code true} if permission is allowed, {@code false} if not.
339     * @throws ArrayIndexOutOfBoundsException if either of the parameters is out of range
340     */
341    public void setPermission(final int access, final int permission, final boolean value) {
342        // TODO: only allow permission setting if file is valid
343        permissions[access][permission] = value;
344    }
345
346    /**
347     * Sets the original FTP server raw listing from which the FTPFile was created.
348     *
349     * @param rawListing The raw FTP server listing.
350     */
351    public void setRawListing(final String rawListing) {
352        this.rawListing = rawListing;
353    }
354
355    /**
356     * Sets the file size in bytes.
357     *
358     * @param size The file size in bytes.
359     */
360    public void setSize(final long size) {
361        this.size = size;
362    }
363
364    /**
365     * Sets the file timestamp. This usually the last modification time. The parameter is not cloned, so do not alter its value after calling this method.
366     *
367     * @param date A Calendar instance representing the file timestamp.
368     */
369    public void setTimestamp(final Calendar date) {
370        this.calendar = date;
371    }
372
373    /**
374     * Sets the type of the file ({@code DIRECTORY_TYPE}, {@code FILE_TYPE}, etc.).
375     *
376     * @param type The integer code representing the type of the file.
377     */
378    public void setType(final int type) {
379        this.type = type;
380    }
381
382    /**
383     * Sets the name of the user owning the file. This may be a string representation of the user number;
384     *
385     * @param user The name of the user owning the file.
386     */
387    public void setUser(final String user) {
388        this.user = user;
389    }
390
391    /**
392     * Gets a string representation of the FTPFile information. This currently mimics the Unix listing format. This method uses the time zone of the Calendar
393     * entry, which is the server time zone (if one was provided) otherwise it is the local time zone.
394     * <p>
395     * Note: if the instance is not valid {@link #isValid()}, no useful information can be returned. In this case, use {@link #getRawListing()} instead.
396     * </p>
397     *
398     * @return A string representation of the FTPFile information.
399     * @since 3.0
400     */
401    public String toFormattedString() {
402        return toFormattedString(null);
403    }
404
405    /**
406     * Gets a string representation of the FTPFile information. This currently mimics the Unix listing format. This method allows the Calendar time zone to be
407     * overridden.
408     * <p>
409     * Note: if the instance is not valid {@link #isValid()}, no useful information can be returned. In this case, use {@link #getRawListing()} instead.
410     * </p>
411     *
412     * @param timezone the time zone to use for displaying the time stamp If {@code null}, then use the Calendar ({@link #getTimestamp()}) entry
413     * @return A string representation of the FTPFile information.
414     * @since 3.4
415     */
416    public String toFormattedString(final String timezone) {
417
418        if (!isValid()) {
419            return "[Invalid: could not parse file entry]";
420        }
421        final StringBuilder sb = new StringBuilder();
422        try (final Formatter fmt = new Formatter(sb)) {
423            sb.append(formatType());
424            sb.append(permissionToString(USER_ACCESS));
425            sb.append(permissionToString(GROUP_ACCESS));
426            sb.append(permissionToString(WORLD_ACCESS));
427            fmt.format(" %4d", Integer.valueOf(getHardLinkCount()));
428            fmt.format(" %-8s %-8s", getUser(), getGroup());
429            fmt.format(" %8d", Long.valueOf(getSize()));
430            Calendar timestamp = getTimestamp();
431            if (timestamp != null) {
432                if (timezone != null) {
433                    final TimeZone newZone = TimeZone.getTimeZone(timezone);
434                    if (!newZone.equals(timestamp.getTimeZone())) {
435                        final Date original = timestamp.getTime();
436                        final Calendar newStamp = Calendar.getInstance(newZone);
437                        newStamp.setTime(original);
438                        timestamp = newStamp;
439                    }
440                }
441                fmt.format(" %1$tY-%1$tm-%1$td", timestamp);
442                // Only display time units if they are present
443                if (timestamp.isSet(Calendar.HOUR_OF_DAY)) {
444                    fmt.format(" %1$tH", timestamp);
445                    if (timestamp.isSet(Calendar.MINUTE)) {
446                        fmt.format(":%1$tM", timestamp);
447                        if (timestamp.isSet(Calendar.SECOND)) {
448                            fmt.format(":%1$tS", timestamp);
449                            if (timestamp.isSet(Calendar.MILLISECOND)) {
450                                fmt.format(".%1$tL", timestamp);
451                            }
452                        }
453                    }
454                    fmt.format(" %1$tZ", timestamp);
455                }
456            }
457            sb.append(' ');
458            sb.append(getName());
459        }
460        return sb.toString();
461    }
462
463    /*
464     * Serialization is unnecessary for this class. Reject attempts to do so until such time as the Serializable attribute can be dropped.
465     */
466
467    /**
468     * Gets a string representation of the FTPFile information.
469     * Delegates to {@link #getRawListing()}
470     *
471     * @see #getRawListing()
472     * @return A string representation of the FTPFile information.
473     */
474    @Override
475    public String toString() {
476        return getRawListing();
477    }
478
479    private void writeObject(final java.io.ObjectOutputStream out) {
480        throw new UnsupportedOperationException("Serialization is not supported");
481    }
482
483}