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.FileNotFoundException; |
9 | import java.io.IOException; |
10 | import java.io.InputStream; |
11 | import java.io.OutputStream; |
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.Enumeration; |
17 | import java.util.zip.ZipEntry; |
18 | import java.util.zip.ZipFile; |
19 | import org.h2.message.DbException; |
20 | import org.h2.util.IOUtils; |
21 | import org.h2.util.New; |
22 | |
23 | /** |
24 | * This is a read-only file system that allows |
25 | * to access databases stored in a .zip or .jar file. |
26 | */ |
27 | public class FilePathZip extends FilePath { |
28 | |
29 | @Override |
30 | public FilePathZip getPath(String path) { |
31 | FilePathZip p = new FilePathZip(); |
32 | p.name = path; |
33 | return p; |
34 | } |
35 | |
36 | @Override |
37 | public void createDirectory() { |
38 | // ignore |
39 | } |
40 | |
41 | @Override |
42 | public boolean createFile() { |
43 | throw DbException.getUnsupportedException("write"); |
44 | } |
45 | |
46 | @Override |
47 | public void delete() { |
48 | throw DbException.getUnsupportedException("write"); |
49 | } |
50 | |
51 | @Override |
52 | public boolean exists() { |
53 | try { |
54 | String entryName = getEntryName(); |
55 | if (entryName.length() == 0) { |
56 | return true; |
57 | } |
58 | ZipFile file = openZipFile(); |
59 | try { |
60 | return file.getEntry(entryName) != null; |
61 | } finally { |
62 | file.close(); |
63 | } |
64 | } catch (IOException e) { |
65 | return false; |
66 | } |
67 | } |
68 | |
69 | @Override |
70 | public long lastModified() { |
71 | return 0; |
72 | } |
73 | |
74 | @Override |
75 | public FilePath getParent() { |
76 | int idx = name.lastIndexOf('/'); |
77 | return idx < 0 ? null : getPath(name.substring(0, idx)); |
78 | } |
79 | |
80 | @Override |
81 | public boolean isAbsolute() { |
82 | String fileName = translateFileName(name); |
83 | return FilePath.get(fileName).isAbsolute(); |
84 | } |
85 | |
86 | @Override |
87 | public FilePath unwrap() { |
88 | return FilePath.get(name.substring(getScheme().length() + 1)); |
89 | } |
90 | |
91 | @Override |
92 | public boolean isDirectory() { |
93 | try { |
94 | String entryName = getEntryName(); |
95 | if (entryName.length() == 0) { |
96 | return true; |
97 | } |
98 | ZipFile file = openZipFile(); |
99 | try { |
100 | Enumeration<? extends ZipEntry> en = file.entries(); |
101 | while (en.hasMoreElements()) { |
102 | ZipEntry entry = en.nextElement(); |
103 | String n = entry.getName(); |
104 | if (n.equals(entryName)) { |
105 | return entry.isDirectory(); |
106 | } else if (n.startsWith(entryName)) { |
107 | if (n.length() == entryName.length() + 1) { |
108 | if (n.equals(entryName + "/")) { |
109 | return true; |
110 | } |
111 | } |
112 | } |
113 | } |
114 | } finally { |
115 | file.close(); |
116 | } |
117 | return false; |
118 | } catch (IOException e) { |
119 | return false; |
120 | } |
121 | } |
122 | |
123 | @Override |
124 | public boolean canWrite() { |
125 | return false; |
126 | } |
127 | |
128 | @Override |
129 | public boolean setReadOnly() { |
130 | return true; |
131 | } |
132 | |
133 | @Override |
134 | public long size() { |
135 | try { |
136 | ZipFile file = openZipFile(); |
137 | try { |
138 | ZipEntry entry = file.getEntry(getEntryName()); |
139 | return entry == null ? 0 : entry.getSize(); |
140 | } finally { |
141 | file.close(); |
142 | } |
143 | } catch (IOException e) { |
144 | return 0; |
145 | } |
146 | } |
147 | |
148 | @Override |
149 | public ArrayList<FilePath> newDirectoryStream() { |
150 | String path = name; |
151 | ArrayList<FilePath> list = New.arrayList(); |
152 | try { |
153 | if (path.indexOf('!') < 0) { |
154 | path += "!"; |
155 | } |
156 | if (!path.endsWith("/")) { |
157 | path += "/"; |
158 | } |
159 | ZipFile file = openZipFile(); |
160 | try { |
161 | String dirName = getEntryName(); |
162 | String prefix = path.substring(0, path.length() - dirName.length()); |
163 | Enumeration<? extends ZipEntry> en = file.entries(); |
164 | while (en.hasMoreElements()) { |
165 | ZipEntry entry = en.nextElement(); |
166 | String name = entry.getName(); |
167 | if (!name.startsWith(dirName)) { |
168 | continue; |
169 | } |
170 | if (name.length() <= dirName.length()) { |
171 | continue; |
172 | } |
173 | int idx = name.indexOf('/', dirName.length()); |
174 | if (idx < 0 || idx >= name.length() - 1) { |
175 | list.add(getPath(prefix + name)); |
176 | } |
177 | } |
178 | } finally { |
179 | file.close(); |
180 | } |
181 | return list; |
182 | } catch (IOException e) { |
183 | throw DbException.convertIOException(e, "listFiles " + path); |
184 | } |
185 | } |
186 | |
187 | @Override |
188 | public InputStream newInputStream() throws IOException { |
189 | return new FileChannelInputStream(open("r"), true); |
190 | } |
191 | |
192 | @Override |
193 | public FileChannel open(String mode) throws IOException { |
194 | ZipFile file = openZipFile(); |
195 | ZipEntry entry = file.getEntry(getEntryName()); |
196 | if (entry == null) { |
197 | file.close(); |
198 | throw new FileNotFoundException(name); |
199 | } |
200 | return new FileZip(file, entry); |
201 | } |
202 | |
203 | @Override |
204 | public OutputStream newOutputStream(boolean append) throws IOException { |
205 | throw new IOException("write"); |
206 | } |
207 | |
208 | @Override |
209 | public void moveTo(FilePath newName, boolean atomicReplace) { |
210 | throw DbException.getUnsupportedException("write"); |
211 | } |
212 | |
213 | private static String translateFileName(String fileName) { |
214 | if (fileName.startsWith("zip:")) { |
215 | fileName = fileName.substring("zip:".length()); |
216 | } |
217 | int idx = fileName.indexOf('!'); |
218 | if (idx >= 0) { |
219 | fileName = fileName.substring(0, idx); |
220 | } |
221 | return FilePathDisk.expandUserHomeDirectory(fileName); |
222 | } |
223 | |
224 | @Override |
225 | public FilePath toRealPath() { |
226 | return this; |
227 | } |
228 | |
229 | private String getEntryName() { |
230 | int idx = name.indexOf('!'); |
231 | String fileName; |
232 | if (idx <= 0) { |
233 | fileName = ""; |
234 | } else { |
235 | fileName = name.substring(idx + 1); |
236 | } |
237 | fileName = fileName.replace('\\', '/'); |
238 | if (fileName.startsWith("/")) { |
239 | fileName = fileName.substring(1); |
240 | } |
241 | return fileName; |
242 | } |
243 | |
244 | private ZipFile openZipFile() throws IOException { |
245 | String fileName = translateFileName(name); |
246 | return new ZipFile(fileName); |
247 | } |
248 | |
249 | @Override |
250 | public FilePath createTempFile(String suffix, boolean deleteOnExit, |
251 | boolean inTempDir) throws IOException { |
252 | if (!inTempDir) { |
253 | throw new IOException("File system is read-only"); |
254 | } |
255 | return new FilePathDisk().getPath(name).createTempFile(suffix, |
256 | deleteOnExit, true); |
257 | } |
258 | |
259 | @Override |
260 | public String getScheme() { |
261 | return "zip"; |
262 | } |
263 | |
264 | } |
265 | |
266 | /** |
267 | * The file is read from a stream. When reading from start to end, the same |
268 | * input stream is re-used, however when reading from end to start, a new input |
269 | * stream is opened for each request. |
270 | */ |
271 | class FileZip extends FileBase { |
272 | |
273 | private static final byte[] SKIP_BUFFER = new byte[1024]; |
274 | |
275 | private final ZipFile file; |
276 | private final ZipEntry entry; |
277 | private long pos; |
278 | private InputStream in; |
279 | private long inPos; |
280 | private final long length; |
281 | private boolean skipUsingRead; |
282 | |
283 | FileZip(ZipFile file, ZipEntry entry) { |
284 | this.file = file; |
285 | this.entry = entry; |
286 | length = entry.getSize(); |
287 | } |
288 | |
289 | @Override |
290 | public long position() { |
291 | return pos; |
292 | } |
293 | |
294 | @Override |
295 | public long size() { |
296 | return length; |
297 | } |
298 | |
299 | @Override |
300 | public int read(ByteBuffer dst) throws IOException { |
301 | seek(); |
302 | int len = in.read(dst.array(), dst.arrayOffset() + dst.position(), |
303 | dst.remaining()); |
304 | if (len > 0) { |
305 | dst.position(dst.position() + len); |
306 | pos += len; |
307 | inPos += len; |
308 | } |
309 | return len; |
310 | } |
311 | |
312 | private void seek() throws IOException { |
313 | if (inPos > pos) { |
314 | if (in != null) { |
315 | in.close(); |
316 | } |
317 | in = null; |
318 | } |
319 | if (in == null) { |
320 | in = file.getInputStream(entry); |
321 | inPos = 0; |
322 | } |
323 | if (inPos < pos) { |
324 | long skip = pos - inPos; |
325 | if (!skipUsingRead) { |
326 | try { |
327 | IOUtils.skipFully(in, skip); |
328 | } catch (NullPointerException e) { |
329 | // workaround for Android |
330 | skipUsingRead = true; |
331 | } |
332 | } |
333 | if (skipUsingRead) { |
334 | while (skip > 0) { |
335 | int s = (int) Math.min(SKIP_BUFFER.length, skip); |
336 | s = in.read(SKIP_BUFFER, 0, s); |
337 | skip -= s; |
338 | } |
339 | } |
340 | inPos = pos; |
341 | } |
342 | } |
343 | |
344 | @Override |
345 | public FileChannel position(long newPos) { |
346 | this.pos = newPos; |
347 | return this; |
348 | } |
349 | |
350 | @Override |
351 | public FileChannel truncate(long newLength) throws IOException { |
352 | throw new IOException("File is read-only"); |
353 | } |
354 | |
355 | @Override |
356 | public void force(boolean metaData) throws IOException { |
357 | // nothing to do |
358 | } |
359 | |
360 | @Override |
361 | public int write(ByteBuffer src) throws IOException { |
362 | throw new IOException("File is read-only"); |
363 | } |
364 | |
365 | @Override |
366 | public synchronized FileLock tryLock(long position, long size, |
367 | boolean shared) throws IOException { |
368 | if (shared) { |
369 | // cast to FileChannel to avoid JDK 1.7 ambiguity |
370 | return new FileLock((FileChannel) null, position, size, shared) { |
371 | |
372 | @Override |
373 | public boolean isValid() { |
374 | return true; |
375 | } |
376 | |
377 | @Override |
378 | public void release() throws IOException { |
379 | // ignore |
380 | }}; |
381 | } |
382 | return null; |
383 | } |
384 | |
385 | @Override |
386 | protected void implCloseChannel() throws IOException { |
387 | if (in != null) { |
388 | in.close(); |
389 | in = null; |
390 | } |
391 | file.close(); |
392 | } |
393 | |
394 | } |