View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.transport.file;
20  
21  import java.io.IOException;
22  import java.io.UncheckedIOException;
23  import java.nio.ByteBuffer;
24  import java.nio.channels.FileChannel;
25  import java.nio.file.FileSystem;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  
29  import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
30  import org.eclipse.aether.spi.connector.transport.GetTask;
31  import org.eclipse.aether.spi.connector.transport.PeekTask;
32  import org.eclipse.aether.spi.connector.transport.PutTask;
33  import org.eclipse.aether.spi.connector.transport.TransportTask;
34  
35  import static java.util.Objects.requireNonNull;
36  
37  /**
38   * A transporter using {@link java.nio.file.Path} that is reading and writing from specified base directory
39   * of given {@link java.nio.file.FileSystem}. It supports multiple {@link WriteOp} and obeys read-only property.
40   */
41  final class FileTransporter extends AbstractTransporter {
42      /**
43       * The write operation transport can use to write contents to the target (usually in local repository) of the
44       * file in remote repository reached by this transporter. Historically, and in some special cases (ZIP file system),
45       * it is only {@link #COPY} that can be used.
46       * <p>
47       * In case when contents of remote repository reached by this transport and target are on same volume,
48       * then {@link #SYMLINK} and {@link #HARDLINK} can be used as well, to reduce storage redundancy. Still, Resolver
49       * cannot do much smartness here, it is user who should evaluate this possibility, and if all conditions are met,
50       * apply it. Resolver does not try play smart here, it will obey configuration and most probably fail (ie cross
51       * volume hardlink).
52       *
53       * @since 2.0.2
54       */
55      enum WriteOp {
56          COPY,
57          SYMLINK,
58          HARDLINK;
59      }
60  
61      private final FileSystem fileSystem;
62      private final boolean closeFileSystem;
63      private final boolean writableFileSystem;
64      private final Path basePath;
65      private final WriteOp writeOp;
66  
67      FileTransporter(
68              FileSystem fileSystem,
69              boolean closeFileSystem,
70              boolean writableFileSystem,
71              Path basePath,
72              WriteOp writeOp) {
73          this.fileSystem = requireNonNull(fileSystem);
74          this.closeFileSystem = closeFileSystem;
75          this.writableFileSystem = writableFileSystem;
76          this.basePath = requireNonNull(basePath);
77          this.writeOp = requireNonNull(writeOp);
78  
79          // sanity check
80          if (basePath.getFileSystem() != fileSystem) {
81              throw new IllegalArgumentException("basePath must originate from the fileSystem");
82          }
83      }
84  
85      Path getBasePath() {
86          return basePath;
87      }
88  
89      @Override
90      public int classify(Throwable error) {
91          if (error instanceof ResourceNotFoundException) {
92              return ERROR_NOT_FOUND;
93          }
94          return ERROR_OTHER;
95      }
96  
97      private WriteOp effectiveFileOp(WriteOp wanted, GetTask task) {
98          if (task.getDataPath() != null) {
99              return wanted;
100         }
101         // not default FS or task carries no path (caller wants in-memory read) = COPY must be used
102         return WriteOp.COPY;
103     }
104 
105     @Override
106     protected void implPeek(PeekTask task) throws Exception {
107         getPath(task, true);
108     }
109 
110     @Override
111     protected void implGet(GetTask task) throws Exception {
112         Path path = getPath(task, true);
113         long size = Files.size(path);
114         WriteOp effective = effectiveFileOp(writeOp, task);
115         switch (effective) {
116             case COPY:
117                 utilGet(task, Files.newInputStream(path), true, size, false);
118                 break;
119             case SYMLINK:
120             case HARDLINK:
121                 Files.deleteIfExists(task.getDataPath());
122                 task.getListener().transportStarted(0L, size);
123                 if (effective == WriteOp.HARDLINK) {
124                     Files.createLink(task.getDataPath(), path);
125                 } else {
126                     Files.createSymbolicLink(task.getDataPath(), path);
127                 }
128                 if (size > 0) {
129                     try (FileChannel fc = FileChannel.open(path)) {
130                         try {
131                             task.getListener().transportProgressed(fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()));
132                         } catch (UnsupportedOperationException e) {
133                             // not all FS support mmap: fallback to "plain read loop"
134                             ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 32);
135                             while (fc.read(byteBuffer) != -1) {
136                                 byteBuffer.flip();
137                                 task.getListener().transportProgressed(byteBuffer);
138                                 byteBuffer.clear();
139                             }
140                         }
141                     }
142                 }
143                 break;
144             default:
145                 throw new IllegalStateException("Unknown fileOp " + writeOp);
146         }
147     }
148 
149     @Override
150     protected void implPut(PutTask task) throws Exception {
151         if (!writableFileSystem) {
152             throw new UnsupportedOperationException("Read only FileSystem");
153         }
154         Path path = getPath(task, false);
155         Files.createDirectories(path.getParent());
156         try {
157             utilPut(task, Files.newOutputStream(path), true);
158         } catch (Exception e) {
159             Files.deleteIfExists(path);
160             throw e;
161         }
162     }
163 
164     private Path getPath(TransportTask task, boolean required) throws Exception {
165         String path = task.getLocation().getPath();
166         if (path.contains("../")) {
167             throw new IllegalArgumentException("illegal resource path: " + path);
168         }
169         Path file = basePath.resolve(path);
170         if (required && !Files.isRegularFile(file)) {
171             throw new ResourceNotFoundException("Could not locate " + file);
172         }
173         return file;
174     }
175 
176     @Override
177     protected void implClose() {
178         if (closeFileSystem) {
179             try {
180                 fileSystem.close();
181             } catch (IOException e) {
182                 throw new UncheckedIOException(e);
183             }
184         }
185     }
186 }