1 package org.eclipse.aether.connector.basic;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
37
38
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
97
98
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
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
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
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
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 }