Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
37651 vikas 1
package com.spice.profitmandi.service.storage;
2
 
3
import com.amazonaws.ClientConfiguration;
4
import com.amazonaws.auth.AWSStaticCredentialsProvider;
5
import com.amazonaws.auth.BasicAWSCredentials;
6
import com.amazonaws.client.builder.AwsClientBuilder;
7
import com.amazonaws.services.s3.AmazonS3;
8
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
9
import com.amazonaws.services.s3.model.ObjectMetadata;
10
import com.amazonaws.services.s3.model.S3Object;
11
import org.apache.logging.log4j.LogManager;
12
import org.apache.logging.log4j.Logger;
13
import org.springframework.beans.factory.annotation.Value;
14
import org.springframework.stereotype.Service;
15
 
16
import java.io.InputStream;
17
 
18
/**
19
 * S3-compatible object storage, added for LMS call-recording retention (SOP §17 — 24 months).
20
 *
21
 * <p>The app's existing file store ({@code dtr.document} + {@code new File(path + name)}) is local
22
 * disk, which does not carry the volume recordings imply (~1 MB/min of stereo audio) and does not
23
 * survive a multi-node deploy. This service is deliberately generic rather than recording-specific
24
 * so other bulk artefacts can move off local disk later.
25
 *
26
 * <p>Works against AWS S3 or any S3-compatible endpoint (MinIO, Wasabi, DO Spaces): leave
27
 * {@code endpoint} blank for real AWS, or set it plus {@code pathStyle=true} for the others.
28
 *
29
 * <p>The client is built lazily and the service reports {@link #isEnabled()} rather than throwing at
30
 * startup — an unconfigured store must not stop the app from booting, it must only stop the archival
31
 * worker from claiming it stored anything.
32
 */
33
@Service
34
public class ObjectStoreService {
35
 
36
    private static final Logger LOGGER = LogManager.getLogger(ObjectStoreService.class);
37
 
38
    @Value("${lms.recording.store.enabled:false}")
39
    private boolean enabled;
40
 
41
    /** Blank for real AWS; set for MinIO/Wasabi/Spaces. */
42
    @Value("${lms.recording.store.endpoint:}")
43
    private String endpoint;
44
 
45
    @Value("${lms.recording.store.region:ap-south-1}")
46
    private String region;
47
 
48
    @Value("${lms.recording.store.bucket:}")
49
    private String bucket;
50
 
51
    @Value("${lms.recording.store.accessKey:}")
52
    private String accessKey;
53
 
54
    @Value("${lms.recording.store.secretKey:}")
55
    private String secretKey;
56
 
57
    /** Non-AWS S3 implementations generally need path-style addressing. */
58
    @Value("${lms.recording.store.pathStyle:true}")
59
    private boolean pathStyle;
60
 
61
    private volatile AmazonS3 client;
62
 
63
    /** False when the store is switched off or half-configured — callers must not assume a write. */
64
    public boolean isEnabled() {
65
        return enabled
66
                && bucket != null && !bucket.trim().isEmpty()
67
                && accessKey != null && !accessKey.trim().isEmpty()
68
                && secretKey != null && !secretKey.trim().isEmpty();
69
    }
70
 
71
    public String getBucket() {
72
        return bucket;
73
    }
74
 
75
    /**
76
     * Store an object. {@code contentLength} must be accurate — S3 streams without buffering the
77
     * whole body, and a wrong length is a corrupt upload rather than an error.
78
     *
79
     * @return the key it was stored under, for persisting alongside the row that owns it
80
     */
81
    public String put(String key, InputStream content, long contentLength, String contentType) {
82
        requireEnabled();
83
        ObjectMetadata metadata = new ObjectMetadata();
84
        metadata.setContentLength(contentLength);
85
        if (contentType != null && !contentType.trim().isEmpty()) {
86
            metadata.setContentType(contentType);
87
        }
88
        client().putObject(bucket, key, content, metadata);
89
        LOGGER.info("Stored object {} ({} bytes) in bucket {}", key, contentLength, bucket);
90
        return key;
91
    }
92
 
93
    /**
94
     * Open an object for reading. The caller owns the stream and MUST close it — an unclosed S3
95
     * object holds a connection out of the pool until it times out.
96
     */
97
    public S3Object get(String key) {
98
        requireEnabled();
99
        return client().getObject(bucket, key);
100
    }
101
 
102
    public boolean exists(String key) {
103
        if (!isEnabled()) {
104
            return false;
105
        }
106
        try {
107
            return client().doesObjectExist(bucket, key);
108
        } catch (Exception e) {
109
            LOGGER.warn("Could not check object {} in bucket {}", key, bucket, e);
110
            return false;
111
        }
112
    }
113
 
114
    public void delete(String key) {
115
        requireEnabled();
116
        client().deleteObject(bucket, key);
117
    }
118
 
119
    private void requireEnabled() {
120
        if (!isEnabled()) {
121
            throw new IllegalStateException(
122
                    "Object store is not configured — set lms.recording.store.{enabled,bucket,accessKey,secretKey}");
123
        }
124
    }
125
 
126
    /** Double-checked lazy build so a bad config surfaces on first use, not at context startup. */
127
    private AmazonS3 client() {
128
        AmazonS3 local = client;
129
        if (local == null) {
130
            synchronized (this) {
131
                local = client;
132
                if (local == null) {
133
                    local = build();
134
                    client = local;
135
                }
136
            }
137
        }
138
        return local;
139
    }
140
 
141
    private AmazonS3 build() {
142
        ClientConfiguration config = new ClientConfiguration();
143
        config.setConnectionTimeout(5000);
144
        // Recordings are a few MB; generous enough for a slow link, bounded enough that the
145
        // archival worker cannot wedge on a hanging endpoint.
146
        config.setSocketTimeout(60000);
147
 
148
        AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard()
149
                .withCredentials(new AWSStaticCredentialsProvider(
150
                        new BasicAWSCredentials(accessKey.trim(), secretKey.trim())))
151
                .withClientConfiguration(config);
152
 
153
        if (endpoint != null && !endpoint.trim().isEmpty()) {
154
            builder = builder
155
                    .withEndpointConfiguration(
156
                            new AwsClientBuilder.EndpointConfiguration(endpoint.trim(), region))
157
                    .withPathStyleAccessEnabled(pathStyle);
158
        } else {
159
            builder = builder.withRegion(region);
160
        }
161
        LOGGER.info("Object store client built for bucket {} (endpoint={})", bucket,
162
                endpoint == null || endpoint.trim().isEmpty() ? "aws" : endpoint);
163
        return builder.build();
164
    }
165
}