1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.akubraproject.fs;
23
24 import org.akubraproject.Blob;
25 import org.akubraproject.DuplicateBlobException;
26 import org.akubraproject.MissingBlobException;
27 import org.akubraproject.UnsupportedIdException;
28 import org.akubraproject.impl.AbstractBlob;
29 import org.akubraproject.impl.StreamManager;
30 import org.apache.commons.io.IOUtils;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 import java.io.File;
35 import java.io.FileInputStream;
36 import java.io.FileOutputStream;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.io.OutputStream;
40 import java.net.URI;
41 import java.net.URISyntaxException;
42 import java.nio.channels.FileChannel;
43 import java.util.Map;
44 import java.util.Set;
45
46
47
48
49
50
51
52
53
54
55 class FSBlob extends AbstractBlob {
56 private static final Logger log = LoggerFactory.getLogger(FSBlob.class);
57
58
59
60
61
62
63 public static final String FORCE_MOVE_AS_COPY_AND_DELETE = "org.akubraproject.force_move_as_copy_and_delete";
64
65 static final String scheme = "file";
66 private final URI canonicalId;
67 private final File file;
68 private final StreamManager manager;
69 private final Set<File> modified;
70
71
72
73
74
75
76
77
78
79
80
81 FSBlob(FSBlobStoreConnection connection, File baseDir, URI blobId, StreamManager manager,
82 Set<File> modified) throws UnsupportedIdException {
83 super(connection, blobId);
84 this.canonicalId = validateId(blobId);
85 this.file = new File(baseDir, canonicalId.getRawSchemeSpecificPart());
86 this.manager = manager;
87 this.modified = modified;
88 }
89
90 @Override
91 public URI getCanonicalId() {
92 return canonicalId;
93 }
94
95 @Override
96 public InputStream openInputStream() throws IOException {
97 ensureOpen();
98
99 if (!file.exists())
100 throw new MissingBlobException(getId());
101
102 return manager.manageInputStream(getConnection(), new FileInputStream(file));
103 }
104
105 @Override
106 public OutputStream openOutputStream(long estimatedSize, boolean overwrite) throws IOException {
107 ensureOpen();
108
109 if (!overwrite && file.exists())
110 throw new DuplicateBlobException(getId());
111
112 makeParentDirs(file);
113
114 if (modified != null)
115 modified.add(file);
116
117 return manager.manageOutputStream(getConnection(), new FileOutputStream(file));
118 }
119
120 @Override
121 public long getSize() throws IOException {
122 ensureOpen();
123
124 if (!file.exists())
125 throw new MissingBlobException(getId());
126
127 return file.length();
128 }
129
130 @Override
131 public boolean exists() throws IOException {
132 ensureOpen();
133
134 return file.exists();
135 }
136
137 @Override
138 public void delete() throws IOException {
139 ensureOpen();
140
141 if (!file.delete() && file.exists())
142 throw new IOException("Failed to delete file: " + file);
143
144 if (modified != null)
145 modified.remove(file);
146 }
147
148
149
150
151
152
153
154
155
156
157
158
159
160 @Override
161 public Blob moveTo(URI blobId, Map<String, String> hints) throws IOException {
162 boolean force_move = false;
163
164 ensureOpen();
165 FSBlob dest = (FSBlob) getConnection().getBlob(blobId, hints);
166
167 File other = dest.file;
168
169 if (other.exists())
170 throw new DuplicateBlobException(blobId);
171
172 makeParentDirs(other);
173
174 if (hints != null)
175 force_move = Boolean.parseBoolean(hints.get(FORCE_MOVE_AS_COPY_AND_DELETE));
176
177 if (force_move || !file.renameTo(other)) {
178 if (!file.exists())
179 throw new MissingBlobException(getId());
180
181 boolean success = false;
182 try {
183 nioCopy(file, other);
184
185 if (file.length() != other.length()) {
186 throw new IOException("Source and destination file sizes do not match: source '" + file
187 + "' is " + file.length()
188 + " and destination '" + other + "' is " + other.length());
189 }
190
191 if (!file.delete() && file.exists())
192 throw new IOException("Failed to delete file: " + file);
193
194 success = true;
195 } finally {
196 if (!success && other.exists() && !other.delete()) {
197 log.error("Error deleting destination file '" + other + "' after source file '" + file
198 + "' copy failure");
199 }
200 }
201 }
202
203 if (modified != null && modified.remove(file))
204 modified.add(other);
205
206 return dest;
207 }
208
209 static URI validateId(URI blobId) throws UnsupportedIdException {
210 if (blobId == null)
211 throw new NullPointerException("Id cannot be null");
212 if (!blobId.getScheme().equalsIgnoreCase(scheme))
213 throw new UnsupportedIdException(blobId, "Id must be in " + scheme + " scheme");
214 String path = blobId.getRawSchemeSpecificPart();
215 if (path.startsWith("/"))
216 throw new UnsupportedIdException(blobId, "Id must specify a relative path");
217 try {
218
219 URI tmp = new URI(scheme + ":/" + path);
220 String nPath = tmp.normalize().getRawSchemeSpecificPart().substring(1);
221 if (nPath.equals("..") || nPath.startsWith("../"))
222 throw new UnsupportedIdException(blobId, "Id cannot be outside top-level directory");
223 if (nPath.endsWith("/"))
224 throw new UnsupportedIdException(blobId, "Id cannot specify a directory");
225 return new URI(scheme + ":" + nPath);
226 } catch (URISyntaxException wontHappen) {
227 throw new Error(wontHappen);
228 }
229 }
230
231 private void makeParentDirs(File file) throws IOException {
232 File parent = file.getParentFile();
233
234 if (parent != null && !parent.exists()) {
235 parent.mkdirs();
236 if (!parent.exists())
237 throw new IOException("Unable to create parent directory: " + parent.getPath());
238 }
239 }
240
241 private static void nioCopy(File source, File dest) throws IOException {
242 FileInputStream f_in = null;
243 FileOutputStream f_out = null;
244
245 log.debug("Performing force copy-and-delete of source '" + source + "' to '"
246 + dest + "'");
247 try {
248 f_in = new FileInputStream(source);
249
250 try {
251 f_out = new FileOutputStream(dest);
252
253 FileChannel in = f_in.getChannel();
254 FileChannel out = f_out.getChannel();
255 in.transferTo(0, source.length(), out);
256 } finally {
257 IOUtils.closeQuietly(f_out);
258 }
259 } finally {
260 IOUtils.closeQuietly(f_in);
261 }
262
263 if (!dest.exists()) throw new IOException("Failed to copy file to new location: " + dest);
264 }
265 }