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