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("subtitles");
315 staticCollections.add("uploaded");
316 staticCollections.add("videoeditor");
317 staticCollections.add("videosegments");
318 staticCollections.add("waveform");
319
320
321 assetManagerPaths = AssetPathUtils.getAssetManagerPath(cc);
322
323
324 downloadUrl = DistributionPathUtils.getDownloadUrl(cc);
325 downloadPath = DistributionPathUtils.getDownloadPath(cc);
326 }
327
328
329 @Deactivate
330 public void deactivate() {
331 JmxUtil.unregisterMXBean(registeredMXBean);
332 if (workspaceCleaner != null) {
333 workspaceCleaner.shutdown();
334 }
335 }
336
337
338
339
340
341
342
343
344
345
346
347 @Override
348 public String toSafeName(String fileName) {
349 return wfr.toSafeName(fileName);
350 }
351
352 @Override
353 public File get(final URI uri) throws NotFoundException, IOException {
354 return get(uri, false);
355 }
356
357 @Override
358 public File get(final URI uri, final boolean uniqueFilename) throws NotFoundException, IOException {
359 File inWs = toWorkspaceFile(uri);
360
361 if (uniqueFilename) {
362 inWs = new File(FilenameUtils.removeExtension(inWs.getAbsolutePath()) + '-' + UUID.randomUUID() + '.'
363 + FilenameUtils.getExtension(inWs.getName()));
364 logger.debug("Created unique filename: {}", inWs);
365 }
366
367 if (pathMappable != null && StringUtils.isNotBlank(pathMappable.getPathPrefix())
368 && StringUtils.isNotBlank(pathMappable.getUrlPrefix())) {
369 if (uri.toString().startsWith(pathMappable.getUrlPrefix())) {
370 final String localPath = uri.toString().substring(pathMappable.getUrlPrefix().length());
371 final File wfrCopy = workingFileRepositoryFile(localPath);
372
373 logger.trace("Looking up {} at {}", uri.toString(), wfrCopy.getAbsolutePath());
374 if (wfrCopy.isFile()) {
375 final long workspaceFileLastModified = inWs.isFile() ? inWs.lastModified() : 0L;
376
377 if (workspaceFileLastModified < wfrCopy.lastModified()) {
378 logger.debug("Replacing {} with an updated version from the file repository", inWs.getAbsolutePath());
379 locked(inWs, copyOrLink(wfrCopy));
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 void copyOrLink(final File src, final File dst) throws IOException {
441 if (linkingEnabled) {
442 FileUtils.deleteQuietly(dst);
443 FileSupport.link(src, dst);
444 } else {
445 FileSupport.copy(src, dst);
446 }
447 }
448
449
450 private Effect<File> copyOrLink(final File src) {
451 return new Effect.X<>() {
452 @Override
453 protected void xrun(File dst) throws IOException {
454 copyOrLink(src, dst);
455 }
456 };
457 }
458
459
460
461
462
463
464
465
466 private Either<String, Option<File>> handleDownloadResponse(HttpResponse response, URI src, File dst)
467 throws IOException {
468 final String url = src.toString();
469 final int status = response.getStatusLine().getStatusCode();
470 switch (status) {
471 case HttpServletResponse.SC_NOT_FOUND:
472 return right(none(File.class));
473 case HttpServletResponse.SC_NOT_MODIFIED:
474 logger.debug("{} has not been modified.", url);
475 return right(some(dst));
476 case HttpServletResponse.SC_ACCEPTED:
477 logger.debug("{} is not ready, try again later.", url);
478 return left(response.getHeaders("token")[0].getValue());
479 case HttpServletResponse.SC_OK:
480 logger.debug("Downloading {} to {}", url, dst.getAbsolutePath());
481 return right(some(downloadTo(response, dst)));
482 default:
483 logger.warn("Received unexpected response status {} while trying to download from {}", status, url);
484 FileUtils.deleteQuietly(dst);
485 return right(none(File.class));
486 }
487 }
488
489
490 private HttpGet createGetRequest(final URI src, final File dst, final Map<String, String> params) throws IOException {
491 try {
492 URIBuilder builder = new URIBuilder(src.toString());
493 for (Map.Entry<String, String> param : params.entrySet()) {
494 builder.setParameter(param.getKey(), param.getValue());
495 }
496 final HttpGet get = new HttpGet(builder.build());
497
498 if (dst.isFile() && dst.length() > 0) {
499 get.setHeader("If-None-Match", md5(dst));
500 }
501 return get;
502 } catch (URISyntaxException e) {
503 throw new IOException(e);
504 }
505 }
506
507
508
509
510
511
512
513 private File downloadIfNecessary(final URI src, final File dst) throws IOException, NotFoundException {
514 HttpGet get = createGetRequest(src, dst, Collections.emptyMap());
515 while (true) {
516
517 try {
518 HttpResponse response = null;
519 final Either<String, Option<File>> result;
520 try {
521 response = trustedHttpClient.execute(get);
522 result = handleDownloadResponse(response, src, dst);
523 } finally {
524 if (response != null) {
525 trustedHttpClient.close(response);
526 }
527 }
528 for (Option<File> ff : result.right()) {
529 for (File f : ff) {
530 return f;
531 }
532 FileUtils.deleteQuietly(dst);
533
534 throw new NotFoundException();
535 }
536
537 for (String token : result.left()) {
538 get = createGetRequest(src, dst, Collections.singletonMap("token", token));
539 sleep(60000);
540 }
541 } catch (TrustedHttpClientException e) {
542 FileUtils.deleteQuietly(dst);
543 throw new NotFoundException(String.format("Could not copy %s to %s", src, dst.getAbsolutePath()), e);
544 }
545 }
546 }
547
548
549
550
551
552 private Function<File, File> downloadIfNecessary(final URI src) {
553 return new Function.X<File, File>() {
554 @Override
555 public File xapply(final File dst) throws Exception {
556 return downloadIfNecessary(src, dst);
557 }
558 };
559 }
560
561
562
563
564
565
566 private static File downloadTo(final HttpResponse response, final File dst) throws IOException {
567
568 dst.createNewFile();
569 try (InputStream in = response.getEntity().getContent()) {
570 try (OutputStream out = new FileOutputStream(dst)) {
571 IOUtils.copyLarge(in, out);
572 }
573 }
574 return dst;
575 }
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590 protected String md5(File file) throws IOException, IllegalArgumentException, IllegalStateException {
591 if (file == null) {
592 throw new IllegalArgumentException("File must not be null");
593 }
594 if (!file.isFile()) {
595 throw new IllegalArgumentException("File " + file.getAbsolutePath() + " can not be read");
596 }
597
598 try (InputStream in = new FileInputStream(file)) {
599 return DigestUtils.md5Hex(in);
600 }
601 }
602
603 @Override
604 public void delete(URI uri) throws NotFoundException, IOException {
605
606 String uriPath = uri.toString();
607 String[] uriElements = uriPath.split("/");
608 String collectionId = null;
609 boolean isMediaPackage = false;
610
611 logger.trace("delete {}", uriPath);
612
613 if (uriPath.startsWith(wfr.getBaseUri().toString())) {
614 if (uriPath.indexOf(WorkingFileRepository.COLLECTION_PATH_PREFIX) > 0) {
615 if (uriElements.length > 2) {
616 collectionId = uriElements[uriElements.length - 2];
617 String filename = uriElements[uriElements.length - 1];
618 wfr.deleteFromCollection(collectionId, filename);
619 }
620 } else if (uriPath.indexOf(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX) > 0) {
621 isMediaPackage = true;
622 if (uriElements.length >= 3) {
623 String mediaPackageId = uriElements[uriElements.length - 3];
624 String elementId = uriElements[uriElements.length - 2];
625 wfr.delete(mediaPackageId, elementId);
626 }
627 }
628 }
629
630
631 File f = toWorkspaceFile(uri);
632 if (f.isFile()) {
633 synchronized (lock) {
634 File mpElementDir = f.getParentFile();
635 FileUtils.forceDelete(f);
636
637
638 if (isMediaPackage || !isStaticCollection(collectionId)) {
639 FileSupport.delete(mpElementDir);
640 }
641
642
643 if (isMediaPackage) {
644 FileSupport.delete(mpElementDir.getParentFile());
645 }
646 }
647 }
648
649
650 waitForResource(uri, HttpServletResponse.SC_NOT_FOUND, "File %s does not disappear in WFR");
651 }
652
653 @Override
654 public void delete(String mediaPackageID, String mediaPackageElementID) throws NotFoundException, IOException {
655
656 final File f = workspaceFile(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX, mediaPackageID, mediaPackageElementID);
657 FileUtils.deleteQuietly(f);
658 FileSupport.delete(f.getParentFile());
659
660 wfr.delete(mediaPackageID, mediaPackageElementID);
661
662 }
663
664 @Override
665 public URI put(String mediaPackageID, String mediaPackageElementID, String fileName, InputStream in)
666 throws IOException {
667 String safeFileName = toSafeName(fileName);
668 final URI uri = wfr.getURI(mediaPackageID, mediaPackageElementID, fileName);
669 notNull(in, "in");
670
671
672 File workspaceFile = null;
673 synchronized (lock) {
674 workspaceFile = toWorkspaceFile(uri);
675 FileUtils.touch(workspaceFile);
676 }
677
678
679 if (linkingEnabled) {
680
681
682 wfr.put(mediaPackageID, mediaPackageElementID, fileName, in);
683 File workingFileRepoDirectory = workingFileRepositoryFile(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX,
684 mediaPackageID, mediaPackageElementID);
685 File workingFileRepoCopy = new File(workingFileRepoDirectory, safeFileName);
686 FileSupport.link(workingFileRepoCopy, workspaceFile, true);
687 } else {
688 try (FileOutputStream out = new FileOutputStream(workspaceFile)) {
689 try (InputStream tee = new TeeInputStream(in, out, true)) {
690 wfr.put(mediaPackageID, mediaPackageElementID, fileName, tee);
691 }
692 }
693 }
694
695 waitForResource(uri, HttpServletResponse.SC_OK, "File %s does not appear in WFR");
696 return uri;
697 }
698
699 @Override
700 public URI putInCollection(String collectionId, String fileName, InputStream in) throws IOException {
701 String safeFileName = toSafeName(fileName);
702 URI uri = wfr.getCollectionURI(collectionId, fileName);
703
704
705 InputStream tee = null;
706 File tempFile = null;
707 FileOutputStream out = null;
708 try {
709 synchronized (lock) {
710 tempFile = toWorkspaceFile(uri);
711 FileUtils.touch(tempFile);
712 out = new FileOutputStream(tempFile);
713 }
714
715
716 if (linkingEnabled) {
717 tee = in;
718 wfr.putInCollection(collectionId, fileName, tee);
719 FileUtils.forceMkdir(tempFile.getParentFile());
720 File workingFileRepoDirectory = workingFileRepositoryFile(WorkingFileRepository.COLLECTION_PATH_PREFIX,
721 collectionId);
722 File workingFileRepoCopy = new File(workingFileRepoDirectory, safeFileName);
723 FileSupport.link(workingFileRepoCopy, tempFile, true);
724 } else {
725 tee = new TeeInputStream(in, out, true);
726 wfr.putInCollection(collectionId, fileName, tee);
727 }
728 } catch (IOException e) {
729 FileUtils.deleteQuietly(tempFile);
730 throw e;
731 } finally {
732 IoSupport.closeQuietly(tee);
733 IoSupport.closeQuietly(out);
734 }
735 waitForResource(uri, HttpServletResponse.SC_OK, "File %s does not appear in WFR");
736 return uri;
737 }
738
739 @Override
740 public URI getURI(String mediaPackageID, String mediaPackageElementID) {
741 return wfr.getURI(mediaPackageID, mediaPackageElementID);
742 }
743
744 @Override
745 public URI getCollectionURI(String collectionID, String fileName) {
746 return wfr.getCollectionURI(collectionID, fileName);
747 }
748
749 @Override
750 public URI moveTo(URI collectionURI, String toMediaPackage, String toMediaPackageElement, String toFileName)
751 throws NotFoundException, IOException {
752 String path = collectionURI.toString();
753 String filename = FilenameUtils.getName(path);
754 String collection = getCollection(collectionURI);
755 logger.debug("Moving {} from {} to {}/{}", filename, collection, toMediaPackage, toMediaPackageElement);
756
757 File original = toWorkspaceFile(collectionURI);
758 if (original.isFile()) {
759 URI copyURI = wfr.getURI(toMediaPackage, toMediaPackageElement, toFileName);
760 File copy = toWorkspaceFile(copyURI);
761 FileUtils.forceMkdir(copy.getParentFile());
762 FileUtils.deleteQuietly(copy);
763 FileUtils.moveFile(original, copy);
764 if (!isStaticCollection(collection)) {
765 FileSupport.delete(original.getParentFile());
766 }
767 }
768
769 final URI wfrUri = wfr.moveTo(collection, filename, toMediaPackage, toMediaPackageElement, toFileName);
770
771 waitForResource(wfrUri, SC_OK, "File %s does not appear in WFR");
772 return wfrUri;
773 }
774
775 @Override
776 public URI[] getCollectionContents(String collectionId) throws NotFoundException {
777 return wfr.getCollectionContents(collectionId);
778 }
779
780 private void deleteFromCollection(String collectionId, String fileName, boolean removeCollection)
781 throws NotFoundException, IOException {
782
783 final File f = workspaceFile(WorkingFileRepository.COLLECTION_PATH_PREFIX, collectionId, toSafeName(fileName));
784 FileUtils.deleteQuietly(f);
785 if (removeCollection) {
786 FileSupport.delete(f.getParentFile());
787 }
788
789 try {
790 wfr.deleteFromCollection(collectionId, fileName, removeCollection);
791 } catch (IllegalArgumentException e) {
792 throw new NotFoundException(e);
793 }
794
795 waitForResource(wfr.getCollectionURI(collectionId, fileName), SC_NOT_FOUND, "File %s does not disappear in WFR");
796 }
797
798 @Override
799 public void deleteFromCollection(String collectionId, String fileName) throws NotFoundException, IOException {
800 deleteFromCollection(collectionId, fileName, false);
801 }
802
803
804
805
806
807
808
809
810
811
812 File toWorkspaceFile(URI uri) {
813
814
815 String uriString = UriBuilder.fromUri(uri).replaceQuery(null).build().toString();
816 String wfrPrefix = wfr.getBaseUri().toString();
817 String serverPath = FilenameUtils.getPath(uriString);
818 if (uriString.startsWith(wfrPrefix)) {
819 serverPath = serverPath.substring(wfrPrefix.length());
820 } else {
821 serverPath = serverPath.replaceAll(":/*", "_");
822 }
823 String wsDirectoryPath = PathSupport.concat(wsRoot, serverPath);
824 File wsDirectory = new File(wsDirectoryPath);
825 wsDirectory.mkdirs();
826
827 String safeFileName = toSafeName(FilenameUtils.getName(uriString));
828 if (StringUtils.isBlank(safeFileName)) {
829 safeFileName = UNKNOWN_FILENAME;
830 }
831 return new File(wsDirectory, safeFileName);
832 }
833
834
835 private File workspaceFile(String... path) {
836 return new File(path(cons(String.class, wsRoot, path)));
837 }
838
839
840 private File workingFileRepositoryFile(String... path) {
841 return new File(path(cons(String.class, pathMappable.getPathPrefix(), path)));
842 }
843
844
845
846
847
848
849
850
851
852
853
854
855
856 private String getCollection(URI uri) {
857 String path = uri.toString();
858 if (path.indexOf(WorkingFileRepository.COLLECTION_PATH_PREFIX) < 0) {
859 throw new IllegalArgumentException(uri + " must point to a working file repository collection");
860 }
861
862 String collection = FilenameUtils.getPath(path);
863 if (collection.endsWith("/")) {
864 collection = collection.substring(0, collection.length() - 1);
865 }
866 collection = collection.substring(collection.lastIndexOf("/"));
867 collection = collection.substring(collection.lastIndexOf("/") + 1, collection.length());
868 return collection;
869 }
870
871 private boolean isStaticCollection(String collection) {
872 return staticCollections.contains(collection);
873 }
874
875 @Override
876 public Option<Long> getTotalSpace() {
877 return some(new File(wsRoot).getTotalSpace());
878 }
879
880 @Override
881 public Option<Long> getUsableSpace() {
882 return some(new File(wsRoot).getUsableSpace());
883 }
884
885 @Override
886 public Option<Long> getUsedSpace() {
887 return some(FileUtils.sizeOfDirectory(new File(wsRoot)));
888 }
889
890 @Override
891 public URI getBaseUri() {
892 return wfr.getBaseUri();
893 }
894
895 @Reference
896 public void setRepository(WorkingFileRepository repo) {
897 this.wfr = repo;
898 if (repo instanceof PathMappable) {
899 this.pathMappable = (PathMappable) repo;
900 logger.info("Mapping workspace to working file repository using {}", pathMappable.getPathPrefix());
901 }
902 }
903
904 @Reference
905 public void setTrustedHttpClient(TrustedHttpClient trustedHttpClient) {
906 this.trustedHttpClient = trustedHttpClient;
907 }
908
909 @Reference
910 public void setSecurityService(SecurityService securityService) {
911 this.securityService = securityService;
912 }
913
914 private static final long TIMEOUT = 2L * 60L * 1000L;
915 private static final long INTERVAL = 1000L;
916
917 private void waitForResource(final URI uri, final int expectedStatus, final String errorMsg) throws IOException {
918 if (waitForResourceFlag) {
919 HttpUtil.waitForResource(trustedHttpClient, uri, expectedStatus, TIMEOUT, INTERVAL)
920 .fold(Misc.<Exception, Void> chuck(), new Effect.X<Integer>() {
921 @Override
922 public void xrun(Integer status) throws Exception {
923 if (ne(status, expectedStatus)) {
924 final String msg = format(errorMsg, uri.toString());
925 logger.warn(msg);
926 throw new IOException(msg);
927 }
928 }
929 });
930 }
931 }
932
933 @Override
934 public void cleanup(final int maxAgeInSeconds) {
935
936 if (maxAgeInSeconds < 0) {
937 logger.debug("Canceling cleanup of workspace due to maxAge ({}) <= 0", maxAgeInSeconds);
938 return;
939 }
940
941
942
943
944 if (maxAgeInSeconds < 60 * 60 * 24 * 2) {
945 logger.warn("The max age for the workspace cleaner is dangerously low. Please consider increasing the value to "
946 + "avoid deleting data in use by running workflows.");
947 }
948
949
950 RecursiveDirectoryCleaner.cleanDirectory(Paths.get(wsRoot), Duration.ofSeconds(maxAgeInSeconds));
951 }
952
953 @Override
954 public void cleanup(Id mediaPackageId) throws IOException {
955 cleanup(mediaPackageId, false);
956 }
957
958 @Override
959 public void cleanup(Id mediaPackageId, boolean filesOnly) throws IOException {
960 final File mediaPackageDir = workspaceFile(
961 WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX, mediaPackageId.toString());
962
963 if (filesOnly) {
964 logger.debug("Clean workspace media package directory {} (files only)", mediaPackageDir);
965 FileSupport.delete(mediaPackageDir, FileSupport.DELETE_FILES);
966 }
967 else {
968 logger.debug("Clean workspace media package directory {}", mediaPackageDir);
969 FileUtils.deleteDirectory(mediaPackageDir);
970 }
971 }
972
973 @Override
974 public String rootDirectory() {
975 return wsRoot;
976 }
977
978 private class DeleteOnCloseFileInputStream extends FileInputStream {
979 private File file;
980
981 DeleteOnCloseFileInputStream(File file) throws FileNotFoundException {
982 super(file);
983 this.file = file;
984 }
985
986 public void close() throws IOException {
987 try {
988 super.close();
989 } finally {
990 if (file != null) {
991 logger.debug("Cleaning up {}", file);
992 file.delete();
993 file = null;
994 }
995 }
996 }
997 }
998 }