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.Channel;
27  import java.nio.channels.FileLock;
28  import java.nio.channels.OverlappingFileLockException;
29  import java.util.UUID;
30  import java.util.concurrent.atomic.AtomicBoolean;
31  
32  import org.eclipse.aether.spi.log.Logger;
33  
34  /**
35   * A partially downloaded file with optional support for resume. If resume is enabled, a well-known location is used for
36   * the partial file in combination with a lock file to prevent concurrent requests from corrupting it (and wasting
37   * network bandwith). Otherwise, a (non-locked) unique temporary file is used.
38   */
39  final class PartialFile
40      implements Closeable
41  {
42  
43      static final String EXT_PART = ".part";
44  
45      static final String EXT_LOCK = ".lock";
46  
47      interface RemoteAccessChecker
48      {
49  
50          void checkRemoteAccess()
51              throws Exception;
52  
53      }
54  
55      static class LockFile
56      {
57  
58          private final File lockFile;
59  
60          private final FileLock lock;
61  
62          private final AtomicBoolean concurrent;
63  
64          public LockFile( File partFile, int requestTimeout, RemoteAccessChecker checker, Logger logger )
65              throws Exception
66          {
67              lockFile = new File( partFile.getPath() + EXT_LOCK );
68              concurrent = new AtomicBoolean( false );
69              lock = lock( lockFile, partFile, requestTimeout, checker, logger, concurrent );
70          }
71  
72          private static FileLock lock( File lockFile, File partFile, int requestTimeout, RemoteAccessChecker checker,
73                                        Logger logger, AtomicBoolean concurrent )
74              throws Exception
75          {
76              boolean interrupted = false;
77              try
78              {
79                  for ( long lastLength = -1L, lastTime = 0L;; )
80                  {
81                      FileLock lock = tryLock( lockFile );
82                      if ( lock != null )
83                      {
84                          return lock;
85                      }
86  
87                      long currentLength = partFile.length();
88                      long currentTime = System.currentTimeMillis();
89                      if ( currentLength != lastLength )
90                      {
91                          if ( lastLength < 0L )
92                          {
93                              concurrent.set( true );
94                              /*
95                               * NOTE: We're going with the optimistic assumption that the other thread is downloading the
96                               * file from an equivalent repository. As a bare minimum, ensure the repository we are given
97                               * at least knows about the file and is accessible to us.
98                               */
99                              checker.checkRemoteAccess();
100                             logger.debug( "Concurrent download of " + partFile + " in progress, awaiting completion" );
101                         }
102                         lastLength = currentLength;
103                         lastTime = currentTime;
104                     }
105                     else if ( requestTimeout > 0 && currentTime - lastTime > Math.max( requestTimeout, 3 * 1000 ) )
106                     {
107                         throw new IOException( "Timeout while waiting for concurrent download of " + partFile
108                                                    + " to progress" );
109                     }
110 
111                     try
112                     {
113                         Thread.sleep( Math.max( requestTimeout / 2, 100 ) );
114                     }
115                     catch ( InterruptedException e )
116                     {
117                         interrupted = true;
118                     }
119                 }
120             }
121             finally
122             {
123                 if ( interrupted )
124                 {
125                     Thread.currentThread().interrupt();
126                 }
127             }
128         }
129 
130         private static FileLock tryLock( File lockFile )
131             throws IOException
132         {
133             RandomAccessFile raf = null;
134             FileLock lock = null;
135             try
136             {
137                 raf = new RandomAccessFile( lockFile, "rw" );
138                 lock = raf.getChannel().tryLock( 0, 1, false );
139 
140                 if ( lock == null )
141                 {
142                     raf.close();
143                     raf = null;
144                 }
145             }
146             catch ( OverlappingFileLockException e )
147             {
148                 close( raf );
149                 raf = null;
150                 lock = null;
151             }
152             catch ( RuntimeException e )
153             {
154                 close( raf );
155                 raf = null;
156                 if ( !lockFile.delete() )
157                 {
158                     lockFile.deleteOnExit();
159                 }
160                 throw e;
161             }
162             catch ( IOException e )
163             {
164                 close( raf );
165                 raf = null;
166                 if ( !lockFile.delete() )
167                 {
168                     lockFile.deleteOnExit();
169                 }
170                 throw e;
171             }
172             finally
173             {
174                 try
175                 {
176                     if ( lock == null && raf != null )
177                     {
178                         raf.close();
179                     }
180                 }
181                 catch ( final IOException e )
182                 {
183                     // Suppressed due to an exception already thrown in the try block.
184                 }
185             }
186 
187             return lock;
188         }
189 
190         private static void close( Closeable file )
191         {
192             try
193             {
194                 if ( file != null )
195                 {
196                     file.close();
197                 }
198             }
199             catch ( IOException e )
200             {
201                 // Suppressed.
202             }
203         }
204 
205         public boolean isConcurrent()
206         {
207             return concurrent.get();
208         }
209 
210         public void close() throws IOException
211         {
212             Channel channel = null;
213             try
214             {
215                 channel = lock.channel();
216                 lock.release();
217                 channel.close();
218                 channel = null;
219             }
220             finally
221             {
222                 try
223                 {
224                     if ( channel != null )
225                     {
226                         channel.close();
227                     }
228                 }
229                 catch ( final IOException e )
230                 {
231                     // Suppressed due to an exception already thrown in the try block.
232                 }
233                 finally
234                 {
235                     if ( !lockFile.delete() )
236                     {
237                         lockFile.deleteOnExit();
238                     }
239                 }
240             }
241         }
242 
243         @Override
244         public String toString()
245         {
246             return lockFile + " - " + lock.isValid();
247         }
248 
249     }
250 
251     static class Factory
252     {
253 
254         private final boolean resume;
255 
256         private final long resumeThreshold;
257 
258         private final int requestTimeout;
259 
260         private final Logger logger;
261 
262         public Factory( boolean resume, long resumeThreshold, int requestTimeout, Logger logger )
263         {
264             this.resume = resume;
265             this.resumeThreshold = resumeThreshold;
266             this.requestTimeout = requestTimeout;
267             this.logger = logger;
268         }
269 
270         public PartialFile newInstance( File dstFile, RemoteAccessChecker checker )
271             throws Exception
272         {
273             if ( resume )
274             {
275                 File partFile = new File( dstFile.getPath() + EXT_PART );
276 
277                 long reqTimestamp = System.currentTimeMillis();
278                 LockFile lockFile = new LockFile( partFile, requestTimeout, checker, logger );
279                 if ( lockFile.isConcurrent() && dstFile.lastModified() >= reqTimestamp - 100L )
280                 {
281                     lockFile.close();
282                     return null;
283                 }
284                 try
285                 {
286                     if ( !partFile.createNewFile() && !partFile.isFile() )
287                     {
288                         throw new IOException( partFile.exists() ? "Path exists but is not a file" : "Unknown error" );
289                     }
290                     return new PartialFile( partFile, lockFile, resumeThreshold, logger );
291                 }
292                 catch ( IOException e )
293                 {
294                     lockFile.close();
295                     logger.debug( "Cannot create resumable file " + partFile.getAbsolutePath() + ": " + e );
296                     // fall through and try non-resumable/temporary file location
297                 }
298             }
299 
300             File tempFile =
301                 File.createTempFile( dstFile.getName() + '-' + UUID.randomUUID().toString().replace( "-", "" ), ".tmp",
302                                      dstFile.getParentFile() );
303             return new PartialFile( tempFile, logger );
304         }
305 
306     }
307 
308     private final File partFile;
309 
310     private final LockFile lockFile;
311 
312     private final long threshold;
313 
314     private final Logger logger;
315 
316     private PartialFile( File partFile, Logger logger )
317     {
318         this( partFile, null, 0L, logger );
319     }
320 
321     private PartialFile( File partFile, LockFile lockFile, long threshold, Logger logger )
322     {
323         this.partFile = partFile;
324         this.lockFile = lockFile;
325         this.threshold = threshold;
326         this.logger = logger;
327     }
328 
329     public File getFile()
330     {
331         return partFile;
332     }
333 
334     public boolean isResume()
335     {
336         return lockFile != null && partFile.length() >= threshold;
337     }
338 
339     public void close() throws IOException
340     {
341         if ( partFile.exists() && !isResume() )
342         {
343             if ( !partFile.delete() && partFile.exists() )
344             {
345                 logger.debug( "Could not delete temorary file " + partFile );
346             }
347         }
348         if ( lockFile != null )
349         {
350             lockFile.close();
351         }
352     }
353 
354     @Override
355     public String toString()
356     {
357         return String.valueOf( getFile() );
358     }
359 
360 }