Merge pull request #42 in ITERATE/cyberduck from feature/java.nio.file to master
[cyberduck.git] / source / ch / cyberduck / core / Local.java
blobfaeab20691e9562abde00674f62b7f1b78fc6317
1 package ch.cyberduck.core;
3 /*
4 * Copyright (c) 2012 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:
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;
38 import java.io.File;
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;
53 /**
54 * @version $Id$
56 public class Local extends AbstractPath implements Referenceable, Serializable {
57 private static final Logger log = Logger.getLogger(Local.class);
59 /**
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));
78 /**
79 * @param name Absolute path
81 public Local(final String name) {
82 path = 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);
95 @Override
96 public <T> T serialize(final Serializer dict) {
97 dict.setStringForKey(path, "Path");
98 return dict.getSerialized();
101 @Override
102 public EnumSet<Type> getType() {
103 final EnumSet<Type> set = EnumSet.noneOf(Type.class);
104 if(this.isFile()) {
105 set.add(Type.file);
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);
116 return set;
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() {
147 try {
148 return !this.equals(this.getSymlinkTarget());
150 catch(NotfoundException e) {
151 return false;
155 public Local getSymlinkTarget() throws NotfoundException {
156 try {
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() {
168 return attributes;
171 @Override
172 public char getDelimiter() {
173 return CharUtils.toChar(PreferencesFactory.get().getProperty("local.delimiter"));
176 public void mkdir() throws AccessDeniedException {
177 try {
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);
187 * Delete the file
189 public void delete() throws AccessDeniedException {
190 try {
191 Files.deleteIfExists(Paths.get(path));
193 catch(IOException e) {
194 throw new LocalAccessDeniedException(String.format("Delete %s failed", path), e);
199 * Delete file
201 * @param deferred On application quit
203 public void delete(boolean deferred) throws AccessDeniedException {
204 this.delete();
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>() {
210 @Override
211 public boolean accept(final Path entry) throws IOException {
212 return filter.accept(entry.getFileName().toString());
214 })) {
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);
222 return children;
225 public AttributedList<Local> list() throws AccessDeniedException {
226 return this.list(new Filter<String>() {
227 @Override
228 public boolean accept(final String file) {
229 return true;
234 @Override
235 public String getAbsolute() {
236 return path;
240 * @return Security scoped bookmark outside of sandbox to store in preferences
242 public String getBookmark() {
243 return path;
246 public void setBookmark(final String data) {
250 public Local withBookmark(final String data) {
251 this.setBookmark(data);
252 return this;
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
266 * @see #getName()
268 public String getDisplayName() {
269 return this.getName();
273 * @return The last path component.
275 @Override
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 {
296 try {
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()));
312 else {
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;
318 try {
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);
326 finally {
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;
338 return this;
341 @Override
342 public String toString() {
343 final StringBuilder sb = new StringBuilder("CopyOptions{");
344 sb.append("append=").append(append);
345 sb.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.
354 @Override
355 public boolean equals(final Object o) {
356 if(this == o) {
357 return true;
359 if(!(o instanceof Local)) {
360 return false;
362 final Local local = (Local) o;
363 if(path != null ? !path.equalsIgnoreCase(local.path) : local.path != null) {
364 return false;
366 return true;
369 @Override
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 {
379 try {
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 {
388 try {
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 {
397 return null;
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
411 return false;
413 if(this.isRoot()) {
414 // Root cannot be a child of any other path
415 return false;
417 if(directory.isRoot()) {
418 // Any other path is a child
419 return true;
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
423 return false;
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())) {
427 return true;
430 return false;
433 @Override
434 public String toString() {
435 final StringBuilder sb = new StringBuilder("Local{");
436 sb.append("path='").append(path).append('\'');
437 sb.append('}');
438 return sb.toString();