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