View Javadoc
1   /*
2    * Copyright 2012-2025 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.app.web.api.admin.backup;
17  
18  import static org.codelibs.core.stream.StreamUtil.stream;
19  import static org.codelibs.fess.app.web.admin.backup.AdminBackupAction.NDJSON_EXTENTION;
20  import static org.codelibs.fess.app.web.admin.backup.AdminBackupAction.getBackupItems;
21  import static org.codelibs.fess.app.web.admin.backup.AdminBackupAction.getClickLogNdjsonWriteCall;
22  import static org.codelibs.fess.app.web.admin.backup.AdminBackupAction.getFavoriteLogNdjsonWriteCall;
23  import static org.codelibs.fess.app.web.admin.backup.AdminBackupAction.getSearchLogNdjsonWriteCall;
24  import static org.codelibs.fess.app.web.admin.backup.AdminBackupAction.getUserInfoNdjsonWriteCall;
25  
26  import java.io.BufferedWriter;
27  import java.io.ByteArrayInputStream;
28  import java.io.ByteArrayOutputStream;
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.io.OutputStreamWriter;
32  import java.io.Writer;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.function.Consumer;
36  
37  import org.apache.commons.text.StringEscapeUtils;
38  import org.codelibs.core.exception.IORuntimeException;
39  import org.codelibs.fess.Constants;
40  import org.codelibs.fess.app.web.api.ApiResult;
41  import org.codelibs.fess.app.web.api.ApiResult.ApiBackupFilesResponse;
42  import org.codelibs.fess.app.web.api.admin.FessApiAdminAction;
43  import org.codelibs.fess.mylasta.direction.FessConfig;
44  import org.codelibs.fess.util.ComponentUtil;
45  import org.codelibs.fess.util.SearchEngineUtil;
46  import org.lastaflute.web.Execute;
47  import org.lastaflute.web.response.JsonResponse;
48  import org.lastaflute.web.response.StreamResponse;
49  
50  /**
51   * API action for admin backup.
52   *
53   */
54  public class ApiAdminBackupAction extends FessApiAdminAction {
55  
56      /**
57       * Default constructor.
58       */
59      public ApiAdminBackupAction() {
60          super();
61      }
62  
63      /**
64       * Retrieves a list of available backup files.
65       *
66       * @return JSON response with backup file list
67       */
68      // GET /api/admin/backup/files
69      @Execute
70      public JsonResponse<ApiResult> files() {
71          final List<Map<String, String>> list = getBackupItems();
72          return asJson(new ApiBackupFilesResponse().files(list).total(list.size()).status(ApiResult.Status.OK).result());
73      }
74  
75      /**
76       * Downloads a specific backup file by ID.
77       * Supports various backup formats including system properties, bulk data, and NDJSON logs.
78       *
79       * @param id the backup file ID to download
80       * @return stream response containing the backup file data
81       */
82      // GET /api/admin/backup/file/{id}
83      @Execute
84      public StreamResponse get$file(final String id) {
85          final FessConfig fessConfig = ComponentUtil.getFessConfig();
86          if (stream(fessConfig.getIndexBackupAllTargets()).get(stream -> stream.anyMatch(s -> s.equals(id)))) {
87              if ("system.properties".equals(id)) {
88                  return asStream(id).contentTypeOctetStream().stream(out -> {
89                      try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
90                          ComponentUtil.getSystemProperties().store(baos, id);
91                          try (final InputStream in = new ByteArrayInputStream(baos.toByteArray())) {
92                              out.write(in);
93                          }
94                      }
95                  });
96              }
97              if (!id.endsWith(NDJSON_EXTENTION)) {
98                  final String index;
99                  final String filename;
100                 if (id.endsWith(".bulk")) {
101                     index = id.substring(0, id.length() - 5);
102                     filename = id;
103                 } else {
104                     index = id;
105                     filename = id + ".bulk";
106                 }
107                 return asStream(filename).contentTypeOctetStream().stream(out -> {
108                     try (final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out.stream(), Constants.CHARSET_UTF_8))) {
109                         SearchEngineUtil.scroll(index, hit -> {
110                             try {
111                                 writer.write("{\"index\":{\"_index\":\"" + index + "\",\"_id\":\""
112                                         + StringEscapeUtils.escapeJson(hit.getId()) + "\"}}\n");
113                                 writer.write(hit.getSourceAsString());
114                                 writer.write("\n");
115                             } catch (final IOException e) {
116                                 throw new IORuntimeException(e);
117                             }
118                             return true;
119                         });
120                         writer.flush();
121                     }
122                 });
123             }
124             final String name = id.substring(0, id.length() - NDJSON_EXTENTION.length());
125             if ("search_log".equals(name)) {
126                 return writeNdjsonResponse(id, getSearchLogNdjsonWriteCall());
127             }
128             if ("user_info".equals(name)) {
129                 return writeNdjsonResponse(id, getUserInfoNdjsonWriteCall());
130             }
131             if ("click_log".equals(name)) {
132                 return writeNdjsonResponse(id, getClickLogNdjsonWriteCall());
133             }
134             if ("favorite_log".equals(name)) {
135                 return writeNdjsonResponse(id, getFavoriteLogNdjsonWriteCall());
136             }
137         }
138 
139         throwValidationErrorApi(messages -> messages.addErrorsCouldNotFindBackupIndex(GLOBAL));
140         return StreamResponse.asEmptyBody(); // no-op
141     }
142 
143     private StreamResponse writeNdjsonResponse(final String id, final Consumer<Writer> writeCall) {
144         return asStream(id)//
145                 .header("Pragma", "no-cache")//
146                 .header("Cache-Control", "no-cache")//
147                 .header("Expires", "Thu, 01 Dec 1994 16:00:00 GMT")//
148                 .header("Content-Type", "application/x-ndjson")//
149                 .stream(out -> {
150                     try (final Writer writer = new BufferedWriter(new OutputStreamWriter(out.stream(), Constants.CHARSET_UTF_8))) {
151                         writeCall.accept(writer);
152                         writer.flush();
153                     }
154                 });
155     }
156 }