1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.store.fs; |
7 | |
8 | import java.io.File; |
9 | import java.io.FileInputStream; |
10 | import java.io.FileNotFoundException; |
11 | import java.io.FileOutputStream; |
12 | import java.io.IOException; |
13 | import java.io.InputStream; |
14 | import java.io.OutputStream; |
15 | import java.io.RandomAccessFile; |
16 | import java.net.URL; |
17 | import java.nio.ByteBuffer; |
18 | import java.nio.channels.FileChannel; |
19 | import java.nio.channels.FileLock; |
20 | import java.nio.channels.NonWritableChannelException; |
21 | import java.util.ArrayList; |
22 | import java.util.List; |
23 | |
24 | import org.h2.api.ErrorCode; |
25 | import org.h2.engine.SysProperties; |
26 | import org.h2.message.DbException; |
27 | import org.h2.util.IOUtils; |
28 | import org.h2.util.New; |
29 | |
30 | /** |
31 | * This file system stores files on disk. |
32 | * This is the most common file system. |
33 | */ |
34 | public class FilePathDisk extends FilePath { |
35 | |
36 | private static final String CLASSPATH_PREFIX = "classpath:"; |
37 | |
38 | @Override |
39 | public FilePathDisk getPath(String path) { |
40 | FilePathDisk p = new FilePathDisk(); |
41 | p.name = translateFileName(path); |
42 | return p; |
43 | } |
44 | |
45 | @Override |
46 | public long size() { |
47 | return new File(name).length(); |
48 | } |
49 | |
50 | /** |
51 | * Translate the file name to the native format. This will replace '\' with |
52 | * '/' and expand the home directory ('~'). |
53 | * |
54 | * @param fileName the file name |
55 | * @return the native file name |
56 | */ |
57 | protected static String translateFileName(String fileName) { |
58 | fileName = fileName.replace('\\', '/'); |
59 | if (fileName.startsWith("file:")) { |
60 | fileName = fileName.substring("file:".length()); |
61 | } |
62 | return expandUserHomeDirectory(fileName); |
63 | } |
64 | |
65 | /** |
66 | * Expand '~' to the user home directory. It is only be expanded if the '~' |
67 | * stands alone, or is followed by '/' or '\'. |
68 | * |
69 | * @param fileName the file name |
70 | * @return the native file name |
71 | */ |
72 | public static String expandUserHomeDirectory(String fileName) { |
73 | if (fileName.startsWith("~") && (fileName.length() == 1 || |
74 | fileName.startsWith("~/"))) { |
75 | String userDir = SysProperties.USER_HOME; |
76 | fileName = userDir + fileName.substring(1); |
77 | } |
78 | return fileName; |
79 | } |
80 | |
81 | @Override |
82 | public void moveTo(FilePath newName, boolean atomicReplace) { |
83 | File oldFile = new File(name); |
84 | File newFile = new File(newName.name); |
85 | if (oldFile.getAbsolutePath().equals(newFile.getAbsolutePath())) { |
86 | return; |
87 | } |
88 | if (!oldFile.exists()) { |
89 | throw DbException.get(ErrorCode.FILE_RENAME_FAILED_2, |
90 | name + " (not found)", |
91 | newName.name); |
92 | } |
93 | // Java 7: use java.nio.file.Files.move(Path source, Path target, |
94 | // CopyOption... options) |
95 | // with CopyOptions "REPLACE_EXISTING" and "ATOMIC_MOVE". |
96 | if (atomicReplace) { |
97 | boolean ok = oldFile.renameTo(newFile); |
98 | if (ok) { |
99 | return; |
100 | } |
101 | throw DbException.get(ErrorCode.FILE_RENAME_FAILED_2, |
102 | new String[]{name, newName.name}); |
103 | } |
104 | if (newFile.exists()) { |
105 | throw DbException.get(ErrorCode.FILE_RENAME_FAILED_2, |
106 | new String[] { name, newName + " (exists)" }); |
107 | } |
108 | for (int i = 0; i < SysProperties.MAX_FILE_RETRY; i++) { |
109 | IOUtils.trace("rename", name + " >" + newName, null); |
110 | boolean ok = oldFile.renameTo(newFile); |
111 | if (ok) { |
112 | return; |
113 | } |
114 | wait(i); |
115 | } |
116 | throw DbException.get(ErrorCode.FILE_RENAME_FAILED_2, |
117 | new String[]{name, newName.name}); |
118 | } |
119 | |
120 | private static void wait(int i) { |
121 | if (i == 8) { |
122 | System.gc(); |
123 | } |
124 | try { |
125 | // sleep at most 256 ms |
126 | long sleep = Math.min(256, i * i); |
127 | Thread.sleep(sleep); |
128 | } catch (InterruptedException e) { |
129 | // ignore |
130 | } |
131 | } |
132 | |
133 | @Override |
134 | public boolean createFile() { |
135 | File file = new File(name); |
136 | for (int i = 0; i < SysProperties.MAX_FILE_RETRY; i++) { |
137 | try { |
138 | return file.createNewFile(); |
139 | } catch (IOException e) { |
140 | // 'access denied' is really a concurrent access problem |
141 | wait(i); |
142 | } |
143 | } |
144 | return false; |
145 | } |
146 | |
147 | @Override |
148 | public boolean exists() { |
149 | return new File(name).exists(); |
150 | } |
151 | |
152 | @Override |
153 | public void delete() { |
154 | File file = new File(name); |
155 | for (int i = 0; i < SysProperties.MAX_FILE_RETRY; i++) { |
156 | IOUtils.trace("delete", name, null); |
157 | boolean ok = file.delete(); |
158 | if (ok || !file.exists()) { |
159 | return; |
160 | } |
161 | wait(i); |
162 | } |
163 | throw DbException.get(ErrorCode.FILE_DELETE_FAILED_1, name); |
164 | } |
165 | |
166 | @Override |
167 | public List<FilePath> newDirectoryStream() { |
168 | ArrayList<FilePath> list = New.arrayList(); |
169 | File f = new File(name); |
170 | try { |
171 | String[] files = f.list(); |
172 | if (files != null) { |
173 | String base = f.getCanonicalPath(); |
174 | if (!base.endsWith(SysProperties.FILE_SEPARATOR)) { |
175 | base += SysProperties.FILE_SEPARATOR; |
176 | } |
177 | for (int i = 0, len = files.length; i < len; i++) { |
178 | list.add(getPath(base + files[i])); |
179 | } |
180 | } |
181 | return list; |
182 | } catch (IOException e) { |
183 | throw DbException.convertIOException(e, name); |
184 | } |
185 | } |
186 | |
187 | @Override |
188 | public boolean canWrite() { |
189 | return canWriteInternal(new File(name)); |
190 | } |
191 | |
192 | @Override |
193 | public boolean setReadOnly() { |
194 | File f = new File(name); |
195 | return f.setReadOnly(); |
196 | } |
197 | |
198 | @Override |
199 | public FilePathDisk toRealPath() { |
200 | try { |
201 | String fileName = new File(name).getCanonicalPath(); |
202 | return getPath(fileName); |
203 | } catch (IOException e) { |
204 | throw DbException.convertIOException(e, name); |
205 | } |
206 | } |
207 | |
208 | @Override |
209 | public FilePath getParent() { |
210 | String p = new File(name).getParent(); |
211 | return p == null ? null : getPath(p); |
212 | } |
213 | |
214 | @Override |
215 | public boolean isDirectory() { |
216 | return new File(name).isDirectory(); |
217 | } |
218 | |
219 | @Override |
220 | public boolean isAbsolute() { |
221 | return new File(name).isAbsolute(); |
222 | } |
223 | |
224 | @Override |
225 | public long lastModified() { |
226 | return new File(name).lastModified(); |
227 | } |
228 | |
229 | private static boolean canWriteInternal(File file) { |
230 | try { |
231 | if (!file.canWrite()) { |
232 | return false; |
233 | } |
234 | } catch (Exception e) { |
235 | // workaround for GAE which throws a |
236 | // java.security.AccessControlException |
237 | return false; |
238 | } |
239 | // File.canWrite() does not respect windows user permissions, |
240 | // so we must try to open it using the mode "rw". |
241 | // See also http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4420020 |
242 | RandomAccessFile r = null; |
243 | try { |
244 | r = new RandomAccessFile(file, "rw"); |
245 | return true; |
246 | } catch (FileNotFoundException e) { |
247 | return false; |
248 | } finally { |
249 | if (r != null) { |
250 | try { |
251 | r.close(); |
252 | } catch (IOException e) { |
253 | // ignore |
254 | } |
255 | } |
256 | } |
257 | } |
258 | |
259 | @Override |
260 | public void createDirectory() { |
261 | File dir = new File(name); |
262 | for (int i = 0; i < SysProperties.MAX_FILE_RETRY; i++) { |
263 | if (dir.exists()) { |
264 | if (dir.isDirectory()) { |
265 | return; |
266 | } |
267 | throw DbException.get(ErrorCode.FILE_CREATION_FAILED_1, |
268 | name + " (a file with this name already exists)"); |
269 | } else if (dir.mkdir()) { |
270 | return; |
271 | } |
272 | wait(i); |
273 | } |
274 | throw DbException.get(ErrorCode.FILE_CREATION_FAILED_1, name); |
275 | } |
276 | |
277 | @Override |
278 | public OutputStream newOutputStream(boolean append) throws IOException { |
279 | try { |
280 | File file = new File(name); |
281 | File parent = file.getParentFile(); |
282 | if (parent != null) { |
283 | FileUtils.createDirectories(parent.getAbsolutePath()); |
284 | } |
285 | FileOutputStream out = new FileOutputStream(name, append); |
286 | IOUtils.trace("openFileOutputStream", name, out); |
287 | return out; |
288 | } catch (IOException e) { |
289 | freeMemoryAndFinalize(); |
290 | return new FileOutputStream(name); |
291 | } |
292 | } |
293 | |
294 | @Override |
295 | public InputStream newInputStream() throws IOException { |
296 | int index = name.indexOf(':'); |
297 | if (index > 1 && index < 20) { |
298 | // if the ':' is in position 1, a windows file access is assumed: |
299 | // C:.. or D:, and if the ':' is not at the beginning, assume its a |
300 | // file name with a colon |
301 | if (name.startsWith(CLASSPATH_PREFIX)) { |
302 | String fileName = name.substring(CLASSPATH_PREFIX.length()); |
303 | if (!fileName.startsWith("/")) { |
304 | fileName = "/" + fileName; |
305 | } |
306 | InputStream in = getClass().getResourceAsStream(fileName); |
307 | if (in == null) { |
308 | in = Thread.currentThread().getContextClassLoader(). |
309 | getResourceAsStream(fileName); |
310 | } |
311 | if (in == null) { |
312 | throw new FileNotFoundException("resource " + fileName); |
313 | } |
314 | return in; |
315 | } |
316 | // otherwise an URL is assumed |
317 | URL url = new URL(name); |
318 | InputStream in = url.openStream(); |
319 | return in; |
320 | } |
321 | FileInputStream in = new FileInputStream(name); |
322 | IOUtils.trace("openFileInputStream", name, in); |
323 | return in; |
324 | } |
325 | |
326 | /** |
327 | * Call the garbage collection and run finalization. This close all files |
328 | * that were not closed, and are no longer referenced. |
329 | */ |
330 | static void freeMemoryAndFinalize() { |
331 | IOUtils.trace("freeMemoryAndFinalize", null, null); |
332 | Runtime rt = Runtime.getRuntime(); |
333 | long mem = rt.freeMemory(); |
334 | for (int i = 0; i < 16; i++) { |
335 | rt.gc(); |
336 | long now = rt.freeMemory(); |
337 | rt.runFinalization(); |
338 | if (now == mem) { |
339 | break; |
340 | } |
341 | mem = now; |
342 | } |
343 | } |
344 | |
345 | @Override |
346 | public FileChannel open(String mode) throws IOException { |
347 | FileDisk f; |
348 | try { |
349 | f = new FileDisk(name, mode); |
350 | IOUtils.trace("open", name, f); |
351 | } catch (IOException e) { |
352 | freeMemoryAndFinalize(); |
353 | try { |
354 | f = new FileDisk(name, mode); |
355 | } catch (IOException e2) { |
356 | throw e; |
357 | } |
358 | } |
359 | return f; |
360 | } |
361 | |
362 | @Override |
363 | public String getScheme() { |
364 | return "file"; |
365 | } |
366 | |
367 | @Override |
368 | public FilePath createTempFile(String suffix, boolean deleteOnExit, |
369 | boolean inTempDir) throws IOException { |
370 | String fileName = name + "."; |
371 | String prefix = new File(fileName).getName(); |
372 | File dir; |
373 | if (inTempDir) { |
374 | dir = new File(System.getProperty("java.io.tmpdir", ".")); |
375 | } else { |
376 | dir = new File(fileName).getAbsoluteFile().getParentFile(); |
377 | } |
378 | FileUtils.createDirectories(dir.getAbsolutePath()); |
379 | while (true) { |
380 | File f = new File(dir, prefix + getNextTempFileNamePart(false) + suffix); |
381 | if (f.exists() || !f.createNewFile()) { |
382 | // in theory, the random number could collide |
383 | getNextTempFileNamePart(true); |
384 | continue; |
385 | } |
386 | if (deleteOnExit) { |
387 | try { |
388 | f.deleteOnExit(); |
389 | } catch (Throwable e) { |
390 | // sometimes this throws a NullPointerException |
391 | // at java.io.DeleteOnExitHook.add(DeleteOnExitHook.java:33) |
392 | // we can ignore it |
393 | } |
394 | } |
395 | return get(f.getCanonicalPath()); |
396 | } |
397 | } |
398 | |
399 | } |
400 | |
401 | /** |
402 | * Uses java.io.RandomAccessFile to access a file. |
403 | */ |
404 | class FileDisk extends FileBase { |
405 | |
406 | private final RandomAccessFile file; |
407 | private final String name; |
408 | private final boolean readOnly; |
409 | |
410 | FileDisk(String fileName, String mode) throws FileNotFoundException { |
411 | this.file = new RandomAccessFile(fileName, mode); |
412 | this.name = fileName; |
413 | this.readOnly = mode.equals("r"); |
414 | } |
415 | |
416 | @Override |
417 | public void force(boolean metaData) throws IOException { |
418 | String m = SysProperties.SYNC_METHOD; |
419 | if ("".equals(m)) { |
420 | // do nothing |
421 | } else if ("sync".equals(m)) { |
422 | file.getFD().sync(); |
423 | } else if ("force".equals(m)) { |
424 | file.getChannel().force(true); |
425 | } else if ("forceFalse".equals(m)) { |
426 | file.getChannel().force(false); |
427 | } else { |
428 | file.getFD().sync(); |
429 | } |
430 | } |
431 | |
432 | @Override |
433 | public FileChannel truncate(long newLength) throws IOException { |
434 | // compatibility with JDK FileChannel#truncate |
435 | if (readOnly) { |
436 | throw new NonWritableChannelException(); |
437 | } |
438 | if (newLength < file.length()) { |
439 | file.setLength(newLength); |
440 | } |
441 | return this; |
442 | } |
443 | |
444 | @Override |
445 | public synchronized FileLock tryLock(long position, long size, |
446 | boolean shared) throws IOException { |
447 | return file.getChannel().tryLock(position, size, shared); |
448 | } |
449 | |
450 | @Override |
451 | public void implCloseChannel() throws IOException { |
452 | file.close(); |
453 | } |
454 | |
455 | @Override |
456 | public long position() throws IOException { |
457 | return file.getFilePointer(); |
458 | } |
459 | |
460 | @Override |
461 | public long size() throws IOException { |
462 | return file.length(); |
463 | } |
464 | |
465 | @Override |
466 | public int read(ByteBuffer dst) throws IOException { |
467 | int len = file.read(dst.array(), dst.arrayOffset() + dst.position(), |
468 | dst.remaining()); |
469 | if (len > 0) { |
470 | dst.position(dst.position() + len); |
471 | } |
472 | return len; |
473 | } |
474 | |
475 | @Override |
476 | public FileChannel position(long pos) throws IOException { |
477 | file.seek(pos); |
478 | return this; |
479 | } |
480 | |
481 | @Override |
482 | public int write(ByteBuffer src) throws IOException { |
483 | int len = src.remaining(); |
484 | file.write(src.array(), src.arrayOffset() + src.position(), len); |
485 | src.position(src.position() + len); |
486 | return len; |
487 | } |
488 | |
489 | @Override |
490 | public String toString() { |
491 | return name; |
492 | } |
493 | |
494 | } |