1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.resourceloader.file;
35
36 import static java.util.stream.Collectors.toList;
37
38 import info.magnolia.cms.util.ExceptionUtil;
39 import info.magnolia.dirwatch.DirectoryWatcherService;
40 import info.magnolia.init.MagnoliaConfigurationProperties;
41 import info.magnolia.resourceloader.AbstractResourceOrigin;
42 import info.magnolia.resourceloader.ResourceOriginFactory;
43
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.nio.charset.Charset;
47 import java.nio.charset.StandardCharsets;
48 import java.nio.file.DirectoryStream;
49 import java.nio.file.Files;
50 import java.nio.file.LinkOption;
51 import java.nio.file.Path;
52 import java.nio.file.Paths;
53 import java.nio.file.attribute.FileTime;
54 import java.util.Arrays;
55 import java.util.Collections;
56 import java.util.List;
57 import java.util.function.Function;
58 import java.util.stream.StreamSupport;
59
60 import org.apache.commons.io.FilenameUtils;
61 import org.apache.commons.lang3.StringUtils;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64
65 import com.google.auto.factory.AutoFactory;
66 import com.google.auto.factory.Provided;
67 import com.google.common.base.Preconditions;
68 import com.google.common.collect.Lists;
69
70
71
72
73 @AutoFactory(implementing = ResourceOriginFactory.class)
74 public class FileSystemResourceOrigin extends AbstractResourceOrigin<FileSystemResource> {
75 private static final Logger log = LoggerFactory.getLogger(FileSystemResourceOrigin.class);
76
77 public static final String RESOURCES_DIR_PROPERTY = "magnolia.resources.dir";
78 private static final LinkOption[] LINK_OPTIONS = new LinkOption[0];
79 private static final List<String> EXCLUDED_DIRECTORIES = Arrays.asList("META-INF", "WEB-INF", "cache", "docroot", "logs", "repositories", "tmp");
80
81 private final Path rootPath;
82
83 private final ExclusionsFilter exclusionsFilter;
84 private final Function<Path, FileSystemResource> transformFunction;
85 private final String excludedDirectories;
86 private final DirectoryWatcherService directoryWatcherService;
87
88 FileSystemResourceOrigin(@Provided MagnoliaConfigurationProperties mcp, @Provided DirectoryWatcherService directoryWatcherService, String name) {
89 super(name);
90 this.directoryWatcherService = directoryWatcherService;
91
92 String resourcesDir = mcp.getProperty(RESOURCES_DIR_PROPERTY);
93 if (resourcesDir == null) {
94 log.warn("Could not find magnolia.resources.dir property, please update your magnolia.properties. Falling back to magnolia.home.");
95 resourcesDir = mcp.getProperty("magnolia.home");
96 }
97 this.excludedDirectories = mcp.getProperty("magnolia.resources.filesystem.observation.excludedDirectories");
98 this.rootPath = validateRootPath(resourcesDir);
99 this.exclusionsFilter = buildExclusionsFilter();
100 this.transformFunction = this::newResource;
101 }
102
103
104
105
106
107 protected ExclusionsFilter buildExclusionsFilter() {
108 List<String> configuredExcludedDirectories = Lists.newArrayList();
109 if (StringUtils.isNotBlank(excludedDirectories)) {
110 for (String directory : StringUtils.split(excludedDirectories, ",")) {
111 configuredExcludedDirectories.add(directory.trim());
112 }
113 } else {
114 configuredExcludedDirectories = EXCLUDED_DIRECTORIES;
115 }
116 return new ExclusionsFilter(rootPath, configuredExcludedDirectories, Collections.emptyList(), Collections.emptyList());
117 }
118
119 @Override
120 public FileSystemResource getRoot() {
121 return newResource(rootPath);
122 }
123
124
125 @Override
126 public FileSystemResource getByPath(final String path) {
127 final Path filePath = getPath(path);
128 if (!existsAndAllowed(filePath)) {
129 throw new ResourceNotFoundException(this, path);
130 }
131 return newResource(filePath);
132 }
133
134 @Override
135 public boolean hasPath(final String path) {
136 final Path filePath = getPath(path);
137 return existsAndAllowed(filePath);
138 }
139
140 @Override
141 protected void initializeResourceChangeMonitoring() {
142 try {
143
144
145
146
147 directoryWatcherService.register(rootPath, exclusionsFilter, new FileWatcherCallback(this, exclusionsFilter));
148 } catch (IOException e) {
149 log.error("Failed to initialise file system observation: {}! No file system changes are going to be communicated!", e.getMessage(), e);
150 }
151 }
152
153 @Override
154 protected boolean isFile(FileSystemResource resource) {
155 return Files.isRegularFile(resource.getRealPath());
156 }
157
158 @Override
159 protected boolean isDirectory(FileSystemResource resource) {
160 return Files.isDirectory(resource.getRealPath());
161 }
162
163 @Override
164 protected String getPath(FileSystemResource resource) {
165 return parseResourcePath(resource.getRealPath());
166 }
167
168
169
170
171
172
173
174 public String parseResourcePath(Path realPath) {
175 final Path path = rootPath.relativize(realPath);
176 return "/" + FilenameUtils.normalize(path.toString(), true);
177 }
178
179 @Override
180 protected String getName(FileSystemResource resource) {
181 return resource.getRealPath().getFileName().toString();
182 }
183
184 @Override
185 protected long getLastModified(FileSystemResource resource) {
186 try {
187 final FileTime lastModifiedTime = Files.getLastModifiedTime(resource.getRealPath(), LINK_OPTIONS);
188 return lastModifiedTime.toMillis();
189 } catch (IOException e) {
190 throw new RuntimeException("Last modified time could not be retrieved for path " + resource + " : " + e, e);
191 }
192 }
193
194 @Override
195 protected List<FileSystemResource> listChildren(FileSystemResource resource) {
196 if (resource.isFile()) {
197 throw new IllegalStateException(resource.getPath() + " is not a directory.");
198 }
199
200 Path parent = resource.getRealPath();
201
202 try (DirectoryStream<Path> ds = Files.newDirectoryStream(parent, exclusionsFilter)) {
203 return StreamSupport.stream(ds.spliterator(), false)
204 .map(transformFunction)
205 .collect(toList());
206 } catch (IOException e) {
207 log.error("Failed to list the child resources of a resource at [{}]: {}. Returning empty list...", resource, e.getMessage(), e);
208 return Collections.emptyList();
209 }
210 }
211
212 @Override
213 protected FileSystemResource getParent(FileSystemResource resource) {
214 if (resource == null) {
215 throw new IllegalStateException("The provided path is null.");
216 }
217 if (!resource.getRealPath().startsWith(rootPath)) {
218 throw new IllegalStateException("The provided path '" + resource + "' is not a descendant of this origin's root path.");
219 }
220 if (resource.getRealPath().equals(rootPath)) {
221 return null;
222 }
223 return newResource(resource.getRealPath().getParent());
224 }
225
226 @Override
227 protected InputStream doOpenStream(FileSystemResource resource) throws IOException {
228 return Files.newInputStream(resource.getRealPath());
229 }
230
231 @Override
232 protected Charset getCharsetFor(FileSystemResource resource) {
233 return StandardCharsets.UTF_8;
234 }
235
236 protected Path validateRootPath(String path) {
237 Path rootPath = Paths.get(path);
238 if (!Files.exists(rootPath) && Files.exists(rootPath.getParent()) && Files.isDirectory(rootPath.getParent())) {
239 try {
240 Files.createDirectory(rootPath);
241 } catch (IOException e) {
242 throw new IllegalStateException("Tried to create " + rootPath + " and failed: " + ExceptionUtil.exceptionToWords(e), e);
243 }
244 log.info("Created {}", rootPath);
245 }
246
247 if (Files.exists(rootPath) && !Files.isDirectory(rootPath)) {
248 throw new IllegalStateException("Trying to load resources from " + rootPath + ", which is not a directory.");
249 }
250
251 if (!Files.exists(rootPath)) {
252 log.warn("Trying to load resources from {}, but this directory does not exist", rootPath);
253 }
254 return rootPath;
255 }
256
257 protected Path getPath(String path) {
258 return Paths.get(rootPath.toString(), path);
259 }
260
261 protected boolean existsAndAllowed(Path filePath) {
262 return Files.exists(filePath) && exclusionsFilter.test(filePath);
263 }
264
265
266
267
268
269
270 protected FileSystemResource newResource(Path path) {
271
272 final Path normPath;
273 if (!path.isAbsolute()) {
274 normPath = rootPath.resolve(path).normalize();
275 } else {
276 normPath = path;
277 }
278
279 if (!normPath.startsWith(rootPath)) {
280 throw new IllegalStateException(path + " is not within the bounds of the " + rootPath + " root.");
281 }
282 final boolean isDirectory = Files.isDirectory(normPath);
283 final boolean isFile = Files.isRegularFile(normPath);
284 Preconditions.checkArgument((isDirectory || isFile) && (isDirectory != isFile), "%s is neither or both a directory and/or a file.", normPath);
285 return new FileSystemResource(this, normPath);
286 }
287 }