View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.staticfiles.impl;
23  
24  import static java.lang.String.format;
25  import static org.opencastproject.util.RequireUtil.notNull;
26  
27  import org.opencastproject.security.api.Organization;
28  import org.opencastproject.security.api.OrganizationDirectoryService;
29  import org.opencastproject.security.api.SecurityService;
30  import org.opencastproject.staticfiles.api.StaticFileService;
31  import org.opencastproject.util.NotFoundException;
32  import org.opencastproject.util.OsgiUtil;
33  import org.opencastproject.util.ProgressInputStream;
34  
35  import com.google.common.util.concurrent.AbstractScheduledService;
36  import com.google.common.util.concurrent.MoreExecutors;
37  import com.google.common.util.concurrent.Service.Listener;
38  import com.google.common.util.concurrent.Service.State;
39  
40  import org.apache.commons.io.FileUtils;
41  import org.apache.commons.lang3.StringUtils;
42  import org.osgi.service.component.ComponentContext;
43  import org.osgi.service.component.ComponentException;
44  import org.osgi.service.component.annotations.Activate;
45  import org.osgi.service.component.annotations.Component;
46  import org.osgi.service.component.annotations.Deactivate;
47  import org.osgi.service.component.annotations.Reference;
48  import org.slf4j.Logger;
49  import org.slf4j.LoggerFactory;
50  
51  import java.io.File;
52  import java.io.IOException;
53  import java.io.InputStream;
54  import java.nio.file.DirectoryStream;
55  import java.nio.file.Files;
56  import java.nio.file.Path;
57  import java.nio.file.Paths;
58  import java.util.Date;
59  import java.util.UUID;
60  import java.util.concurrent.TimeUnit;
61  
62  /**
63   * Stores and retrieves static file resources.
64   */
65  @Component(
66      immediate = true,
67      service = StaticFileService.class,
68      property = {
69          "service.description=Static File Service",
70          "service.PID=org.opencastproject.staticfiles.impl.StaticFileServiceImpl"
71      }
72  )
73  public class StaticFileServiceImpl implements StaticFileService {
74  
75    /** The logger */
76    private static final Logger logger = LoggerFactory.getLogger(StaticFileServiceImpl.class);
77  
78    /** The key to find the root directory for the static file service in the OSGi properties. */
79    public static final String STATICFILES_ROOT_DIRECTORY_KEY = "org.opencastproject.staticfiles.rootdir";
80  
81    // OSGi service references
82    private SecurityService securityService = null;
83    private OrganizationDirectoryService orgDirectory = null;
84  
85    /** The root directory for storing static files. */
86    private String rootDirPath;
87  
88    private PurgeTemporaryStorageService purgeService;
89  
90    /**
91     * OSGI callback for activating this component
92     *
93     * @param cc
94     *          the osgi component context
95     */
96    @Activate
97    public void activate(ComponentContext cc) {
98      logger.info("Upload Static Resource Service started.");
99      rootDirPath = OsgiUtil.getContextProperty(cc, STATICFILES_ROOT_DIRECTORY_KEY);
100 
101     final File rootFile = new File(rootDirPath);
102     if (!rootFile.exists()) {
103       try {
104         FileUtils.forceMkdir(rootFile);
105       } catch (IOException e) {
106         throw new ComponentException(
107                 String.format("%s does not exists and could not be created", rootFile.getAbsolutePath()));
108       }
109     }
110     if (!rootFile.canRead()) {
111       throw new ComponentException(String.format("Cannot read from %s", rootFile.getAbsolutePath()));
112     }
113 
114     purgeService = new PurgeTemporaryStorageService();
115     purgeService.addListener(new Listener() {
116       @Override
117       public void failed(State from, Throwable failure) {
118         logger.warn("Temporary storage purging service failed:", failure);
119       }
120     }, MoreExecutors.directExecutor());
121     purgeService.startAsync();
122     logger.info("Purging of temporary storage section scheduled");
123   }
124 
125   /**
126    * Callback from OSGi on service deactivation.
127    */
128   @Deactivate
129   public void deactivate() {
130     purgeService.stopAsync();
131     purgeService = null;
132   }
133 
134   /** OSGi DI */
135   @Reference
136   public void setSecurityService(SecurityService securityService) {
137     this.securityService = securityService;
138   }
139 
140   /** OSGi DI */
141   @Reference
142   public void setOrganizationDirectoryService(OrganizationDirectoryService directoryService) {
143     orgDirectory = directoryService;
144   }
145 
146   @Override
147   public String storeFile(String filename, InputStream inputStream) throws IOException {
148     notNull(filename, "filename");
149     notNull(inputStream, "inputStream");
150     final String uuid = UUID.randomUUID().toString();
151     final String org = securityService.getOrganization().getId();
152 
153     Path file = getTemporaryStorageDir(org).resolve(Paths.get(uuid, filename));
154     try (ProgressInputStream progressInputStream = new ProgressInputStream(inputStream)) {
155       Files.createDirectories(file.getParent());
156       Files.copy(progressInputStream, file);
157     } catch (IOException e) {
158       logger.error("Unable to save file '{}' to {}", filename, file, e);
159       throw e;
160     }
161 
162     return uuid;
163   }
164 
165   @Override
166   public InputStream getFile(final String uuid) throws NotFoundException, IOException {
167     if (StringUtils.isBlank(uuid)) {
168       throw new IllegalArgumentException("The uuid must not be blank");
169     }
170 
171     final String org = securityService.getOrganization().getId();
172 
173     return Files.newInputStream(getFile(org, uuid));
174   }
175 
176   @Override
177   public void persistFile(final String uuid) throws NotFoundException, IOException {
178     final String org = securityService.getOrganization().getId();
179     try (DirectoryStream<Path> folders = Files.newDirectoryStream(getTemporaryStorageDir(org),
180             getDirsEqualsUuidFilter(uuid))) {
181       for (Path folder : folders) {
182         Files.move(folder, getDurableStorageDir(org).resolve(folder.getFileName()));
183       }
184     }
185   }
186 
187   @Override
188   public void deleteFile(String uuid) throws NotFoundException, IOException {
189     final String org = securityService.getOrganization().getId();
190     Path file = getFile(org, uuid);
191     Files.deleteIfExists(file);
192   }
193 
194   @Override
195   public String getFileName(String uuid) throws NotFoundException {
196     final String org = securityService.getOrganization().getId();
197     try {
198       Path file = getFile(org, uuid);
199       return file.getFileName().toString();
200     } catch (IOException e) {
201       logger.warn("Error while reading file:", e);
202       throw new NotFoundException(e);
203     }
204   }
205 
206   @Override
207   public Long getContentLength(String uuid) throws NotFoundException {
208     final String org = securityService.getOrganization().getId();
209     try {
210       Path file = getFile(org, uuid);
211       return Files.size(file);
212     } catch (IOException e) {
213       logger.warn("Error while reading file:", e);
214       throw new NotFoundException(e);
215     }
216   }
217 
218   /**
219    * Returns a {@link DirectoryStream.Filter} to filter the entries of a directory and only return items which filename
220    * starts with the UUID.
221    *
222    * @param uuid
223    *          The UUID to filter by
224    * @return the filter
225    */
226   private static DirectoryStream.Filter<Path> getDirsEqualsUuidFilter(final String uuid) {
227     return new DirectoryStream.Filter<Path>() {
228       @Override
229       public boolean accept(Path entry) throws IOException {
230         return Files.isDirectory(entry) && entry.getFileName().toString().equals(uuid);
231       }
232     };
233   };
234 
235   /**
236    * Returns the temporary storage directory for an organization.
237    *
238    * @param org
239    *          The organization
240    * @return Path to the temporary storage directory
241    */
242   private Path getTemporaryStorageDir(final String org) {
243     return Paths.get(rootDirPath, org, "temp");
244   }
245 
246   private Path getDurableStorageDir(final String org) {
247     return Paths.get(rootDirPath, org);
248   }
249 
250   private Path getFile(final String org, final String uuid) throws NotFoundException, IOException {
251     // First check if the file is part of the durable storage section
252     try (DirectoryStream<Path> dirs = Files.newDirectoryStream(getDurableStorageDir(org),
253             getDirsEqualsUuidFilter(uuid))) {
254       for (Path dir : dirs) {
255         try (DirectoryStream<Path> files = Files.newDirectoryStream(dir)) {
256           for (Path file : files) {
257             return file;
258           }
259         }
260       }
261     }
262 
263     // Second check if the file is part of the temporary storage section
264     try (DirectoryStream<Path> dirs = Files.newDirectoryStream(getTemporaryStorageDir(org),
265             getDirsEqualsUuidFilter(uuid))) {
266       for (Path dir : dirs) {
267         try (DirectoryStream<Path> files = Files.newDirectoryStream(dir)) {
268           for (Path file : files) {
269             return file;
270           }
271         }
272       }
273     }
274 
275     throw new NotFoundException(format("No file with UUID '%s' found.", uuid));
276   }
277 
278   /**
279    * Deletes all files found in the temporary storage section of an organization.
280    *
281    * @param org
282    *          The organization identifier
283    * @throws IOException
284    *           if there was an error while deleting the files.
285    */
286   void purgeTemporaryStorageSection(final String org, final long lifetime) throws IOException {
287     logger.debug("Purge temporary storage section of organization '{}'", org);
288     final Path temporaryStorageDir = getTemporaryStorageDir(org);
289     if (Files.exists(temporaryStorageDir)) {
290       try (DirectoryStream<Path> tempFilesStream = Files.newDirectoryStream(temporaryStorageDir,
291           new DirectoryStream.Filter<Path>() {
292             @Override
293             public boolean accept(Path path) throws IOException {
294               return (Files.getLastModifiedTime(path).toMillis() < (new Date()).getTime() - lifetime);
295             }
296           })) {
297         for (Path file : tempFilesStream) {
298           FileUtils.deleteQuietly(file.toFile());
299         }
300       }
301     }
302   }
303 
304   /**
305    * Deletes all files found in the temporary storage section of all known organizations.
306    *
307    * @throws IOException
308    *           if there was an error while deleting the files.
309    */
310   void purgeTemporaryStorageSection() throws IOException {
311     logger.info("Start purging temporary storage section of all known organizations");
312     for (Organization org : orgDirectory.getOrganizations()) {
313       purgeTemporaryStorageSection(org.getId(), TimeUnit.DAYS.toMillis(1));
314     }
315   }
316 
317   /** Scheduled service for purging temporary storage sections. */
318   private class PurgeTemporaryStorageService extends AbstractScheduledService {
319 
320     @Override
321     protected void runOneIteration() throws Exception {
322       StaticFileServiceImpl.this.purgeTemporaryStorageSection();
323     }
324 
325     @Override
326     protected Scheduler scheduler() {
327       return Scheduler.newFixedRateSchedule(0, 1, TimeUnit.HOURS);
328     }
329 
330   }
331 
332 }