Improved the store library massively and added a preliminary filesystem store.
[jsvc.git] / src / dolda / jsvc / store / FileStore.java
1 package dolda.jsvc.store;
2
3 import dolda.jsvc.*;
4 import dolda.jsvc.util.*;
5 import java.io.*;
6 import java.util.*;
7 import java.security.*;
8
9 class FileStore extends Store {
10     private final java.io.File base;
11     private static final int smbuflimit;
12     private static int txserial = 0;
13     
14     static {
15         int res; /* Java is stupid, as usual... */
16         try {
17             String p = System.getProperty("dolda.jsvc.store.smallbuf");
18             if(p != null)
19                 res = Integer.parseInt(p);
20             else
21                 res = 65536;
22         } catch(SecurityException e) {
23             res = 65536;
24         }
25         smbuflimit = res;
26     }
27
28     private FileStore(Package pkg, java.io.File root) {
29         super(pkg);
30         String nm = pkg.getName();
31         java.io.File base = root;
32         int p = 0;
33         int p2;
34         while((p2 = nm.indexOf('.', p)) >= 0) {
35             base = new java.io.File(base, nm.substring(p, p2));
36             p = p2 + 1;
37         }
38         this.base = new java.io.File(base, nm.substring(p));
39         AccessController.doPrivileged(new PrivilegedAction<Object>() {
40                 public Object run() {
41                     if(!FileStore.this.base.mkdirs())
42                         throw(new RuntimeException("Could not create store directory (Java won't tell me why)"));
43                     return(null);
44                 }
45             });
46     }
47
48     private static String mangle(String in) {
49         byte[] bytes = in.getBytes(Misc.utf8);
50         StringBuilder buf = new StringBuilder();
51         for(int i = 0; i < bytes.length; i++) {
52             byte b = bytes[i];
53             if(((b >= '0') && (b <= '9')) || ((b >= 'A') && (b <= 'Z')) || ((b >= 'a') && (b <= 'z'))) {
54                 buf.append((char)b);
55             } else {
56                 buf.append('_');
57                 buf.append(Misc.int2hex((b & 0xf0) >> 4, true));
58                 buf.append(Misc.int2hex(b & 0x0f, true));
59             }
60         }
61         return(buf.toString());
62     }
63     
64     private static String demangle(String in) {
65         ByteArrayOutputStream buf = new ByteArrayOutputStream();
66         for(int i = 0; i < in.length(); i++) {
67             char c = in.charAt(i);
68             if(c == '_') {
69                 char d1 = in.charAt(i + 1);
70                 char d2 = in.charAt(i + 2);
71                 i += 2;
72                 buf.write((byte)((Misc.hex2int(d1) << 4) | Misc.hex2int(d2)));
73             } else {
74                 if(c >= 256)
75                     throw(new RuntimeException("Invalid filename in store"));
76                 buf.write(c);
77             }
78         }
79         byte[] bytes = buf.toByteArray();
80         return(new String(bytes, Misc.utf8));
81     }
82     
83     private class RFile implements File {
84         private final java.io.File fs;
85         private final String name;
86         
87         private class TXStream extends OutputStream {
88             private FileOutputStream buf = null;
89             private java.io.File tmpfile;
90             private boolean closed = false;
91             
92             private void init() throws IOException {
93                 try {
94                     buf = AccessController.doPrivileged(new PrivilegedExceptionAction<FileOutputStream>() {
95                             public FileOutputStream run() throws IOException {
96                                 synchronized(RFile.class) {
97                                     int serial = txserial++;
98                                     tmpfile = new java.io.File(fs.getPath() + ".new." + txserial);
99                                     if(tmpfile.exists()) {
100                                         if(!tmpfile.delete())
101                                             throw(new IOException("Could not delete previous temporary file (Java won't tell my why)"));
102                                     }
103                                     return(new FileOutputStream(tmpfile));
104                                 }
105                             }
106                         });
107                 } catch(PrivilegedActionException e) {
108                     throw((IOException)e.getCause());
109                 }
110             }
111
112             public void write(byte[] b, int off, int len) throws IOException {
113                 if(closed)
114                     throw(new IOException("This file has already been committed"));
115                 if(buf == null)
116                     init();
117                 buf.write(b, off, len);
118             }
119
120             public void write(byte[] b) throws IOException {
121                 if(closed)
122                     throw(new IOException("This file has already been committed"));
123                 if(buf == null)
124                     init();
125                 buf.write(b);
126             }
127
128             public void write(int b) throws IOException {
129                 if(closed)
130                     throw(new IOException("This file has already been committed"));
131                 if(buf == null)
132                     init();
133                 buf.write(b);
134             }
135             
136             public void flush() throws IOException {
137                 if(buf == null)
138                     init();
139                 buf.flush();
140             }
141             
142             public void close() throws IOException {
143                 flush();
144                 closed = true;
145                 buf.close();
146                 try {
147                     AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
148                             public Object run() throws IOException {
149                                 if(!tmpfile.renameTo(fs)) {
150                                     fs.delete();
151                                     if(!tmpfile.renameTo(fs))
152                                         throw(new IOException("Could not replace previous file contents with new (Java won't tell me why)"));
153                                 }
154                                 return(null);
155                             }
156                         });
157                 } catch(PrivilegedActionException e) {
158                     throw((IOException)e.getCause());
159                 }
160             }
161         }
162
163         private RFile(java.io.File fs, String name) {
164             this.fs = fs;
165             this.name = name;
166         }
167         
168         public String name() {
169             return(name);
170         }
171         
172         public InputStream read() {
173             return(AccessController.doPrivileged(new PrivilegedAction<InputStream>() {
174                         public InputStream run() {
175                             try {
176                                 return(new FileInputStream(fs));
177                             } catch(FileNotFoundException e) {
178                                 return(null);
179                             }
180                         }
181                     }));
182         }
183         
184         public OutputStream store() {
185             return(new TXStream());
186         }
187         
188         public long mtime() {
189             return(AccessController.doPrivileged(new PrivilegedAction<Long>() {
190                         public Long run() {
191                             return(fs.lastModified());
192                         }
193                     }));
194         }
195         
196         public void remove() {
197             AccessController.doPrivileged(new PrivilegedAction<Object>() {
198                     public InputStream run() {
199                         if(!fs.delete())
200                             throw(new RuntimeException("Could not delete the file " + fs.getPath() + " (Java won't tell me why)"));
201                         return(null);
202                     }
203                 });
204         }
205     }
206
207     public File get(String name) {
208         return(new RFile(new java.io.File(base, mangle(name)), name));
209     }
210     
211     public Iterator<File> iterator() {
212         final java.io.File[] ls = base.listFiles();
213         return(new Iterator<File>() {
214                 private int i = 0;
215                 private File cur = null;
216                 
217                 public boolean hasNext() {
218                     return(i < ls.length);
219                 }
220                 
221                 public File next() {
222                     java.io.File f = ls[i++];
223                     cur = new RFile(f, demangle(f.getName()));
224                     return(cur);
225                 }
226                 
227                 public void remove() {
228                     if(cur == null)
229                         throw(new IllegalStateException());
230                     cur.remove();
231                     cur = null;
232                 }
233             });
234     }
235     
236     public static void register() {
237         Store.register("file", new Factory() {
238                 public Store create(String rootname, Package pkg) {
239                     java.io.File root = new java.io.File(rootname);
240                     ThreadContext ctx = ThreadContext.current();
241                     if(ctx != null) {
242                         if(ctx.server().name() != null)
243                             root = new java.io.File(root, ctx.server().name());
244                     }
245                     return(new FileStore(pkg, root));
246                 }
247             });
248     }
249 }