1 package ch
.cyberduck
.core
;
4 * Copyright (c) 2012 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:
18 * dkocher@cyberduck.ch
21 import ch
.cyberduck
.core
.exception
.AccessDeniedException
;
22 import ch
.cyberduck
.core
.exception
.LocalAccessDeniedException
;
23 import ch
.cyberduck
.core
.exception
.LocalNotfoundException
;
24 import ch
.cyberduck
.core
.exception
.NotfoundException
;
25 import ch
.cyberduck
.core
.io
.LocalRepeatableFileInputStream
;
26 import ch
.cyberduck
.core
.local
.TildeExpander
;
27 import ch
.cyberduck
.core
.local
.WorkdirPrefixer
;
28 import ch
.cyberduck
.core
.preferences
.PreferencesFactory
;
29 import ch
.cyberduck
.core
.serializer
.Serializer
;
30 import ch
.cyberduck
.core
.unicode
.NFCNormalizer
;
32 import org
.apache
.commons
.io
.FilenameUtils
;
33 import org
.apache
.commons
.io
.IOUtils
;
34 import org
.apache
.commons
.lang3
.CharUtils
;
35 import org
.apache
.commons
.lang3
.StringUtils
;
36 import org
.apache
.log4j
.Logger
;
39 import java
.io
.FileNotFoundException
;
40 import java
.io
.FileOutputStream
;
41 import java
.io
.IOException
;
42 import java
.io
.InputStream
;
43 import java
.io
.OutputStream
;
44 import java
.nio
.file
.DirectoryStream
;
45 import java
.nio
.file
.Files
;
46 import java
.nio
.file
.InvalidPathException
;
47 import java
.nio
.file
.Path
;
48 import java
.nio
.file
.Paths
;
49 import java
.text
.MessageFormat
;
50 import java
.util
.EnumSet
;
51 import java
.util
.Objects
;
56 public class Local
extends AbstractPath
implements Referenceable
, Serializable
{
57 private static final Logger log
= Logger
.getLogger(Local
.class);
60 * Absolute path in local file system
62 protected String path
;
64 private LocalAttributes attributes
;
66 public Local(final String parent
, final String name
) {
67 this(parent
.endsWith(PreferencesFactory
.get().getProperty("local.delimiter")) ?
68 String
.format("%s%s", parent
, name
) :
69 String
.format("%s%c%s", parent
, CharUtils
.toChar(PreferencesFactory
.get().getProperty("local.delimiter")), name
));
72 public Local(final Local parent
, final String name
) {
73 this(parent
.isRoot() ?
74 String
.format("%s%s", parent
.getAbsolute(), name
) :
75 String
.format("%s%c%s", parent
.getAbsolute(), CharUtils
.toChar(PreferencesFactory
.get().getProperty("local.delimiter")), name
));
79 * @param name Absolute path
81 public Local(final String name
) {
83 if(PreferencesFactory
.get().getBoolean("local.normalize.unicode")) {
84 path
= new NFCNormalizer().normalize(path
);
86 if(PreferencesFactory
.get().getBoolean("local.normalize.tilde")) {
87 path
= new TildeExpander().expand(path
);
89 if(PreferencesFactory
.get().getBoolean("local.normalize.prefix")) {
90 path
= new WorkdirPrefixer().normalize(path
);
92 attributes
= new LocalAttributes(path
);
96 public <T
> T
serialize(final Serializer dict
) {
97 dict
.setStringForKey(path
, "Path");
98 return dict
.getSerialized();
102 public EnumSet
<Type
> getType() {
103 final EnumSet
<Type
> set
= EnumSet
.noneOf(Type
.class);
107 if(this.isDirectory()) {
108 set
.add(Type
.directory
);
110 if(this.isVolume()) {
111 set
.add(Type
.volume
);
113 if(this.isSymbolicLink()) {
114 set
.add(Type
.symboliclink
);
119 public boolean isVolume() {
120 return null == Paths
.get(path
).getParent();
124 * This is only returning the correct result if the file already exists.
126 * @see Local#exists()
128 public boolean isDirectory() {
129 return Files
.isDirectory(Paths
.get(path
));
133 * This is only returning the correct result if the file already exists.
135 * @see Local#exists()
137 public boolean isFile() {
138 return Files
.isRegularFile(Paths
.get(path
));
142 * Checks whether a given file is a symbolic link.
144 * @return true if the file is a symbolic link.
146 public boolean isSymbolicLink() {
148 return !this.equals(this.getSymlinkTarget());
150 catch(NotfoundException e
) {
155 public Local
getSymlinkTarget() throws NotfoundException
{
157 // For a link that actually points to something (either a file or a directory),
158 // the absolute path is the path through the link, whereas the canonical path
159 // is the path the link references.
160 return LocalFactory
.get(Paths
.get(this.getAbsolute()).toRealPath().toString());
162 catch(InvalidPathException
| IOException e
) {
163 throw new LocalNotfoundException(String
.format("Resolving symlink target for %s failed", path
), e
);
167 public LocalAttributes
attributes() {
172 public char getDelimiter() {
173 return CharUtils
.toChar(PreferencesFactory
.get().getProperty("local.delimiter"));
176 public void mkdir() throws AccessDeniedException
{
178 Files
.createDirectories(Paths
.get(path
));
180 catch(IOException e
) {
181 throw new LocalAccessDeniedException(MessageFormat
.format(LocaleFactory
.localizedString(
182 "Cannot create folder {0}", "Error"), path
), e
);
189 public void delete() throws AccessDeniedException
{
191 Files
.deleteIfExists(Paths
.get(path
));
193 catch(IOException e
) {
194 throw new LocalAccessDeniedException(String
.format("Delete %s failed", path
), e
);
201 * @param deferred On application quit
203 public void delete(boolean deferred
) throws AccessDeniedException
{
207 public AttributedList
<Local
> list(final Filter
<String
> filter
) throws AccessDeniedException
{
208 final AttributedList
<Local
> children
= new AttributedList
<Local
>();
209 try (DirectoryStream
<Path
> stream
= Files
.newDirectoryStream(Paths
.get(path
), new DirectoryStream
.Filter
<Path
>() {
211 public boolean accept(final Path entry
) throws IOException
{
212 return filter
.accept(entry
.getFileName().toString());
215 for(Path entry
: stream
) {
216 children
.add(LocalFactory
.get(entry
.toString()));
219 catch(IOException e
) {
220 throw new LocalAccessDeniedException(String
.format("Error listing files in directory %s", path
), e
);
225 public AttributedList
<Local
> list() throws AccessDeniedException
{
226 return this.list(new Filter
<String
>() {
228 public boolean accept(final String file
) {
235 public String
getAbsolute() {
240 * @return Security scoped bookmark outside of sandbox to store in preferences
242 public String
getBookmark() {
246 public void setBookmark(final String data
) {
250 public Local
withBookmark(final String data
) {
251 this.setBookmark(data
);
256 * @return A shortened path representation.
258 public String
getAbbreviatedPath() {
259 return new TildeExpander().abbreviate(path
);
263 * Subclasses may override to return a user friendly representation of the name denoting this path.
265 * @return Name of the file
268 public String
getDisplayName() {
269 return this.getName();
273 * @return The last path component.
276 public String
getName() {
277 return FilenameUtils
.getName(path
);
280 public Local
getVolume() {
281 return LocalFactory
.get(String
.valueOf(this.getDelimiter()));
284 public Local
getParent() {
285 return LocalFactory
.get(Paths
.get(path
).getParent().toString());
289 * @return True if the path exists on the file system.
291 public boolean exists() {
292 return Files
.exists(Paths
.get(path
));
295 public void rename(final Local renamed
) throws AccessDeniedException
{
297 Files
.move(Paths
.get(path
), Paths
.get(renamed
.getAbsolute()));
299 catch(IOException e
) {
300 throw new LocalAccessDeniedException(String
.format("Rename failed for %s", renamed
), e
);
304 public void copy(final Local copy
) throws AccessDeniedException
{
305 this.copy(copy
, new CopyOptions());
308 public void copy(final Local copy
, final CopyOptions options
) throws AccessDeniedException
{
309 if(copy
.equals(this)) {
310 log
.warn(String
.format("%s and %s are identical. Not copied.", this.getName(), copy
.getName()));
313 if(log
.isDebugEnabled()) {
314 log
.debug(String
.format("Copy to %s with options %s", copy
, options
));
316 InputStream in
= null;
317 OutputStream out
= null;
319 in
= this.getInputStream();
320 out
= copy
.getOutputStream(options
.append
);
321 IOUtils
.copy(in
, out
);
323 catch(IOException e
) {
324 throw new LocalAccessDeniedException(e
.getMessage(), e
);
327 IOUtils
.closeQuietly(in
);
328 IOUtils
.closeQuietly(out
);
333 public static final class CopyOptions
{
334 public boolean append
;
336 public CopyOptions
append(final boolean append
) {
337 this.append
= append
;
342 public String
toString() {
343 final StringBuilder sb
= new StringBuilder("CopyOptions{");
344 sb
.append("append=").append(append
);
346 return sb
.toString();
351 * Compares the two files using their path with a string comparision ignoring case.
352 * Implementations should override this depending on the case sensitivity of the file system.
355 public boolean equals(final Object o
) {
359 if(!(o
instanceof Local
)) {
362 final Local local
= (Local
) o
;
363 if(path
!= null ?
!path
.equalsIgnoreCase(local
.path
) : local
.path
!= null) {
370 public int hashCode() {
371 return path
!= null ? StringUtils
.lowerCase(path
).hashCode() : 0;
374 public String
toURL() {
375 return String
.format("file:%s", path
);
378 public InputStream
getInputStream() throws AccessDeniedException
{
380 return new LocalRepeatableFileInputStream(new File(path
));
382 catch(FileNotFoundException e
) {
383 throw new LocalAccessDeniedException(e
.getMessage(), e
);
387 public OutputStream
getOutputStream(final boolean append
) throws AccessDeniedException
{
389 return new FileOutputStream(new File(path
), append
);
391 catch(FileNotFoundException e
) {
392 throw new LocalAccessDeniedException(e
.getMessage(), e
);
396 public Object
lock() throws AccessDeniedException
{
400 public void release(Object lock
) {
405 * @param directory Parent directory
406 * @return True if this is a child in the path hierarchy of the argument passed
408 public boolean isChild(final Local directory
) {
409 if(directory
.isFile()) {
410 // If a file we don't have any children at all
414 // Root cannot be a child of any other path
417 if(directory
.isRoot()) {
418 // Any other path is a child
421 if(Objects
.equals(PathNormalizer
.parent(this.getAbsolute(), this.getDelimiter()), PathNormalizer
.parent(directory
.getAbsolute(), this.getDelimiter()))) {
422 // Cannot be a child if the same parent
425 for(String parent
= PathNormalizer
.parent(this.getAbsolute(), this.getDelimiter()); !parent
.equals(String
.valueOf(this.getDelimiter())); parent
= PathNormalizer
.parent(parent
, this.getDelimiter())) {
426 if(parent
.equals(directory
.getAbsolute())) {
434 public String
toString() {
435 final StringBuilder sb
= new StringBuilder("Local{");
436 sb
.append("path='").append(path
).append('\'');
438 return sb
.toString();