001 package de.deepamehta.plugins.files;
002
003 import de.deepamehta.plugins.files.service.FilesService;
004
005 import de.deepamehta.core.Topic;
006 import de.deepamehta.core.model.AssociationModel;
007 import de.deepamehta.core.model.CompositeValueModel;
008 import de.deepamehta.core.model.SimpleValue;
009 import de.deepamehta.core.model.TopicModel;
010 import de.deepamehta.core.model.TopicRoleModel;
011 import de.deepamehta.core.osgi.PluginActivator;
012 import de.deepamehta.core.service.ClientState;
013 import de.deepamehta.core.service.SecurityHandler;
014 import de.deepamehta.core.util.DeepaMehtaUtils;
015 import de.deepamehta.core.util.JavaUtils;
016
017 import javax.ws.rs.Consumes;
018 import javax.ws.rs.GET;
019 import javax.ws.rs.HeaderParam;
020 import javax.ws.rs.Path;
021 import javax.ws.rs.PathParam;
022 import javax.ws.rs.POST;
023 import javax.ws.rs.Produces;
024 import javax.ws.rs.WebApplicationException;
025 import javax.ws.rs.core.Response.Status;
026
027 import javax.servlet.http.HttpServletRequest;
028 import javax.servlet.http.HttpServletResponse;
029
030 import java.awt.Desktop;
031 import java.io.File;
032 import java.net.URL;
033 import java.util.logging.Logger;
034
035
036
037 @Path("/files")
038 @Produces("application/json")
039 public class FilesPlugin extends PluginActivator implements FilesService, SecurityHandler {
040
041 // ------------------------------------------------------------------------------------------------------- Constants
042
043 private static final String FILE_REPOSITORY_PATH = System.getProperty("dm4.filerepo.path");
044 private static final String FILE_REPOSITORY_URI = "/filerepo";
045
046 // ---------------------------------------------------------------------------------------------- Instance Variables
047
048 private Logger logger = Logger.getLogger(getClass().getName());
049
050 // -------------------------------------------------------------------------------------------------- Public Methods
051
052
053
054 // ***********************************
055 // *** FilesService Implementation ***
056 // ***********************************
057
058
059
060 // === File System Representation ===
061
062 @POST
063 @Path("/file/{path:.+}") // Note: we also match slashes as they are already decoded by an apache reverse proxy
064 @Override
065 public Topic createFileTopic(@PathParam("path") String path, @HeaderParam("Cookie") ClientState clientState) {
066 String operation = "Creating file topic for repository path \"" + path + "\"";
067 try {
068 logger.info(operation);
069 // ### FIXME: drag'n'drop files from arbitrary locations (in particular different Windows drives)
070 // collides with the concept of a single-rooted file repository (as realized by the Files module).
071 // For the moment we just strip a possible drive letter to be compatible with the Files module.
072 path = JavaUtils.stripDriveLetter(path);
073 //
074 // 1) enforce security
075 File file = enforeSecurity(path);
076 //
077 // 2) check if topic already exists
078 Topic fileTopic = fetchFileTopic(file);
079 if (fileTopic != null) {
080 logger.info(operation + " ABORTED -- already exists");
081 return fileTopic;
082 }
083 // 3) create topic
084 return createFileTopic(file, clientState);
085 } catch (FileRepositoryException e) {
086 throw new WebApplicationException(new RuntimeException(operation + " failed", e), e.getStatus());
087 } catch (Exception e) {
088 throw new RuntimeException(operation + " failed", e);
089 }
090 }
091
092 @POST
093 @Path("/folder/{path:.+}") // Note: we also match slashes as they are already decoded by an apache reverse proxy
094 @Override
095 public Topic createFolderTopic(@PathParam("path") String path, @HeaderParam("Cookie") ClientState clientState) {
096 String operation = "Creating folder topic for repository path \"" + path + "\"";
097 try {
098 logger.info(operation);
099 // ### FIXME: drag'n'drop folders from arbitrary locations (in particular different Windows drives)
100 // collides with the concept of a single-rooted file repository (as realized by the Files module).
101 // For the moment we just strip a possible drive letter to be compatible with the Files module.
102 path = JavaUtils.stripDriveLetter(path);
103 //
104 // 1) enforce security
105 File file = enforeSecurity(path);
106 //
107 // 2) check if topic already exists
108 Topic folderTopic = fetchFolderTopic(file);
109 if (folderTopic != null) {
110 logger.info(operation + " ABORTED -- already exists");
111 return folderTopic;
112 }
113 // 3) create topic
114 return createFolderTopic(file, clientState);
115 } catch (FileRepositoryException e) {
116 throw new WebApplicationException(new RuntimeException(operation + " failed", e), e.getStatus());
117 } catch (Exception e) {
118 throw new RuntimeException(operation + " failed", e);
119 }
120 }
121
122 // ---
123
124 @POST
125 @Path("/parent/{id}/file/{path:.+}") // Note: we also match slashes as they are already decoded by an apache ...
126 @Override
127 public Topic createChildFileTopic(@PathParam("id") long folderTopicId, @PathParam("path") String path,
128 @HeaderParam("Cookie") ClientState clientState) {
129 Topic childTopic = createFileTopic(path, clientState);
130 associateChildTopic(folderTopicId, childTopic.getId());
131 return childTopic;
132 }
133
134 @POST
135 @Path("/parent/{id}/folder/{path:.+}") // Note: we also match slashes as they are already decoded by an apache ...
136 @Override
137 public Topic createChildFolderTopic(@PathParam("id") long folderTopicId, @PathParam("path") String path,
138 @HeaderParam("Cookie") ClientState clientState) {
139 Topic childTopic = createFolderTopic(path, clientState);
140 associateChildTopic(folderTopicId, childTopic.getId());
141 return childTopic;
142 }
143
144
145
146 // === File Repository ===
147
148 @POST
149 @Path("/{path:.+}") // Note: we also match slashes as they are already decoded by an apache reverse proxy
150 @Consumes("multipart/form-data")
151 @Override
152 public StoredFile storeFile(UploadedFile file, @PathParam("path") String path,
153 @HeaderParam("Cookie") ClientState clientState) {
154 String operation = "Storing " + file + " at repository path \"" + path + "\"";
155 try {
156 logger.info(operation);
157 // 1) enforce security
158 File directory = enforeSecurity(path);
159 //
160 // 2) store file
161 File repoFile = repoFile(directory, file);
162 file.write(repoFile);
163 //
164 // 3) create topic
165 Topic fileTopic = createFileTopic(repoFile, clientState);
166 return new StoredFile(repoFile.getName(), fileTopic.getId());
167 } catch (FileRepositoryException e) {
168 throw new WebApplicationException(new RuntimeException(operation + " failed", e), e.getStatus());
169 } catch (Exception e) {
170 throw new RuntimeException(operation + " failed", e);
171 }
172 }
173
174 @POST
175 @Path("/{path:.+}/folder/{folder_name}") // Note: we also match slashes as they are already decoded by an apache ...
176 @Override
177 public void createFolder(@PathParam("folder_name") String folderName, @PathParam("path") String path) {
178 String operation = "Creating folder \"" + folderName + "\" at repository path \"" + path + "\"";
179 try {
180 logger.info(operation);
181 // 1) enforce security
182 File directory = enforeSecurity(path);
183 //
184 // 2) create directory
185 File repoFile = repoFile(directory, folderName);
186 if (repoFile.exists()) {
187 throw new RuntimeException("File or directory \"" + repoFile + "\" already exists");
188 }
189 //
190 boolean success = repoFile.mkdir();
191 //
192 if (!success) {
193 throw new RuntimeException("File.mkdir() failed (file=\"" + repoFile + "\")");
194 }
195 } catch (FileRepositoryException e) {
196 throw new WebApplicationException(new RuntimeException(operation + " failed", e), e.getStatus());
197 } catch (Exception e) {
198 throw new RuntimeException(operation + " failed", e);
199 }
200 }
201
202 // ---
203
204 @GET
205 @Path("/{path:.+}/info") // Note: we also match slashes as they are already decoded by an apache reverse proxy
206 @Override
207 public ResourceInfo getResourceInfo(@PathParam("path") String path) {
208 String operation = "Getting resource info for repository path \"" + path + "\"";
209 try {
210 logger.info(operation);
211 //
212 File file = enforeSecurity(path);
213 //
214 return new ResourceInfo(file);
215 } catch (FileRepositoryException e) {
216 throw new WebApplicationException(new RuntimeException(operation + " failed", e), e.getStatus());
217 } catch (Exception e) {
218 throw new RuntimeException(operation + " failed", e);
219 }
220 }
221
222 @GET
223 @Path("/{path:.+}") // Note: we also match slashes as they are already decoded by an apache reverse proxy
224 @Override
225 public DirectoryListing getDirectoryListing(@PathParam("path") String path) {
226 String operation = "Getting directory listing for repository path \"" + path + "\"";
227 try {
228 logger.info(operation);
229 //
230 File folder = enforeSecurity(path);
231 //
232 return new DirectoryListing(folder); // ### TODO: if folder is no directory send NOT FOUND
233 } catch (FileRepositoryException e) {
234 throw new WebApplicationException(new RuntimeException(operation + " failed", e), e.getStatus());
235 } catch (Exception e) {
236 throw new RuntimeException(operation + " failed", e);
237 }
238 }
239
240 @Override
241 public String getRepositoryPath(URL url) {
242 String operation = "Checking for file repository URL (\"" + url + "\")";
243 try {
244 if (!DeepaMehtaUtils.isDeepaMehtaURL(url)) {
245 logger.info(operation + " => null");
246 return null;
247 }
248 //
249 String path = url.getPath();
250 if (!path.startsWith(FILE_REPOSITORY_URI)) {
251 logger.info(operation + " => null");
252 return null;
253 }
254 //
255 String repoPath = path.substring(FILE_REPOSITORY_URI.length());
256 logger.info(operation + " => \"" + repoPath + "\"");
257 return repoPath;
258 } catch (Exception e) {
259 throw new RuntimeException(operation + " failed", e);
260 }
261 }
262
263 // ---
264
265 // Note: this is not a resource method.
266 // To access a file remotely use the /filerepo resource.
267 @Override
268 public File getFile(String path) {
269 String operation = "Accessing the file at \"" + path + "\"";
270 try {
271 logger.info(operation);
272 //
273 File file = enforeSecurity(path);
274 return file;
275 } catch (Exception e) {
276 throw new RuntimeException(operation + " failed", e);
277 }
278 }
279
280 // Note: this is not a resource method.
281 // To access a file remotely use the /filerepo resource.
282 @Override
283 public File getFile(long fileTopicId) {
284 String operation = "Accessing the file of file topic " + fileTopicId;
285 try {
286 logger.info(operation);
287 //
288 String path = repoPath(fileTopicId);
289 File file = enforeSecurity(path);
290 return file;
291 } catch (Exception e) {
292 throw new RuntimeException(operation + " failed", e);
293 }
294 }
295
296 // ---
297
298 @POST
299 @Path("/open/{id}")
300 @Override
301 public void openFile(@PathParam("id") long fileTopicId) {
302 String operation = "Opening the file of file topic " + fileTopicId;
303 try {
304 logger.info(operation);
305 //
306 String path = repoPath(fileTopicId);
307 File file = enforeSecurity(path);
308 //
309 logger.info("### Opening file \"" + file + "\"");
310 Desktop.getDesktop().open(file);
311 } catch (FileRepositoryException e) {
312 throw new WebApplicationException(new RuntimeException(operation + " failed", e), e.getStatus());
313 } catch (Exception e) {
314 throw new RuntimeException(operation + " failed", e);
315 }
316 }
317
318
319
320 // **************************************
321 // *** SecurityHandler Implementation ***
322 // **************************************
323
324
325
326 // ### TODO: to be dropped?
327 @Override
328 public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response) {
329 try {
330 String path = request.getRequestURI().substring(FILE_REPOSITORY_URI.length());
331 path = JavaUtils.decodeURIComponent(path);
332 logger.info("### repository path=\"" + path + "\"");
333 enforeSecurity(path);
334 return true;
335 } catch (FileRepositoryException e) {
336 response.setStatus(e.getStatusCode());
337 return false;
338 } catch (Exception e) {
339 response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
340 return false;
341 }
342 }
343
344
345
346 // ****************************
347 // *** Hook Implementations ***
348 // ****************************
349
350
351
352 @Override
353 public void init() {
354 publishDirectory(FILE_REPOSITORY_PATH, FILE_REPOSITORY_URI, this); // securityHandler=this
355 }
356
357 // ------------------------------------------------------------------------------------------------- Private Methods
358
359
360
361 // === File System Representation ===
362
363 private Topic fetchFileTopic(File file) {
364 String path = repoPath(file);
365 Topic topic = dms.getTopic("dm4.files.path", new SimpleValue(path), false); // fetchComposite=false
366 if (topic != null) {
367 return topic.getRelatedTopic("dm4.core.composition", "dm4.core.child", "dm4.core.parent",
368 "dm4.files.file", true, false);
369 }
370 return null;
371 }
372
373 private Topic fetchFolderTopic(File file) {
374 String path = repoPath(file);
375 Topic topic = dms.getTopic("dm4.files.path", new SimpleValue(path), false); // fetchComposite=false
376 if (topic != null) {
377 return topic.getRelatedTopic("dm4.core.composition", "dm4.core.child", "dm4.core.parent",
378 "dm4.files.folder", true, false);
379 }
380 return null;
381 }
382
383 // ---
384
385 private Topic createFileTopic(File file, ClientState clientState) {
386 String mediaType = JavaUtils.getFileType(file.getName());
387 //
388 CompositeValueModel comp = new CompositeValueModel();
389 comp.put("dm4.files.file_name", file.getName());
390 comp.put("dm4.files.path", repoPath(file));
391 if (mediaType != null) {
392 comp.put("dm4.files.media_type", mediaType);
393 }
394 comp.put("dm4.files.size", file.length());
395 //
396 return dms.createTopic(new TopicModel("dm4.files.file", comp), clientState);
397 }
398
399 private Topic createFolderTopic(File file, ClientState clientState) {
400 String folderName = file.getName();
401 String path = repoPath(file);
402 //
403 // root folder needs special treatment
404 if (path.equals("/")) {
405 folderName = "";
406 }
407 //
408 CompositeValueModel comp = new CompositeValueModel();
409 comp.put("dm4.files.folder_name", folderName);
410 comp.put("dm4.files.path", path);
411 //
412 return dms.createTopic(new TopicModel("dm4.files.folder", comp), clientState);
413 }
414
415 // ---
416
417 private void associateChildTopic(long folderTopicId, long childTopicId) {
418 if (!childAssociationExists(folderTopicId, childTopicId)) {
419 dms.createAssociation(new AssociationModel("dm4.core.aggregation",
420 new TopicRoleModel(folderTopicId, "dm4.core.parent"),
421 new TopicRoleModel(childTopicId, "dm4.core.child")), null); // clientState=null
422 }
423 }
424
425 private boolean childAssociationExists(long folderTopicId, long childTopicId) {
426 return dms.getAssociations(folderTopicId, childTopicId, "dm4.core.aggregation").size() > 0;
427 }
428
429
430
431 // === File Repository ===
432
433 /**
434 * Maps a repository path to a repository file.
435 */
436 private File repoFile(String path) {
437 try {
438 File file = new File(FILE_REPOSITORY_PATH, path);
439 // Note 1: we use getCanonicalPath() to fight directory traversal attacks (../../).
440 // Note 2: A directory path returned by getCanonicalPath() never contains a "/" at the end.
441 // Thats why "dm4.filerepo.path" is expected to have no "/" at the end as well.
442 return file.getCanonicalFile(); // throws IOException
443 } catch (Exception e) {
444 throw new RuntimeException("Mapping repository path \"" + path + "\" to repository file failed", e);
445 }
446 }
447
448 /**
449 * Calculates the storage location for the uploaded file.
450 */
451 private File repoFile(File directory, UploadedFile file) {
452 return JavaUtils.findUnusedFile(repoFile(directory, file.getName()));
453 }
454
455 private File repoFile(File directory, String fileName) {
456 return new File(directory, fileName);
457 }
458
459 // ---
460
461 /**
462 * Maps a repository file to a repository path.
463 */
464 private String repoPath(File file) {
465 String path = file.getPath().substring(FILE_REPOSITORY_PATH.length());
466 // root folder needs special treatment
467 if (path.equals("")) {
468 path = "/";
469 }
470 //
471 return path;
472 // ### TODO: there is a principle copy in DirectoryListing
473 // ### FIXME: Windows drive letter? See DirectoryListing
474 }
475
476 private String repoPath(long fileTopicId) {
477 Topic fileTopic = dms.getTopic(fileTopicId, true); // fetchComposite=true
478 return fileTopic.getCompositeValue().getString("dm4.files.path");
479 }
480
481
482
483 // === Security ===
484
485 private File enforeSecurity(String path) throws FileRepositoryException {
486 File file = repoFile(path);
487 checkFilePath(file);
488 checkFileExistence(file);
489 //
490 return file;
491 }
492
493 // --- File Access ---
494
495 /**
496 * Prerequisite: the file's path is canonical.
497 */
498 private void checkFilePath(File file) throws FileRepositoryException {
499 boolean pointsToRepository = file.getPath().startsWith(FILE_REPOSITORY_PATH);
500 //
501 logger.info("Checking file repository access to \"" + file + "\"\n dm4.filerepo.path=" +
502 "\"" + FILE_REPOSITORY_PATH + "\" => " + (pointsToRepository ? "ALLOWED" : "FORBIDDEN"));
503 //
504 if (!pointsToRepository) {
505 throw new FileRepositoryException("\"" + file + "\" is not a file repository path", Status.FORBIDDEN);
506 }
507 }
508
509 private void checkFileExistence(File file) throws FileRepositoryException {
510 if (!file.exists()) {
511 logger.info("File or directory \"" + file + "\" does not exist => NOT FOUND");
512 throw new FileRepositoryException("\"" + file + "\" does not exist in file repository", Status.NOT_FOUND);
513 }
514 }
515 }