From c8ae75dd341ba258a90d2aa75f60d766fdcfef0a Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Thu, 27 Apr 2023 15:18:32 +0800 Subject: [PATCH 1/3] [Docs] Top scrolling notifications are supported --- docs/docs/css/datacap.css | 146 ++++++++++++++++++++++++++++++++++++++ docs/docs/index.md | 8 ++- docs/docs/index.zh.md | 8 ++- docs/docs/js/datacap.js | 34 +++++++++ docs/mkdocs.yml | 15 +++- docs/overrides/main.html | 81 +++++++++++++-------- 6 files changed, 258 insertions(+), 34 deletions(-) create mode 100644 docs/docs/css/datacap.css create mode 100644 docs/docs/js/datacap.js diff --git a/docs/docs/css/datacap.css b/docs/docs/css/datacap.css new file mode 100644 index 000000000..5d9d6f8eb --- /dev/null +++ b/docs/docs/css/datacap.css @@ -0,0 +1,146 @@ +.termynal-comment { + color: #4a968f; + font-style: italic; + display: block; +} + +.termy { + /* For right to left languages */ + direction: ltr; +} + +.termy [data-termynal] { + white-space: pre-wrap; +} + +a.external-link { + /* For right to left languages */ + direction: ltr; + display: inline-block; +} + +a.external-link::after { + /* \00A0 is a non-breaking space + to make the mark be on the same line as the link + */ + content: "\00A0[↪]"; +} + +a.internal-link::after { + /* \00A0 is a non-breaking space + to make the mark be on the same line as the link + */ + content: "\00A0↪"; +} + +.shadow { + box-shadow: 5px 5px 10px #999; +} + +/* Give space to lower icons so Gitter chat doesn't get on top of them */ +.md-footer-meta { + padding-bottom: 2em; +} + +.user-list { + display: flex; + flex-wrap: wrap; + margin-bottom: 2rem; +} + +.user-list-center { + justify-content: space-evenly; +} + +.user { + margin: 1em; + min-width: 7em; +} + +.user .avatar-wrapper { + width: 80px; + height: 80px; + margin: 10px auto; + overflow: hidden; + border-radius: 50%; + position: relative; +} + +.user .avatar-wrapper img { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.user .title { + text-align: center; +} + +.user .count { + font-size: 80%; + text-align: center; +} + +a.announce-link:link, +a.announce-link:visited { + color: #fff; +} + +a.announce-link:hover { + color: var(--md-accent-fg-color); +} + +.announce-wrapper { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + align-items: center; +} + +.announce-wrapper div.item { + display: none; +} + +.announce-wrapper .sponsor-badge { + display: block; + position: absolute; + top: -10px; + right: 0; + font-size: 0.5rem; + color: #999; + background-color: #666; + border-radius: 10px; + padding: 0 10px; + z-index: 10; +} + +.announce-wrapper .sponsor-image { + display: block; + border-radius: 20px; +} + +.announce-wrapper > div { + min-height: 40px; + display: flex; + align-items: center; +} + +.twitter { + color: #00acee; +} + +/* Right to left languages */ +code { + direction: ltr; + display: inline-block; +} + +.md-content__inner h1 { + direction: ltr !important; +} + +.illustration { + margin-top: 2em; + margin-bottom: 2em; +} diff --git a/docs/docs/index.md b/docs/docs/index.md index be795c354..6737b68c3 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -70,7 +70,13 @@ Datacap is fast, lightweight, intuitive system. --- - DataCap can connect to any SQL based datasource through JDBC and native and http. + DataCap can connect to any SQL based datasource through JDBC and Native and Http. + +- __Highly Customizable__ + + --- + + DataCap can quickly connect to new data sources by implementing the methods provided by SPI. - __Join (DingTalk | WeChat)__ diff --git a/docs/docs/index.zh.md b/docs/docs/index.zh.md index 5efd7b72e..b57aace74 100644 --- a/docs/docs/index.zh.md +++ b/docs/docs/index.zh.md @@ -70,7 +70,13 @@ Datacap 是快速、轻量级、直观的系统。 --- - DataCap 可以通过 JDBC, Native, http 连接到任何基于 SQL 的数据源。 + DataCap 可以通过 JDBC, Native, Http 连接到任何基于 SQL 的数据源。 + +- __高度定制化__ + + --- + + DataCap 可以通过实现 SPI 提供的方式可以实现快速对接新的数据源。 - __加入 (钉钉 | 微信)__ diff --git a/docs/docs/js/datacap.js b/docs/docs/js/datacap.js new file mode 100644 index 000000000..bcb98c2b4 --- /dev/null +++ b/docs/docs/js/datacap.js @@ -0,0 +1,34 @@ +function shuffle(array) { + let currentIndex = array.length, temporaryValue, randomIndex; + while (0 !== currentIndex) { + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex -= 1; + temporaryValue = array[currentIndex]; + array[currentIndex] = array[randomIndex]; + array[randomIndex] = temporaryValue; + } + return array; +} + +function showRandomAnnouncement(groupId, timeInterval) { + const announceFastAPI = document.getElementById(groupId); + if (announceFastAPI) { + let children = [].slice.call(announceFastAPI.children); + children = shuffle(children) + let index = 0 + const announceRandom = () => { + children.forEach((el, i) => {el.style.display = "none"}); + children[index].style.display = "block" + index = (index + 1) % children.length + } + announceRandom() + setInterval(announceRandom, timeInterval) + } +} + +async function main() { + showRandomAnnouncement('announce-left', 5000) + showRandomAnnouncement('announce-right', 10000) +} + +main() diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index dbcfa2717..2f668396c 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -6,7 +6,17 @@ site_description: >- repo_name: EdurtIO/datacap repo_url: https://github.com/EdurtIO/datacap edit_uri: "https://github.com/EdurtIO/datacap/blob/dev/docs/docs" -current_version: 1.8.0 + +banners: + - title: DataCap 1.8.0 is released + link: /release-latest.html + description: Do you ❤️ DataCap? Give us a 🌟 on GitHub + - title: Support ceresdb + link: /reference/connectors/http/ceresdb.html + description: newsletter 🎉 + - title: Support greptimedb + link: /reference/connectors/http/greptimedb.html + description: newsletter 🎉 copyright: Copyright © 2022 EdurtIO @@ -65,6 +75,9 @@ extra: lang: zh extra_css: - stylesheets/extra.css + - css/datacap.css +extra_javascript: + - js/datacap.js markdown_extensions: - admonition - abbr diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 2efaafebb..cbe19cffa 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -3,22 +3,40 @@ -#} {% extends "base.html" %} {% block extrahead %} - - + + {% endblock %} {% block announce %} -
- - DataCap {{config.current_version}} is released ... - - - Do you ❤️ DataCap? Give us a 🌟 on GitHub - -
+
+
+ {% for banner in config.banners %} + + {% endfor %} +
+
+
+
+
+
{% endblock %} {% block content %} @@ -44,30 +62,31 @@

{{ lang.t("meta.comments") }}

{% endblock %} From d3a1bc6a53ee064836d61d00b9344b045dbf35d4 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Fri, 28 Apr 2023 15:19:09 +0800 Subject: [PATCH 2/3] [Plugin] Support apache hadoop hdfs --- .gitignore | 4 + README.md | 3 + core/datacap-server/pom.xml | 5 + .../src/main/etc/conf/application.properties | 4 + .../main/etc/conf/plugins/native/hdfs.yaml | 26 ++++ .../server/common/IConfigureCommon.java | 6 + .../controller/user/SourceController.java | 54 ++++++- .../datacap/server/entity/SourceEntity.java | 3 + .../plugin/configure/IConfigureFieldName.java | 1 + .../plugin/configure/IConfigureFieldType.java | 3 +- .../service/impl/ExecuteServiceImpl.java | 16 ++- .../service/impl/SourceServiceImpl.java | 62 +++++++- .../src/main/resources/schema.sql | 20 ++- .../src/main/schema/1.9.0/update.sql | 1 + .../src/main/schema/datacap.sql | 5 +- .../java/io/edurt/datacap/spi/Plugin.java | 10 +- .../io/edurt/datacap/spi/PluginModule.java | 7 +- .../io/edurt/datacap/spi/model/Configure.java | 4 + .../public/static/images/plugin/Hdfs.png | Bin 0 -> 27101 bytes .../console-fe/src/i18n/langs/en/common.ts | 4 +- .../console-fe/src/i18n/langs/zhCn/common.ts | 4 +- .../console-fe/src/model/SourceModel.ts | 1 + .../views/pages/admin/source/SourceDetail.vue | 29 +++- docs/docs/assets/plugin/hdfs.png | Bin 0 -> 27101 bytes docs/docs/index.md | 3 + docs/docs/index.zh.md | 3 + .../connectors/native/hadoop/hdfs.md | 50 +++++++ docs/mkdocs.yml | 2 + plugin/datacap-native-hdfs/pom.xml | 132 ++++++++++++++++++ .../edurt/datacap/natived/hdfs/HdfsAdapter.kt | 95 +++++++++++++ .../datacap/natived/hdfs/HdfsConnection.kt | 49 +++++++ .../edurt/datacap/natived/hdfs/HdfsModule.kt | 23 +++ .../edurt/datacap/natived/hdfs/HdfsPlugin.kt | 63 +++++++++ .../io.edurt.datacap.spi.PluginModule | 1 + .../datacap/natived/hdfs/HdfsModuleTest.kt | 27 ++++ .../datacap/natived/hdfs/HdfsPluginTest.kt | 48 +++++++ .../src/test/resources/default/core-site.xml | 61 ++++++++ .../src/test/resources/default/hdfs-site.xml | 28 ++++ pom.xml | 3 +- 39 files changed, 836 insertions(+), 24 deletions(-) create mode 100644 core/datacap-server/src/main/etc/conf/plugins/native/hdfs.yaml create mode 100644 core/datacap-server/src/main/schema/1.9.0/update.sql create mode 100644 core/datacap-web/console-fe/public/static/images/plugin/Hdfs.png create mode 100644 docs/docs/assets/plugin/hdfs.png create mode 100644 docs/docs/reference/connectors/native/hadoop/hdfs.md create mode 100644 plugin/datacap-native-hdfs/pom.xml create mode 100644 plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsAdapter.kt create mode 100644 plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsConnection.kt create mode 100644 plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsModule.kt create mode 100644 plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsPlugin.kt create mode 100644 plugin/datacap-native-hdfs/src/main/resources/META-INF/services/io.edurt.datacap.spi.PluginModule create mode 100644 plugin/datacap-native-hdfs/src/test/kotlin/io/edurt/datacap/natived/hdfs/HdfsModuleTest.kt create mode 100644 plugin/datacap-native-hdfs/src/test/kotlin/io/edurt/datacap/natived/hdfs/HdfsPluginTest.kt create mode 100644 plugin/datacap-native-hdfs/src/test/resources/default/core-site.xml create mode 100644 plugin/datacap-native-hdfs/src/test/resources/default/hdfs-site.xml diff --git a/.gitignore b/.gitignore index 5eae0a6ec..cbc39e5fe 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,7 @@ list # shaded # shaded/*/dependency-reduced-pom.xml + +# datacap # +cache/ +config/ diff --git a/README.md b/README.md index 8982e1065..8e57e0ea6 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,9 @@ Here are some of the major database solutions that are supported:   Hologres +   + + Apache Hdfs

diff --git a/core/datacap-server/pom.xml b/core/datacap-server/pom.xml index 5b9e80879..50e82a048 100644 --- a/core/datacap-server/pom.xml +++ b/core/datacap-server/pom.xml @@ -345,6 +345,11 @@ datacap-jdbc-hologres ${project.version} + + io.edurt.datacap + datacap-native-hdfs + ${project.version} + io.edurt.datacap diff --git a/core/datacap-server/src/main/etc/conf/application.properties b/core/datacap-server/src/main/etc/conf/application.properties index 7dce293a6..04e0b4f5a 100644 --- a/core/datacap-server/src/main/etc/conf/application.properties +++ b/core/datacap-server/src/main/etc/conf/application.properties @@ -40,3 +40,7 @@ spring.redis.database=0 ### If this directory is not set, the system will get the project root directory to build the data subdirectory datacap.executor.data= datacap.executor.seatunnel.home=/opt/lib/seatunnel + +################################ Upload configure ################################# +datacap.config.data= +datacap.cache.data= diff --git a/core/datacap-server/src/main/etc/conf/plugins/native/hdfs.yaml b/core/datacap-server/src/main/etc/conf/plugins/native/hdfs.yaml new file mode 100644 index 000000000..d4a67c22b --- /dev/null +++ b/core/datacap-server/src/main/etc/conf/plugins/native/hdfs.yaml @@ -0,0 +1,26 @@ +name: HDFS +supportTime: '2023-04-27' +configures: + - field: name + type: String + required: true + message: name is a required field, please be sure to enter + - field: host + type: String + required: true + value: '-' + disabled: true + message: host is a required field, please be sure to enter + - field: port + type: Number + required: true + min: 1 + max: 65535 + value: 1 + disabled: true + message: port is a required field, please be sure to enter + - field: file + type: File + required: true + value: [] + group: advanced diff --git a/core/datacap-server/src/main/java/io/edurt/datacap/server/common/IConfigureCommon.java b/core/datacap-server/src/main/java/io/edurt/datacap/server/common/IConfigureCommon.java index 7e17638e7..b4e9bf8fe 100644 --- a/core/datacap-server/src/main/java/io/edurt/datacap/server/common/IConfigureCommon.java +++ b/core/datacap-server/src/main/java/io/edurt/datacap/server/common/IConfigureCommon.java @@ -65,6 +65,9 @@ public static Configure preparedConfigure(List configures) case configures: configure.setEnv(Optional.ofNullable(IConfigureCommon.getMapValue(configures, IConfigureFieldName.configures))); break; + case file: + configure.setUsedConfig(true); + break; } }); configure.setFormat(FormatType.JSON); @@ -149,6 +152,9 @@ public static SourceEntity preparedSourceEntity(List configures case configures: configure.setConfigure(JSON.toJSON(IConfigureCommon.getMapValue(configures, IConfigureFieldName.configures))); break; + case file: + configure.setUsedConfig(true); + break; } }); configure.setCreateTime(Timestamp.from(Instant.now())); diff --git a/core/datacap-server/src/main/java/io/edurt/datacap/server/controller/user/SourceController.java b/core/datacap-server/src/main/java/io/edurt/datacap/server/controller/user/SourceController.java index e9b70d4b7..47ed2859f 100644 --- a/core/datacap-server/src/main/java/io/edurt/datacap/server/controller/user/SourceController.java +++ b/core/datacap-server/src/main/java/io/edurt/datacap/server/controller/user/SourceController.java @@ -1,12 +1,19 @@ package io.edurt.datacap.server.controller.user; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.edurt.datacap.server.body.SharedSourceBody; import io.edurt.datacap.server.common.Response; import io.edurt.datacap.server.entity.PageEntity; import io.edurt.datacap.server.entity.PluginEntity; import io.edurt.datacap.server.entity.SourceEntity; +import io.edurt.datacap.server.entity.UserEntity; +import io.edurt.datacap.server.security.UserDetailsService; import io.edurt.datacap.server.service.SourceService; import io.edurt.datacap.server.validation.ValidationGroup; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.env.Environment; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; @@ -16,22 +23,30 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import java.io.File; +import java.io.IOException; import java.util.List; import java.util.Map; +@SuppressFBWarnings(value = {"RV_RETURN_VALUE_IGNORED_BAD_PRACTICE"}) @RestController() @RequestMapping(value = "/api/v1/source") +@Slf4j public class SourceController { private final SourceService sourceService; + private final Environment environment; - public SourceController(SourceService sourceService) + public SourceController(SourceService sourceService, Environment environment) { this.sourceService = sourceService; + this.environment = environment; } @Deprecated @@ -49,8 +64,7 @@ public Response update(@RequestBody @Validated(ValidationGroup.Cru } @GetMapping - public Response> getAll(@RequestParam(value = "page", defaultValue = "1") int start, - @RequestParam(value = "size", defaultValue = "10") int end) + public Response> getAll(@RequestParam(value = "page", defaultValue = "1") int start, @RequestParam(value = "size", defaultValue = "10") int end) { return this.sourceService.getAll(start, end); } @@ -86,4 +100,38 @@ public Response shared(@RequestBody SharedSourceBody configure) { return this.sourceService.shared(configure); } + + @SneakyThrows + @PostMapping("uploadFile") + public Response uploadFile(@RequestParam("file") MultipartFile file, @RequestHeader("PluginType") String pluginType) + { + UserEntity user = UserDetailsService.getUser(); + + String cacheHome = environment.getProperty("datacap.cache.data"); + if (StringUtils.isEmpty(cacheHome)) { + cacheHome = String.join(File.separator, System.getProperty("user.dir"), "cache"); + } + String userCacheHome = String.join(File.separator, cacheHome, user.getUsername(), pluginType); + + String originalFilename = file.getOriginalFilename(); + File targetFile = new File(String.join(File.separator, userCacheHome, originalFilename)); + if (!targetFile.getParentFile().exists()) { + targetFile.getParentFile().mkdirs(); + } + else { + // If you already have cache files, clean and delete all files + File[] files = targetFile.getParentFile().listFiles(); + for (File f : files) { + log.info("Removing cache file {} state {}", f.getName(), f.delete()); + } + } + + try { + file.transferTo(targetFile); + } + catch (IOException e) { + log.warn("File upload exception on user {} by type {} ", user.getUsername(), pluginType, e); + } + return Response.success(targetFile.getPath()); + } } diff --git a/core/datacap-server/src/main/java/io/edurt/datacap/server/entity/SourceEntity.java b/core/datacap-server/src/main/java/io/edurt/datacap/server/entity/SourceEntity.java index 29cc3939d..8bf0c5d85 100644 --- a/core/datacap-server/src/main/java/io/edurt/datacap/server/entity/SourceEntity.java +++ b/core/datacap-server/src/main/java/io/edurt/datacap/server/entity/SourceEntity.java @@ -108,6 +108,9 @@ public class SourceEntity @Transient private IConfigure schema; + @Column(name = "used_config") + private boolean usedConfig; + @ManyToOne @JoinColumn(name = "user_id") @JsonIncludeProperties(value = {"id", "username"}) diff --git a/core/datacap-server/src/main/java/io/edurt/datacap/server/plugin/configure/IConfigureFieldName.java b/core/datacap-server/src/main/java/io/edurt/datacap/server/plugin/configure/IConfigureFieldName.java index 0afe77eb3..409b5744d 100644 --- a/core/datacap-server/src/main/java/io/edurt/datacap/server/plugin/configure/IConfigureFieldName.java +++ b/core/datacap-server/src/main/java/io/edurt/datacap/server/plugin/configure/IConfigureFieldName.java @@ -13,5 +13,6 @@ public enum IConfigureFieldName ssl, catalog, database, + file, configures } diff --git a/core/datacap-server/src/main/java/io/edurt/datacap/server/plugin/configure/IConfigureFieldType.java b/core/datacap-server/src/main/java/io/edurt/datacap/server/plugin/configure/IConfigureFieldType.java index 4168f661c..2367f7cf4 100644 --- a/core/datacap-server/src/main/java/io/edurt/datacap/server/plugin/configure/IConfigureFieldType.java +++ b/core/datacap-server/src/main/java/io/edurt/datacap/server/plugin/configure/IConfigureFieldType.java @@ -7,5 +7,6 @@ public enum IConfigureFieldType List, Boolean, Array, - Map + Map, + File } diff --git a/core/datacap-server/src/main/java/io/edurt/datacap/server/service/impl/ExecuteServiceImpl.java b/core/datacap-server/src/main/java/io/edurt/datacap/server/service/impl/ExecuteServiceImpl.java index f2e80d204..18b2fd5cb 100644 --- a/core/datacap-server/src/main/java/io/edurt/datacap/server/service/impl/ExecuteServiceImpl.java +++ b/core/datacap-server/src/main/java/io/edurt/datacap/server/service/impl/ExecuteServiceImpl.java @@ -16,8 +16,10 @@ import io.edurt.datacap.spi.Plugin; import io.edurt.datacap.spi.model.Configure; import org.apache.commons.lang3.StringUtils; +import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; +import java.io.File; import java.util.Optional; @Service @@ -26,11 +28,13 @@ public class ExecuteServiceImpl { private final Injector injector; private final SourceRepository sourceRepository; + private final Environment environment; - public ExecuteServiceImpl(Injector injector, SourceRepository sourceRepository) + public ExecuteServiceImpl(Injector injector, SourceRepository sourceRepository, Environment environment) { this.injector = injector; this.sourceRepository = sourceRepository; + this.environment = environment; } @AuditPlugin @@ -59,6 +63,16 @@ public Response execute(ExecuteEntity configure) _configure.setSsl(Optional.ofNullable(entity.getSsl())); _configure.setEnv(Optional.ofNullable(entity.getConfigures())); _configure.setFormat(configure.getFormat()); + _configure.setUsedConfig(entity.isUsedConfig()); + if (entity.isUsedConfig()) { + _configure.setUsername(Optional.of(entity.getUser().getUsername())); + String configHome = environment.getProperty("datacap.config.data"); + if (StringUtils.isEmpty(configHome)) { + configHome = String.join(File.separator, System.getProperty("user.dir"), "config"); + } + _configure.setHome(configHome); + _configure.setId(String.valueOf(entity.getId())); + } plugin.connect(_configure); io.edurt.datacap.spi.model.Response response = plugin.execute(configure.getContent()); plugin.destroy(); diff --git a/core/datacap-server/src/main/java/io/edurt/datacap/server/service/impl/SourceServiceImpl.java b/core/datacap-server/src/main/java/io/edurt/datacap/server/service/impl/SourceServiceImpl.java index d6e0d8399..d9684f554 100644 --- a/core/datacap-server/src/main/java/io/edurt/datacap/server/service/impl/SourceServiceImpl.java +++ b/core/datacap-server/src/main/java/io/edurt/datacap/server/service/impl/SourceServiceImpl.java @@ -1,8 +1,10 @@ package io.edurt.datacap.server.service.impl; +import com.google.common.io.Files; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.TypeLiteral; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.edurt.datacap.server.adapter.PageRequestAdapter; import io.edurt.datacap.server.body.SharedSourceBody; import io.edurt.datacap.server.body.SourceBody; @@ -24,12 +26,15 @@ import io.edurt.datacap.spi.FormatType; import io.edurt.datacap.spi.Plugin; import io.edurt.datacap.spi.model.Configure; +import lombok.SneakyThrows; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.core.env.Environment; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -39,6 +44,7 @@ import java.util.stream.Collectors; @Service +@SuppressFBWarnings(value = {"RV_RETURN_VALUE_IGNORED_BAD_PRACTICE"}) public class SourceServiceImpl implements SourceService { @@ -72,10 +78,23 @@ public Response> getAll(int offset, int limit) return Response.success(PageEntity.build(this.sourceRepository.findAllByUserOrPublishIsTrue(user, pageable))); } + @SneakyThrows @Override public Response delete(Long id) { - this.sourceRepository.deleteById(id); + Optional entityOptional = this.sourceRepository.findById(id); + if (entityOptional.isPresent()) { + SourceEntity source = entityOptional.get(); + if (source.isUsedConfig()) { + String configHome = environment.getProperty("datacap.config.data"); + if (StringUtils.isEmpty(configHome)) { + configHome = String.join(File.separator, System.getProperty("user.dir"), "config"); + } + String destination = String.join(File.separator, configHome, source.getUser().getUsername(), source.getType(), String.valueOf(source.getId())); + FileUtils.deleteDirectory(new File(destination)); + } + this.sourceRepository.deleteById(id); + } return Response.success(id); } @@ -183,6 +202,15 @@ public Response testConnectionV2(SourceBody configure) // The filter parameter value is null data List applyConfigures = IConfigureCommon.filterNotEmpty(configure.getConfigure().getConfigures()); Configure _configure = IConfigureCommon.preparedConfigure(applyConfigures); + // Adapter file configure + if (_configure.isUsedConfig()) { + String cacheHome = environment.getProperty("datacap.cache.data"); + if (StringUtils.isEmpty(cacheHome)) { + cacheHome = String.join(File.separator, System.getProperty("user.dir"), "cache"); + } + _configure.setHome(cacheHome); + _configure.setUsername(Optional.of(UserDetailsService.getUser().getUsername())); + } plugin.connect(_configure); io.edurt.datacap.spi.model.Response response = plugin.execute(plugin.validator()); if (response.getIsSuccessful()) { @@ -218,10 +246,38 @@ public Response saveOrUpdateV2(SourceBody configure) source.setProtocol(configure.getType()); source.setType(configure.getName()); source.setUser(UserDetailsService.getUser()); - if (ObjectUtils.isNotEmpty(configure.getId())) { + if (ObjectUtils.isNotEmpty(configure.getId()) && configure.getId() > 0) { source.setId(configure.getId()); } - return Response.success(this.sourceRepository.save(source)); + + this.sourceRepository.save(source); + // Copy file to local data + if (source.isUsedConfig()) { + String cacheHome = environment.getProperty("datacap.cache.data"); + if (StringUtils.isEmpty(cacheHome)) { + cacheHome = String.join(File.separator, System.getProperty("user.dir"), "cache"); + } + String configHome = environment.getProperty("datacap.config.data"); + if (StringUtils.isEmpty(configHome)) { + configHome = String.join(File.separator, System.getProperty("user.dir"), "config"); + } + File sourceFile = new File(String.join(File.separator, cacheHome, source.getUser().getUsername(), source.getType())); + String destination = String.join(File.separator, configHome, source.getUser().getUsername(), source.getType(), String.valueOf(source.getId())); + File directory = new File(destination); + try { + if (!directory.exists()) { + directory.mkdirs(); + } + for (File file : sourceFile.listFiles()) { + Files.copy(file, new File(String.join(File.separator, destination, file.getName()))); + } + FileUtils.deleteDirectory(sourceFile); + } + catch (Exception e) { + return Response.failure("Copy failed: " + e.getMessage()); + } + } + return Response.success(source); } @Override diff --git a/core/datacap-server/src/main/resources/schema.sql b/core/datacap-server/src/main/resources/schema.sql index 0e4d31c07..0a0a911d7 100644 --- a/core/datacap-server/src/main/resources/schema.sql +++ b/core/datacap-server/src/main/resources/schema.sql @@ -59,7 +59,8 @@ CREATE TABLE IF NOT EXISTS role create_time datetime(5) NULL DEFAULT CURRENT_TIMESTAMP(5) ); TRUNCATE TABLE role; -ALTER TABLE role ALTER COLUMN id RESTART WITH 1; +ALTER TABLE role + ALTER COLUMN id RESTART WITH 1; INSERT INTO role (name, description) VALUES ('Admin', 'Admin role'); INSERT INTO role (name, description) @@ -79,7 +80,8 @@ CREATE TABLE IF NOT EXISTS scheduled_task update_time date NULL ON UPDATE CURRENT_TIMESTAMP ); TRUNCATE TABLE scheduled_task; -ALTER TABLE scheduled_task ALTER COLUMN id RESTART WITH 1; +ALTER TABLE scheduled_task + ALTER COLUMN id RESTART WITH 1; -- -------------------------------- -- Table structure for snippet -- -------------------------------- @@ -115,10 +117,12 @@ CREATE TABLE IF NOT EXISTS source publish boolean NULL DEFAULT 0, public boolean NULL DEFAULT 0, user_id bigint NULL, - configure text NULL + configure text NULL, + used_config boolean default false ); TRUNCATE TABLE scheduled_task; -ALTER TABLE scheduled_task ALTER COLUMN id RESTART WITH 1; +ALTER TABLE scheduled_task + ALTER COLUMN id RESTART WITH 1; INSERT INTO source(name, _database, password, host, port, protocol, username, _type, publish, user_id, public) VALUES ('Built-in database', 'datacap', 'h2', '-', 1, 'NATIVE', 'h2', 'H2', FALSE, 2, TRUE); -- -------------------------------- @@ -137,7 +141,8 @@ CREATE TABLE IF NOT EXISTS template_sql `system` boolean NULL DEFAULT 0 ); TRUNCATE TABLE template_sql; -ALTER TABLE template_sql ALTER COLUMN id RESTART WITH 1; +ALTER TABLE template_sql + ALTER COLUMN id RESTART WITH 1; INSERT INTO template_sql ( name, content, description, plugin, configure , create_time, update_time, `system`) VALUES ( 'getAllDatabase', 'SHOW DATABASES', 'Gets a list of all databases', 'ClickHouse,MySQL,H2', '[]' @@ -199,7 +204,7 @@ INSERT INTO template_sql ( name, content, description, plugin, configure VALUES ( 'getAllColumnsFromDatabaseAndTable', 'DESC ${table:String}', 'Get the data column from the database and table', 'MySQL,ClickHouse' , '[{"column":"table","type":"String","expression":"${table:String}"}]' , '2023-01-10 11:59:23', '2023-01-10 11:59:23', 0); -INSERT INTO template_sql ( name, content, description, plugin, configure, `system`) +INSERT INTO template_sql (name, content, description, plugin, configure, `system`) VALUES ( 'getAllColumnsFromDatabaseAndTable', 'SHOW COLUMNS FROM ${table:String}', 'Get the data column from the database and table', 'H2' , '[{"column":"table","type":"String","expression":"${table:String}"}]', 1); INSERT INTO template_sql ( name, content, description, plugin, configure @@ -266,7 +271,8 @@ CREATE TABLE IF NOT EXISTS users third_configure text NULL ); TRUNCATE TABLE users; -ALTER TABLE users ALTER COLUMN id RESTART WITH 1; +ALTER TABLE users + ALTER COLUMN id RESTART WITH 1; INSERT INTO users (username, password, create_time) VALUES ('admin', '$2a$10$ee2yg.Te14GpHppDUROAi.HzYR5Q.q2/5vrZvAr4TFY3J2iT663JG', NULL); INSERT INTO users (username, password, create_time) diff --git a/core/datacap-server/src/main/schema/1.9.0/update.sql b/core/datacap-server/src/main/schema/1.9.0/update.sql new file mode 100644 index 000000000..ba1307d7e --- /dev/null +++ b/core/datacap-server/src/main/schema/1.9.0/update.sql @@ -0,0 +1 @@ +alter table `source` add column `used_config` boolean default false; \ No newline at end of file diff --git a/core/datacap-server/src/main/schema/datacap.sql b/core/datacap-server/src/main/schema/datacap.sql index b89d41c46..d30945b08 100644 --- a/core/datacap-server/src/main/schema/datacap.sql +++ b/core/datacap-server/src/main/schema/datacap.sql @@ -130,7 +130,8 @@ create table datacap.source publish tinyint(1) default 0 null, public tinyint(1) default 0 null, user_id bigint null, - configure text null + configure text null, + used_config boolean default false ) comment 'The storage is used to query the data connection source'; @@ -212,7 +213,7 @@ OFFSET ${page:Integer}', 'Get all data from table by limited', 'MySQL,ClickHouse '[{"column":"table","type":"String","expression":"${table:String}"},{"column":"size","type":"Integer","expression":"${size:Integer}"},{"column":"page","type":"Integer","expression":"${page:Integer}"}]', '2023-01-10 13:31:10', '2023-01-10 13:31:10', 0); -INSERT INTO template_sql ( name, content, description, plugin, configure, `system`) +INSERT INTO template_sql (name, content, description, plugin, configure, `system`) VALUES ( 'getAllColumnsFromDatabaseAndTable', 'SHOW COLUMNS FROM ${table:String}', 'Get the data column from the database and table', 'H2' , '[{"column":"table","type":"String","expression":"${table:String}"}]', 1); diff --git a/core/datacap-spi/src/main/java/io/edurt/datacap/spi/Plugin.java b/core/datacap-spi/src/main/java/io/edurt/datacap/spi/Plugin.java index 4d5aefc76..f4da83d67 100644 --- a/core/datacap-spi/src/main/java/io/edurt/datacap/spi/Plugin.java +++ b/core/datacap-spi/src/main/java/io/edurt/datacap/spi/Plugin.java @@ -15,9 +15,15 @@ default PluginType type() return PluginType.JDBC; } - String name(); + default String name() + { + return this.getClass().getSimpleName().replace("Plugin", ""); + } - String description(); + default String description() + { + return String.format("Integrate %s data sources", this.name()); + } void connect(Configure configure); diff --git a/core/datacap-spi/src/main/java/io/edurt/datacap/spi/PluginModule.java b/core/datacap-spi/src/main/java/io/edurt/datacap/spi/PluginModule.java index cc1ad27f7..6630af59c 100644 --- a/core/datacap-spi/src/main/java/io/edurt/datacap/spi/PluginModule.java +++ b/core/datacap-spi/src/main/java/io/edurt/datacap/spi/PluginModule.java @@ -2,7 +2,12 @@ public interface PluginModule { - String getName(); + default String getName() + { + return this.getClass().getSimpleName() + .replace("PluginModule", "") + .replace("Module", ""); + } PluginType getType(); diff --git a/core/datacap-spi/src/main/java/io/edurt/datacap/spi/model/Configure.java b/core/datacap-spi/src/main/java/io/edurt/datacap/spi/model/Configure.java index b2c39a4e3..f8feacdff 100644 --- a/core/datacap-spi/src/main/java/io/edurt/datacap/spi/model/Configure.java +++ b/core/datacap-spi/src/main/java/io/edurt/datacap/spi/model/Configure.java @@ -25,4 +25,8 @@ public class Configure private FormatType format = FormatType.NONE; // if `to`: skip private Optional query = Optional.empty(); + // Support for custom upload configuration plugins + private String home; + private boolean usedConfig; + private String id; } diff --git a/core/datacap-web/console-fe/public/static/images/plugin/Hdfs.png b/core/datacap-web/console-fe/public/static/images/plugin/Hdfs.png new file mode 100644 index 0000000000000000000000000000000000000000..e5640e81265bbcddc0d0b9a37b708548439b28de GIT binary patch literal 27101 zcmcF~Ra}(c6Zf;bz|tWg4NFP4w3MWRh|)-kl+@AK;p8;%ytnCYc)(eg}K4QbPAB70A0&@dI(UlI@A!wyeq$7dU?>zR=MBhXvZaaOA)c zb!3@h7D{F8Zidl{gZK}~kMs%p*FU@q`xHRz|ApPc-Fed6D{0;0UwC+X-Uyrv$W+MvaX8r|F4L7S0lz(wNv@~{}YM@i89EjIxuo`Km_C2&_|3BsUPBi2c zN?_fri0DR!tKtXb%_wxKt=Z}F$3mOu^u?b4sm!f4q-|mU@bd3|>PFTMfa7;s_;9k{ z&z3#!*DG^*(=2nb(I&q6a24?I{PwT@Sku+*_!0N(xM>&VEAB4ws})=8qmS1KP8f3P zJ#E*mB!%5)zmM+!DG$#M!G%ePsFK5453=uT06y8>i-0}dgUh?6tQTUB08qN^^di+! z>mlA!)s12s+SKC^)xO`J?N@apUODuAW0b}?z~-X9;b&d&Hn-MhFrcAU$s_d8 z!*;5U$)ES45A_QpK5M2PN8^O^9&MMz^J00R=fIh=-j6M)U04V4h32Q_fU@@% zm@?uOZFd)~QUHJB4@H*0yoPfo{x@?Tl~+R}yE#zZi}I!GsRhl~t@ao3GDn}9H?n-r z_BRhk6!W$F}t^9{4mm`(dtm!{MjJ z#g9UUce4AY7pFo{JZKZJ7AGUi{C#VR1fS6@ERyv;B&v;r9b066_B!c(-lD1i&_@${iQ) zx?bPj0+O-^sf9%DBRm^nVO}8)46F;*s7rwr?u?b@#j{y77$m2#GJ*G`N)GO`jdcMs zbq*;dtbv&1-w$D+{tGE!6atNw>}5lF5^hjT&0zThWI%XPv(LRB-L1~P*Zji@wJ|?S$cYBbI)pzgt0e`8EENHNAn* zo#DPX_|xibe)GfX>pX#V-Mf;J*}Kz#yExuQc!&g|=!s+1QoiMvUGY;M?*d)R;{U_3 zc#IKzM4ba_%x5q*aBJ#~I_+1jz~kMm37C1PlJx8@+{2Us5wBv+XJ8S@?ORbFI;~wC zIWi!H~^Hx>w!Q1f0VFiUpXL&IRQFyKV_rv=YrQhO=A+wYTd`#7?vC5|y-* z*>3W`)bllTfUaM>oo=~r=x#0H5p49p3}{FCY^}&I&j$Ti9f%Jwy|^dxH{ZCHkh<7j zSbh^^f>`*qwhufD)F9s7V5NtJ$F21)1ytQkClhtJVG%2=DX}EcXyp+M^J>R~?6ei8 ziVebcYPs1iSsPMJ#ivI8OL&5~-^El87j=BqDq_Sg@0ZVm8u5M}MZIg2M&R%g#E~x; z>ABJ{4h$?fVcY_uvV`g}JNJ*VG5hBlG(9etiP*58UlM-hgJDyoRS0r`2SH$eZDp0K zW|I7^+cmh~l52I=f4TZXyJ!Ut4=Ag)He@g)i5_lj%n62^W;I^V1Vc}yR)`OIqYFv4Sh~!$^Ww+wGJ~XY4r^)hAbdsXbccGwl=h`Co-;py&nw^ zUS6`Y7GhmsO+SHyLJU0WZU<&aD5}N@l!y}`A)cX^^BYuME4V?~XR2@cE)O2=;^{EB zEFrh}NyX+nk! ze3|^m*Zc}Ltf%GBo@G5zt2gDE9b!2n|76%8_(%ByKTksc(O3%oa@lp(Kkq&lJ{Q|G z#*WHhFohOw9E&tQNvbhhE6QMC;-g)Px$9keL^uG~folgWzawdK&afM$gE+q!PBm4`5S-t3+EzSE4UkSAEv*cv%C)0el^@*fmB$z~_pCHwqelKSP{{8qU+W|&)1 z*UF~aD)SK@W)qBD7w7xG4w;TJVdl@f;XH{0o#g1E3~zVFBW zNk4x}xuG@0_O@)HnT4=m^+iaYFQub7S2pmUZfn6;Q7>d2OI&}DJ8^;C&^b%MXZV{tWw zKG3k&_U3ulce5)--REQMyF$T3dE3ZSC)F<-PPc1ik#!vnA+p7J06j5H^m&-w10u)j7u!V!O6XWn>!f57PE+=9?lK4p?&MBRS zWc1+#k;t71d7&3Q@f0go11%!v;%a{6RUF+g@gXz4Mc(b_p{`ZGwiC$*CwSDO=J|=G zd8@iFYX<>W@(-N*E?HP9X#+F=|Ma++}+p&C=`qU1CSUU5fY51T7y5^O&EN#dl+uMvfqBCr41W7ka z52#YTNO$!j>RSGj4o?MEn%1$~LGht&MD*89&B3ab!>hsBt#5q@gWlcG%3MOfN4r4u zI_M9B2ewpbd*t38Y~&@c>HmBkCTR=LWK%GfpB3J1R{GO2L^VO6ftex}^Wv%_zwVw! zK>xSZcREgzhI&<>^IwklD_MW_(V|I9qs+BpKJ-#&`p@kH3~cSD{cI*CcFriM1-p+9 zK3p|5RvPj>RR}s-a(}j4I(UEIzh0g@gYA1=Z?nb1i=!1CqptRBg`W%T4rcJV#PLo9 zWbkp|T1zLW&xW>yPxE$;PkJ;P?JHw~b6!4$)+lwfOx_8*O!Q!MYSts%#-k6o%*(`F6cQsY_ z@3W+jcchJ%H0^`P%Dw6s$^bVe@i*CTWtN*gc3nI!{Zv1g8;-F5EOzh#vvJu9>m;()eql3WT*k%X_08L|MJ=-`I6M2cM z3e1WTgoMBY!jV5xpOfa7GBA#mLNRVBt%-x74$@klb(D|0$5^b2WEE^in;$0I4LZYGA$SCr`Kil_y2nHB7iX#-) z5|O~fbL1JlWLOClRK%PBBQzWZV1FSux} zBAmL8wWRXWxq%Fzf>eG3OcFpb9Nm6e=)cyquqtyKx1f7Js_Y)H8DZsNa@IAn+qysS z8hY1V`e1XBzjRn=j{13=9N>>%J#n`u!bA~9Qme!?Tp-^S;07K7)$x_jMwEo_E*nR< z-iiNhiizCundi_@rqxavcibv<_H%ptyv%QR=iYEae9tU>l+eU&PwDnAy}go;AY-@s zO1WAj1zSSeMb>mH^U?hE;0yHH2YzV}rYQ4SF1|9Qfy5rG{lp$T^Yo~T1FiFu-{&LJ zMwO9wD!6pd%XAtuQN8Dy03}dVK{SdNZL!5dDf~3LQCkbnPQ(%$V%rD!+v%XiS1ObO z{pR%gC)3@siP2z{lJsD_ldRAO;~+;N=~D&WSi zVG`0=n4|n5z~}kGXPb1&@}C{V;3>|C%sBv{aN2IT`{jYT>DQmQ5qw`*I&!cGKWkcW zL|)tBx5(UA5bgReQ}`-&J^VDe(k>T|SzRh}Id-*MnhZ8M#*1JCF1Pvo zo^_d?h8t0TG}Y~FwKb>wvU7GR(5jNZdbjtqK(iCuf0T0LS)lJw&EqH^^QK0T9`W*w z4Fb090*$+P&B}oGb`lCy=XUs4qX&`Xe8wkZf7n+dW!QeTl+{Mh9@j-Jz!%lIzD zOmFk(^J4Uk@OgW&wq@cWP>7=f?52{*h!Fgcug4VQa%ii0bp*m&eRl|5%(n{IEb5Sk0tCw zpH9-|oDsi|4L++xGBr1uy3*846d4Sp_?%c`sMfimKZ|#H&Th<6t?(OAtX5B;uzNjW zPjk7oU)HJtq0O9`l*y`vKMgP0@!bXOLQ~jjDq3K;|LRNx1Tb7>5WCsL@cE3KH1}4} zVgmOa7rXD@uSo2X@BQeqTB*!YOU3vJEG+b2oUtE0;2K_K)kRHt+qA{RU#ZyGOIF#= z8)W~KUYUsp8Y_K0T*@p)YVDItQAv@So$1XFtY&^ekHpZAA@`6qS4!w3s2f_V(t;q@{M zA%8qxxGSX~u}}oF9u~0(fN%Qqhr=hLKS#8*bm-7<8p%GgLsEc)bX|6cl{X`2iQ@P3 zy)ns?wJfBHxa@Jz_-@kDll%L5lLyvzdu;!<)8jp$t#`5cOFI-AF3nGNFo@{Hv|J7S zFw!i~Yy~ZoZ2+6mu@mx3B%9G$&b5(Kn*1?=o@h$!V1wCwu4eM~B|J9zM>zQ798s?U z-#f>>y>^UKuE%m%hc|V)9?haW7`oW9p=KX!WOOf6GDESUUYNV_2^YtFBN?$6ZHcFf zYD7_q$6Mp=j^#CVQVsd&+5YNs>OG6j+inx3kLM&Fz4O@g{!h|>=&HKOE4q2;&latnIq3>Ov zAZ*C}K#?WWtF&*(h_y1vhgjrDV4ZT*T->=u^SaNJ=a*-2KYlZ@)0qR>I?0TJ&3)Zp zeQ_>|PtXhjI)PrAccnE~oCPQNv}~jikk`O%x5-r$<9+K@#D=>g(pLzJ8o+M%0uT>_ z*;nZ{Znu%s%MflCe*$K@@(@4J)aG7ktKmEC=iz3 zK3a0rv@%-Q_QDDAv1x=At-CNETBIdpiYW}<3f|*dhJt$jgj1cR;eaNF$FGYOS&Wvj^mexBTS!>D0W z$>}1|K|`871~vI?>djM`tjToQYMC?>Opx_Z_EKwSYi#hb7*p(q;A6bWFdJ zA2)5IJtN?MH%ZfMaz*gqQOoe2f2klEyvuk(KUfnnu`;=X^XO{dkYdx;=xFW*YKgX2 zP%Pr=R>jan)&1TSX)yizugRPn=I%*Cy9Z?@X&GPPLtLNe${WI)n)Q#SGiQ(C6q_kU zz6<*3DfkUB08$}4+QeSAT)wU59zL1Y# z;MV;0t!ohvKeiyDieq^;H|xB+o2Eq6HVgUyqixN1M`{dWcbAE%{`~C*^}70Qr%O{@ zVL!@3l7aQI-!HAwg6V;J>3HfmTkT|@T{ZiRl@xXM<36JML0zTJPyic4_LSL`#Wx+` zMiF)FL4Xj|E)ojf*k~be8X77FQVP%Cd?42R#(BMcqCAhDqNcz)D=$f7mvD9g!)#EX z+!#FQHWuPczuP+cE3z9mx&l(qZConRhsGk(rOVgZ8Mt%%cmPp>jKW7i{?u}#qIc++ zz1FqjPC)wf6`UsA2p;kF~9@OmmEeQRH-wk2x*v#J3~*jm8D| zFidp6wYFzNz1f}Z{d>ji&<|!|7O|sC#QGdiq*Y_ZAKr4>H=P;hb1u<~OJ(ebBY7wG3NV(NnDJYoc9VcYdNDZv94nKcVf9m-y!J zHXsc`b0`sKZoDePxTcJ@*LVNZE?caPvvg}%TkmlTadlSjMSkn1E~WjOnct-e zu|<5>RHS+c6W2zah#|}H_6iS7K>uJsd|v=q?z<}<9B1GYA!>{>4X-2TpIp}eE$Z&% z05B2bJY|lJoF zL+fsD`DhTZWaPos6JCeoBk5_|+oKDWMMoR3maBoQE&ht$OPkiSpUtdy?_JnURx?!E z{+hLTXC9k>xOOw4zm{QR5togomTg_D^bhC}4hYoZ=UJcaDs~DxzSx-x$Yj~MNw^3n zP3IXqfFflU4P(O(S#aKK|B-J8sAfFes?u*N8*q6N$HH1&NFOCRbn+d2Qh1_YY>+K z9EDVfi`}V5cH$&Q`j#7^*mQbHp>oL~als(AOUC;Z#~xt?^vKQ05`D%CVhGVbqyu-W zsf{(+PVU}aoI(pr{vzun7jQ%^rV>ZE$QAP9c4&Y4K0BbCsKl5aNN|o(-FPNL6lKrx`+m{U zO<_(v&fd^loog;7>~5tSw&WpXMhNS^Lr&D3|E7p9;~IO?#y;L%#FZERlm$1a!QGD{ zh4|AXD|eefi0PHZA=sT#L9ClDoq`8I_=4kh*G*$ykrO$CU8Wx%OyJt zgr)<)GC z-WyPnt5m}EvkYI~81HP`B+Tb6itDgTxrsLaRQOsRriOzUKGxYzfb9f_+wNEKSsbLM zqpXrRJ^Lc&Ip07%0F)Yx+J-ustG4}>Oj>p*K3DF`Phb9l7)G9P z%tb|<*$1rp_}pFcdJH_2UB`2GdS(h9?W;M9ADKi7gcs4`$)q`Pi=i;uCqsBC!o|Rz zAaBa&VjTn>m^%AJMp>%AFl3sZ?stA`m#f$p;M!acW%As@V?WdaO3;{npHlo1R#;{C zEOCGm!&w`aLv4BJ#+k7ONHmIzT8+gmEOF(Ih!jW#Ml}nF(D}QRgR)b=ae-~=NsTmO z6G9aNFa{H0K8tRGf0Eep&XmJr%yr_~U)5WvfK-K#K2rzg&RGxb(v8#l-G5%!VPb;mC;C8IiN?URi~J%^iiy(!OD@xyp&w-DSw@6Qo- zBWo}9?V1GwZfxgmH_jT4C?R#UZ>;ho-(=Z}+f3(wnJN6rrKnpH$k*6P<~Nt5`-%mN z^*1KieTcv8142yqHZdLY44{{L34pLi&z6hu0dPoTSK}()#rz>4D-`uHk=%N%0-#-S zCIsG_eS*01RMI@!g-FQ#a$Qg+#;xLU;_6$7z))(E9n*LS!$k>?ea0AixeUM2a;C)> z0TCbioW4Yw@e@;p$R)dI%>6+W5F+PYaOf^*cW*<#d_cJ5na8rxk)ZJ21gaYVsn0y4 zpL!!PbC$I*<^2n@xXb?-uC`V%Yamolic2e`Z~xx;GDx@szuxkN=O+%nm;C__2EDq% zEM>PX4O4M!+TSKPF1f;4occcUmJjX&UrY#dX+jZ_4a!0_kl4rx3SPA!0SmU*i;2Fz zqFGiNQ#g6*O!GDVm_~E2fCio>-9gfAQrE8u%2I#c>LQ#o`#fdFA5SojpL?99KiOBk zEif)3JsVGgpc(DQND_eO;q|z12bhWx$6nFN6S|v9QsS*QgoBUW##bCpT2mwC-{qV8 zi9hxw+KIRZLMAD`6JY?IBnA((4V^*Hr94}ZhKQ8L9`u*LB}9$l_2J4x+7H@_vLNE& z3SOwZRbl+ojO=FNP9Ar}N9FOS3nc+~>XjgvVKKlimIbtaEig1{d)Q5al)XP>Ki>a7 zaHn|P@aUE-AoxbJX~9y#k{ud^faL%|8AALT9WE8ppL^U!T}F0#@J8(*Gqt-4?wnZA zOeO^hS5`bLb0`6qD@|+{g@T&NkJ0LvLEj+*)e?wHx(uFD!^QN2LAPU5Kw(x@oa0^i z+bBuJ>0U%`>)NPF*oI4PUp0h)x8KxSv)#<8*VaS8)8vGsucklo z5<7(L;QO-)49FW8oU+ahNvyU&G)m2*c=JfkJb9Mhpq<_a+R4vyE41`QWFwOWaV;X6)fC81N|NrmmUc?S`m|K~L%?Wax~?_YT`QO)|7?<^QG zM2pyIu4SeVMf?_3jL7_-v;($0Bh2bGb!2Gt`FR&x`Ea7JI-HCT^NFIVj4lo^@ZwR7 zRlm>Ly(j%4-jDNhgQ-h9ONexP)#v8SX8W8?wiwi#1&&7Y#><&Nf?w4?VVUGsFU8PSRWvolnB8E}UoNqDp zpvA!q#LFHg+tU(lkf(BCxjW z!wf}6G}IBHf#u(y(Ne5f*{xMgFwSe0n+zys5P6lK|G?!mu3^AYc**@&(I_@R zjndv(d-li3rT_Vxx#9cwO=7c!n6eeq?qL~l erzH8gfpk!n_0WL4G((rl<^m8kY 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 literal 0 HcmV?d00001 diff --git a/core/datacap-web/console-fe/src/i18n/langs/en/common.ts b/core/datacap-web/console-fe/src/i18n/langs/en/common.ts index 8467b1e04..3b05cc036 100644 --- a/core/datacap-web/console-fe/src/i18n/langs/en/common.ts +++ b/core/datacap-web/console-fe/src/i18n/langs/en/common.ts @@ -102,5 +102,7 @@ export default { yAxis: 'yAxis', tag: 'Tag', data: 'Data', - export: 'Export' + export: 'Export', + file: 'File', + upload: 'Upload' } diff --git a/core/datacap-web/console-fe/src/i18n/langs/zhCn/common.ts b/core/datacap-web/console-fe/src/i18n/langs/zhCn/common.ts index c9a642880..314887f39 100644 --- a/core/datacap-web/console-fe/src/i18n/langs/zhCn/common.ts +++ b/core/datacap-web/console-fe/src/i18n/langs/zhCn/common.ts @@ -102,5 +102,7 @@ export default { yAxis: '纵轴', tag: '标签', data: '数据', - export: '导出' + export: '导出', + file: '文件', + upload: '上传' } diff --git a/core/datacap-web/console-fe/src/model/SourceModel.ts b/core/datacap-web/console-fe/src/model/SourceModel.ts index 4ef492b41..142668c94 100644 --- a/core/datacap-web/console-fe/src/model/SourceModel.ts +++ b/core/datacap-web/console-fe/src/model/SourceModel.ts @@ -14,4 +14,5 @@ export interface SourceModel createTime?: number; ssl?: boolean; configures: {}; + file?: [] } diff --git a/core/datacap-web/console-fe/src/views/pages/admin/source/SourceDetail.vue b/core/datacap-web/console-fe/src/views/pages/admin/source/SourceDetail.vue index 19bcb56d4..a67f26f45 100644 --- a/core/datacap-web/console-fe/src/views/pages/admin/source/SourceDetail.vue +++ b/core/datacap-web/console-fe/src/views/pages/admin/source/SourceDetail.vue @@ -60,6 +60,17 @@ + + +
@@ -122,6 +133,8 @@ import {defineComponent, reactive, ref} from "vue"; import {Configure} from "@/model/Configure"; import {clone} from 'lodash' import SourceV2Service from "@/services/SourceV2Service"; +import Common from "@/common/Common"; +import {ResponseModel} from "@/model/ResponseModel"; interface TestInfo { @@ -169,11 +182,13 @@ export default defineComponent({ loading: { test: false, save: false - } + }, + auth: null } }, created() { + this.auth = JSON.parse(localStorage.getItem(Common.token) || '{}'); if (this.id <= 0) { this.title = 'Create New Source'; this.formState = reactive(emptySource); @@ -293,6 +308,18 @@ export default defineComponent({ return; } this.pluginTabConfigure = this.pluginConfigure.filter(field => field.group === group); + }, + handlerUploadSuccess(response: ResponseModel) + { + if (response.status) { + const configure = this.applyPlugin.configures.filter(configure => configure.field === 'file') + configure[0].value.push(response.data) + } + }, + handlerUploadRemove(file) + { + const configure = this.applyPlugin.configures.filter(configure => configure.field === 'file') + configure[0].value = configure[0].value.filter(value => !value.endsWith(file.name)) } }, computed: { diff --git a/docs/docs/assets/plugin/hdfs.png b/docs/docs/assets/plugin/hdfs.png new file mode 100644 index 0000000000000000000000000000000000000000..e5640e81265bbcddc0d0b9a37b708548439b28de GIT binary patch literal 27101 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 erzH8gfpk!n_0WL4G((rl<^m8kY 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 literal 0 HcmV?d00001 diff --git a/docs/docs/index.md b/docs/docs/index.md index 6737b68c3..de8d0613a 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -206,6 +206,9 @@ Datacap is fast, lightweight, intuitive system.   Hologres +   + + Apache Hdfs

diff --git a/docs/docs/index.zh.md b/docs/docs/index.zh.md index b57aace74..f1fc77a80 100644 --- a/docs/docs/index.zh.md +++ b/docs/docs/index.zh.md @@ -206,6 +206,9 @@ Datacap 是快速、轻量级、直观的系统。   Hologres +   + + Apache Hdfs

diff --git a/docs/docs/reference/connectors/native/hadoop/hdfs.md b/docs/docs/reference/connectors/native/hadoop/hdfs.md new file mode 100644 index 000000000..fef373e6d --- /dev/null +++ b/docs/docs/reference/connectors/native/hadoop/hdfs.md @@ -0,0 +1,50 @@ +--- +title: Apache Hadoop HDFS +--- + + + +#### What is HDFS ? + +The Hadoop Distributed File System (HDFS) is a distributed file system designed to run on commodity hardware. It has many similarities with existing distributed file systems. + +#### Environment + +--- + +!!! note + + If you need to use this data source, you need to upgrade the DataCap service to >= `1.9.x` + +Support Time: `2023-04-27` + +#### Configure + +--- + + +!!! note + + If your HDFS service version requires other special configurations, please refer to modifying the configuration file and restarting the DataCap service. + +=== "Configure" + + | Field | Required | Default Value | + |:------:|:---------------------------------:|:-------------------------------------:| + | `Name` | :material-check-circle: { .red } | - | + +=== "Advanced" + + | Field | Required | Description | Default Value | + |:----------:|:--------------------------------:|:-----------:|:-------------:| + | `file` | :material-check-circle: { .red } | `core-site.xml`
`hdfs-site.xml` | `[]` | + +#### Version (Validation) + +--- + +!!! warning + + The online service has not been tested yet, if you have detailed test results, please submit [issues](https://github.com/EdurtIO/datacap/issues/new/choose) to us + +- [x] `3.x` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 2f668396c..c715a8ed1 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -171,6 +171,8 @@ nav: - MySQL: reference/connectors/jdbc/mysql.md - ClickHouse: reference/connectors/jdbc/clickhouse.md - Native: + - Apache Hadoop: + - HDFS: reference/connectors/native/hadoop/hdfs.md - H2 Database: reference/connectors/native/h2.md - Apache Kafka: - reference/connectors/native/kafka/index.md diff --git a/plugin/datacap-native-hdfs/pom.xml b/plugin/datacap-native-hdfs/pom.xml new file mode 100644 index 000000000..201fbd367 --- /dev/null +++ b/plugin/datacap-native-hdfs/pom.xml @@ -0,0 +1,132 @@ + + + 4.0.0 + + io.edurt.datacap + datacap + 1.9.0-SNAPSHOT + ../../pom.xml + + + datacap-native-hdfs + DataCap - Apache Hadoop HDFS + + + 3.3.4 + native-hdfs + + + + + io.edurt.datacap + datacap-spi + provided + + + commons-beanutils + commons-beanutils + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple + test + + + org.jetbrains.kotlin + kotlin-reflect + + + org.apache.hadoop + hadoop-common + ${hadoop.version} + + + org.eclipse.jetty.aggregate + jetty-all + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-webapp + + + org.eclipse.jetty + jetty-util + + + org.eclipse.jetty + jetty-servlet + + + javax.servlet + servlet-api + + + log4j-slf4j-impl + org.apache.logging.log4j + + + slf4j-log4j12 + org.slf4j + + + org.slf4j + slf4j-reload4j + + + com.sun.jersey + jersey-* + + + javax.servlet + javax.servlet-api + + + + + org.apache.hadoop + hadoop-hdfs-client + ${hadoop.version} + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + ${assembly-plugin.version} + + ${plugin.name} + + ../../configure/assembly/assembly-plugin.xml + + ../../dist/plugins/${plugin.name} + + + + make-assembly + package + + single + + + + + + org.jetbrains.dokka + dokka-maven-plugin + + + + + diff --git a/plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsAdapter.kt b/plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsAdapter.kt new file mode 100644 index 000000000..e215ee0bf --- /dev/null +++ b/plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsAdapter.kt @@ -0,0 +1,95 @@ +package io.edurt.datacap.natived.hdfs + +import io.edurt.datacap.spi.adapter.NativeAdapter +import io.edurt.datacap.spi.model.Configure +import io.edurt.datacap.spi.model.Response +import io.edurt.datacap.spi.model.Time +import io.edurt.datacap.spi.parser.SqlParser +import io.edurt.datacap.sql.SqlBase +import io.edurt.datacap.sql.SqlBaseToken +import org.apache.commons.lang3.ObjectUtils +import org.apache.hadoop.conf.Configuration +import org.apache.hadoop.fs.FileSystem +import org.apache.hadoop.fs.Path +import org.slf4j.Logger +import org.slf4j.LoggerFactory.getLogger +import java.lang.Boolean +import java.util.* +import kotlin.Any +import kotlin.Exception +import kotlin.String +import kotlin.require +import kotlin.requireNotNull + + +class HdfsAdapter : NativeAdapter { + private val log: Logger = getLogger(HdfsAdapter::class.java) + + private var hdfsConnection: HdfsConnection? = null + + constructor(connection: HdfsConnection?, parser: SqlParser?) : super(connection, parser) { + this.hdfsConnection = connection + } + + override fun handlerExecute(content: String?): Response { + val processorTime = Time() + processorTime.start = Date().time + val response: Response = this.connection.response + val configure: Configure = this.connection.configure + if (response.isConnected) { + val headers: MutableList = ArrayList() + val types: MutableList = ArrayList() + val columns: MutableList = ArrayList() + try { + val sqlBase = parser.sqlBase + if (sqlBase.isSuccessful) { + val configuration = this.hdfsConnection?.hdfsConfigure + val sqlBase = this.parser.sqlBase + if (sqlBase.isSuccessful) { + if (ObjectUtils.isNotEmpty(parser.sqlBase.columns)) { + headers.addAll(parser.sqlBase.columns) + } else { + headers.add("*") + } + types.add("String") + this.adapter(configuration, sqlBase) + .forEach { column -> columns.add(handlerFormatter(configure.format, headers, Collections.singletonList(column) as List?)) } + response.isSuccessful = Boolean.TRUE + } else { + response.isSuccessful = false + response.message = sqlBase.message + } + } else { + response.isSuccessful = Boolean.FALSE + response.message = sqlBase.message + } + } catch (ex: Exception) { + log.error("Execute content failed content {} exception ", content, ex) + response.isSuccessful = Boolean.FALSE + response.message = ex.message + } finally { + response.headers = headers + response.types = types + response.columns = columns + } + } + processorTime.end = Date().time + response.processor = processorTime + return response + } + + private fun adapter(configuration: Configuration?, info: SqlBase): List { + requireNotNull(info.token) { "Token must not be null" } + require(info.token.equals(SqlBaseToken.SHOW.name, ignoreCase = true)) { "Token not supported" } + + val fileSystem = FileSystem.get(configuration) + + if (info.childToken.equals("DATABASES", ignoreCase = true)) { + info.table = "/" + } + + return fileSystem.listStatus(Path("/" + info.table)) + .map { it.path.name } + .toList() + } +} diff --git a/plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsConnection.kt b/plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsConnection.kt new file mode 100644 index 000000000..c383e3a32 --- /dev/null +++ b/plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsConnection.kt @@ -0,0 +1,49 @@ +package io.edurt.datacap.natived.hdfs + +import io.edurt.datacap.spi.connection.Connection +import io.edurt.datacap.spi.model.Configure +import io.edurt.datacap.spi.model.Response +import org.apache.commons.lang3.StringUtils +import org.apache.hadoop.conf.Configuration +import org.apache.hadoop.fs.Path +import java.io.File + +class HdfsConnection : Connection { + private var configure: Configure? = null + private var response: Response? = null + + var hdfsConfigure: Configuration? = null + + constructor(configure: Configure, response: Response) : super(configure, response) + + override fun formatJdbcUrl(): String { + return TODO("Provide the return value") + } + + override fun openConnection(): java.sql.Connection? { + try { + this.configure = getConfigure() + this.response = getResponse() + this.hdfsConfigure = Configuration() + this.hdfsConfigure!!.addResource(this.getPath("core-site.xml")) + this.hdfsConfigure!!.addResource(this.getPath("hdfs-site.xml")) + response?.isConnected = true + } catch (ex: Exception) { + response?.isConnected = false + response?.message = ex.message + } + return null + } + + override fun destroy() { + this.hdfsConfigure = null + } + + private fun getPath(file: String): Path { + var path = mutableListOf(this.configure?.home, this.configure?.username?.get(), "Hdfs", file) + if (StringUtils.isNotEmpty(this.configure?.id)) { + path = mutableListOf(this.configure?.home, this.configure?.username?.get(), "Hdfs", this.configure?.id, file) + } + return Path(path.joinToString(separator = File.separator)) + } +} diff --git a/plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsModule.kt b/plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsModule.kt new file mode 100644 index 000000000..3c2f3fd05 --- /dev/null +++ b/plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsModule.kt @@ -0,0 +1,23 @@ +package io.edurt.datacap.natived.hdfs + +import com.google.inject.multibindings.Multibinder +import io.edurt.datacap.spi.AbstractPluginModule +import io.edurt.datacap.spi.Plugin +import io.edurt.datacap.spi.PluginModule +import io.edurt.datacap.spi.PluginType + +class HdfsModule : AbstractPluginModule(), PluginModule { + override fun getType(): PluginType { + return PluginType.NATIVE + } + + override fun get(): AbstractPluginModule { + return this + } + + override fun configure() { + Multibinder.newSetBinder(binder(), Plugin::class.java) + .addBinding() + .to(HdfsPlugin::class.java) + } +} diff --git a/plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsPlugin.kt b/plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsPlugin.kt new file mode 100644 index 000000000..4d6aec5e7 --- /dev/null +++ b/plugin/datacap-native-hdfs/src/main/kotlin/io/edurt/datacap/natived/hdfs/HdfsPlugin.kt @@ -0,0 +1,63 @@ +package io.edurt.datacap.natived.hdfs + +import io.edurt.datacap.spi.Plugin +import io.edurt.datacap.spi.PluginType +import io.edurt.datacap.spi.adapter.Adapter +import io.edurt.datacap.spi.model.Configure +import io.edurt.datacap.spi.model.Response +import io.edurt.datacap.spi.parser.SqlParser +import org.apache.commons.beanutils.BeanUtils +import org.apache.commons.lang3.ObjectUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory.getLogger + +class HdfsPlugin : Plugin { + private val log: Logger = getLogger(HdfsPlugin::class.java) + + private var configure: Configure? = null + private var connection: HdfsConnection? = null + private var response: Response? = null + + override fun type(): PluginType { + return PluginType.NATIVE + } + + override fun validator(): String { + return "SHOW DATABASES" + } + + override fun description(): String { + return String.format("Integrate %s data sources", this.name()) + } + + override fun connect(configure: Configure?) { + try { + this.response = Response() + this.configure = Configure() + BeanUtils.copyProperties(this.configure, configure) + this.connection = HdfsConnection(this.configure!!, this.response!!) + } catch (ex: Exception) { + this.response?.isConnected = false + this.response?.message = ex.message + } + } + + override fun execute(content: String?): Response { + if (ObjectUtils.isNotEmpty(connection)) { + log.info("Execute hdfs plugin logic started") + response = connection!!.response + val processor: Adapter = HdfsAdapter(connection, SqlParser(content)) + response = processor.handlerExecute(content) + log.info("Execute hdfs plugin logic end") + } + destroy() + return response!! + } + + override fun destroy() { + if (ObjectUtils.isNotEmpty(this.connection)) { + this.connection?.destroy() + this.connection = null + } + } +} diff --git a/plugin/datacap-native-hdfs/src/main/resources/META-INF/services/io.edurt.datacap.spi.PluginModule b/plugin/datacap-native-hdfs/src/main/resources/META-INF/services/io.edurt.datacap.spi.PluginModule new file mode 100644 index 000000000..26876db46 --- /dev/null +++ b/plugin/datacap-native-hdfs/src/main/resources/META-INF/services/io.edurt.datacap.spi.PluginModule @@ -0,0 +1 @@ +io.edurt.datacap.natived.hdfs.HdfsModule diff --git a/plugin/datacap-native-hdfs/src/test/kotlin/io/edurt/datacap/natived/hdfs/HdfsModuleTest.kt b/plugin/datacap-native-hdfs/src/test/kotlin/io/edurt/datacap/natived/hdfs/HdfsModuleTest.kt new file mode 100644 index 000000000..38e99e7a0 --- /dev/null +++ b/plugin/datacap-native-hdfs/src/test/kotlin/io/edurt/datacap/natived/hdfs/HdfsModuleTest.kt @@ -0,0 +1,27 @@ +package io.edurt.datacap.natived.hdfs + +import com.google.inject.Guice +import com.google.inject.Injector +import com.google.inject.Key +import com.google.inject.TypeLiteral +import io.edurt.datacap.spi.Plugin +import org.apache.commons.lang3.ObjectUtils +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +class HdfsModuleTest { + private var injector: Injector? = null + + @Before + fun before() { + injector = Guice.createInjector(HdfsModule()) + } + + @Test + fun test() { + val plugin: Plugin? = injector?.getInstance(Key.get(object : TypeLiteral?>() {})) + ?.first { v -> v?.name().equals("Hdfs") } + Assert.assertTrue(ObjectUtils.isNotEmpty(plugin)) + } +} \ No newline at end of file diff --git a/plugin/datacap-native-hdfs/src/test/kotlin/io/edurt/datacap/natived/hdfs/HdfsPluginTest.kt b/plugin/datacap-native-hdfs/src/test/kotlin/io/edurt/datacap/natived/hdfs/HdfsPluginTest.kt new file mode 100644 index 000000000..b3f4bf0b7 --- /dev/null +++ b/plugin/datacap-native-hdfs/src/test/kotlin/io/edurt/datacap/natived/hdfs/HdfsPluginTest.kt @@ -0,0 +1,48 @@ +package io.edurt.datacap.natived.hdfs + +import com.google.inject.Guice +import com.google.inject.Injector +import com.google.inject.Key +import com.google.inject.TypeLiteral +import io.edurt.datacap.spi.Plugin +import io.edurt.datacap.spi.model.Configure +import io.edurt.datacap.spi.model.Response +import org.apache.commons.lang3.ObjectUtils +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.slf4j.LoggerFactory.getLogger +import java.util.* + +class HdfsPluginTest { + private val log = getLogger(this::class.java) + + private var injector: Injector? = null + private var configure: Configure? = null + + @Before + fun before() { + injector = Guice.createInjector(HdfsModule()) + configure = Configure() + configure?.home = this.javaClass.getResource("/").file.toString() + configure?.username = Optional.of("default") + } + + @Test + fun test() { + val plugin: Plugin? = injector?.getInstance(Key.get(object : TypeLiteral?>() {})) + ?.first { v -> v?.name().equals("Hdfs") } + if (ObjectUtils.isNotEmpty(plugin)) { + plugin?.connect(configure) + val sql = "SHOW DATABASES" + val response: Response = plugin!!.execute(sql) + log.info("================ plugin executed information =================") + if (!response.isSuccessful) { + log.error("Message: {}", response.message) + } else { + response.columns.forEach { column -> log.info(column.toString()) } + } + Assert.assertTrue(response.isSuccessful) + } + } +} diff --git a/plugin/datacap-native-hdfs/src/test/resources/default/core-site.xml b/plugin/datacap-native-hdfs/src/test/resources/default/core-site.xml new file mode 100644 index 000000000..48629e91e --- /dev/null +++ b/plugin/datacap-native-hdfs/src/test/resources/default/core-site.xml @@ -0,0 +1,61 @@ + + + + + + + + + hadoop.tmp.dir + /usr/local/hadoop/tmp + Abase for other temporary directories. + + + fs.defaultFS + hdfs://localhost:9000 + + + hadoop.proxyuser.knox.hosts + * + + + hadoop.proxyuser.flowagent.hosts + * + + + hadoop.proxyuser.hdfs.groups + * + + + hadoop.proxyuser.hdfs.hosts + * + + + hadoop.proxyuser.hadoop.groups + * + + + hadoop.proxyuser.hadoop.hosts + * + + + hadoop.proxyuser.dw_super.groups + * + + + hadoop.proxyuser.dw_super.hosts + * + + \ No newline at end of file diff --git a/plugin/datacap-native-hdfs/src/test/resources/default/hdfs-site.xml b/plugin/datacap-native-hdfs/src/test/resources/default/hdfs-site.xml new file mode 100644 index 000000000..dd84a705a --- /dev/null +++ b/plugin/datacap-native-hdfs/src/test/resources/default/hdfs-site.xml @@ -0,0 +1,28 @@ + + + + + + + + + dfs.replication + 2 + + + dfs.webhdfs.enabled + true + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 00e5d7531..088821d5e 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ plugin/datacap-native-redis plugin/datacap-native-kafka plugin/datacap-native-h2 + plugin/datacap-native-hdfs plugin/datacap-http-cratedb plugin/datacap-http-clickhouse plugin/datacap-http-ceresdb @@ -65,7 +66,7 @@ shaded/datacap-shaded-ydb - DataCap + datacap DataCap is integrated software for data transformation, integration and visualization. https://datacap.edurt.io/ From 47513e9e8d622daef56aad7cbc8180d7a853b0d5 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Fri, 28 Apr 2023 16:14:17 +0800 Subject: [PATCH 3/3] [Web] Fixed upload file domain --- .github/workflows/bofore_checker.yml | 4 ++-- .../console-fe/src/views/pages/admin/source/SourceDetail.vue | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bofore_checker.yml b/.github/workflows/bofore_checker.yml index 4e703a77e..5e6e36a0b 100644 --- a/.github/workflows/bofore_checker.yml +++ b/.github/workflows/bofore_checker.yml @@ -27,7 +27,7 @@ jobs: java-version: '11' distribution: 'temurin' - run: chmod 755 ./mvnw - - run: ./mvnw -T 1C clean install checkstyle:checkstyle -Dfindbugs.skip -Dgpg.skip -Dskip.yarn -DskipTests=true + - run: ./mvnw clean install checkstyle:checkstyle -Dfindbugs.skip -Dgpg.skip -Dskip.yarn -DskipTests=true before_checker_bugs: runs-on: ubuntu-latest @@ -42,7 +42,7 @@ jobs: java-version: '11' distribution: 'temurin' - run: chmod 755 ./mvnw - - run: ./mvnw -T 1C clean install findbugs:findbugs -Dcheckstyle.skip -Dgpg.skip -Dskip.yarn -DskipTests=true + - run: ./mvnw clean install findbugs:findbugs -Dcheckstyle.skip -Dgpg.skip -Dskip.yarn -DskipTests=true before_checker_package: runs-on: ubuntu-latest diff --git a/core/datacap-web/console-fe/src/views/pages/admin/source/SourceDetail.vue b/core/datacap-web/console-fe/src/views/pages/admin/source/SourceDetail.vue index a67f26f45..9243e4328 100644 --- a/core/datacap-web/console-fe/src/views/pages/admin/source/SourceDetail.vue +++ b/core/datacap-web/console-fe/src/views/pages/admin/source/SourceDetail.vue @@ -68,7 +68,7 @@ :format="['xml']" :on-success="handlerUploadSuccess" :on-remove="handlerUploadRemove" - action="http://localhost:9096/api/v1/source/uploadFile"> + action="/api/v1/source/uploadFile">