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 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
34
35
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
95
96
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
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
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 }