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; |
7 | |
8 | import java.io.IOException; |
9 | import java.lang.ref.Reference; |
10 | import java.nio.ByteBuffer; |
11 | import java.nio.channels.FileChannel; |
12 | import java.util.Arrays; |
13 | |
14 | import org.h2.api.ErrorCode; |
15 | import org.h2.engine.Constants; |
16 | import org.h2.engine.SysProperties; |
17 | import org.h2.message.DbException; |
18 | import org.h2.security.SecureFileStore; |
19 | import org.h2.store.fs.FileUtils; |
20 | |
21 | /** |
22 | * This class is an abstraction of a random access file. |
23 | * Each file contains a magic header, and reading / writing is done in blocks. |
24 | * See also {@link SecureFileStore} |
25 | */ |
26 | public class FileStore { |
27 | |
28 | /** |
29 | * The size of the file header in bytes. |
30 | */ |
31 | public static final int HEADER_LENGTH = 3 * Constants.FILE_BLOCK_SIZE; |
32 | |
33 | /** |
34 | * The magic file header. |
35 | */ |
36 | private static final String HEADER = |
37 | "-- H2 0.5/B -- ".substring(0, Constants.FILE_BLOCK_SIZE - 1) + "\n"; |
38 | |
39 | /** |
40 | * The file name. |
41 | */ |
42 | protected String name; |
43 | |
44 | /** |
45 | * The callback object is responsible to check access rights, and free up |
46 | * disk space if required. |
47 | */ |
48 | private final DataHandler handler; |
49 | |
50 | private FileChannel file; |
51 | private long filePos; |
52 | private long fileLength; |
53 | private Reference<?> autoDeleteReference; |
54 | private boolean checkedWriting = true; |
55 | private final String mode; |
56 | private java.nio.channels.FileLock lock; |
57 | |
58 | /** |
59 | * Create a new file using the given settings. |
60 | * |
61 | * @param handler the callback object |
62 | * @param name the file name |
63 | * @param mode the access mode ("r", "rw", "rws", "rwd") |
64 | */ |
65 | protected FileStore(DataHandler handler, String name, String mode) { |
66 | this.handler = handler; |
67 | this.name = name; |
68 | try { |
69 | boolean exists = FileUtils.exists(name); |
70 | if (exists && !FileUtils.canWrite(name)) { |
71 | mode = "r"; |
72 | } else { |
73 | FileUtils.createDirectories(FileUtils.getParent(name)); |
74 | } |
75 | file = FileUtils.open(name, mode); |
76 | if (exists) { |
77 | fileLength = file.size(); |
78 | } |
79 | } catch (IOException e) { |
80 | throw DbException.convertIOException( |
81 | e, "name: " + name + " mode: " + mode); |
82 | } |
83 | this.mode = mode; |
84 | } |
85 | |
86 | /** |
87 | * Open a non encrypted file store with the given settings. |
88 | * |
89 | * @param handler the data handler |
90 | * @param name the file name |
91 | * @param mode the access mode (r, rw, rws, rwd) |
92 | * @return the created object |
93 | */ |
94 | public static FileStore open(DataHandler handler, String name, String mode) { |
95 | return open(handler, name, mode, null, null, 0); |
96 | } |
97 | |
98 | /** |
99 | * Open an encrypted file store with the given settings. |
100 | * |
101 | * @param handler the data handler |
102 | * @param name the file name |
103 | * @param mode the access mode (r, rw, rws, rwd) |
104 | * @param cipher the name of the cipher algorithm |
105 | * @param key the encryption key |
106 | * @return the created object |
107 | */ |
108 | public static FileStore open(DataHandler handler, String name, String mode, |
109 | String cipher, byte[] key) { |
110 | return open(handler, name, mode, cipher, key, |
111 | Constants.ENCRYPTION_KEY_HASH_ITERATIONS); |
112 | } |
113 | |
114 | /** |
115 | * Open an encrypted file store with the given settings. |
116 | * |
117 | * @param handler the data handler |
118 | * @param name the file name |
119 | * @param mode the access mode (r, rw, rws, rwd) |
120 | * @param cipher the name of the cipher algorithm |
121 | * @param key the encryption key |
122 | * @param keyIterations the number of iterations the key should be hashed |
123 | * @return the created object |
124 | */ |
125 | public static FileStore open(DataHandler handler, String name, String mode, |
126 | String cipher, byte[] key, int keyIterations) { |
127 | FileStore store; |
128 | if (cipher == null) { |
129 | store = new FileStore(handler, name, mode); |
130 | } else { |
131 | store = new SecureFileStore(handler, name, mode, |
132 | cipher, key, keyIterations); |
133 | } |
134 | return store; |
135 | } |
136 | |
137 | /** |
138 | * Generate the random salt bytes if required. |
139 | * |
140 | * @return the random salt or the magic |
141 | */ |
142 | protected byte[] generateSalt() { |
143 | return HEADER.getBytes(Constants.UTF8); |
144 | } |
145 | |
146 | /** |
147 | * Initialize the key using the given salt. |
148 | * |
149 | * @param salt the salt |
150 | */ |
151 | protected void initKey(byte[] salt) { |
152 | // do nothing |
153 | } |
154 | |
155 | public void setCheckedWriting(boolean value) { |
156 | this.checkedWriting = value; |
157 | } |
158 | |
159 | private void checkWritingAllowed() { |
160 | if (handler != null && checkedWriting) { |
161 | handler.checkWritingAllowed(); |
162 | } |
163 | } |
164 | |
165 | private void checkPowerOff() { |
166 | if (handler != null) { |
167 | handler.checkPowerOff(); |
168 | } |
169 | } |
170 | |
171 | /** |
172 | * Initialize the file. This method will write or check the file header if |
173 | * required. |
174 | */ |
175 | public void init() { |
176 | int len = Constants.FILE_BLOCK_SIZE; |
177 | byte[] salt; |
178 | byte[] magic = HEADER.getBytes(Constants.UTF8); |
179 | if (length() < HEADER_LENGTH) { |
180 | // write unencrypted |
181 | checkedWriting = false; |
182 | writeDirect(magic, 0, len); |
183 | salt = generateSalt(); |
184 | writeDirect(salt, 0, len); |
185 | initKey(salt); |
186 | // write (maybe) encrypted |
187 | write(magic, 0, len); |
188 | checkedWriting = true; |
189 | } else { |
190 | // read unencrypted |
191 | seek(0); |
192 | byte[] buff = new byte[len]; |
193 | readFullyDirect(buff, 0, len); |
194 | if (!Arrays.equals(buff, magic)) { |
195 | throw DbException.get(ErrorCode.FILE_VERSION_ERROR_1, name); |
196 | } |
197 | salt = new byte[len]; |
198 | readFullyDirect(salt, 0, len); |
199 | initKey(salt); |
200 | // read (maybe) encrypted |
201 | readFully(buff, 0, Constants.FILE_BLOCK_SIZE); |
202 | if (!Arrays.equals(buff, magic)) { |
203 | throw DbException.get(ErrorCode.FILE_ENCRYPTION_ERROR_1, name); |
204 | } |
205 | } |
206 | } |
207 | |
208 | /** |
209 | * Close the file. |
210 | */ |
211 | public void close() { |
212 | if (file != null) { |
213 | try { |
214 | trace("close", name, file); |
215 | file.close(); |
216 | } catch (IOException e) { |
217 | throw DbException.convertIOException(e, name); |
218 | } finally { |
219 | file = null; |
220 | } |
221 | } |
222 | } |
223 | |
224 | /** |
225 | * Close the file without throwing any exceptions. Exceptions are simply |
226 | * ignored. |
227 | */ |
228 | public void closeSilently() { |
229 | try { |
230 | close(); |
231 | } catch (Exception e) { |
232 | // ignore |
233 | } |
234 | } |
235 | |
236 | /** |
237 | * Close the file (ignoring exceptions) and delete the file. |
238 | */ |
239 | public void closeAndDeleteSilently() { |
240 | if (file != null) { |
241 | closeSilently(); |
242 | handler.getTempFileDeleter().deleteFile(autoDeleteReference, name); |
243 | name = null; |
244 | } |
245 | } |
246 | |
247 | /** |
248 | * Read a number of bytes without decrypting. |
249 | * |
250 | * @param b the target buffer |
251 | * @param off the offset |
252 | * @param len the number of bytes to read |
253 | */ |
254 | protected void readFullyDirect(byte[] b, int off, int len) { |
255 | readFully(b, off, len); |
256 | } |
257 | |
258 | /** |
259 | * Read a number of bytes. |
260 | * |
261 | * @param b the target buffer |
262 | * @param off the offset |
263 | * @param len the number of bytes to read |
264 | */ |
265 | public void readFully(byte[] b, int off, int len) { |
266 | if (SysProperties.CHECK && |
267 | (len < 0 || len % Constants.FILE_BLOCK_SIZE != 0)) { |
268 | DbException.throwInternalError( |
269 | "unaligned read " + name + " len " + len); |
270 | } |
271 | checkPowerOff(); |
272 | try { |
273 | FileUtils.readFully(file, ByteBuffer.wrap(b, off, len)); |
274 | } catch (IOException e) { |
275 | throw DbException.convertIOException(e, name); |
276 | } |
277 | filePos += len; |
278 | } |
279 | |
280 | /** |
281 | * Go to the specified file location. |
282 | * |
283 | * @param pos the location |
284 | */ |
285 | public void seek(long pos) { |
286 | if (SysProperties.CHECK && |
287 | pos % Constants.FILE_BLOCK_SIZE != 0) { |
288 | DbException.throwInternalError( |
289 | "unaligned seek " + name + " pos " + pos); |
290 | } |
291 | try { |
292 | if (pos != filePos) { |
293 | file.position(pos); |
294 | filePos = pos; |
295 | } |
296 | } catch (IOException e) { |
297 | throw DbException.convertIOException(e, name); |
298 | } |
299 | } |
300 | |
301 | /** |
302 | * Write a number of bytes without encrypting. |
303 | * |
304 | * @param b the source buffer |
305 | * @param off the offset |
306 | * @param len the number of bytes to write |
307 | */ |
308 | protected void writeDirect(byte[] b, int off, int len) { |
309 | write(b, off, len); |
310 | } |
311 | |
312 | /** |
313 | * Write a number of bytes. |
314 | * |
315 | * @param b the source buffer |
316 | * @param off the offset |
317 | * @param len the number of bytes to write |
318 | */ |
319 | public void write(byte[] b, int off, int len) { |
320 | if (SysProperties.CHECK && (len < 0 || |
321 | len % Constants.FILE_BLOCK_SIZE != 0)) { |
322 | DbException.throwInternalError( |
323 | "unaligned write " + name + " len " + len); |
324 | } |
325 | checkWritingAllowed(); |
326 | checkPowerOff(); |
327 | try { |
328 | FileUtils.writeFully(file, ByteBuffer.wrap(b, off, len)); |
329 | } catch (IOException e) { |
330 | closeFileSilently(); |
331 | throw DbException.convertIOException(e, name); |
332 | } |
333 | filePos += len; |
334 | fileLength = Math.max(filePos, fileLength); |
335 | } |
336 | |
337 | /** |
338 | * Set the length of the file. This will expand or shrink the file. |
339 | * |
340 | * @param newLength the new file size |
341 | */ |
342 | public void setLength(long newLength) { |
343 | if (SysProperties.CHECK && newLength % Constants.FILE_BLOCK_SIZE != 0) { |
344 | DbException.throwInternalError( |
345 | "unaligned setLength " + name + " pos " + newLength); |
346 | } |
347 | checkPowerOff(); |
348 | checkWritingAllowed(); |
349 | try { |
350 | if (newLength > fileLength) { |
351 | long pos = filePos; |
352 | file.position(newLength - 1); |
353 | FileUtils.writeFully(file, ByteBuffer.wrap(new byte[1])); |
354 | file.position(pos); |
355 | } else { |
356 | file.truncate(newLength); |
357 | } |
358 | fileLength = newLength; |
359 | } catch (IOException e) { |
360 | closeFileSilently(); |
361 | throw DbException.convertIOException(e, name); |
362 | } |
363 | } |
364 | |
365 | /** |
366 | * Get the file size in bytes. |
367 | * |
368 | * @return the file size |
369 | */ |
370 | public long length() { |
371 | try { |
372 | long len = fileLength; |
373 | if (SysProperties.CHECK2) { |
374 | len = file.size(); |
375 | if (len != fileLength) { |
376 | DbException.throwInternalError( |
377 | "file " + name + " length " + len + " expected " + fileLength); |
378 | } |
379 | } |
380 | if (SysProperties.CHECK2 && len % Constants.FILE_BLOCK_SIZE != 0) { |
381 | long newLength = len + Constants.FILE_BLOCK_SIZE - |
382 | (len % Constants.FILE_BLOCK_SIZE); |
383 | file.truncate(newLength); |
384 | fileLength = newLength; |
385 | DbException.throwInternalError( |
386 | "unaligned file length " + name + " len " + len); |
387 | } |
388 | return len; |
389 | } catch (IOException e) { |
390 | throw DbException.convertIOException(e, name); |
391 | } |
392 | } |
393 | |
394 | /** |
395 | * Get the current location of the file pointer. |
396 | * |
397 | * @return the location |
398 | */ |
399 | public long getFilePointer() { |
400 | if (SysProperties.CHECK2) { |
401 | try { |
402 | if (file.position() != filePos) { |
403 | DbException.throwInternalError(); |
404 | } |
405 | } catch (IOException e) { |
406 | throw DbException.convertIOException(e, name); |
407 | } |
408 | } |
409 | return filePos; |
410 | } |
411 | |
412 | /** |
413 | * Call fsync. Depending on the operating system and hardware, this may or |
414 | * may not in fact write the changes. |
415 | */ |
416 | public void sync() { |
417 | try { |
418 | file.force(true); |
419 | } catch (IOException e) { |
420 | closeFileSilently(); |
421 | throw DbException.convertIOException(e, name); |
422 | } |
423 | } |
424 | |
425 | /** |
426 | * Automatically delete the file once it is no longer in use. |
427 | */ |
428 | public void autoDelete() { |
429 | if (autoDeleteReference == null) { |
430 | autoDeleteReference = handler.getTempFileDeleter().addFile(name, this); |
431 | } |
432 | } |
433 | |
434 | /** |
435 | * No longer automatically delete the file once it is no longer in use. |
436 | */ |
437 | public void stopAutoDelete() { |
438 | handler.getTempFileDeleter().stopAutoDelete(autoDeleteReference, name); |
439 | autoDeleteReference = null; |
440 | } |
441 | |
442 | /** |
443 | * Close the file. The file may later be re-opened using openFile. |
444 | */ |
445 | public void closeFile() throws IOException { |
446 | file.close(); |
447 | file = null; |
448 | } |
449 | |
450 | /** |
451 | * Just close the file, without setting the reference to null. This method |
452 | * is called when writing failed. The reference is not set to null so that |
453 | * there are no NullPointerExceptions later on. |
454 | */ |
455 | private void closeFileSilently() { |
456 | try { |
457 | file.close(); |
458 | } catch (IOException e) { |
459 | // ignore |
460 | } |
461 | } |
462 | |
463 | /** |
464 | * Re-open the file. The file pointer will be reset to the previous |
465 | * location. |
466 | */ |
467 | public void openFile() throws IOException { |
468 | if (file == null) { |
469 | file = FileUtils.open(name, mode); |
470 | file.position(filePos); |
471 | } |
472 | } |
473 | |
474 | private static void trace(String method, String fileName, Object o) { |
475 | if (SysProperties.TRACE_IO) { |
476 | System.out.println("FileStore." + method + " " + fileName + " " + o); |
477 | } |
478 | } |
479 | |
480 | /** |
481 | * Try to lock the file. |
482 | * |
483 | * @return true if successful |
484 | */ |
485 | public synchronized boolean tryLock() { |
486 | try { |
487 | lock = file.tryLock(); |
488 | return lock != null; |
489 | } catch (Exception e) { |
490 | // ignore OverlappingFileLockException |
491 | return false; |
492 | } |
493 | } |
494 | |
495 | /** |
496 | * Release the file lock. |
497 | */ |
498 | public synchronized void releaseLock() { |
499 | if (file != null && lock != null) { |
500 | try { |
501 | lock.release(); |
502 | } catch (Exception e) { |
503 | // ignore |
504 | } |
505 | lock = null; |
506 | } |
507 | } |
508 | |
509 | } |