1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.app.web.admin.storage;
17
18 import static org.codelibs.core.stream.StreamUtil.split;
19
20 import java.io.ByteArrayOutputStream;
21 import java.io.InputStream;
22 import java.net.URLEncoder;
23 import java.util.ArrayList;
24 import java.util.Base64;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28
29 import org.apache.logging.log4j.LogManager;
30 import org.apache.logging.log4j.Logger;
31 import org.codelibs.core.lang.StringUtil;
32 import org.codelibs.fess.annotation.Secured;
33 import org.codelibs.fess.app.web.base.FessAdminAction;
34 import org.codelibs.fess.crawler.Constants;
35 import org.codelibs.fess.exception.StorageException;
36 import org.codelibs.fess.mylasta.direction.FessConfig;
37 import org.codelibs.fess.storage.StorageClient;
38 import org.codelibs.fess.storage.StorageClientFactory;
39 import org.codelibs.fess.storage.StorageItem;
40 import org.codelibs.fess.util.ComponentUtil;
41 import org.codelibs.fess.util.RenderDataUtil;
42 import org.dbflute.optional.OptionalThing;
43 import org.lastaflute.web.Execute;
44 import org.lastaflute.web.response.ActionResponse;
45 import org.lastaflute.web.response.HtmlResponse;
46 import org.lastaflute.web.response.StreamResponse;
47 import org.lastaflute.web.ruts.multipart.MultipartFormFile;
48 import org.lastaflute.web.ruts.process.ActionRuntime;
49
50
51
52
53
54 public class AdminStorageAction extends FessAdminAction {
55
56
57
58
59 public AdminStorageAction() {
60 super();
61 }
62
63
64 public static final String ROLE = "admin-storage";
65
66 private static final Logger logger = LogManager.getLogger(AdminStorageAction.class);
67
68 @Override
69 protected void setupHtmlData(final ActionRuntime runtime) {
70 super.setupHtmlData(runtime);
71 runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameStorage()));
72 }
73
74 @Override
75 protected String getActionRole() {
76 return ROLE;
77 }
78
79
80
81
82
83
84 @Execute
85 @Secured({ ROLE, ROLE + VIEW })
86 public HtmlResponse index() {
87 saveToken();
88 return asListHtml(StringUtil.EMPTY);
89 }
90
91
92
93
94
95
96
97 @Execute
98 @Secured({ ROLE, ROLE + VIEW })
99 public ActionResponse list(final OptionalThing<String> id) {
100 saveToken();
101 return id.filter(StringUtil::isNotBlank).map(s -> asListHtml(decodePath(s))).orElse(redirect(getClass()));
102 }
103
104
105
106
107
108
109
110 @Execute
111 @Secured({ ROLE })
112 public HtmlResponse upload(final ItemForm form) {
113 validate(form, messages -> {}, () -> asListHtml(form.path));
114 if (form.uploadFile == null) {
115 throwValidationError(messages -> messages.addErrorsStorageNoUploadFile(GLOBAL), () -> asListHtml(form.path));
116 }
117 verifyToken(() -> asListHtml(form.path));
118 try {
119 uploadObject(getObjectName(form.path, form.uploadFile.getFileName()), form.uploadFile);
120 } catch (final StorageException e) {
121 logger.warn("Failed to upload {}", form.uploadFile.getFileName(), e);
122 throwValidationError(messages -> messages.addErrorsStorageFileUploadFailure(GLOBAL, form.uploadFile.getFileName()),
123 () -> asListHtml(encodeId(form.path)));
124
125 }
126 saveInfo(messages -> messages.addSuccessUploadFileToStorage(GLOBAL, form.uploadFile.getFileName()));
127 return redirectWith(getClass(), moreUrl("list/" + encodeId(form.path)));
128 }
129
130
131
132
133
134
135
136 @Execute
137 @Secured({ ROLE, ROLE + VIEW })
138 public ActionResponse download(final String id) {
139 final PathInfo pi = convertToItem(id);
140 if (StringUtil.isEmpty(pi.getName())) {
141 throwValidationError(messages -> messages.addErrorsStorageFileNotFound(GLOBAL), () -> asListHtml(encodeId(pi.getPath())));
142 }
143 final StreamResponse response = new StreamResponse(StringUtil.EMPTY);
144 final String name = pi.getName();
145 final String encodedName = URLEncoder.encode(name, Constants.UTF_8_CHARSET).replace("+", "%20");
146 response.header("Content-Disposition", "attachment; filename=\"" + name + "\"; filename*=utf-8''" + encodedName);
147 response.header("Pragma", "no-cache");
148 response.header("Cache-Control", "no-cache");
149 response.header("Expires", "Thu, 01 Dec 1994 16:00:00 GMT");
150 response.contentTypeOctetStream();
151 return response.stream(out -> {
152 try {
153 downloadObject(getObjectName(pi.getPath(), pi.getName()), out);
154 } catch (final StorageException e) {
155 logger.warn("Failed to download {}", pi.getName(), e);
156 throwValidationError(messages -> messages.addErrorsStorageFileDownloadFailure(GLOBAL, pi.getName()),
157 () -> asListHtml(encodeId(pi.getPath())));
158 }
159 });
160 }
161
162
163
164
165
166
167
168 @Execute
169 @Secured({ ROLE })
170 public HtmlResponse delete(final String id) {
171 final PathInfo pi = convertToItem(id);
172 if (StringUtil.isEmpty(pi.getName())) {
173 throwValidationError(messages -> messages.addErrorsStorageFileNotFound(GLOBAL), () -> asListHtml(encodeId(pi.getPath())));
174 }
175 final String objectName = getObjectName(pi.getPath(), pi.getName());
176 try {
177 deleteObject(objectName);
178 } catch (final StorageException e) {
179 logger.warn("Failed to delete {}", pi.getName(), e);
180 throwValidationError(messages -> messages.addErrorsFailedToDeleteFile(GLOBAL, pi.getName()),
181 () -> asListHtml(encodeId(pi.getPath())));
182 }
183 saveInfo(messages -> messages.addSuccessDeleteFile(GLOBAL, pi.getName()));
184 return redirectWith(getClass(), moreUrl("list/" + encodeId(pi.getPath())));
185 }
186
187
188
189
190
191
192
193 @Execute
194 @Secured({ ROLE })
195 public HtmlResponse createDir(final ItemForm form) {
196 validate(form, messages -> {}, () -> asListHtml(form.path));
197 if (StringUtil.isBlank(form.name)) {
198 throwValidationError(messages -> messages.addErrorsStorageDirectoryNameIsInvalid(GLOBAL), () -> asListHtml(form.path));
199 }
200 return redirectWith(getClass(), moreUrl("list/" + encodeId(getObjectName(form.path, form.name))));
201 }
202
203
204
205
206
207
208
209 @Execute
210 @Secured({ ROLE })
211 public HtmlResponse editTags(final TagForm form) {
212 validate(form, messages -> {}, () -> asEditTagsHtml(form.path, form.name));
213 saveToken();
214 return asEditTagsHtml(form.path, form.name);
215 }
216
217
218
219
220
221
222
223 @Execute
224 @Secured({ ROLE })
225 public HtmlResponse updateTags(final TagForm form) {
226 validate(form, messages -> {}, () -> asEditTagsHtml(form.path, form.name));
227 final String objectName = getObjectName(form.path, form.name);
228 try {
229 updateObjectTags(objectName, form.tags);
230 } catch (final StorageException e) {
231 logger.warn("Failed to update tags in {}", form.path, e);
232 throwValidationError(messages -> messages.addErrorsStorageTagsUpdateFailure(GLOBAL, objectName),
233 () -> asEditTagsHtml(form.path, form.name));
234 }
235 saveInfo(messages -> messages.addSuccessUpdateStorageTags(GLOBAL, objectName));
236 return redirectWith(getClass(), moreUrl("list/" + encodeId(form.path)));
237 }
238
239
240
241
242
243
244
245
246 public static void updateObjectTags(final String objectName, final Map<String, String> tagItems) {
247 final Map<String, String> tags = new HashMap<>();
248 tagItems.keySet().stream().filter(s -> s.startsWith("name")).forEach(nameKey -> {
249 final String valueKey = nameKey.replace("name", "value");
250 final String name = tagItems.get(nameKey);
251 if (StringUtil.isNotBlank(name)) {
252 tags.put(name, tagItems.get(valueKey));
253 }
254 });
255 if (logger.isDebugEnabled()) {
256 logger.debug("Tags updated: from={}, to={}", tagItems, tags);
257 }
258 try (StorageClient client = StorageClientFactory.createClient()) {
259 client.setObjectTags(objectName, tags);
260 } catch (final Exception e) {
261 throw new StorageException("Failed to update tags for " + objectName, e);
262 }
263 }
264
265
266
267
268
269
270
271
272 public static Map<String, String> getObjectTags(final String objectName) {
273 try (StorageClient client = StorageClientFactory.createClient()) {
274 return client.getObjectTags(objectName);
275 } catch (final Exception e) {
276 throw new StorageException("Failed to get tags from " + objectName, e);
277 }
278 }
279
280
281
282
283
284
285
286
287 public static void uploadObject(final String objectName, final MultipartFormFile uploadFile) {
288 try (final InputStream in = uploadFile.getInputStream(); final StorageClient client = StorageClientFactory.createClient()) {
289 client.uploadObject(objectName, in, uploadFile.getFileSize(), "application/octet-stream");
290 } catch (final Exception e) {
291 throw new StorageException("Failed to upload " + objectName, e);
292 }
293 }
294
295
296
297
298
299
300
301
302 public static void downloadObject(final String objectName, final org.lastaflute.web.servlet.request.stream.WrittenStreamOut out) {
303 try (final StorageClient client = StorageClientFactory.createClient()) {
304 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
305 client.downloadObject(objectName, baos);
306 out.write(new java.io.ByteArrayInputStream(baos.toByteArray()));
307 } catch (final Exception e) {
308 throw new StorageException("Failed to download " + objectName, e);
309 }
310 }
311
312
313
314
315
316
317
318 public static void deleteObject(final String objectName) {
319 try (final StorageClient client = StorageClientFactory.createClient()) {
320 client.deleteObject(objectName);
321 } catch (final Exception e) {
322 throw new StorageException("Failed to delete " + objectName, e);
323 }
324 }
325
326
327
328
329
330
331
332 public static List<Map<String, Object>> getFileItems(final String prefix) {
333 final FessConfig fessConfig = ComponentUtil.getFessConfig();
334 final List<Map<String, Object>> list = new ArrayList<>();
335 final List<Map<String, Object>> fileList = new ArrayList<>();
336
337 try (final StorageClient client = StorageClientFactory.createClient(fessConfig)) {
338
339 client.ensureBucketExists();
340
341 final List<StorageItem> items = client.listObjects(prefix, fessConfig.getStorageMaxItemsInPageAsInteger());
342
343 for (final StorageItem item : items) {
344 final Map<String, Object> map = new HashMap<>();
345 map.put("id", item.getEncodedId());
346 map.put("path", item.getPath());
347 map.put("name", item.getName());
348 map.put("hashCode", item.hashCode());
349 map.put("size", item.getSize());
350 map.put("directory", item.isDirectory());
351 if (!item.isDirectory()) {
352 map.put("lastModified", item.getLastModified());
353 fileList.add(map);
354 } else {
355 list.add(map);
356 }
357 }
358 } catch (final Exception e) {
359 if (logger.isDebugEnabled()) {
360 logger.debug("Failed to access storage endpoint: {}", fessConfig.getStorageEndpoint(), e);
361 }
362 }
363
364 list.addAll(fileList);
365 return list;
366 }
367
368
369
370
371
372
373
374 private static String getName(final String objectName) {
375 final String[] values = objectName.split("/");
376 if (values.length == 0) {
377 return StringUtil.EMPTY;
378 }
379 return values[values.length - 1];
380 }
381
382
383
384
385
386
387
388 public static String decodePath(final String id) {
389 final PathInfo pi = convertToItem(id);
390 if (StringUtil.isEmpty(pi.getPath()) && StringUtil.isEmpty(pi.getName())) {
391 return StringUtil.EMPTY;
392 }
393 if (StringUtil.isEmpty(pi.getPath())) {
394 return pi.getName();
395 }
396 return pi.getPath() + "/" + pi.getName();
397 }
398
399
400
401
402
403
404
405 public static PathInfo convertToItem(final String id) {
406 final String value = decodeId(id);
407 final String[] values = split(value, "/").get(stream -> stream.filter(StringUtil::isNotEmpty).toArray(n -> new String[n]));
408 if (values.length == 0) {
409
410 return new PathInfo(StringUtil.EMPTY, StringUtil.EMPTY);
411 }
412 if (values.length == 1) {
413 return new PathInfo(StringUtil.EMPTY, values[0]);
414 }
415 final StringBuilder buf = new StringBuilder();
416 for (int i = 0; i < values.length - 1; i++) {
417 if (buf.length() > 0) {
418 buf.append('/');
419 }
420 buf.append(values[i]);
421 }
422 return new PathInfo(buf.toString(), values[values.length - 1]);
423 }
424
425
426
427
428
429
430
431 protected static String createParentId(final String prefix) {
432 if (prefix == null) {
433 return StringUtil.EMPTY;
434 }
435 final String[] values = prefix.split("/");
436 if (values.length > 1) {
437 final StringBuilder buf = new StringBuilder();
438 for (int i = 0; i < values.length - 1; i++) {
439 if (buf.length() > 0) {
440 buf.append('/');
441 }
442 buf.append(values[i]);
443 }
444 return encodeId(buf.toString());
445 }
446 return StringUtil.EMPTY;
447 }
448
449
450
451
452
453
454
455 protected static List<Map<String, String>> createPathItems(final String prefix) {
456 final List<Map<String, String>> list = new ArrayList<>();
457 final StringBuilder buf = new StringBuilder();
458 split(prefix, "/").of(stream -> stream.filter(StringUtil::isNotEmpty).forEach(s -> {
459 if (buf.length() > 0) {
460 buf.append('/');
461 }
462 buf.append(s);
463 final Map<String, String> map = new HashMap<>();
464 map.put("id", encodeId(buf.toString()));
465 map.put("name", s);
466 list.add(map);
467 }));
468 return list;
469 }
470
471
472
473
474
475
476
477 protected static String getPathPrefix(final String path) {
478 return StringUtil.isEmpty(path) ? StringUtil.EMPTY : path + "/";
479 }
480
481
482
483
484
485
486
487
488 public static String getObjectName(final String path, final String name) {
489 return getPathPrefix(path) + name;
490 }
491
492
493
494
495
496
497
498 protected static String encodeId(final String objectName) {
499 if (objectName == null) {
500 return StringUtil.EMPTY;
501 }
502 return new String(Base64.getUrlEncoder().encode(objectName.getBytes(Constants.UTF_8_CHARSET)), Constants.UTF_8_CHARSET);
503 }
504
505
506
507
508
509
510
511 protected static String decodeId(final String id) {
512 if (id == null) {
513 return StringUtil.EMPTY;
514 }
515 return new String(Base64.getUrlDecoder().decode(id.getBytes(Constants.UTF_8_CHARSET)), Constants.UTF_8_CHARSET);
516 }
517
518 private HtmlResponse asListHtml(final String path) {
519 return asHtml(path_AdminStorage_AdminStorageJsp).useForm(ItemForm.class).renderWith(data -> {
520 RenderDataUtil.register(data, "endpoint", fessConfig.getStorageEndpoint());
521 RenderDataUtil.register(data, "bucket", fessConfig.getStorageBucket());
522 RenderDataUtil.register(data, "path", path);
523 RenderDataUtil.register(data, "pathItems", createPathItems(path));
524 RenderDataUtil.register(data, "parentId", createParentId(path));
525 RenderDataUtil.register(data, "fileItems", getFileItems(path));
526 });
527 }
528
529 private HtmlResponse asEditTagsHtml(final String path, final String name) {
530 return asHtml(path_AdminStorage_AdminStorageTagEditJsp).renderWith(data -> {
531 RenderDataUtil.register(data, "endpoint", fessConfig.getStorageEndpoint());
532 RenderDataUtil.register(data, "bucket", fessConfig.getStorageBucket());
533 RenderDataUtil.register(data, "pathItems", createPathItems(path));
534 RenderDataUtil.register(data, "parentId", encodeId(path));
535 RenderDataUtil.register(data, "path", path);
536 RenderDataUtil.register(data, "name", name);
537 final Map<String, String> tags = new HashMap<>();
538 getObjectTags(getObjectName(path, name)).entrySet().forEach(e -> {
539 final int index = tags.size() / 2 + 1;
540 tags.put("name" + index, e.getKey());
541 tags.put("value" + index, e.getValue());
542 });
543 RenderDataUtil.register(data, "savedTags", tags);
544 });
545 }
546
547
548
549
550 public static class PathInfo {
551 private final String path;
552 private final String name;
553
554
555
556
557
558
559
560 public PathInfo(final String path, final String name) {
561 this.path = path;
562 this.name = name;
563 }
564
565
566
567
568
569
570 public String getPath() {
571 return path;
572 }
573
574
575
576
577
578
579 public String getName() {
580 return name;
581 }
582 }
583 }