• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmrs / openmrs-core / 14617411652

23 Apr 2025 11:50AM UTC coverage: 63.801% (+0.1%) from 63.69%
14617411652

push

github

rkorytkowski
TRUNK-6300 Add Storage Service

(cherry picked from commit 8a14c70a3)

137 of 227 new or added lines in 6 files covered. (60.35%)

1 existing line in 1 file now uncovered.

22160 of 34733 relevant lines covered (63.8%)

0.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.29
/api/src/main/java/org/openmrs/api/storage/LocalStorageService.java
1
/**
2
 * This Source Code Form is subject to the terms of the Mozilla Public License,
3
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
4
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6
 *
7
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8
 * graphic logo is a trademark of OpenMRS Inc.
9
 */
10
package org.openmrs.api.storage;
11

12
import javax.activation.MimetypesFileTypeMap;
13
import java.io.File;
14
import java.io.IOException;
15
import java.io.InputStream;
16
import java.io.UnsupportedEncodingException;
17
import java.net.URLDecoder;
18
import java.net.URLEncoder;
19
import java.nio.file.Files;
20
import java.nio.file.Path;
21
import java.nio.file.Paths;
22
import java.nio.file.attribute.BasicFileAttributes;
23
import java.time.LocalDateTime;
24
import java.time.format.DateTimeFormatter;
25
import java.util.ArrayList;
26
import java.util.List;
27
import java.util.stream.Stream;
28

29
import org.apache.commons.lang3.RandomStringUtils;
30
import org.openmrs.api.StorageService;
31
import org.openmrs.api.stream.StreamDataService;
32
import org.openmrs.util.OpenmrsUtil;
33
import org.slf4j.Logger;
34
import org.slf4j.LoggerFactory;
35
import org.springframework.beans.factory.annotation.Autowired;
36
import org.springframework.beans.factory.annotation.Qualifier;
37
import org.springframework.beans.factory.annotation.Value;
38
import org.springframework.context.annotation.Conditional;
39
import org.springframework.stereotype.Service;
40

41
/**
42
 * Used to persist data in a local file system or volumes.
43
 * <p>
44
 * It is the default implementation of StorageService.
45
 * 
46
 * @since 2.8.0, 2.7.4, 2.6.16, 2.5.15
47
 */
