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.IOException; |
9 | import java.io.InputStream; |
10 | import java.io.OutputStream; |
11 | import java.io.SequenceInputStream; |
12 | import java.nio.ByteBuffer; |
13 | import java.nio.channels.FileChannel; |
14 | import java.nio.channels.FileLock; |
15 | import java.util.ArrayList; |
16 | import java.util.List; |
17 | |
18 | import org.h2.engine.SysProperties; |
19 | import org.h2.message.DbException; |
20 | import org.h2.util.New; |
21 | |
22 | /** |
23 | * A file system that may split files into multiple smaller files. |
24 | * (required for a FAT32 because it only support files up to 2 GB). |
25 | */ |
26 | public class FilePathSplit extends FilePathWrapper { |
27 | |
28 | private static final String PART_SUFFIX = ".part"; |
29 | |
30 | @Override |
31 | protected String getPrefix() { |
32 | return getScheme() + ":" + parse(name)[0] + ":"; |
33 | } |
34 | |
35 | @Override |
36 | public FilePath unwrap(String fileName) { |
37 | return FilePath.get(parse(fileName)[1]); |
38 | } |
39 | |
40 | @Override |
41 | public boolean setReadOnly() { |
42 | boolean result = false; |
43 | for (int i = 0;; i++) { |
44 | FilePath f = getBase(i); |
45 | if (f.exists()) { |
46 | result = f.setReadOnly(); |
47 | } else { |
48 | break; |
49 | } |
50 | } |
51 | return result; |
52 | } |
53 | |
54 | @Override |
55 | public void delete() { |
56 | for (int i = 0;; i++) { |
57 | FilePath f = getBase(i); |
58 | if (f.exists()) { |
59 | f.delete(); |
60 | } else { |
61 | break; |
62 | } |
63 | } |
64 | } |
65 | |
66 | @Override |
67 | public long lastModified() { |
68 | long lastModified = 0; |
69 | for (int i = 0;; i++) { |
70 | FilePath f = getBase(i); |
71 | if (f.exists()) { |
72 | long l = f.lastModified(); |
73 | lastModified = Math.max(lastModified, l); |
74 | } else { |
75 | break; |
76 | } |
77 | } |
78 | return lastModified; |
79 | } |
80 | |
81 | @Override |
82 | public long size() { |
83 | long length = 0; |
84 | for (int i = 0;; i++) { |
85 | FilePath f = getBase(i); |
86 | if (f.exists()) { |
87 | length += f.size(); |
88 | } else { |
89 | break; |
90 | } |
91 | } |
92 | return length; |
93 | } |
94 | |
95 | @Override |
96 | public ArrayList<FilePath> newDirectoryStream() { |
97 | List<FilePath> list = getBase().newDirectoryStream(); |
98 | ArrayList<FilePath> newList = New.arrayList(); |
99 | for (int i = 0, size = list.size(); i < size; i++) { |
100 | FilePath f = list.get(i); |
101 | if (!f.getName().endsWith(PART_SUFFIX)) { |
102 | newList.add(wrap(f)); |
103 | } |
104 | } |
105 | return newList; |
106 | } |
107 | |
108 | @Override |
109 | public InputStream newInputStream() throws IOException { |
110 | InputStream input = getBase().newInputStream(); |
111 | for (int i = 1;; i++) { |
112 | FilePath f = getBase(i); |
113 | if (f.exists()) { |
114 | InputStream i2 = f.newInputStream(); |
115 | input = new SequenceInputStream(input, i2); |
116 | } else { |
117 | break; |
118 | } |
119 | } |
120 | return input; |
121 | } |
122 | |
123 | @Override |
124 | public FileChannel open(String mode) throws IOException { |
125 | ArrayList<FileChannel> list = New.arrayList(); |
126 | list.add(getBase().open(mode)); |
127 | for (int i = 1;; i++) { |
128 | FilePath f = getBase(i); |
129 | if (f.exists()) { |
130 | list.add(f.open(mode)); |
131 | } else { |
132 | break; |
133 | } |
134 | } |
135 | FileChannel[] array = new FileChannel[list.size()]; |
136 | list.toArray(array); |
137 | long maxLength = array[0].size(); |
138 | long length = maxLength; |
139 | if (array.length == 1) { |
140 | long defaultMaxLength = getDefaultMaxLength(); |
141 | if (maxLength < defaultMaxLength) { |
142 | maxLength = defaultMaxLength; |
143 | } |
144 | } else { |
145 | if (maxLength == 0) { |
146 | closeAndThrow(0, array, array[0], maxLength); |
147 | } |
148 | for (int i = 1; i < array.length - 1; i++) { |
149 | FileChannel c = array[i]; |
150 | long l = c.size(); |
151 | length += l; |
152 | if (l != maxLength) { |
153 | closeAndThrow(i, array, c, maxLength); |
154 | } |
155 | } |
156 | FileChannel c = array[array.length - 1]; |
157 | long l = c.size(); |
158 | length += l; |
159 | if (l > maxLength) { |
160 | closeAndThrow(array.length - 1, array, c, maxLength); |
161 | } |
162 | } |
163 | return new FileSplit(this, mode, array, length, maxLength); |
164 | } |
165 | |
166 | private long getDefaultMaxLength() { |
167 | return 1L << Integer.decode(parse(name)[0]).intValue(); |
168 | } |
169 | |
170 | private void closeAndThrow(int id, FileChannel[] array, FileChannel o, |
171 | long maxLength) throws IOException { |
172 | String message = "Expected file length: " + maxLength + " got: " + |
173 | o.size() + " for " + getName(id); |
174 | for (FileChannel f : array) { |
175 | f.close(); |
176 | } |
177 | throw new IOException(message); |
178 | } |
179 | |
180 | @Override |
181 | public OutputStream newOutputStream(boolean append) throws IOException { |
182 | return new FileChannelOutputStream(open("rw"), append); |
183 | } |
184 | |
185 | @Override |
186 | public void moveTo(FilePath path, boolean atomicReplace) { |
187 | FilePathSplit newName = (FilePathSplit) path; |
188 | for (int i = 0;; i++) { |
189 | FilePath o = getBase(i); |
190 | if (o.exists()) { |
191 | o.moveTo(newName.getBase(i), atomicReplace); |
192 | } else { |
193 | break; |
194 | } |
195 | } |
196 | } |
197 | |
198 | /** |
199 | * Split the file name into size and base file name. |
200 | * |
201 | * @param fileName the file name |
202 | * @return an array with size and file name |
203 | */ |
204 | private String[] parse(String fileName) { |
205 | if (!fileName.startsWith(getScheme())) { |
206 | DbException.throwInternalError(fileName + " doesn't start with " + getScheme()); |
207 | } |
208 | fileName = fileName.substring(getScheme().length() + 1); |
209 | String size; |
210 | if (fileName.length() > 0 && Character.isDigit(fileName.charAt(0))) { |
211 | int idx = fileName.indexOf(':'); |
212 | size = fileName.substring(0, idx); |
213 | try { |
214 | fileName = fileName.substring(idx + 1); |
215 | } catch (NumberFormatException e) { |
216 | // ignore |
217 | } |
218 | } else { |
219 | size = Long.toString(SysProperties.SPLIT_FILE_SIZE_SHIFT); |
220 | } |
221 | return new String[] { size, fileName }; |
222 | } |
223 | |
224 | /** |
225 | * Get the file name of a part file. |
226 | * |
227 | * @param id the part id |
228 | * @return the file name including the part id |
229 | */ |
230 | FilePath getBase(int id) { |
231 | return FilePath.get(getName(id)); |
232 | } |
233 | |
234 | private String getName(int id) { |
235 | return id > 0 ? getBase().name + "." + id + PART_SUFFIX : getBase().name; |
236 | } |
237 | |
238 | @Override |
239 | public String getScheme() { |
240 | return "split"; |
241 | } |
242 | |
243 | } |
244 | |
245 | /** |
246 | * A file that may be split into multiple smaller files. |
247 | */ |
248 | class FileSplit extends FileBase { |
249 | |
250 | private final FilePathSplit file; |
251 | private final String mode; |
252 | private final long maxLength; |
253 | private FileChannel[] list; |
254 | private long filePointer; |
255 | private long length; |
256 | |
257 | FileSplit(FilePathSplit file, String mode, FileChannel[] list, long length, |
258 | long maxLength) { |
259 | this.file = file; |
260 | this.mode = mode; |
261 | this.list = list; |
262 | this.length = length; |
263 | this.maxLength = maxLength; |
264 | } |
265 | |
266 | @Override |
267 | public void implCloseChannel() throws IOException { |
268 | for (FileChannel c : list) { |
269 | c.close(); |
270 | } |
271 | } |
272 | |
273 | @Override |
274 | public long position() { |
275 | return filePointer; |
276 | } |
277 | |
278 | @Override |
279 | public long size() { |
280 | return length; |
281 | } |
282 | |
283 | @Override |
284 | public int read(ByteBuffer dst) throws IOException { |
285 | int len = dst.remaining(); |
286 | if (len == 0) { |
287 | return 0; |
288 | } |
289 | len = (int) Math.min(len, length - filePointer); |
290 | if (len <= 0) { |
291 | return -1; |
292 | } |
293 | long offset = filePointer % maxLength; |
294 | len = (int) Math.min(len, maxLength - offset); |
295 | FileChannel channel = getFileChannel(); |
296 | channel.position(offset); |
297 | len = channel.read(dst); |
298 | filePointer += len; |
299 | return len; |
300 | } |
301 | |
302 | @Override |
303 | public FileChannel position(long pos) { |
304 | filePointer = pos; |
305 | return this; |
306 | } |
307 | |
308 | private FileChannel getFileChannel() throws IOException { |
309 | int id = (int) (filePointer / maxLength); |
310 | while (id >= list.length) { |
311 | int i = list.length; |
312 | FileChannel[] newList = new FileChannel[i + 1]; |
313 | System.arraycopy(list, 0, newList, 0, i); |
314 | FilePath f = file.getBase(i); |
315 | newList[i] = f.open(mode); |
316 | list = newList; |
317 | } |
318 | return list[id]; |
319 | } |
320 | |
321 | @Override |
322 | public FileChannel truncate(long newLength) throws IOException { |
323 | if (newLength >= length) { |
324 | return this; |
325 | } |
326 | filePointer = Math.min(filePointer, newLength); |
327 | int newFileCount = 1 + (int) (newLength / maxLength); |
328 | if (newFileCount < list.length) { |
329 | // delete some of the files |
330 | FileChannel[] newList = new FileChannel[newFileCount]; |
331 | // delete backwards, so that truncating is somewhat transactional |
332 | for (int i = list.length - 1; i >= newFileCount; i--) { |
333 | // verify the file is writable |
334 | list[i].truncate(0); |
335 | list[i].close(); |
336 | try { |
337 | file.getBase(i).delete(); |
338 | } catch (DbException e) { |
339 | throw DbException.convertToIOException(e); |
340 | } |
341 | } |
342 | System.arraycopy(list, 0, newList, 0, newList.length); |
343 | list = newList; |
344 | } |
345 | long size = newLength - maxLength * (newFileCount - 1); |
346 | list[list.length - 1].truncate(size); |
347 | this.length = newLength; |
348 | return this; |
349 | } |
350 | |
351 | @Override |
352 | public void force(boolean metaData) throws IOException { |
353 | for (FileChannel c : list) { |
354 | c.force(metaData); |
355 | } |
356 | } |
357 | |
358 | @Override |
359 | public int write(ByteBuffer src) throws IOException { |
360 | if (filePointer >= length && filePointer > maxLength) { |
361 | // may need to extend and create files |
362 | long oldFilePointer = filePointer; |
363 | long x = length - (length % maxLength) + maxLength; |
364 | for (; x < filePointer; x += maxLength) { |
365 | if (x > length) { |
366 | // expand the file size |
367 | position(x - 1); |
368 | write(ByteBuffer.wrap(new byte[1])); |
369 | } |
370 | filePointer = oldFilePointer; |
371 | } |
372 | } |
373 | long offset = filePointer % maxLength; |
374 | int len = src.remaining(); |
375 | FileChannel channel = getFileChannel(); |
376 | channel.position(offset); |
377 | int l = (int) Math.min(len, maxLength - offset); |
378 | if (l == len) { |
379 | l = channel.write(src); |
380 | } else { |
381 | int oldLimit = src.limit(); |
382 | src.limit(src.position() + l); |
383 | l = channel.write(src); |
384 | src.limit(oldLimit); |
385 | } |
386 | filePointer += l; |
387 | length = Math.max(length, filePointer); |
388 | return l; |
389 | } |
390 | |
391 | @Override |
392 | public synchronized FileLock tryLock(long position, long size, |
393 | boolean shared) throws IOException { |
394 | return list[0].tryLock(position, size, shared); |
395 | } |
396 | |
397 | @Override |
398 | public String toString() { |
399 | return file.toString(); |
400 | } |
401 | |
402 | } |