View Javadoc
1   package org.eclipse.aether.connector.basic;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   * 
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   * 
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.Closeable;
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.RandomAccessFile;
26  import java.nio.channels.FileLock;
27  import java.nio.channels.OverlappingFileLockException;
28  import java.util.UUID;
29  
30  import org.eclipse.aether.spi.log.Logger;
31  
32  /**
33   * A partially downloaded file with optional support for resume. If resume is enabled, a well-known location is used for
34   * the partial file in combination with a lock file to prevent concurrent requests from corrupting it (and wasting
35   * network bandwith). Otherwise, a (non-locked) unique temporary file is used.
36   */
37  final class PartialFile
38      implements Closeable
39  {
40  
41      static final String EXT_PART = ".part";
42  
43      static final String EXT_LOCK = ".lock";
44  
45      interface RemoteAccessChecker
46      {
47  
48          void checkRemoteAccess()
49              throws Exception;
50  
51      }
52  
53      static class LockFile
54      {
55  
56          private final File lockFile;
57  
58          private final FileLock lock;
59  
60          private final boolean concurrent;
61  
62          public LockFile( File partFile, int requestTimeout, RemoteAccessChecker checker, Logger logger )
63              throws Exception
64          {
65              lockFile = new File( partFile.getPath() + EXT_LOCK );
66              boolean[] concurrent = { false };
67              lock = lock( lockFile, partFile, requestTimeout, checker, logger, concurrent );
68              this.concurrent = concurrent[0];
69          }
70  
71          private static FileLock lock( File lockFile, File partFile, int requestTimeout, RemoteAccessChecker checker,
72                                        Logger logger, boolean[] concurrent )
73              throws Exception
74          {
75              boolean interrupted = false;
76              try
77              {
78                  for ( long lastLength = -1, lastTime = 0;; )
79                  {
80                      FileLock lock = tryLock( lockFile );
81                      if ( lock != null )
82                      {
83                          return lock;
84                      }
85  
86                      long currentLength = partFile.length();
87                      long currentTime = System.currentTimeMillis();
88                      if ( currentLength != lastLength )
89                      {
90                          if ( lastLength < 0 )
91                          {
92                              concurrent[0] = true;
93                              /*
94                               * NOTE: We're going with the optimistic assumption that the other thread is downloading the
95                               * file from an equivalent repository. As a bare minimum, ensure the repository we are given
96                               * at least knows about the file and is accessible to us.
97                               */
98                              checker.checkRemoteAccess();
99                              logger.debug( "Concurrent download of " + partFile + " in progress, awaiting completion" );
100                         }
101                         lastLength = currentLength;
102                         lastTime = currentTime;
103                     }
104                     else if ( requestTimeout > 0 && currentTime - lastTime > Math.max( requestTimeout, 3 * 1000 ) )
105                     {
106                         throw new IOException( "Timeout while waiting for concurrent download of " + partFile
107                             + " to progress" );
108                     }
109 
110                     try
111                     {
112                         Thread.sleep( 100 );
113                     }
114                     catch ( InterruptedException e )
115                     {
116                         interrupted = true;
117                     }
118                 }
119             }
120             finally
121             {
122                 if ( interrupted )
123                 {
124                     Thread.currentThread().interrupt();
125                 }
126             }
127         }
128 
129         private static FileLock tryLock( File lockFile )
130             throws IOException
131         {
132             RandomAccessFile raf = new RandomAccessFile( lockFile, "rw" );
133             try
134             {
135                 FileLock lock = raf.getChannel().tryLock( 0, 1, false );
136                 if ( lock == null )
137                 {
138                     close( raf );
139                 }
140                 return lock;
141             }
142             catch ( OverlappingFileLockException e )
143             {
144                 close( raf );
145                 return null;
146             }
147             catch ( RuntimeException e )
148             {
149                 close( raf );
150                 lockFile.delete();
151                 throw e;
152             }
153             catch ( IOException e )
154             {
155                 close( raf );
156                 lockFile.delete();
157                 throw e;
158             }
159         }
160 
161         private static void close( Closeable file )
162         {
163             try
164             {
165                 file.close();
166             }
167             catch ( IOException e )
168             {
169                 // irrelevant
170             }
171         }
172 
173         public boolean isConcurrent()
174         {
175             return concurrent;
176         }
177 
178         public void close()
179         {
180             close( lock.channel() );
181             lockFile.delete();
182         }
183 
184         @Override
185         public String toString()
186         {
187             return lockFile + " - " + lock.isValid();
188         }
189 
190     }
191 
192     static class Factory
193     {
194 
195         private final boolean resume;
196 
197         private final long resumeThreshold;
198 
199         private final int requestTimeout;
200 
201         private final Logger logger;
202 
203         public Factory( boolean resume, long resumeThreshold, int requestTimeout, Logger logger )
204         {
205             this.resume = resume;
206             this.resumeThreshold = resumeThreshold;
207             this.requestTimeout = requestTimeout;
208             this.logger = logger;
209         }
210 
211         public PartialFile newInstance( File dstFile, RemoteAccessChecker checker )
212             throws Exception
213         {
214             if ( resume )
215             {
216                 File partFile = new File( dstFile.getPath() + EXT_PART );
217 
218                 long reqTimestamp = System.currentTimeMillis();
219                 LockFile lockFile = new LockFile( partFile, requestTimeout, checker, logger );
220                 if ( lockFile.isConcurrent() && dstFile.lastModified() >= reqTimestamp - 100 )
221                 {
222                     lockFile.close();
223                     return null;
224                 }
225                 try
226                 {
227                     if ( !partFile.createNewFile() && !partFile.isFile() )
228                     {
229                         throw new IOException( partFile.exists() ? "Path exists but is not a file" : "Unknown error" );
230                     }
231                     return new PartialFile( partFile, lockFile, resumeThreshold, logger );
232                 }
233                 catch ( IOException e )
234                 {
235                     lockFile.close();
236                     logger.debug( "Cannot create resumable file " + partFile.getAbsolutePath() + ": " + e );
237                     // fall through and try non-resumable/temporary file location
238                 }
239             }
240 
241             File tempFile =
242                 File.createTempFile( dstFile.getName() + '-' + UUID.randomUUID().toString().replace( "-", "" ), ".tmp",
243                                      dstFile.getParentFile() );
244             return new PartialFile( tempFile, logger );
245         }
246 
247     }
248 
249     private final File partFile;
250 
251     private final LockFile lockFile;
252 
253     private final long threshold;
254 
255     private final Logger logger;
256 
257     private PartialFile( File partFile, Logger logger )
258     {
259         this( partFile, null, 0, logger );
260     }
261 
262     private PartialFile( File partFile, LockFile lockFile, long threshold, Logger logger )
263     {
264         this.partFile = partFile;
265         this.lockFile = lockFile;
266         this.threshold = threshold;
267         this.logger = logger;
268     }
269 
270     public File getFile()
271     {
272         return partFile;
273     }
274 
275     public boolean isResume()
276     {
277         return lockFile != null && partFile.length() >= threshold;
278     }
279 
280     public void close()
281     {
282         if ( partFile.exists() && !isResume() )
283         {
284             if ( !partFile.delete() && partFile.exists() )
285             {
286                 logger.debug( "Could not delete temorary file " + partFile );
287             }
288         }
289         if ( lockFile != null )
290         {
291             lockFile.close();
292         }
293     }
294 
295     @Override
296     public String toString()
297     {
298         return String.valueOf( getFile() );
299     }
300 
301 }