48
@Service
49
@Conditional(StorageServiceCondition.class)
50
@Qualifier("local")
51
public class LocalStorageService extends BaseStorageService implements StorageService {
52
        
53
        protected static final Logger log = LoggerFactory.getLogger(LocalStorageService.class);
1✔
54
        
55
        private final Path storageDir;
56
        
57
        private final DateTimeFormatter keyDateTimeFormat = DateTimeFormatter.ofPattern("yyyy/MM-dd/yyyy-MM-dd-HH-mm-ss-SSS-");
1✔
58
        
59
        private final MimetypesFileTypeMap mimetypes = new MimetypesFileTypeMap();
1✔
60
        
61
        public LocalStorageService(@Value("${storage_dir}") String storageDir, @Autowired StreamDataService streamService) {
62
                super(streamService);
1✔
63
                this.storageDir = "${storage_dir}".equals(storageDir) ? Paths.get(OpenmrsUtil.getApplicationDataDirectory(), 
1✔
64
                        "storage").toAbsolutePath() : Paths.get(storageDir).toAbsolutePath();
1✔
65
        }
1✔
66
        
67
        @Override
68
        public InputStream getData(final String key) throws IOException {
69
                return Files.newInputStream(getPath(key));
1✔
70
        }
71

72
        /**
73
         * It needs to be evaluated each time as it changes over time in tests...
74
         * <p>
75
         * It's only added to support legacy storage location, which will be removed in some later version.
76
         * 
77
         * @return the legacy storage dir
78
         */
79
        private Path getLegacyStorageDir() {
80
                return Paths.get(OpenmrsUtil.getApplicationDataDirectory());
1✔
81
        }
82

83
        @Override
84
        public ObjectMetadata getMetadata(final String key) throws IOException {
NEW
85
                Path path = getPath(key);
×
86

NEW
87
                BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
×
NEW
88
                String filename = decodeKey(path.getFileName().toString());
×
89
                
NEW
90
                return ObjectMetadata.builder()
×
NEW
91
                                .setLength(attributes.size())
×
NEW
92
                                .setMimeType(mimetypes.getContentType(filename))
×
NEW
93
                                .setFilename(filename)
×
NEW
94
                                .setCreationTime(attributes.creationTime().toInstant()).build();
×
95
        }
96

97
        Path getPath(String key) {
98
                key = key.replace('/', File.separatorChar); //MS Windows support
1✔
99
                Path legacyStorageDir = getLegacyStorageDir();
1✔
100
                Path legacyPath = legacyStorageDir.resolve(key);
1✔
101
                if (Files.exists(legacyPath)) {
1✔
102
                        if (!legacyPath.normalize().startsWith(legacyStorageDir)) {
1✔
103
                                throw new IllegalArgumentException("Key must not point outside legacy storage dir. Wrong key: " + key);
1✔
104
                        }
105
                        return legacyPath;
1✔
106
                } else {
107
                        Path path = storageDir.resolve(encodeKey(key));
1✔
108
                        assertKeyInStorageDir(path, key);
1✔
109
                        return path;
1✔
110
                }
111
        }
112
        
113
        @Override
114
        public Stream<String> getKeys(final String moduleIdOrGroup, final String keyPrefix) throws IOException {
115
                List<String> dirs = new ArrayList<>();
1✔
116
                final Path storagePath;
117
                if (moduleIdOrGroup != null) {
1✔
118
                        dirs.add(moduleIdOrGroup);
1✔
119
                        storagePath = storageDir.resolve(moduleIdOrGroup);
1✔
120
                } else {
121
                        storagePath = storageDir;
1✔
122
                }
123
                
124
                String encodedPrefix = encodeKey(keyPrefix);
1✔
125
                encodedPrefix = encodedPrefix.replace('/', File.separatorChar);
1✔
126
                
127
                Path pathPrefix = Paths.get(encodedPrefix);
1✔
128
                final Path parent;
129
                final String filename;
130
                if (encodedPrefix.endsWith(File.separator) && Files.isDirectory(storagePath.resolve(pathPrefix))) {
1✔
131
                        parent = pathPrefix;
1✔
132
                        filename = "";
1✔
133
                } else {
134
                        parent = pathPrefix.getParent();
1✔
135
                        filename = pathPrefix.getFileName().toString();
1✔
136
                }
137
                
138
                if (parent != null) {
1✔
139
                        dirs.add(parent.toString());
1✔
140
                }
141
                @SuppressWarnings("resource")
142
                Stream<Path> stream = Files.list(Paths.get(storageDir.toString(), dirs.toArray(new String[0])));
1✔
143
                // Filter out files that start with dot (hidden files)
144
                return stream.filter(
1✔
145
                    path -> path.getFileName().toString().startsWith(filename) && !path.getFileName().toString().startsWith("."))
1✔
146
                        .map(path -> {
1✔
147
                                        String key = storagePath.relativize(path).toString();
1✔
148
                                        key = decodeKey(key);
1✔
149
                                        key += (Files.isDirectory(path)) ? File.separator : "";
1✔
150
                                        key = key.replace(File.separatorChar, '/'); //MS Windows support
1✔
151
                                        return key;
1✔
152
                                });
153
        }
154

155
        static String decodeKey(String key) {
156
                try {
157
                        return URLDecoder.decode(key, "UTF-8");
1✔
NEW
158
                } catch (UnsupportedEncodingException e) {
×
NEW
159
                        throw new RuntimeException(e);
×
160
                }
161
        }
162

163
        static String encodeKey(String key) {
164
                try {
165
                        return URLEncoder.encode(key, "UTF-8").replace(".", "%2E")
1✔
166
                                .replace("*", "%2A").replace("%2F", "/");
1✔
NEW
167
                } catch (UnsupportedEncodingException e) {
×
NEW
168
                        throw new RuntimeException(e);
×
169
                }
170
        }
171

172
        Path newPath(String key) throws IOException {
173
                key = encodeKey(key);
1✔
174
                key = key.replace('/', File.separatorChar);
1✔
175
                Path newPath = storageDir.resolve(key);
1✔
176
                assertKeyInStorageDir(newPath, key);
1✔
177
                
178
                Files.createDirectories(newPath.getParent());
1✔
179
                
180
                return newPath;
1✔
181
        }
182
        
183
        void assertKeyInStorageDir(Path path, String key) {
184
                if (!path.normalize().startsWith(storageDir)) {
1✔
185
                        throw new IllegalArgumentException("Key must not point outside storage dir. Wrong key: " + key);
1✔
186
                }
187
        }
1✔
188
        
189
        String newKey(String moduleIdOrGroup, String keySuffix, String filename) {
190
                if (keySuffix == null) {
1✔
191
                        keySuffix = LocalDateTime.now().format(keyDateTimeFormat) + RandomStringUtils.randomAlphanumeric(8);
1✔
192
                }
193
                if (filename != null) {
1✔
NEW
194
                        keySuffix += '-' + filename.replace(File.separator, "");
×
195
                }
196
                
197
                if (moduleIdOrGroup == null) {
1✔
198
                        return keySuffix;
1✔
199
                } else {
200
                        if (!moduleIdOrGroupPattern.matcher(moduleIdOrGroup).matches()) {
1✔
201
                                throw new IllegalArgumentException("moduleIdOrGroup '" + moduleIdOrGroup + "' does not match [\\w-./]+");
1✔
202
                        }
203
                        return moduleIdOrGroup + '/' + keySuffix;
1✔
204
                }
205
        }
206
        
207
        @Override
208
        public String saveData(InputStream inputStream, ObjectMetadata metadata, String moduleIdOrGroup, String keySuffix) throws IOException {
209
                String key = newKey(moduleIdOrGroup, keySuffix, metadata != null ? metadata.getFilename() : null);
1✔
210
                Path target = newPath(key);
1✔
211
                try {
212
                        Files.copy(inputStream, target);
1✔
213
                } catch (IOException e) {
1✔
214
                        purgeData(key);
1✔
215
                        throw e;
1✔
216
                }
1✔
217
                return key;
1✔
218
        }
219
        
220
        @Override
221
        public boolean purgeData(String key) throws IOException {
222
                if (key == null) return false;
1✔
223
                
224
                try {
225
                        return Files.deleteIfExists(getPath(key));
1✔
226
                }
NEW
227
                catch (Exception e) {
×
NEW
228
                        log.error("Error deleting key: {}", key, e);
×
229
                        try {
NEW
230
                                File file = getPath(key).toFile();
×
NEW
231
                                if (file.exists()) {
×
NEW
232
                                        file.deleteOnExit();
×
NEW
233
                                        return true;
×
234
                                } else {
NEW
235
                                        return false;
×
236
                                }
NEW
237
                        } catch (Exception deleteException) {
×
NEW
238
                                log.error("Error marking key for deletion: {}", key, deleteException);
×
239
                        }
NEW
240
                        return false;
×
241
                }
242
        }
243
        
244
        @Override
245
        public boolean exists(String key) {
246
                return Files.exists(getPath(key));
1✔
247
        }
248
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc