OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 180 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
174 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
182 ilm 4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
174 ilm 5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.utils.sync;
15
 
16
import org.openconcerto.utils.CollectionUtils;
17
import org.openconcerto.utils.MessageDigestUtils;
177 ilm 18
import org.openconcerto.utils.NetUtils;
174 ilm 19
import org.openconcerto.utils.StreamUtils;
177 ilm 20
import org.openconcerto.utils.net.HTTPClient;
174 ilm 21
 
177 ilm 22
import java.io.BufferedInputStream;
174 ilm 23
import java.io.BufferedOutputStream;
177 ilm 24
import java.io.ByteArrayOutputStream;
174 ilm 25
import java.io.File;
177 ilm 26
import java.io.FileInputStream;
174 ilm 27
import java.io.IOException;
28
import java.io.InputStream;
29
import java.io.OutputStream;
182 ilm 30
import java.io.Serializable;
174 ilm 31
import java.nio.charset.StandardCharsets;
32
import java.nio.file.Files;
33
import java.nio.file.Path;
34
import java.nio.file.StandardCopyOption;
35
import java.nio.file.attribute.FileTime;
36
import java.security.DigestOutputStream;
37
import java.security.MessageDigest;
38
import java.time.Instant;
39
import java.util.ArrayList;
40
import java.util.Base64;
41
import java.util.Collections;
42
import java.util.List;
43
import java.util.Set;
44
import java.util.concurrent.atomic.AtomicBoolean;
45
import java.util.function.BiFunction;
46
import java.util.function.Predicate;
47
 
48
import javax.net.ssl.HttpsURLConnection;
49
 
50
import net.minidev.json.JSONArray;
51
import net.minidev.json.JSONObject;
52
import net.minidev.json.parser.JSONParser;
53
 
