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.storage;
17  
18  import static org.codelibs.fess.app.web.admin.storage.AdminStorageAction.convertToItem;
19  import static org.codelibs.fess.app.web.admin.storage.AdminStorageAction.decodePath;
20  import static org.codelibs.fess.app.web.admin.storage.AdminStorageAction.deleteObject;
21  import static org.codelibs.fess.app.web.admin.storage.AdminStorageAction.downloadObject;
22  import static org.codelibs.fess.app.web.admin.storage.AdminStorageAction.getFileItems;
23  import static org.codelibs.fess.app.web.admin.storage.AdminStorageAction.getObjectName;
24  import static org.codelibs.fess.app.web.admin.storage.AdminStorageAction.uploadObject;
25  
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.app.web.admin.storage.AdminStorageAction.PathInfo;
33  import org.codelibs.fess.app.web.api.ApiResult;
34  import org.codelibs.fess.app.web.api.admin.FessApiAdminAction;
35  import org.codelibs.fess.exception.ResultOffsetExceededException;
36  import org.codelibs.fess.exception.StorageException;
37  import org.dbflute.optional.OptionalThing;
38  import org.lastaflute.web.Execute;
39  import org.lastaflute.web.response.JsonResponse;
40  import org.lastaflute.web.response.StreamResponse;
41  
42  /**
43   * Handles API requests for storage management in the Fess application.
44   * Provides endpoints for listing, downloading, deleting, and uploading files.
45   */
46  public class ApiAdminStorageAction extends FessApiAdminAction {
47  
48      private static final Logger logger = LogManager.getLogger(ApiAdminStorageAction.class);
49  
50      // ===================================================================================
51      //                                                                         Constructor
52      //                                                                         ===========
53      /**
54       * Default constructor.
55       */
56      public ApiAdminStorageAction() {
57          super();
58      }
59  
60      // ===================================================================================
61      //                                                                      Search Execute
62      //                                                                      ==============
63  
64      // GET /api/admin/storage/list/{id}
65      // PUT /api/admin/storage/list/{id}
66      /**
67       * Lists files and directories in storage.
68       * @param id The ID of the directory to list.
69       * @return A JSON response containing the list of files and directories.
70       */
71      @Execute
72      public JsonResponse<ApiResult> list(final OptionalThing<String> id) {
73          final List<Map<String, Object>> list = getFileItems(id.isPresent() ? decodePath(id.get()) : null);
74          try {
75              return asJson(new ApiResult.ApiStorageResponse().items(list).status(ApiResult.Status.OK).result());
76          } catch (final ResultOffsetExceededException e) {
77              if (logger.isDebugEnabled()) {
78                  logger.debug(e.getMessage(), e);
79              }
80              throwValidationErrorApi(messages -> messages.addErrorsResultSizeExceeded(GLOBAL));
81          }
82  
83          return null;
84      }
85  
86      // GET /api/admin/storage/download/{id}/
87      /**
88       * Downloads a file from storage.
89       * @param id The ID of the file to download.
90       * @return A StreamResponse containing the file content.
91       */
92      @Execute
93      public StreamResponse get$download(final String id) {
94          final PathInfo pi = convertToItem(id);
95          if (StringUtil.isEmpty(pi.getName())) {
96              throwValidationErrorApi(messages -> messages.addErrorsStorageFileNotFound(GLOBAL));
97          }
98          return asStream(pi.getName()).contentTypeOctetStream().stream(out -> {
99              try {
100                 downloadObject(getObjectName(pi.getPath(), pi.getName()), out);
101             } catch (final StorageException e) {
102                 logger.warn("Failed to download {}", id, e);
103                 throwValidationErrorApi(messages -> messages.addErrorsStorageFileDownloadFailure(GLOBAL, pi.getName()));
104             }
105         });
106     }
107 
108     // DELETE /api/admin/storage/delete/{id}/
109     /**
110      * Deletes a file from storage.
111      * @param id The ID of the file to delete.
112      * @return A JSON response indicating the success or failure of the operation.
113      */
114     @Execute
115     public JsonResponse<ApiResult> delete$delete(final String id) {
116         final PathInfo pi = convertToItem(id);
117         if (StringUtil.isEmpty(pi.getName())) {
118             throwValidationErrorApi(messages -> messages.addErrorsStorageAccessError(GLOBAL, "id is invalid"));
119         }
120         final String objectName = getObjectName(pi.getPath(), pi.getName());
121         try {
122             deleteObject(objectName);
123             saveInfo(messages -> messages.addSuccessDeleteFile(GLOBAL, pi.getName()));
124             return asJson(new ApiResult.ApiResponse().status(ApiResult.Status.OK).result());
125         } catch (final StorageException e) {
126             logger.warn("Failed to delete {}", id, e);
127             throwValidationErrorApi(messages -> messages.addErrorsFailedToDeleteFile(GLOBAL, pi.getName()));
128         }
129         return null;
130     }
131 
132     // curl -XPOST -H "Authorization: CHANGEME" localhost:8080/api/admin/storage/upload/ -F path=/ -F file=@...
133     // PUT /api/admin/storage/upload/{pathId}/
134     /**
135      * Uploads a file to storage.
136      * @param form The form containing the file to upload and the target path.
137      * @return A JSON response indicating the success or failure of the operation.
138      */
139     @Execute
140     public JsonResponse<ApiResult> put$upload(final UploadForm form) {
141         validateApi(form, messages -> {});
142         if (form.file == null) {
143             throwValidationErrorApi(messages -> messages.addErrorsStorageNoUploadFile(GLOBAL));
144         }
145         final String fileName = form.file.getFileName();
146         try {
147             uploadObject(getObjectName(form.path, fileName), form.file);
148             saveInfo(messages -> messages.addSuccessUploadFileToStorage(GLOBAL, fileName));
149             return asJson(new ApiResult.ApiResponse().status(ApiResult.Status.OK).result());
150         } catch (final StorageException e) {
151             logger.warn("Failed to upload {}", fileName, e);
152             throwValidationErrorApi(messages -> messages.addErrorsStorageFileUploadFailure(GLOBAL, fileName));
153         }
154         return null;
155     }
156 
157 }