1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.app.web.admin.backup;
17
18 import static org.codelibs.core.stream.StreamUtil.stream;
19
20 import java.io.BufferedReader;
21 import java.io.BufferedWriter;
22 import java.io.ByteArrayInputStream;
23 import java.io.ByteArrayOutputStream;
24 import java.io.File;
25 import java.io.FileInputStream;
26 import java.io.FileOutputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.io.InputStreamReader;
30 import java.io.OutputStream;
31 import java.io.OutputStreamWriter;
32 import java.io.Writer;
33 import java.nio.file.Files;
34 import java.nio.file.Path;
35 import java.time.LocalDateTime;
36 import java.time.ZoneId;
37 import java.time.format.DateTimeFormatter;
38 import java.util.Arrays;
39 import java.util.HashMap;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.concurrent.atomic.AtomicBoolean;
43 import java.util.function.Consumer;
44 import java.util.stream.Collectors;
45
46 import org.apache.commons.text.StringEscapeUtils;
47 import org.apache.logging.log4j.LogManager;
48 import org.apache.logging.log4j.Logger;
49 import org.codelibs.core.exception.IORuntimeException;
50 import org.codelibs.core.io.CopyUtil;
51 import org.codelibs.core.lang.StringUtil;
52 import org.codelibs.core.misc.Pair;
53 import org.codelibs.curl.CurlResponse;
54 import org.codelibs.fess.Constants;
55 import org.codelibs.fess.annotation.Secured;
56 import org.codelibs.fess.app.web.base.FessAdminAction;
57 import org.codelibs.fess.helper.SystemHelper;
58 import org.codelibs.fess.mylasta.direction.FessConfig;
59 import org.codelibs.fess.opensearch.config.exbhv.FileConfigBhv;
60 import org.codelibs.fess.opensearch.config.exbhv.LabelTypeBhv;
61 import org.codelibs.fess.opensearch.config.exbhv.WebConfigBhv;
62 import org.codelibs.fess.opensearch.log.exbhv.ClickLogBhv;
63 import org.codelibs.fess.opensearch.log.exbhv.FavoriteLogBhv;
64 import org.codelibs.fess.opensearch.log.exbhv.SearchLogBhv;
65 import org.codelibs.fess.opensearch.log.exbhv.UserInfoBhv;
66 import org.codelibs.fess.opensearch.log.exentity.ClickLog;
67 import org.codelibs.fess.opensearch.log.exentity.FavoriteLog;
68 import org.codelibs.fess.opensearch.log.exentity.SearchLog;
69 import org.codelibs.fess.opensearch.log.exentity.UserInfo;
70 import org.codelibs.fess.util.ComponentUtil;
71 import org.codelibs.fess.util.GsaConfigParser;
72 import org.codelibs.fess.util.RenderDataUtil;
73 import org.codelibs.fess.util.ResourceUtil;
74 import org.codelibs.fess.util.SearchEngineUtil;
75 import org.dbflute.bhv.readable.EntityRowHandler;
76 import org.lastaflute.core.magic.async.AsyncManager;
77 import org.lastaflute.web.Execute;
78 import org.lastaflute.web.response.ActionResponse;
79 import org.lastaflute.web.response.HtmlResponse;
80 import org.lastaflute.web.response.StreamResponse;
81 import org.lastaflute.web.ruts.process.ActionRuntime;
82 import org.xml.sax.InputSource;
83
84 import com.fasterxml.jackson.core.type.TypeReference;
85 import com.fasterxml.jackson.databind.ObjectMapper;
86
87 import jakarta.annotation.Resource;
88
89
90
91
92
93 public class AdminBackupAction extends FessAdminAction {
94
95
96
97
98 public AdminBackupAction() {
99 super();
100 }
101
102
103
104
105 public static final String ROLE = "admin-backup";
106
107 private static final Logger logger = LogManager.getLogger(AdminBackupAction.class);
108
109
110
111
112 public static final String NDJSON_EXTENTION = ".ndjson";
113
114 private static final DateTimeFormatter ISO_8601_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS");
115
116 @Resource
117 private AsyncManager asyncManager;
118
119 @Resource
120 private WebConfigBhv webConfigBhv;
121
122 @Resource
123 private FileConfigBhv fileConfigBhv;
124
125 @Resource
126 private LabelTypeBhv labelTypeBhv;
127
128 @Override
129 protected void setupHtmlData(final ActionRuntime runtime) {
130 super.setupHtmlData(runtime);
131 runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameBackup()));
132 }
133
134 @Override
135 protected String getActionRole() {
136 return ROLE;
137 }
138
139
140
141
142
143 @Execute
144 @Secured({ ROLE, ROLE + VIEW })
145 public HtmlResponse index() {
146 saveToken();
147 return asListHtml();
148 }
149
150
151
152
153
154
155 @Execute
156 @Secured({ ROLE })
157 public HtmlResponse upload(final UploadForm form) {
158 validate(form, messages -> {}, this::asListHtml);
159 verifyToken(this::asListHtml);
160 final String fileName = form.bulkFile.getFileName();
161
162 if (logger.isDebugEnabled()) {
163 logger.debug("Backup file upload initiated: fileName={}", fileName);
164 }
165
166 final File tempFile = ComponentUtil.getSystemHelper().createTempFile("fess_restore_", ".tmp");
167 try (final InputStream in = form.bulkFile.getInputStream(); final OutputStream out = new FileOutputStream(tempFile)) {
168 CopyUtil.copy(in, out);
169 asyncImport(fileName, tempFile);
170
171 if (logger.isInfoEnabled()) {
172 logger.info("Backup file uploaded successfully and queued for import: fileName={}, tempFile={}", fileName,
173 tempFile.getAbsolutePath());
174 }
175 } catch (final IOException e) {
176 logger.warn("Failed to upload backup file: fileName={}, error={}", fileName, e.getMessage(), e);
177 if (tempFile.exists() && !tempFile.delete()) {
178 logger.warn("Failed to delete temporary file: {}", tempFile.getAbsolutePath());
179 }
180 throwValidationError(messages -> messages.addErrorsFileIsNotSupported(GLOBAL, fileName), this::asListHtml);
181 }
182 saveInfo(messages -> messages.addSuccessBulkProcessStarted(GLOBAL));
183 return redirect(getClass());
184 }
185
186
187
188
189
190
191 protected void asyncImport(final String fileName, final File tempFile) {
192 final int fileType;
193 if (fileName.startsWith("system") && fileName.endsWith(".properties")) {
194 fileType = 1;
195 } else if (fileName.startsWith("gsa") && fileName.endsWith(".xml")) {
196 fileType = 2;
197 } else if (fileName.endsWith(".bulk")) {
198 fileType = 3;
199 } else if (fileName.startsWith("fess") && fileName.endsWith(".json")) {
200 fileType = 4;
201 } else if (fileName.startsWith("doc") && fileName.endsWith(".json")) {
202 fileType = 5;
203 } else {
204 throwValidationError(messages -> messages.addErrorsFileIsNotSupported(GLOBAL, fileName), this::asListHtml);
205 return;
206 }
207
208 asyncManager.async(() -> {
209 switch (fileType) {
210 case 1:
211 importSystemProperties(fileName, tempFile);
212 break;
213 case 2:
214 importGsaXml(fileName, tempFile);
215 break;
216 case 3:
217 importBulk(fileName, tempFile);
218 break;
219 case 4:
220 importFessJson(fileName, tempFile);
221 break;
222 case 5:
223 importDocJson(fileName, tempFile);
224 break;
225 default:
226 break;
227 }
228 });
229 }
230
231 private void importBulk(final String fileName, final File tempFile) {
232 if (logger.isDebugEnabled()) {
233 logger.debug("Bulk data import started: fileName={}", fileName);
234 }
235
236 final ObjectMapper mapper = new ObjectMapper();
237 final AtomicBoolean resetJobs = new AtomicBoolean(false);
238 try (CurlResponse response = ComponentUtil.getCurlHelper().post("/_bulk").onConnect((req, con) -> {
239 con.setDoOutput(true);
240 try (final BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(tempFile)));
241 final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(con.getOutputStream(), Constants.CHARSET_UTF_8))) {
242 String line;
243 while ((line = br.readLine()) != null) {
244 if (StringUtil.isNotBlank(line)) {
245 final Map<String, Map<String, String>> dataObj;
246 if (line.contains("\"_index\"") || line.contains("\"_type\"")) {
247 dataObj = parseObject(mapper, line);
248 } else {
249 dataObj = null;
250 }
251 if (dataObj != null) {
252 final Map<String, String> indexObj = dataObj.get("index");
253 if (indexObj != null) {
254 if (indexObj.containsKey("_type")) {
255 indexObj.remove("_type");
256 }
257 final String index = indexObj.get("_index");
258 if (index != null) {
259 if (index.startsWith(".fess")) {
260 indexObj.put("_index", index.substring(1));
261 }
262 if (index.endsWith("scheduled_job")) {
263 resetJobs.set(true);
264 }
265 }
266 bw.write(mapper.writeValueAsString(dataObj));
267 } else {
268 bw.write(line);
269 }
270 } else {
271 bw.write(line);
272 }
273 }
274 bw.write("\n");
275 }
276 bw.flush();
277 } catch (IOException e) {
278 throw new IORuntimeException(e);
279 }
280 }).execute()) {
281 if (logger.isDebugEnabled()) {
282 logger.debug("Bulk Response:\n{}", response.getContentAsString());
283 }
284 systemHelper.reloadConfiguration(resetJobs.get());
285
286 if (logger.isInfoEnabled()) {
287 logger.info("Bulk data import completed successfully: fileName={}, resetJobs={}", fileName, resetJobs.get());
288 }
289 } catch (final Exception e) {
290 logger.warn("Failed to import bulk file: fileName={}, error={}", fileName, e.getMessage(), e);
291 } finally {
292 deleteTempFile(tempFile);
293 }
294 }
295
296 private void importGsaXml(final String fileName, final File tempFile) {
297 if (logger.isDebugEnabled()) {
298 logger.debug("GSA XML import started: fileName={}", fileName);
299 }
300
301 final GsaConfigParser configParser = ComponentUtil.getComponent(GsaConfigParser.class);
302 try (final InputStream in = new FileInputStream(tempFile)) {
303 configParser.parse(new InputSource(in));
304 } catch (final IOException e) {
305 logger.warn("Failed to read GSA XML file: fileName={}, error={}", fileName, e.getMessage(), e);
306 deleteTempFile(tempFile);
307 return;
308 }
309
310 try {
311 configParser.getWebConfig().ifPresent(c -> webConfigBhv.insert(c));
312 configParser.getFileConfig().ifPresent(c -> fileConfigBhv.insert(c));
313 labelTypeBhv.batchInsert(Arrays.stream(configParser.getLabelTypes()).collect(Collectors.toList()));
314
315 if (logger.isInfoEnabled()) {
316 logger.info("GSA XML import completed successfully: fileName={}", fileName);
317 }
318 } catch (final Exception e) {
319 logger.warn("Failed to insert GSA XML data into database: fileName={}, error={}", fileName, e.getMessage(), e);
320 } finally {
321 deleteTempFile(tempFile);
322 }
323 }
324
325 private void importSystemProperties(final String fileName, final File tempFile) {
326 if (logger.isDebugEnabled()) {
327 logger.debug("System properties import started: fileName={}", fileName);
328 }
329
330 try (final InputStream in = new FileInputStream(tempFile)) {
331 ComponentUtil.getSystemProperties().load(in);
332
333 if (logger.isInfoEnabled()) {
334 logger.info("System properties import completed successfully: fileName={}", fileName);
335 }
336 } catch (final IOException e) {
337 logger.warn("Failed to import system.properties file: fileName={}, error={}", fileName, e.getMessage(), e);
338 } finally {
339 deleteTempFile(tempFile);
340 }
341 }
342
343 private void importFessJson(final String fileName, final File tempFile) {
344 if (logger.isDebugEnabled()) {
345 logger.debug("Fess JSON import started: fileName={}", fileName);
346 }
347
348 try (final InputStream in = new FileInputStream(tempFile); final OutputStream out = Files.newOutputStream(getFessJsonPath())) {
349 CopyUtil.copy(in, out);
350
351 if (logger.isInfoEnabled()) {
352 logger.info("Fess JSON import completed successfully: fileName={}", fileName);
353 }
354 } catch (final IOException e) {
355 logger.warn("Failed to import fess.json file: fileName={}, error={}", fileName, e.getMessage(), e);
356 } finally {
357 deleteTempFile(tempFile);
358 }
359 }
360
361 private void importDocJson(final String fileName, final File tempFile) {
362 if (logger.isDebugEnabled()) {
363 logger.debug("Doc JSON import started: fileName={}", fileName);
364 }
365
366 try (final InputStream in = new FileInputStream(tempFile); final OutputStream out = Files.newOutputStream(getDocJsonPath())) {
367 CopyUtil.copy(in, out);
368
369 if (logger.isInfoEnabled()) {
370 logger.info("Doc JSON import completed successfully: fileName={}", fileName);
371 }
372 } catch (final IOException e) {
373 logger.warn("Failed to import doc.json file: fileName={}, error={}", fileName, e.getMessage(), e);
374 } finally {
375 deleteTempFile(tempFile);
376 }
377 }
378
379 private Map<String, Map<String, String>> parseObject(final ObjectMapper mapper, final String line) {
380 try {
381 return mapper.readValue(line, new TypeReference<Map<String, Map<String, String>>>() {
382 });
383 } catch (final Exception e) {
384 if (logger.isDebugEnabled()) {
385 logger.debug("Failed to parse {}", line, e);
386 }
387 return null;
388 }
389 }
390
391
392
393
394
395
396 @Execute
397 @Secured({ ROLE, ROLE + VIEW })
398 public ActionResponse download(final String id) {
399 if (logger.isDebugEnabled()) {
400 logger.debug("Backup download requested: id={}", id);
401 }
402
403 if (stream(fessConfig.getIndexBackupAllTargets()).get(stream -> stream.anyMatch(s -> s.equals(id)))) {
404 if ("system.properties".equals(id)) {
405 return asStream(id).contentTypeOctetStream().stream(out -> {
406 try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
407 ComponentUtil.getSystemProperties().store(baos, id);
408 try (final InputStream in = new ByteArrayInputStream(baos.toByteArray())) {
409 out.write(in);
410 }
411 }
412 });
413 }
414 if (id.endsWith(NDJSON_EXTENTION)) {
415 final String name = id.substring(0, id.length() - NDJSON_EXTENTION.length());
416 if ("search_log".equals(name)) {
417 return writeNdjsonResponse(id, getSearchLogNdjsonWriteCall());
418 }
419 if ("user_info".equals(name)) {
420 return writeNdjsonResponse(id, getUserInfoNdjsonWriteCall());
421 }
422 if ("click_log".equals(name)) {
423 return writeNdjsonResponse(id, getClickLogNdjsonWriteCall());
424 }
425 if ("favorite_log".equals(name)) {
426 return writeNdjsonResponse(id, getFavoriteLogNdjsonWriteCall());
427 }
428 } else if ("fess.json".equals(id)) {
429 return asStream(id).contentTypeOctetStream().stream(out -> {
430 final Path fessJsonPath = getFessJsonPath();
431 try (final InputStream in = Files.newInputStream(fessJsonPath)) {
432 out.write(in);
433 }
434 });
435 } else if ("doc.json".equals(id)) {
436 return asStream(id).contentTypeOctetStream().stream(out -> {
437 final Path fessJsonPath = getDocJsonPath();
438 try (final InputStream in = Files.newInputStream(fessJsonPath)) {
439 out.write(in);
440 }
441 });
442 } else {
443 String index;
444 final String filename;
445 if (id.endsWith(".bulk")) {
446 index = id.substring(0, id.length() - 5);
447 filename = id;
448 } else {
449 index = id;
450 filename = id + ".bulk";
451 }
452 if ("fess_config".equals(index)) {
453 index = fessConfig.getIndexConfigIndex();
454 } else if ("fess_user".equals(index)) {
455 index = fessConfig.getIndexUserIndex();
456 } else if ("fess_basic_config".equals(index) && !"fess_config".equals(fessConfig.getIndexConfigIndex())) {
457 index = "basic_" + fessConfig.getIndexConfigIndex();
458 }
459 final String alias = index;
460 return asStream(filename).contentTypeOctetStream().stream(out -> {
461 try (final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out.stream(), Constants.CHARSET_UTF_8))) {
462 SearchEngineUtil.scroll(alias, hit -> {
463 try {
464 writer.write("{\"index\":{\"_index\":\"" + hit.getIndex() + "\",\"_id\":\""
465 + StringEscapeUtils.escapeJson(hit.getId()) + "\"}}\n");
466 writer.write(hit.getSourceAsString());
467 writer.write("\n");
468 } catch (final IOException e) {
469 throw new IORuntimeException(e);
470 }
471 return true;
472 });
473 writer.flush();
474 }
475 });
476 }
477 }
478 throwValidationError(messages -> messages.addErrorsCouldNotFindBackupIndex(GLOBAL), this::asListHtml);
479 return redirect(getClass());
480 }
481
482 private Path getDocJsonPath() {
483 return ResourceUtil.getClassesPath("fess_indices", "fess", "doc.json");
484 }
485
486 private Path getFessJsonPath() {
487 return ResourceUtil.getClassesPath("fess_indices", "fess.json");
488 }
489
490 private StreamResponse writeNdjsonResponse(final String id, final Consumer<Writer> writeCall) {
491 return asStream(id)
492 .header("Pragma", "no-cache")
493 .header("Cache-Control", "no-cache")
494 .header("Expires", "Thu, 01 Dec 1994 16:00:00 GMT")
495 .header("Content-Type", "application/x-ndjson")
496 .stream(out -> {
497 try (final Writer writer = new BufferedWriter(new OutputStreamWriter(out.stream(), Constants.CHARSET_UTF_8))) {
498 writeCall.accept(writer);
499 writer.flush();
500 } catch (final Exception e) {
501 logger.warn("Failed to write {} to response.", id, e);
502 }
503 });
504 }
505
506 private static StringBuilder appendJson(final String field, final Object value, final StringBuilder buf) {
507 buf.append('"').append(StringEscapeUtils.escapeJson(field)).append('"').append(':');
508 if (value == null) {
509 buf.append("null");
510 } else if (value instanceof LocalDateTime) {
511 final String format =
512 ((LocalDateTime) value).atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneId.of("UTC")).format(ISO_8601_FORMATTER);
513 buf.append('"').append(StringEscapeUtils.escapeJson(format)).append('"');
514 } else if (value instanceof String[]) {
515 final String json = Arrays.stream((String[]) value)
516 .map(s -> "\"" + StringEscapeUtils.escapeJson(s) + "\"")
517 .collect(Collectors.joining(","));
518 buf.append('[').append(json).append(']');
519 } else if (value instanceof List) {
520 final String json = ((List<?>) value).stream()
521 .map(s -> "\"" + StringEscapeUtils.escapeJson(s.toString()) + "\"")
522 .collect(Collectors.joining(","));
523 buf.append('[').append(json).append(']');
524 } else if (value instanceof Map) {
525 buf.append('{');
526 final String json = ((Map<?, ?>) value).entrySet().stream().map(e -> {
527 final StringBuilder tempBuf = new StringBuilder();
528 appendJson(e.getKey().toString(), e.getValue(), tempBuf);
529 return tempBuf.toString();
530 }).collect(Collectors.joining(","));
531 buf.append(json);
532 buf.append('}');
533 } else if (value instanceof Long || value instanceof Integer) {
534 buf.append(((Number) value).longValue());
535 } else if (value instanceof Number) {
536 buf.append(((Number) value).doubleValue());
537 } else {
538 buf.append('"').append(StringEscapeUtils.escapeJson(value.toString())).append('"');
539 }
540 return buf;
541 }
542
543
544
545
546
547 public static Consumer<Writer> getSearchLogNdjsonWriteCall() {
548 final FessConfig fessConfig = ComponentUtil.getFessConfig();
549 final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
550 final long timeout = fessConfig.getIndexBackupLogLoadTimeoutAsInteger().longValue();
551 return writer -> {
552 final SearchLogBhv bhv = ComponentUtil.getComponent(SearchLogBhv.class);
553 bhv.selectCursor(cb -> {
554 cb.query().matchAll();
555 cb.query().addOrderBy_RequestedAt_Asc();
556 }, new LogEntityRowHandler<SearchLog>() {
557 @Override
558 public void handle(final SearchLog entity) {
559 final StringBuilder buf = new StringBuilder();
560 buf.append('{');
561 appendJson("id", entity.getId(), buf).append(',');
562 appendJson("query-id", entity.getQueryId(), buf).append(',');
563 appendJson("user-info-id", entity.getUserInfoId(), buf).append(',');
564 appendJson("user-session-id", entity.getUserSessionId(), buf).append(',');
565 appendJson("user", entity.getUser(), buf).append(',');
566 appendJson("search-word", entity.getSearchWord(), buf).append(',');
567 appendJson("hit-count", entity.getHitCount(), buf).append(',');
568 appendJson("query-page-size", entity.getQueryPageSize(), buf).append(',');
569 appendJson("query-offset", entity.getQueryOffset(), buf).append(',');
570 appendJson("referer", entity.getReferer(), buf).append(',');
571 appendJson("languages", entity.getLanguages(), buf).append(',');
572 appendJson("roles", entity.getRoles(), buf).append(',');
573 appendJson("user-agent", entity.getUserAgent(), buf).append(',');
574 appendJson("client-ip", entity.getClientIp(), buf).append(',');
575 appendJson("access-type", entity.getAccessType(), buf).append(',');
576 appendJson("query-time", entity.getQueryTime(), buf).append(',');
577 appendJson("response-time", entity.getResponseTime(), buf).append(',');
578 appendJson("requested-at", entity.getRequestedAt(), buf).append(',');
579 final Map<String, List<String>> searchFieldMap = entity.getSearchFieldLogList()
580 .stream()
581 .collect(Collectors.groupingBy(Pair::getFirst, Collectors.mapping(Pair::getSecond, Collectors.toList())));
582 appendJson("search-field", searchFieldMap, buf).append(',');
583 final Map<String, List<String>> requestHeaderMap = entity.getRequestHeaderList()
584 .stream()
585 .collect(Collectors.groupingBy(Pair::getFirst, Collectors.mapping(Pair::getSecond, Collectors.toList())));
586 appendJson("headers", requestHeaderMap, buf);
587 buf.append('}');
588 buf.append('\n');
589 try {
590 writer.write(buf.toString());
591 } catch (final IOException e) {
592 throw new IORuntimeException(e);
593 }
594 if (!systemHelper.calibrateCpuLoad(timeout)) {
595 breakCursor = true;
596 }
597 }
598 });
599 };
600 }
601
602
603
604
605
606 public static Consumer<Writer> getUserInfoNdjsonWriteCall() {
607 final FessConfig fessConfig = ComponentUtil.getFessConfig();
608 final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
609 final long timeout = fessConfig.getIndexBackupLogLoadTimeoutAsInteger().longValue();
610 return writer -> {
611 final UserInfoBhv bhv = ComponentUtil.getComponent(UserInfoBhv.class);
612 bhv.selectCursor(cb -> {
613 cb.query().matchAll();
614 cb.query().addOrderBy_CreatedAt_Asc();
615 }, new LogEntityRowHandler<UserInfo>() {
616 @Override
617 public void handle(final UserInfo entity) {
618 final StringBuilder buf = new StringBuilder();
619 buf.append('{');
620 appendJson("id", entity.getId(), buf).append(',');
621 appendJson("created-at", entity.getCreatedAt(), buf).append(',');
622 appendJson("updated-at", entity.getUpdatedAt(), buf);
623 buf.append('}');
624 buf.append('\n');
625 try {
626 writer.write(buf.toString());
627 } catch (final IOException e) {
628 throw new IORuntimeException(e);
629 }
630 if (!systemHelper.calibrateCpuLoad(timeout)) {
631 breakCursor = true;
632 }
633 }
634 });
635 };
636 }
637
638
639
640
641
642 public static Consumer<Writer> getFavoriteLogNdjsonWriteCall() {
643 final FessConfig fessConfig = ComponentUtil.getFessConfig();
644 final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
645 final long timeout = fessConfig.getIndexBackupLogLoadTimeoutAsInteger().longValue();
646 return writer -> {
647 final FavoriteLogBhv bhv = ComponentUtil.getComponent(FavoriteLogBhv.class);
648 bhv.selectCursor(cb -> {
649 cb.query().matchAll();
650 cb.query().addOrderBy_CreatedAt_Asc();
651 }, new LogEntityRowHandler<FavoriteLog>() {
652 @Override
653 public void handle(final FavoriteLog entity) {
654 final StringBuilder buf = new StringBuilder();
655 buf.append('{');
656 appendJson("id", entity.getId(), buf).append(',');
657 appendJson("created-at", entity.getCreatedAt(), buf).append(',');
658 appendJson("query-id", entity.getQueryId(), buf).append(',');
659 appendJson("user-info-id", entity.getUserInfoId(), buf).append(',');
660 appendJson("doc-id", entity.getDocId(), buf).append(',');
661 appendJson("url", entity.getUrl(), buf);
662 buf.append('}');
663 buf.append('\n');
664 try {
665 writer.write(buf.toString());
666 } catch (final IOException e) {
667 throw new IORuntimeException(e);
668 }
669 if (!systemHelper.calibrateCpuLoad(timeout)) {
670 breakCursor = true;
671 }
672 }
673 });
674 };
675 }
676
677
678
679
680
681 public static Consumer<Writer> getClickLogNdjsonWriteCall() {
682 final FessConfig fessConfig = ComponentUtil.getFessConfig();
683 final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
684 final long timeout = fessConfig.getIndexBackupLogLoadTimeoutAsInteger().longValue();
685 return writer -> {
686 final ClickLogBhv bhv = ComponentUtil.getComponent(ClickLogBhv.class);
687 bhv.selectCursor(cb -> {
688 cb.query().matchAll();
689 cb.query().addOrderBy_RequestedAt_Asc();
690 }, new LogEntityRowHandler<ClickLog>() {
691 @Override
692 public void handle(final ClickLog entity) {
693 final StringBuilder buf = new StringBuilder();
694 buf.append('{');
695 appendJson("id", entity.getId(), buf).append(',');
696 appendJson("query-id", entity.getQueryId(), buf).append(',');
697 appendJson("user-session-id", entity.getUserSessionId(), buf).append(',');
698 appendJson("doc-id", entity.getDocId(), buf).append(',');
699 appendJson("url", entity.getUrl(), buf).append(',');
700 appendJson("order", entity.getOrder(), buf).append(',');
701 appendJson("query-requested-at", entity.getQueryRequestedAt(), buf).append(',');
702 appendJson("requested-at", entity.getRequestedAt(), buf);
703 buf.append('}');
704 buf.append('\n');
705 try {
706 writer.write(buf.toString());
707 } catch (final IOException e) {
708 throw new IORuntimeException(e);
709 }
710 if (!systemHelper.calibrateCpuLoad(timeout)) {
711 breakCursor = true;
712 }
713 }
714 });
715 };
716 }
717
718
719
720
721
722 public static List<Map<String, String>> getBackupItems() {
723 final FessConfig fessConfig = ComponentUtil.getFessConfig();
724 return stream(fessConfig.getIndexBackupAllTargets()).get(stream -> stream.map(name -> {
725 final Map<String, String> map = new HashMap<>();
726 map.put("id", name);
727 map.put("name", name);
728 return map;
729 }).collect(Collectors.toList()));
730 }
731
732 private HtmlResponse asListHtml() {
733 return asHtml(path_AdminBackup_AdminBackupJsp).useForm(UploadForm.class)
734 .renderWith(data -> RenderDataUtil.register(data, "backupItems", getBackupItems()));
735 }
736
737 private void deleteTempFile(final File tempFile) {
738 if (tempFile != null && !tempFile.delete()) {
739 logger.warn("Failed to delete {}", tempFile.getAbsolutePath());
740 }
741 }
742
743 private static abstract class LogEntityRowHandler<ENTITY> implements EntityRowHandler<ENTITY> {
744 protected boolean breakCursor = false;
745
746 @Override
747 public boolean isBreakCursor() {
748 return breakCursor;
749 }
750 }
751 }