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 e )
154             {
155                 close( raf );
156                 raf = null;
157                 if ( !lockFile.delete() )
158                 {
159                     lockFile.deleteOnExit();
160                 }
161                 throw e;
162             }
163             catch ( IOException e )
164             {
165                 close( raf );
166                 raf = null;
167                 if ( !lockFile.delete() )
168                 {
169                     lockFile.deleteOnExit();
170                 }
171                 throw e;
172             }
173             finally
174             {
175                 try
176                 {
177                     if ( lock == null && raf != null )
178                     {
179                         raf.close();
180                     }
181                 }
182                 catch ( final IOException e )
183                 {
184                     // Suppressed due to an exception already thrown in the try block.
185                 }
186             }
187 
188             return lock;
189         }
190 
191         private static void close( Closeable file )
192         {
193             try
194             {
195                 if ( file != null )
196                 {
197                     file.close();
198                 }
199             }
200             catch ( IOException e )
201             {
202                 // Suppressed.
203             }
204         }
205 
206         public boolean isConcurrent()
207         {
208             return concurrent.get();
209         }
210 
211         public void close() throws IOException
212         {
213             Channel channel = null;
214             try
215             {
216                 channel = lock.channel();
217                 lock.release();
218                 channel.close();
219                 channel = null;
220             }
221             finally
222             {
223                 try
224                 {
225                     if ( channel != null )
226                     {
227                         channel.close();
228                     }
229                 }
230                 catch ( final IOException e )
231                 {
232                     // Suppressed due to an exception already thrown in the try block.
233                 }
234                 finally
235                 {
236                     if ( !lockFile.delete() )
237                     {
238                         lockFile.deleteOnExit();
239                     }
240                 }
241             }
242         }
243 
244         @Override
245         public String toString()
246         {
247             return lockFile + " - " + lock.isValid();
248         }
249 
250     }
251 
252     static class Factory
253     {
254 
255         private final boolean resume;
256 
257         private final long resumeThreshold;
258 
259         private final int requestTimeout;
260 
261         private static final Logger LOGGER = LoggerFactory.getLogger( Factory.class );
262 
263         Factory( boolean resume, long resumeThreshold, int requestTimeout )
264         {
265             this.resume = resume;
266             this.resumeThreshold = resumeThreshold;
267             this.requestTimeout = requestTimeout;
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 );
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 );
291                 }
292                 catch ( IOException e )
293                 {
294                     lockFile.close();
295                     LOGGER.debug( "Cannot create resumable file {}: {}", partFile.getAbsolutePath(), e.getMessage(), 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 );
304         }
305 
306     }
307 
308     private final File partFile;
309 
310     private final LockFile lockFile;
311 
312     private final long threshold;
313 
314     private static final Logger LOGGER = LoggerFactory.getLogger( PartialFile.class );
315 
316     private PartialFile( File partFile )
317     {
318         this( partFile, null, 0L );
319     }
320 
321     private PartialFile( File partFile, LockFile lockFile, long threshold )
322     {
323         this.partFile = partFile;
324         this.lockFile = lockFile;
325         this.threshold = threshold;
326     }
327 
328     public File getFile()
329     {
330         return partFile;
331     }
332 
333     public boolean isResume()
334     {
335         return lockFile != null && partFile.length() >= threshold;
336     }
337 
338     public void close() throws IOException
339     {
340         if ( partFile.exists() && !isResume() )
341         {
342             if ( !partFile.delete() && partFile.exists() )
343             {
344                 LOGGER.debug( "Could not delete temporary file {}", partFile );
345             }
346         }
347         if ( lockFile != null )
348         {
349             lockFile.close();
350         }
351     }
352 
353     @Override
354     public String toString()
355     {
356         return String.valueOf( getFile() );
357     }
358 
359 }