1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.opencastproject.workspace.impl;
23
24 import static java.lang.String.format;
25 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
26 import static javax.servlet.http.HttpServletResponse.SC_OK;
27 import static org.opencastproject.util.EqualsUtil.ne;
28 import static org.opencastproject.util.IoSupport.locked;
29 import static org.opencastproject.util.PathSupport.path;
30 import static org.opencastproject.util.RequireUtil.notNull;
31 import static org.opencastproject.util.data.Arrays.cons;
32 import static org.opencastproject.util.data.Either.left;
33 import static org.opencastproject.util.data.Either.right;
34 import static org.opencastproject.util.data.Option.none;
35 import static org.opencastproject.util.data.Option.some;
36 import static org.opencastproject.util.data.Prelude.sleep;
37
38 import org.opencastproject.assetmanager.util.AssetPathUtils;
39 import org.opencastproject.assetmanager.util.DistributionPathUtils;
40 import org.opencastproject.cleanup.RecursiveDirectoryCleaner;
41 import org.opencastproject.mediapackage.identifier.Id;
42 import org.opencastproject.security.api.SecurityService;
43 import org.opencastproject.security.api.TrustedHttpClient;
44 import org.opencastproject.security.api.TrustedHttpClientException;
45 import org.opencastproject.util.FileSupport;
46 import org.opencastproject.util.HttpUtil;
47 import org.opencastproject.util.IoSupport;
48 import org.opencastproject.util.NotFoundException;
49 import org.opencastproject.util.PathSupport;
50 import org.opencastproject.util.data.Effect;
51 import org.opencastproject.util.data.Either;
52 import org.opencastproject.util.data.Function;
53 import org.opencastproject.util.data.Option;
54 import org.opencastproject.util.data.functions.Misc;
55 import org.opencastproject.util.jmx.JmxUtil;
56 import org.opencastproject.workingfilerepository.api.PathMappable;
57 import org.opencastproject.workingfilerepository.api.WorkingFileRepository;
58 import org.opencastproject.workspace.api.Workspace;
59 import org.opencastproject.workspace.impl.jmx.WorkspaceBean;
60
61 import org.apache.commons.codec.digest.DigestUtils;
62 import org.apache.commons.io.FileUtils;
63 import org.apache.commons.io.FilenameUtils;
64 import org.apache.commons.io.IOUtils;
65 import org.apache.commons.io.input.TeeInputStream;
66 import org.apache.commons.lang3.StringUtils;
67 import org.apache.http.HttpResponse;
68 import org.apache.http.client.methods.HttpGet;
69 import org.apache.http.client.utils.URIBuilder;
70 import org.osgi.service.component.ComponentContext;
71 import org.osgi.service.component.annotations.Activate;
72 import org.osgi.service.component.annotations.Component;
73 import org.osgi.service.component.annotations.Deactivate;
74 import org.osgi.service.component.annotations.Reference;
75 import org.slf4j.Logger;
76 import org.slf4j.LoggerFactory;
77
78 import java.io.File;
79 import java.io.FileInputStream;
80 import java.io.FileNotFoundException;
81 import java.io.FileOutputStream;
82 import java.io.IOException;
83 import java.io.InputStream;
84 import java.io.OutputStream;
85 import java.net.URI;
86 import java.net.URISyntaxException;
87 import java.nio.file.Files;
88 import java.nio.file.Paths;
89 import java.nio.file.StandardCopyOption;
90 import java.time.Duration;
91 import java.util.Collections;
92 import java.util.List;
93 import java.util.Map;
94 import java.util.UUID;
95 import java.util.concurrent.CopyOnWriteArraySet;
96
97 import javax.management.ObjectInstance;
98 import javax.servlet.http.HttpServletResponse;
99 import javax.ws.rs.core.UriBuilder;
100
101
102
103
104
105
106
107
108
109
110
111 @Component(
112 property = {
113 "service.description=Workspace"
114 },
115 immediate = true,
116 service = { Workspace.class }
117 )
118 public final class WorkspaceImpl implements Workspace {
119
120 private static final Logger logger = LoggerFactory.getLogger(WorkspaceImpl.class);
121
122
123 public static final String WORKSPACE_DIR_KEY = "org.opencastproject.workspace.rootdir";
124
125 public static final String STORAGE_DIR_KEY = "org.opencastproject.storage.dir";
126
127 public static final String WORKSPACE_CLEANUP_PERIOD_KEY = "org.opencastproject.workspace.cleanup.period";
128
129 public static final String WORKSPACE_CLEANUP_MAX_AGE_KEY = "org.opencastproject.workspace.cleanup.max.age";
130
131
132 private static final String JMX_WORKSPACE_TYPE = "Workspace";
133
134
135 private static final String UNKNOWN_FILENAME = "unknown";
136
137
138 private WorkspaceBean workspaceBean = new WorkspaceBean(this);
139
140
141 private ObjectInstance registeredMXBean;
142
143 private final Object lock = new Object();
144
145
146 private String wsRoot = null;
147
148
149 private boolean linkingEnabled = false;
150
151 private TrustedHttpClient trustedHttpClient;
152
153 private SecurityService securityService = null;
154
155
156 private WorkingFileRepository wfr = null;
157
158
159 private PathMappable pathMappable = null;
160
161 private CopyOnWriteArraySet<String> staticCollections = new CopyOnWriteArraySet<String>();
162
163 private boolean waitForResourceFlag = false;
164
165
166 private List<String> assetManagerPaths = null;
167
168
169 private String downloadUrl = null;
170 private String downloadPath = null;
171
172
173 private WorkspaceCleaner workspaceCleaner = null;
174
175 public WorkspaceImpl() {
176 }
177
178
179
180
181
182
183
184
185
186
187
188 public WorkspaceImpl(String rootDirectory, boolean waitForResource) {
189 this.wsRoot = rootDirectory;
190 this.waitForResourceFlag = waitForResource;
191 }
192
193
194
195
196
197
198
199
200
201 private boolean ensureContextProp(ComponentContext cc, String prop) {
202 return cc != null && cc.getBundleContext().getProperty(prop) != null;
203 }
204
205
206
207
208
209
210
211 @Activate
212 public void activate(ComponentContext cc) {
213 if (this.wsRoot == null) {
214 if (ensureContextProp(cc, WORKSPACE_DIR_KEY)) {
215
216 this.wsRoot = cc.getBundleContext().getProperty(WORKSPACE_DIR_KEY);
217 logger.info("CONFIG " + WORKSPACE_DIR_KEY + ": " + this.wsRoot);
218 } else if (ensureContextProp(cc, STORAGE_DIR_KEY)) {
219
220 this.wsRoot = PathSupport.concat(cc.getBundleContext().getProperty(STORAGE_DIR_KEY), "workspace");
221 logger.warn("CONFIG " + WORKSPACE_DIR_KEY + " is missing: falling back to " + this.wsRoot);
222 } else {
223 throw new IllegalStateException("Configuration '" + WORKSPACE_DIR_KEY + "' is missing");
224 }
225 }
226
227
228 File f = new File(this.wsRoot);
229 if (!f.exists()) {
230 try {
231 FileUtils.forceMkdir(f);
232 } catch (Exception e) {
233 throw new IllegalStateException("Could not create workspace directory.", e);
234 }
235 }
236
237
238 if (pathMappable != null) {
239 String wfrRoot = pathMappable.getPathPrefix();
240 File srcFile = new File(wfrRoot, ".linktest");
241 try {
242 FileUtils.touch(srcFile);
243 } catch (IOException e) {
244 throw new IllegalStateException("The working file repository seems read-only", e);
245 }
246
247
248 File targetFile;
249 try {
250 targetFile = File.createTempFile(".linktest.", ".tmp", new File(wsRoot));
251 targetFile.delete();
252 } catch (IOException e) {
253 throw new IllegalStateException("The workspace seems read-only", e);
254 }
255
256
257 linkingEnabled = FileSupport.supportsLinking(srcFile, targetFile);
258
259
260 FileUtils.deleteQuietly(targetFile);
261
262 if (linkingEnabled) {
263 logger.info("Hard links between the working file repository and the workspace enabled");
264 } else {
265 logger.warn("Hard links between the working file repository and the workspace are not possible");
266 logger.warn("This will increase the overall amount of disk space used");
267 }
268 }
269
270
271 int garbageCollectionPeriodInSeconds = -1;
272 if (ensureContextProp(cc, WORKSPACE_CLEANUP_PERIOD_KEY)) {
273 String period = cc.getBundleContext().getProperty(WORKSPACE_CLEANUP_PERIOD_KEY);
274 try {
275 garbageCollectionPeriodInSeconds = Integer.parseInt(period);
276 } catch (NumberFormatException e) {
277 logger.warn("Invalid configuration for workspace garbage collection period ({}={})",
278 WORKSPACE_CLEANUP_PERIOD_KEY, period);
279 garbageCollectionPeriodInSeconds = -1;
280 }
281 }
282
283
284 int maxAgeInSeconds = -1;
285 if (ensureContextProp(cc, WORKSPACE_CLEANUP_MAX_AGE_KEY)) {
286 String age = cc.getBundleContext().getProperty(WORKSPACE_CLEANUP_MAX_AGE_KEY);
287 try {
288 maxAgeInSeconds = Integer.parseInt(age);
289 } catch (NumberFormatException e) {
290 logger.warn("Invalid configuration for workspace garbage collection max age ({}={})",
291 WORKSPACE_CLEANUP_MAX_AGE_KEY, age);
292 maxAgeInSeconds = -1;
293 }
294 }
295
296 registeredMXBean = JmxUtil.registerMXBean(workspaceBean, JMX_WORKSPACE_TYPE);
297
298
299 if (garbageCollectionPeriodInSeconds > 0) {
300 workspaceCleaner = new WorkspaceCleaner(this, garbageCollectionPeriodInSeconds, maxAgeInSeconds);
301 workspaceCleaner.schedule();
302 }
303
304
305
306 staticCollections.add("archive");
307 staticCollections.add("captions");
308 staticCollections.add("composer");
309 staticCollections.add("composite");
310 staticCollections.add("coverimage");
311 staticCollections.add("executor");
312 staticCollections.add("inbox");
313 staticCollections.add("ocrtext");
314 staticCollections.add("sox");
315 staticCollections.add("subtitles");
316 staticCollections.add("uploaded");
317 staticCollections.add("videoeditor");
318 staticCollections.add("videosegments");
319 staticCollections.add("waveform");
320
321
322 assetManagerPaths = AssetPathUtils.getAssetManagerPath(cc);
323
324
325 downloadUrl = DistributionPathUtils.getDownloadUrl(cc);
326 downloadPath = DistributionPathUtils.getDownloadPath(cc);
327 }
328
329
330 @Deactivate
331 public void deactivate() {
332 JmxUtil.unregisterMXBean(registeredMXBean);
333 if (workspaceCleaner != null) {
334 workspaceCleaner.shutdown();
335 }
336 }
337
338
339
340
341
342
343
344
345
346
347
348 @Override
349 public String toSafeName(String fileName) {
350 return wfr.toSafeName(fileName);
351 }
352
353 @Override
354 public File get(final URI uri) throws NotFoundException, IOException {
355 return get(uri, false);
356 }
357
358 @Override
359 public File get(final URI uri, final boolean uniqueFilename) throws NotFoundException, IOException {
360 File inWs = toWorkspaceFile(uri);
361
362 if (uniqueFilename) {
363 inWs = new File(FilenameUtils.removeExtension(inWs.getAbsolutePath()) + '-' + UUID.randomUUID() + '.'
364 + FilenameUtils.getExtension(inWs.getName()));
365 logger.debug("Created unique filename: {}", inWs);
366 }
367
368 if (pathMappable != null && StringUtils.isNotBlank(pathMappable.getPathPrefix())
369 && StringUtils.isNotBlank(pathMappable.getUrlPrefix())) {
370 if (uri.toString().startsWith(pathMappable.getUrlPrefix())) {
371 final String localPath = uri.toString().substring(pathMappable.getUrlPrefix().length());
372 final File wfrCopy = workingFileRepositoryFile(localPath);
373
374 logger.trace("Looking up {} at {}", uri.toString(), wfrCopy.getAbsolutePath());
375 if (wfrCopy.isFile()) {
376 final long workspaceFileLastModified = inWs.isFile() ? inWs.lastModified() : 0L;
377
378 if (workspaceFileLastModified < wfrCopy.lastModified()) {
379 logger.debug("Replacing {} with an updated version from the file repository", inWs.getAbsolutePath());
380 locked(inWs, copyOrLink(wfrCopy));
381 } else {
382 logger.debug("{} is up to date", inWs);
383 }
384 logger.debug("Getting {} directly from working file repository root at {}", uri, inWs);
385 return new File(inWs.getAbsolutePath());
386 } else {
387 logger.warn("The working file repository and workspace paths don't match. Looking up {} at {} failed",
388 uri.toString(), wfrCopy.getAbsolutePath());
389 }
390 }
391 }
392
393
394 final File asset = AssetPathUtils.getLocalFile(assetManagerPaths, securityService.getOrganization().getId(), uri);
395 if (asset != null) {
396 logger.debug("Copy local file {} from asset manager to workspace", asset);
397 Files.copy(asset.toPath(), inWs.toPath(), StandardCopyOption.REPLACE_EXISTING);
398 return new File(inWs.getAbsolutePath());
399 }
400
401
402 return locked(inWs, downloadIfNecessary(uri));
403 }
404
405 @Override
406 public InputStream read(final URI uri) throws NotFoundException, IOException {
407
408
409 if (pathMappable != null) {
410 if (uri.toString().startsWith(pathMappable.getUrlPrefix())) {
411 final String localPath = uri.toString().substring(pathMappable.getUrlPrefix().length());
412 final File wfrCopy = workingFileRepositoryFile(localPath);
413
414 logger.trace("Looking up {} at {} for read", uri, wfrCopy);
415 if (wfrCopy.isFile()) {
416 logger.debug("Getting {} directly from working file repository root at {} for read", uri, wfrCopy);
417 return new FileInputStream(wfrCopy);
418 }
419 logger.warn("The working file repository URI and paths don't match. Looking up {} at {} failed", uri, wfrCopy);
420 }
421 }
422
423
424 final File asset = AssetPathUtils.getLocalFile(assetManagerPaths, securityService.getOrganization().getId(), uri);
425 if (asset != null) {
426 return new FileInputStream(asset);
427 }
428
429
430 final File publishedFile = DistributionPathUtils.getLocalFile(
431 downloadPath, downloadUrl, securityService.getOrganization().getId(), uri);
432 if (publishedFile != null) {
433 return new FileInputStream(publishedFile);
434 }
435
436
437 return new DeleteOnCloseFileInputStream(get(uri, true));
438 }
439
440
441 private void copyOrLink(final File src, final File dst) throws IOException {
442 if (linkingEnabled) {
443 FileUtils.deleteQuietly(dst);
444 FileSupport.link(src, dst);
445 } else {
446 FileSupport.copy(src, dst);
447 }
448 }
449
450
451 private Effect<File> copyOrLink(final File src) {
452 return new Effect.X<>() {
453 @Override
454 protected void xrun(File dst) throws IOException {
455 copyOrLink(src, dst);
456 }
457 };
458 }
459
460
461
462
463
464
465
466
467 private Either<String, Option<File>> handleDownloadResponse(HttpResponse response, URI src, File dst)
468 throws IOException {
469 final String url = src.toString();
470 final int status = response.getStatusLine().getStatusCode();
471 switch (status) {
472 case HttpServletResponse.SC_NOT_FOUND:
473 return right(none(File.class));
474 case HttpServletResponse.SC_NOT_MODIFIED:
475 logger.debug("{} has not been modified.", url);
476 return right(some(dst));
477 case HttpServletResponse.SC_ACCEPTED:
478 logger.debug("{} is not ready, try again later.", url);
479 return left(response.getHeaders("token")[0].getValue());
480 case HttpServletResponse.SC_OK:
481 logger.debug("Downloading {} to {}", url, dst.getAbsolutePath());
482 return right(some(downloadTo(response, dst)));
483 default:
484 logger.warn("Received unexpected response status {} while trying to download from {}", status, url);
485 FileUtils.deleteQuietly(dst);
486 return right(none(File.class));
487 }
488 }
489
490
491 private HttpGet createGetRequest(final URI src, final File dst, final Map<String, String> params) throws IOException {
492 try {
493 URIBuilder builder = new URIBuilder(src.toString());
494 for (Map.Entry<String, String> param : params.entrySet()) {
495 builder.setParameter(param.getKey(), param.getValue());
496 }
497 final HttpGet get = new HttpGet(builder.build());
498
499 if (dst.isFile() && dst.length() > 0) {
500 get.setHeader("If-None-Match", md5(dst));
501 }
502 return get;
503 } catch (URISyntaxException e) {
504 throw new IOException(e);
505 }
506 }
507
508
509
510
511
512
513
514 private File downloadIfNecessary(final URI src, final File dst) throws IOException, NotFoundException {
515 HttpGet get = createGetRequest(src, dst, Collections.emptyMap());
516 while (true) {
517
518 try {
519 HttpResponse response = null;
520 final Either<String, Option<File>> result;
521 try {
522 response = trustedHttpClient.execute(get);
523 result = handleDownloadResponse(response, src, dst);
524 } finally {
525 if (response != null) {
526 trustedHttpClient.close(response);
527 }
528 }
529 for (Option<File> ff : result.right()) {
530 for (File f : ff) {
531 return f;
532 }
533 FileUtils.deleteQuietly(dst);
534
535 throw new NotFoundException();
536 }
537
538 for (String token : result.left()) {
539 get = createGetRequest(src, dst, Collections.singletonMap("token", token));
540 sleep(60000);
541 }
542 } catch (TrustedHttpClientException e) {
543 FileUtils.deleteQuietly(dst);
544 throw new NotFoundException(String.format("Could not copy %s to %s", src, dst.getAbsolutePath()), e);
545 }
546 }
547 }
548
549
550
551
552
553 private Function<File, File> downloadIfNecessary(final URI src) {
554 return new Function.X<File, File>() {
555 @Override
556 public File xapply(final File dst) throws Exception {
557 return downloadIfNecessary(src, dst);
558 }
559 };
560 }
561
562
563
564
565
566
567 private static File downloadTo(final HttpResponse response, final File dst) throws IOException {
568
569 dst.createNewFile();
570 try (InputStream in = response.getEntity().getContent()) {
571 try (OutputStream out = new FileOutputStream(dst)) {
572 IOUtils.copyLarge(in, out);
573 }
574 }
575 return dst;
576 }
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591 protected String md5(File file) throws IOException, IllegalArgumentException, IllegalStateException {
592 if (file == null) {
593 throw new IllegalArgumentException("File must not be null");
594 }
595 if (!file.isFile()) {
596 throw new IllegalArgumentException("File " + file.getAbsolutePath() + " can not be read");
597 }
598
599 try (InputStream in = new FileInputStream(file)) {
600 return DigestUtils.md5Hex(in);
601 }
602 }
603
604 @Override
605 public void delete(URI uri) throws NotFoundException, IOException {
606
607 String uriPath = uri.toString();
608 String[] uriElements = uriPath.split("/");
609 String collectionId = null;
610 boolean isMediaPackage = false;
611
612 logger.trace("delete {}", uriPath);
613
614 if (uriPath.startsWith(wfr.getBaseUri().toString())) {
615 if (uriPath.indexOf(WorkingFileRepository.COLLECTION_PATH_PREFIX) > 0) {
616 if (uriElements.length > 2) {
617 collectionId = uriElements[uriElements.length - 2];
618 String filename = uriElements[uriElements.length - 1];
619 wfr.deleteFromCollection(collectionId, filename);
620 }
621 } else if (uriPath.indexOf(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX) > 0) {
622 isMediaPackage = true;
623 if (uriElements.length >= 3) {
624 String mediaPackageId = uriElements[uriElements.length - 3];
625 String elementId = uriElements[uriElements.length - 2];
626 wfr.delete(mediaPackageId, elementId);
627 }
628 }
629 }
630
631
632 File f = toWorkspaceFile(uri);
633 if (f.isFile()) {
634 synchronized (lock) {
635 File mpElementDir = f.getParentFile();
636 FileUtils.forceDelete(f);
637
638
639 if (isMediaPackage || !isStaticCollection(collectionId)) {
640 FileSupport.delete(mpElementDir);
641 }
642
643
644 if (isMediaPackage) {
645 FileSupport.delete(mpElementDir.getParentFile());
646 }
647 }
648 }
649
650
651 waitForResource(uri, HttpServletResponse.SC_NOT_FOUND, "File %s does not disappear in WFR");
652 }
653
654 @Override
655 public void delete(String mediaPackageID, String mediaPackageElementID) throws NotFoundException, IOException {
656
657 final File f = workspaceFile(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX, mediaPackageID, mediaPackageElementID);
658 FileUtils.deleteQuietly(f);
659 FileSupport.delete(f.getParentFile());
660
661 wfr.delete(mediaPackageID, mediaPackageElementID);
662
663 }
664
665 @Override
666 public URI put(String mediaPackageID, String mediaPackageElementID, String fileName, InputStream in)
667 throws IOException {
668 String safeFileName = toSafeName(fileName);
669 final URI uri = wfr.getURI(mediaPackageID, mediaPackageElementID, fileName);
670 notNull(in, "in");
671
672
673 File workspaceFile = null;
674 synchronized (lock) {
675 workspaceFile = toWorkspaceFile(uri);
676 FileUtils.touch(workspaceFile);
677 }
678
679
680 if (linkingEnabled) {
681
682
683 wfr.put(mediaPackageID, mediaPackageElementID, fileName, in);
684 File workingFileRepoDirectory = workingFileRepositoryFile(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX,
685 mediaPackageID, mediaPackageElementID);
686 File workingFileRepoCopy = new File(workingFileRepoDirectory, safeFileName);
687 FileSupport.link(workingFileRepoCopy, workspaceFile, true);
688 } else {
689 try (FileOutputStream out = new FileOutputStream(workspaceFile)) {
690 try (InputStream tee = new TeeInputStream(in, out, true)) {
691 wfr.put(mediaPackageID, mediaPackageElementID, fileName, tee);
692 }
693 }
694 }
695
696 waitForResource(uri, HttpServletResponse.SC_OK, "File %s does not appear in WFR");
697 return uri;
698 }
699
700 @Override
701 public URI putInCollection(String collectionId, String fileName, InputStream in) throws IOException {
702 String safeFileName = toSafeName(fileName);
703 URI uri = wfr.getCollectionURI(collectionId, fileName);
704
705
706 InputStream tee = null;
707 File tempFile = null;
708 FileOutputStream out = null;
709 try {
710 synchronized (lock) {
711 tempFile = toWorkspaceFile(uri);
712 FileUtils.touch(tempFile);
713 out = new FileOutputStream(tempFile);
714 }
715
716
717 if (linkingEnabled) {
718 tee = in;
719 wfr.putInCollection(collectionId, fileName, tee);
720 FileUtils.forceMkdir(tempFile.getParentFile());
721 File workingFileRepoDirectory = workingFileRepositoryFile(WorkingFileRepository.COLLECTION_PATH_PREFIX,
722 collectionId);
723 File workingFileRepoCopy = new File(workingFileRepoDirectory, safeFileName);
724 FileSupport.link(workingFileRepoCopy, tempFile, true);
725 } else {
726 tee = new TeeInputStream(in, out, true);
727 wfr.putInCollection(collectionId, fileName, tee);
728 }
729 } catch (IOException e) {
730 FileUtils.deleteQuietly(tempFile);
731 throw e;
732 } finally {
733 IoSupport.closeQuietly(tee);
734 IoSupport.closeQuietly(out);
735 }
736 waitForResource(uri, HttpServletResponse.SC_OK, "File %s does not appear in WFR");
737 return uri;
738 }
739
740 @Override
741 public URI getURI(String mediaPackageID, String mediaPackageElementID) {
742 return wfr.getURI(mediaPackageID, mediaPackageElementID);
743 }
744
745 @Override
746 public URI getCollectionURI(String collectionID, String fileName) {
747 return wfr.getCollectionURI(collectionID, fileName);
748 }
749
750 @Override
751 public URI moveTo(URI collectionURI, String toMediaPackage, String toMediaPackageElement, String toFileName)
752 throws NotFoundException, IOException {
753 String path = collectionURI.toString();
754 String filename = FilenameUtils.getName(path);
755 String collection = getCollection(collectionURI);
756 logger.debug("Moving {} from {} to {}/{}", filename, collection, toMediaPackage, toMediaPackageElement);
757
758 File original = toWorkspaceFile(collectionURI);
759 if (original.isFile()) {
760 URI copyURI = wfr.getURI(toMediaPackage, toMediaPackageElement, toFileName);
761 File copy = toWorkspaceFile(copyURI);
762 FileUtils.forceMkdir(copy.getParentFile());
763 FileUtils.deleteQuietly(copy);
764 FileUtils.moveFile(original, copy);
765 if (!isStaticCollection(collection)) {
766 FileSupport.delete(original.getParentFile());
767 }
768 }
769
770 final URI wfrUri = wfr.moveTo(collection, filename, toMediaPackage, toMediaPackageElement, toFileName);
771
772 waitForResource(wfrUri, SC_OK, "File %s does not appear in WFR");
773 return wfrUri;
774 }
775
776 @Override
777 public URI[] getCollectionContents(String collectionId) throws NotFoundException {
778 return wfr.getCollectionContents(collectionId);
779 }
780
781 private void deleteFromCollection(String collectionId, String fileName, boolean removeCollection)
782 throws NotFoundException, IOException {
783
784 final File f = workspaceFile(WorkingFileRepository.COLLECTION_PATH_PREFIX, collectionId, toSafeName(fileName));
785 FileUtils.deleteQuietly(f);
786 if (removeCollection) {
787 FileSupport.delete(f.getParentFile());
788 }
789
790 try {
791 wfr.deleteFromCollection(collectionId, fileName, removeCollection);
792 } catch (IllegalArgumentException e) {
793 throw new NotFoundException(e);
794 }
795
796 waitForResource(wfr.getCollectionURI(collectionId, fileName), SC_NOT_FOUND, "File %s does not disappear in WFR");
797 }
798
799 @Override
800 public void deleteFromCollection(String collectionId, String fileName) throws NotFoundException, IOException {
801 deleteFromCollection(collectionId, fileName, false);
802 }
803
804
805
806
807
808
809
810
811
812
813 File toWorkspaceFile(URI uri) {
814
815
816 String uriString = UriBuilder.fromUri(uri).replaceQuery(null).build().toString();
817 String wfrPrefix = wfr.getBaseUri().toString();
818 String serverPath = FilenameUtils.getPath(uriString);
819 if (uriString.startsWith(wfrPrefix)) {
820 serverPath = serverPath.substring(wfrPrefix.length());
821 } else {
822 serverPath = serverPath.replaceAll(":/*", "_");
823 }
824 String wsDirectoryPath = PathSupport.concat(wsRoot, serverPath);
825 File wsDirectory = new File(wsDirectoryPath);
826 wsDirectory.mkdirs();
827
828 String safeFileName = toSafeName(FilenameUtils.getName(uriString));
829 if (StringUtils.isBlank(safeFileName)) {
830 safeFileName = UNKNOWN_FILENAME;
831 }
832 return new File(wsDirectory, safeFileName);
833 }
834
835
836 private File workspaceFile(String... path) {
837 return new File(path(cons(String.class, wsRoot, path)));
838 }
839
840
841 private File workingFileRepositoryFile(String... path) {
842 return new File(path(cons(String.class, pathMappable.getPathPrefix(), path)));
843 }
844
845
846
847
848
849
850
851
852
853
854
855
856
857 private String getCollection(URI uri) {
858 String path = uri.toString();
859 if (path.indexOf(WorkingFileRepository.COLLECTION_PATH_PREFIX) < 0) {
860 throw new IllegalArgumentException(uri + " must point to a working file repository collection");
861 }
862
863 String collection = FilenameUtils.getPath(path);
864 if (collection.endsWith("/")) {
865 collection = collection.substring(0, collection.length() - 1);
866 }
867 collection = collection.substring(collection.lastIndexOf("/"));
868 collection = collection.substring(collection.lastIndexOf("/") + 1, collection.length());
869 return collection;
870 }
871
872 private boolean isStaticCollection(String collection) {
873 return staticCollections.contains(collection);
874 }
875
876 @Override
877 public Option<Long> getTotalSpace() {
878 return some(new File(wsRoot).getTotalSpace());
879 }
880
881 @Override
882 public Option<Long> getUsableSpace() {
883 return some(new File(wsRoot).getUsableSpace());
884 }
885
886 @Override
887 public Option<Long> getUsedSpace() {
888 return some(FileUtils.sizeOfDirectory(new File(wsRoot)));
889 }
890
891 @Override
892 public URI getBaseUri() {
893 return wfr.getBaseUri();
894 }
895
896 @Reference
897 public void setRepository(WorkingFileRepository repo) {
898 this.wfr = repo;
899 if (repo instanceof PathMappable) {
900 this.pathMappable = (PathMappable) repo;
901 logger.info("Mapping workspace to working file repository using {}", pathMappable.getPathPrefix());
902 }
903 }
904
905 @Reference
906 public void setTrustedHttpClient(TrustedHttpClient trustedHttpClient) {
907 this.trustedHttpClient = trustedHttpClient;
908 }
909
910 @Reference
911 public void setSecurityService(SecurityService securityService) {
912 this.securityService = securityService;
913 }
914
915 private static final long TIMEOUT = 2L * 60L * 1000L;
916 private static final long INTERVAL = 1000L;
917
918 private void waitForResource(final URI uri, final int expectedStatus, final String errorMsg) throws IOException {
919 if (waitForResourceFlag) {
920 HttpUtil.waitForResource(trustedHttpClient, uri, expectedStatus, TIMEOUT, INTERVAL)
921 .fold(Misc.<Exception, Void> chuck(), new Effect.X<Integer>() {
922 @Override
923 public void xrun(Integer status) throws Exception {
924 if (ne(status, expectedStatus)) {
925 final String msg = format(errorMsg, uri.toString());
926 logger.warn(msg);
927 throw new IOException(msg);
928 }
929 }
930 });
931 }
932 }
933
934 @Override
935 public void cleanup(final int maxAgeInSeconds) {
936
937 if (maxAgeInSeconds < 0) {
938 logger.debug("Canceling cleanup of workspace due to maxAge ({}) <= 0", maxAgeInSeconds);
939 return;
940 }
941
942
943
944
945 if (maxAgeInSeconds < 60 * 60 * 24 * 2) {
946 logger.warn("The max age for the workspace cleaner is dangerously low. Please consider increasing the value to "
947 + "avoid deleting data in use by running workflows.");
948 }
949
950
951 RecursiveDirectoryCleaner.cleanDirectory(Paths.get(wsRoot), Duration.ofSeconds(maxAgeInSeconds));
952 }
953
954 @Override
955 public void cleanup(Id mediaPackageId) throws IOException {
956 cleanup(mediaPackageId, false);
957 }
958
959 @Override
960 public void cleanup(Id mediaPackageId, boolean filesOnly) throws IOException {
961 final File mediaPackageDir = workspaceFile(
962 WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX, mediaPackageId.toString());
963
964 if (filesOnly) {
965 logger.debug("Clean workspace media package directory {} (files only)", mediaPackageDir);
966 FileSupport.delete(mediaPackageDir, FileSupport.DELETE_FILES);
967 }
968 else {
969 logger.debug("Clean workspace media package directory {}", mediaPackageDir);
970 FileUtils.deleteDirectory(mediaPackageDir);
971 }
972 }
973
974 @Override
975 public String rootDirectory() {
976 return wsRoot;
977 }
978
979 private class DeleteOnCloseFileInputStream extends FileInputStream {
980 private File file;
981
982 DeleteOnCloseFileInputStream(File file) throws FileNotFoundException {
983 super(file);
984 this.file = file;
985 }
986
987 public void close() throws IOException {
988 try {
989 super.close();
990 } finally {
991 if (file != null) {
992 logger.debug("Cleaning up {}", file);
993 file.delete();
994 file = null;
995 }
996 }
997 }
998 }
999 }