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

openmrs / openmrs-core / 16357938490

17 Jul 2025 11:06PM UTC coverage: 65.244% (-0.1%) from 65.359%
16357938490

push

github

web-flow
TRUNK-6318: Add S3 Storage Service (#5110)

111 of 156 new or added lines in 5 files covered. (71.15%)

48 existing lines in 8 files now uncovered.

23568 of 36123 relevant lines covered (65.24%)

0.65 hits per line

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

84.93
/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.nio.file.Files;
17
import java.nio.file.Path;
18
import java.nio.file.Paths;
19
import java.nio.file.attribute.BasicFileAttributes;
20
import java.util.ArrayList;
21
import java.util.List;
22
import java.util.stream.Stream;
23
import org.apache.commons.lang.StringUtils;
24
import org.apache.commons.lang3.tuple.Pair;
25
import org.openmrs.api.StorageService;
26
import org.openmrs.api.stream.StreamDataService;
27
import org.openmrs.util.OpenmrsUtil;
28
import org.slf4j.Logger;
29
import org.slf4j.LoggerFactory;
30
import org.springframework.beans.factory.annotation.Autowired;
31
import org.springframework.beans.factory.annotation.Qualifier;
32
import org.springframework.beans.factory.annotation.Value;
33
import org.springframework.context.annotation.Conditional;
34
import org.springframework.stereotype.Service;
35

36

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

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

77
        @Override
78
        public ObjectMetadata getMetadata(final String key) throws IOException {
79
                Path path = getPath(key);
1✔
80

81
                BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
1✔
82
                String filename = decodeKey(path.getFileName().toString());
1✔
83
                
84
                return ObjectMetadata.builder()
1✔
85
                                .setLength(attributes.size())
1✔
86
                                .setMimeType(mimetypes.getContentType(filename))
1✔
87
                                .setFilename(filename)
1✔
88
                                .setCreationTime(attributes.creationTime().toInstant()).build();
1✔
89
        }
90

91
        Path getPath(String key) {
92
                Path legacyStorageDir = getLegacyStorageDir();
1✔
93
                Path legacyPath = legacyStorageDir.resolve(key);
1✔
94
                if (Files.exists(legacyPath)) {
1✔
95
                        if (!legacyPath.normalize().startsWith(legacyStorageDir)) {
1✔
96
                                throw new IllegalArgumentException("Key must not point outside legacy storage dir. Wrong key: " + key);
1✔
97
                        }
98
                        return legacyPath;
1✔
99
                } else {
100
                        Path path = storageDir.resolve(encodeKey(key));
1✔
101
                        assertKeyInStorageDir(path, key);
1✔
102
                        return path;
1✔
103
                }
104
        }
105
        
106
        @Override
107
        public Stream<String> getKeys(final String moduleIdOrGroup, final String keyPrefix) throws IOException {
108
                String key = encodeKey(newKey(moduleIdOrGroup, keyPrefix, null));
1✔
109

110
                int lastDirIndex = key.lastIndexOf("/");
1✔
111
                String lastDir = "";
1✔
112
                if (lastDirIndex != -1) {
1✔
113
                        lastDir = key.substring(0, lastDirIndex + 1);
1✔
114
                }
115

116
                Path searchDir = storageDir.resolve(lastDir);
1✔
117

118
                if (!searchDir.toFile().isDirectory()) {
1✔
NEW
119
                        return Stream.empty();
×
120
                }
121
                
122
                @SuppressWarnings("resource")
123
                Stream<Path> stream = Files.list(searchDir);
1✔
124
                // Filter out files that start with dot (hidden files)
125
                return stream.filter(
1✔
126
                    path -> !path.getFileName().toString().startsWith("."))
1✔
127
                        .map(path -> {
1✔
128
                                        String foundKey = storageDir.relativize(path).toString();
1✔
129
                                        foundKey = decodeKey(foundKey);
1✔
130
                                        foundKey += (Files.isDirectory(path)) ? File.separator : "";
1✔
131
                                        foundKey = foundKey.replace(File.separatorChar, '/'); //MS Windows support
1✔
132
                                        return foundKey;
1✔
133
                                }).filter(foundKey -> foundKey.startsWith(key));
1✔
134
        }
135

136
        Path newPath(String key) throws IOException {
137
                key = encodeKey(key);
1✔
138
                key = key.replace('/', File.separatorChar);
1✔
139
                Path newPath = storageDir.resolve(key);
1✔
140
                assertKeyInStorageDir(newPath, key);
1✔
141
                
142
                Files.createDirectories(newPath.getParent());
1✔
143
                
144
                return newPath;
1✔
145
        }
146
        
147
        void assertKeyInStorageDir(Path path, String key) {
148
                if (!path.normalize().startsWith(storageDir)) {
1✔
149
                        throw new IllegalArgumentException("Key must not point outside storage dir. Wrong key: " + key);
1✔
150
                }
151
        }
1✔
152

153
        @Override
154
        public String saveData(InputStream inputStream, ObjectMetadata metadata, String moduleIdOrGroup, String keySuffix) throws IOException {
155
                String key = newKey(moduleIdOrGroup, keySuffix, metadata != null ? metadata.getFilename() : null);
1✔
156
                Path target = newPath(key);
1✔
157
                try {
158
                        Files.copy(inputStream, target);
1✔
159
                } catch (IOException e) {
1✔
160
                        purgeData(key);
1✔
161
                        throw e;
1✔
162
                }
1✔
163
                return key;
1✔
164
        }
165
        
166
        @Override
167
        public boolean purgeData(String key) throws IOException {
168
                if (key == null) return false;
1✔
169
                
170
                try {
171
                        return Files.deleteIfExists(getPath(key));
1✔
172
                }
173
                catch (Exception e) {
×
174
                        log.error("Error deleting key: {}", key, e);
×
175
                        try {
176
                                File file = getPath(key).toFile();
×
177
                                if (file.exists()) {
×
178
                                        file.deleteOnExit();
×
179
                                        return true;
×
180
                                } else {
181
                                        return false;
×
182
                                }
183
                        } catch (Exception deleteException) {
×
184
                                log.error("Error marking key for deletion: {}", key, deleteException);
×
185
                        }
186
                        return false;
×
187
                }
188
        }
189
        
190
        @Override
191
        public boolean exists(String key) {
192
                return Files.exists(getPath(key));
1✔
193
        }
194
}
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