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