177 ilm 54
public final class SimpleSyncClient extends HTTPClient {
174 ilm 55
 
182 ilm 56
    protected static final long getLong(final Object o) {
174 ilm 57
        return ((Number) o).longValue();
58
    }
59
 
182 ilm 60
    protected static final Instant getInstant(final Object o) {
174 ilm 61
        return Instant.ofEpochMilli(getLong(o));
62
    }
63
 
182 ilm 64
    public static abstract class BaseAttrs implements Serializable {
174 ilm 65
        private final String path;
66
        private final String name;
67
        private final Instant lastModified;
68
 
69
        protected BaseAttrs(final String path, final String name, final Instant lastModified) {
70
            super();
71
            this.path = path;
72
            this.name = name;
73
            this.lastModified = lastModified;
74
        }
75
 
76
        public final String getPath() {
77
            return this.path;
78
        }
79
 
80
        public final String getName() {
81
            return this.name;
82
        }
83
 
84
        public final Instant getLastModified() {
85
            return this.lastModified;
86
        }
87
 
88
        @Override
89
        public String toString() {
90
            return this.getClass().getSimpleName() + " '" + this.getName() + "'";
91
        }
92
    }
93
 
182 ilm 94
    public static final class DirAttrs extends BaseAttrs {
95
        protected static DirAttrs fromJSON(final String path, final JSONArray array) {
174 ilm 96
            return new DirAttrs(path, (String) array.get(0), getInstant(array.get(1)));
97
        }
98
 
99
        protected DirAttrs(final String path, String name, Instant lastModified) {
100
            super(path, name, lastModified);
101
        }
102
    }
103
 
182 ilm 104
    public static final class FileAttrs extends BaseAttrs {
174 ilm 105
 
182 ilm 106
        protected static FileAttrs fromJSON(final String path, final JSONArray array) {
174 ilm 107
            return new FileAttrs(path, (String) array.get(0), getInstant(array.get(2)), getLong(array.get(1)), (String) array.get(3));
108
        }
109
 
110
        private final long size;
111
        private final String sha256;
112
 
113
        protected FileAttrs(final String path, String name, Instant lastModified, long size, String sha256) {
114
            super(path, name, lastModified);
115
            this.size = size;
116
            this.sha256 = sha256;
117
        }
118
 
119
        public final long getSize() {
120
            return this.size;
121
        }
122
 
123
        public final String getSHA256() {
124
            return this.sha256;
125
        }
126
 
127
        public final void saveFile(final InputStream in, final Path localFile) throws IOException {
128
            // Save to temporary file to avoid overwriting old file with a new invalid one. In
129
            // same folder for the move.
130
            final Path tmpFile = Files.createTempFile(localFile.getParent(), "partial", null);
131
            try {
132
                final MessageDigest md = this.getSHA256() == null ? null : MessageDigestUtils.getSHA256();
133
                try (final BufferedOutputStream fileStream = new BufferedOutputStream(Files.newOutputStream(tmpFile));
134
                        //
135
                        final OutputStream out = md == null ? fileStream : new DigestOutputStream(fileStream, md)) {
136
                    StreamUtils.copy(in, out);
137
                }
138
                if (this.getSize() >= 0) {
139
                    final long savedSize = Files.size(tmpFile);
140
                    if (savedSize != this.getSize())
141
                        throw new IOException("Expected " + this.getSize() + " bytes but saved " + savedSize);
142
                }
143
                if (md != null) {
144
                    final String savedHash = MessageDigestUtils.getHashString(md);
145
                    if (!savedHash.equalsIgnoreCase(this.getSHA256()))
146
                        throw new IOException("Expected hash was " + this.getSHA256() + " but saved " + savedHash);
147
                }
148
                Files.move(tmpFile, localFile, StandardCopyOption.REPLACE_EXISTING);
149
            } finally {
150
                Files.deleteIfExists(tmpFile);
151
            }
152
            if (this.getLastModified().compareTo(Instant.EPOCH) > 0)
153
                Files.setLastModifiedTime(localFile, FileTime.from(this.getLastModified()));
154
        }
155
 
156
        @Override
157
        public String toString() {
158
            return super.toString() + " of size " + getSize();
159
        }
160
    }
161
 
182 ilm 162
    public static final class DirContent {
174 ilm 163
        private final String path;
164
        private final JSONObject json;
165
 
166
        protected DirContent(final String path, JSONObject json) {
167
            super();
168
            this.path = path;
169
            this.json = json;
170
        }
171
 
172
        public final List<FileAttrs> getFiles() {
173
            return this.getFiles(null);
174
        }
175
 
176
        public final List<FileAttrs> getFiles(final Predicate<String> namePredicate) {
177
            return this.getContent("files", namePredicate, FileAttrs::fromJSON);
178
        }
179
 
180
        public final List<DirAttrs> getDirs() {
181
            return this.getDirs(null);
182
        }
183
 
184
        public final List<DirAttrs> getDirs(final Predicate<String> namePredicate) {
185
            return this.getContent("dirs", namePredicate, DirAttrs::fromJSON);
186
        }
187
 
188
        protected final <T extends BaseAttrs> List<T> getContent(final String key, final Predicate<String> namePredicate, final BiFunction<String, JSONArray, T> create) {
189
            final JSONArray files = (JSONArray) this.json.get(key);
190
            if (files == null)
191
                return Collections.emptyList();
192
            final List<T> res = new ArrayList<>();
193
            for (final Object f : files) {
194
                final JSONArray array = (JSONArray) f;
195
                if (namePredicate == null || namePredicate.test((String) array.get(0))) {
196
                    res.add(create.apply(this.path, array));
197
                }
198
            }
199
            return res;
200
        }
201
    }
202
 
203
    public SimpleSyncClient(final String url) {
177 ilm 204
        super(url);
174 ilm 205
    }
206
 
207
    public DirContent getDir(final String path) throws Exception {
180 ilm 208
        if (path == null) {
209
            throw new IllegalArgumentException("null path");
210
        }
211
 
174 ilm 212
        final HttpsURLConnection con = openConnection("/getDir");
180 ilm 213
        final Response res = checkResponseCode(send(con, NetUtils.urlEncode("rp", path, "type", "json"), false));
174 ilm 214
        if (!res.isSuccess())
215
            return null;
216
        final JSONParser p = new JSONParser(JSONParser.MODE_STRICTEST);
217
        try (final InputStream in = getInputStream(con)) {
218
            return new DirContent(path, (JSONObject) p.parse(in));
219
        }
220
    }
221
 
222
    @FunctionalInterface
182 ilm 223
    public static interface FileConsumer {
174 ilm 224
        public void accept(FileAttrs attrs, InputStream fileStream) throws IOException;
225
    }
226
 
182 ilm 227
    private static final Set<Integer> GETFILE_OK_CODES = CollectionUtils.createSet(200, 404);
174 ilm 228
 
229
    public Response getFile(final String path, final String fileName, final FileConsumer fileConsumer) throws IOException {
180 ilm 230
        if (path == null) {
231
            throw new IllegalArgumentException("null path");
232
        }
233
        if (fileName == null) {
234
            throw new IllegalArgumentException("null fileName");
235
        }
174 ilm 236
        final HttpsURLConnection con = openConnection("/get");
180 ilm 237
        send(con, NetUtils.urlEncode("rn", fileName, "rp", path), false);
174 ilm 238
        final Response res = checkResponseCode(con, GETFILE_OK_CODES);
239
        if (res.getCode() == 404) {
240
            fileConsumer.accept(null, null);
241
        } else if (res.getCode() == 200) {
242
            final FileAttrs fileAttrs = new FileAttrs(path, fileName, Instant.ofEpochMilli(con.getLastModified()), -1, con.getHeaderField("X-SHA256"));
243
            try (final InputStream in = getInputStream(con)) {
244
                fileConsumer.accept(fileAttrs, in);
245
            }
246
        }
247
        return res;
248
    }
249
 
182 ilm 250
    public Integer getCounter(final String key) throws IOException {
251
        if (key == null) {
252
            throw new IllegalArgumentException("null key");
253
        }
254
        final HttpsURLConnection con = openConnection("/getCounter");
255
        send(con, NetUtils.urlEncode("key", key), false);
256
        final Response res = checkResponseCode(con, GETFILE_OK_CODES);
257
        if (res.getCode() == 200) {
258
            byte[] bytes = new byte[20];
259
            try (final InputStream in = getInputStream(con)) {
260
                int r = in.read(bytes);
261
                String str = new String(bytes, 0, r);
262
                return Integer.parseInt(str);
263
            }
264
 
265
        }
266
        return null;
267
 
268
    }
269
 
174 ilm 270
    // ATTN contrary to other methods, the result isn't if the request was OK : it ignores
271
    // throwsException() and always throws. The return value is true if the file existed and was
272
    // saved.
273
    public boolean saveFile(final String path, final String fileName, final Path localFile) throws IOException {
180 ilm 274
        if (path == null) {
275
            throw new IllegalArgumentException("null path");
276
        }
277
        if (fileName == null) {
278
            throw new IllegalArgumentException("null fileName");
279
        }
280
 
174 ilm 281
        final AtomicBoolean missing = new AtomicBoolean(true);
282
        final Response res = this.getFile(path, fileName, (fileAttrs, in) -> {
283
            missing.set(fileAttrs == null);
284
            if (!missing.get()) {
285
                fileAttrs.saveFile(in, localFile);
286
            }
287
        });
288
        if (!res.isSuccess())
289
            throw new IOException("Couldn't retrieve file " + fileName);
290
        return !missing.get();
291
    }
292
 
177 ilm 293
    public Response deleteFile(final String path, final String fileName) throws IOException {
180 ilm 294
        if (path == null) {
295
            throw new IllegalArgumentException("null path");
296
        }
297
        if (fileName == null) {
298
            throw new IllegalArgumentException("null fileName");
299
        }
174 ilm 300
        final HttpsURLConnection con = openConnection("/delete");
180 ilm 301
        return checkResponseCode(send(con, NetUtils.urlEncode("rn", fileName, "rp", path), false));
174 ilm 302
    }
303
 
177 ilm 304
    public final Response renameFile(final String path, final String fileName, final String newFileName) throws IOException {
182 ilm 305
        return this.renameFile(path, fileName, path, newFileName);
177 ilm 306
    }
174 ilm 307
 
177 ilm 308
    public final Response renameFile(final String path, final String fileName, final String newPath, final String newFileName) throws IOException {
180 ilm 309
        if (path == null) {
310
            throw new IllegalArgumentException("null path");
311
        }
312
        if (fileName == null) {
313
            throw new IllegalArgumentException("null fileName");
314
        }
315
        if (newPath == null) {
316
            throw new IllegalArgumentException("null newPath");
317
        }
318
        if (newFileName == null) {
319
            throw new IllegalArgumentException("null newFileName");
320
        }
177 ilm 321
        final HttpsURLConnection con = openConnection("/rename");
180 ilm 322
        return checkResponseCode(send(con, NetUtils.urlEncode("rn", fileName, "rp", path, "newPath", newPath, "newName", newFileName), false));
177 ilm 323
    }
324
 
325
    public Response sendFile(String path, File localFile) throws IOException {
326
        return this.sendFile(path, localFile, false);
327
    }
328
 
329
    public Response sendFile(String path, File localFile, final boolean overwrite) throws IOException {
180 ilm 330
        if (path == null) {
331
            throw new IllegalArgumentException("null path");
332
        }
333
 
177 ilm 334
        final long size = localFile.length();
335
        if (size >= Integer.MAX_VALUE)
336
            throw new OutOfMemoryError("Required array size too large : " + size);
337
        final ByteArrayOutputStream ba = new ByteArrayOutputStream((int) size);
338
        final byte[] newsha256;
339
        // compute digest at the same time to protect against race conditions
340
        try (final InputStream ins = new BufferedInputStream(new FileInputStream(localFile))) {
341
            newsha256 = MessageDigestUtils.getHash(MessageDigestUtils.getSHA256(), ins, ba);
342
        }
343
 
174 ilm 344
        final HttpsURLConnection con = openConnection("/put");
345
        // We use Base64 because headers are not supporting UTF8
346
        con.setRequestProperty("X_FILENAME_B64", Base64.getEncoder().encodeToString(localFile.getName().getBytes(StandardCharsets.UTF_8)));
347
        con.setRequestProperty("X_PATH_B64", Base64.getEncoder().encodeToString(path.getBytes(StandardCharsets.UTF_8)));
177 ilm 348
        con.setRequestProperty("X-OVERWRITE", Boolean.toString(overwrite));
174 ilm 349
        con.setRequestProperty("X_FILESIZE", String.valueOf(size));
350
        con.setRequestProperty("X_SHA256", HashWriter.bytesToHex(newsha256));
351
        con.setRequestProperty("X-Last-Modified-ms", String.valueOf(localFile.lastModified()));
352
 
177 ilm 353
        return checkResponseCode(send(con, ba.toByteArray(), true));
174 ilm 354
    }
182 ilm 355
 
356
    public Response createDir(String path, String fileName) throws IOException {
357
        if (path == null) {
358
            throw new IllegalArgumentException("null path");
359
        }
360
        if (fileName == null) {
361
            throw new IllegalArgumentException("null fileName");
362
        }
363
        final HttpsURLConnection con = openConnection("/mkdir");
364
        return checkResponseCode(send(con, NetUtils.urlEncode("rn", fileName, "rp", path), false));
365
    }
174 ilm 366
}