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