diff --git a/magic-api-plugins/magic-api-plugin-git-store/pom.xml b/magic-api-plugins/magic-api-plugin-git-store/pom.xml
new file mode 100644
index 00000000..6579153c
--- /dev/null
+++ b/magic-api-plugins/magic-api-plugin-git-store/pom.xml
@@ -0,0 +1,30 @@
+
+
+ 4.0.0
+
+ org.ssssssss
+ magic-api-plugins
+ 2.0.0-alpha.4
+
+ magic-api-plugin-git-store
+ jar
+ magic-api-plugin-git-store
+ magic-api-plugin-git-store
+
+ 5.13.0.202109080827-r
+
+
+
+ org.eclipse.jgit
+ org.eclipse.jgit
+ ${jgit.version}
+
+
+ org.eclipse.jgit
+ org.eclipse.jgit.ssh.jsch
+ ${jgit.version}
+
+
+
diff --git a/magic-api-plugins/magic-api-plugin-git-store/src/main/java/org/ssssssss/magicapi/git/GitRepo.java b/magic-api-plugins/magic-api-plugin-git-store/src/main/java/org/ssssssss/magicapi/git/GitRepo.java
new file mode 100644
index 00000000..72b9156d
--- /dev/null
+++ b/magic-api-plugins/magic-api-plugin-git-store/src/main/java/org/ssssssss/magicapi/git/GitRepo.java
@@ -0,0 +1,160 @@
+package org.ssssssss.magicapi.git;
+
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.Session;
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jgit.api.*;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
+import org.eclipse.jgit.transport.*;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.ssssssss.magicapi.core.exception.MagicAPIException;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * git仓库
+ *
+ * @author soriee
+ * @date 2022/2/20 22:48
+ */
+public class GitRepo {
+ private static final Logger logger = LoggerFactory.getLogger(GitRepo.class);
+ /**
+ * 文件路径地址
+ */
+ private String rootPath;
+ private String gitFilePath;
+ private GitStoreProperties properties;
+ private Git git;
+
+ public GitRepo(String rootPath, GitStoreProperties properties) {
+ this.rootPath = rootPath;
+ this.gitFilePath = rootPath + File.separator + ".git";
+ this.properties = properties;
+ }
+
+ private void valid() {
+ File repoDir = new File(rootPath);
+ File gitFile = new File(gitFilePath);
+ // 如果文件夹不存在 则创建文件夹
+ if (!repoDir.exists()) {
+ repoDir.mkdirs();
+ }
+ if (!gitFile.exists() && repoDir.list().length > 0) {
+ throw new MagicAPIException("初次项目启动时,请保持文件夹为空。");
+ }
+ }
+
+ /**
+ * 设置ssh秘钥或者账号密码
+ *
+ * @param transportCommand
+ * @return
+ * @author soriee
+ * @date 2022/2/28 20:06
+ */
+ private void setSshOrCredentials(TransportCommand transportCommand) {
+ if (this.getProperties().getPrivateKey() != null) {
+ // ssh
+ final SshSessionFactory sshSessionFactory = new JschConfigSessionFactory() {
+ @Override
+ protected void configure(OpenSshConfig.Host host, Session session) {
+ }
+
+ @Override
+ protected JSch createDefaultJSch(FS fs) throws JSchException {
+ JSch defaultJSch = super.createDefaultJSch(fs);
+ defaultJSch.addIdentity(GitRepo.this.getProperties().getPrivateKey());
+ return defaultJSch;
+ }
+ };
+ transportCommand.setTransportConfigCallback(new TransportConfigCallback() {
+ @Override
+ public void configure(Transport transport) {
+ SshTransport sshTransport = (SshTransport) transport;
+ sshTransport.setSshSessionFactory(sshSessionFactory);
+ }
+ });
+ } else if (StringUtils.isNotBlank(properties.getUsername())
+ && StringUtils.isNotBlank(properties.getPassword())) {
+ // 账号密码
+ transportCommand.setCredentialsProvider(new UsernamePasswordCredentialsProvider(
+ properties.getUsername(),
+ properties.getPassword()));
+ }
+ }
+
+ /**
+ * 项目设置仓库
+ *
+ * @return
+ * @author soriee
+ * @date 2022/2/24 20:43
+ */
+ public void setupRepo() throws IOException, GitAPIException {
+ this.valid();
+ File gitFile = new File(gitFilePath);
+ try {
+ if (gitFile.exists()) {
+ // 项目存在,则打开为仓库, 并且强制更新一次
+ FileRepositoryBuilder builder = new FileRepositoryBuilder();
+ Repository repository = builder.create(gitFile);
+ git = new Git(repository);
+ // 更新两次,避免删除文件未更新
+ this.update(false);
+ this.update(true);
+ } else {
+ CloneCommand cloneCommand = Git.cloneRepository()
+ .setURI(properties.getUrl())
+ .setDirectory(new File(rootPath))
+ .setBranch(properties.getBranch());
+ this.setSshOrCredentials(cloneCommand);
+ git = cloneCommand.call();
+ }
+ } catch (IOException | GitAPIException e) {
+ logger.error("初始化git仓库失败", e);
+ throw e;
+ }
+ }
+
+ /**
+ * 更新
+ * 1.git add .
+ * 2.git commit -m "同步数据"
+ * 3.git pull
+ * 4.git push
+ * @param update
+ * @return
+ * @author soriee
+ * @date 2022/2/20 22:54
+ */
+ public boolean update(boolean update) {
+ try {
+ git.add().setUpdate(update).addFilepattern(".").call();
+ git.commit().setMessage("同步数据").call();
+ PullCommand pull = git.pull();
+ this.setSshOrCredentials(pull);
+ PullResult pullResult = pull.call();
+ if (!pullResult.isSuccessful()) {
+ throw new MagicAPIException("git更新失败, 请重试或尝试手动更新");
+ }
+ PushCommand pushCommand = git.push();
+ this.setSshOrCredentials(pushCommand);
+ pushCommand.call();
+ } catch (GitAPIException e) {
+ logger.error("git更新失败", e);
+ throw new MagicAPIException("git更新失败, 请重试或尝试手动更新");
+ }
+ return true;
+ }
+
+ public GitStoreProperties getProperties() {
+ return properties;
+ }
+}
diff --git a/magic-api-plugins/magic-api-plugin-git-store/src/main/java/org/ssssssss/magicapi/git/GitResource.java b/magic-api-plugins/magic-api-plugin-git-store/src/main/java/org/ssssssss/magicapi/git/GitResource.java
new file mode 100644
index 00000000..ee975b85
--- /dev/null
+++ b/magic-api-plugins/magic-api-plugin-git-store/src/main/java/org/ssssssss/magicapi/git/GitResource.java
@@ -0,0 +1,124 @@
+package org.ssssssss.magicapi.git;
+
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.ssssssss.magicapi.core.resource.FileResource;
+import org.ssssssss.magicapi.core.resource.Resource;
+import org.ssssssss.magicapi.core.resource.ResourceAdapter;
+import org.ssssssss.magicapi.git.GitStoreProperties;
+import org.ssssssss.magicapi.utils.IoUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 文件存储实现
+ *
+ * @author mxd
+ */
+public class GitResource extends FileResource {
+ private GitRepo gitRepo;
+
+ public static GitResource of(org.ssssssss.magicapi.core.config.Resource config, GitStoreProperties properties) throws IOException, GitAPIException {
+ File file = new File(config.getLocation());
+ GitRepo gitRepo = new GitRepo(file.getAbsolutePath(), properties);
+ GitResource gitResource = new GitResource(config.isReadonly(), file,
+ file.getAbsolutePath(), gitRepo);
+ // 初始化
+ gitResource.setup();
+ return gitResource;
+ }
+
+
+ public GitResource(boolean readonly, File file, String rootPath, GitRepo gitRepo) {
+ super(file, readonly, rootPath);
+ this.gitRepo = gitRepo;
+ }
+ /**
+ * 进行初始化操作, 仅仅在项目启动时进行初始化
+ * @author soriee
+ * @date 2022/2/20 22:30
+ * @return
+ */
+ private void setup() throws IOException, GitAPIException {
+ synchronized(GitResource.class) {
+ gitRepo.setupRepo();
+ }
+ }
+ private boolean update(boolean update) {
+ return gitRepo.update(update);
+ }
+
+
+ @Override
+ public boolean delete() {
+ return super.delete() && this.update(true);
+ }
+
+ @Override
+ public boolean mkdir() {
+ return super.mkdir() && this.update(false);
+ }
+ @Override
+ public Resource getResource(String name) {
+ return new GitResource(super.readonly(), new File(super.file, name), super.rootPath, this.gitRepo);
+ }
+
+ @Override
+ public Resource getDirectory(String name) {
+ return getResource(name);
+ }
+ @Override
+ public boolean write(byte[] bytes) {
+ return super.write(bytes) && this.update(false);
+ }
+
+ @Override
+ public boolean write(String content) {
+ return super.write(content) && this.update(false);
+ }
+
+ @Override
+ public List resources() {
+ File[] files = this.file.listFiles();
+ return files == null ? Collections.emptyList() : Arrays.stream(files).map(it -> new GitResource(this.readonly(),
+ it, this.rootPath, this.gitRepo)).collect(Collectors.toList());
+ }
+ @Override
+ public Resource parent() {
+ return this.rootPath.equals(this.file.getAbsolutePath()) ? null : new GitResource(this.readonly(),
+ this.file.getParentFile(), this.rootPath, this.gitRepo);
+ }
+ @Override
+ public List dirs() {
+ return IoUtils.dirs(this.file).stream().map(it -> new GitResource(this.readonly(),
+ it, this.rootPath, this.gitRepo)).collect(Collectors.toList());
+ }
+ @Override
+ public List files(String suffix) {
+ return IoUtils.files(this.file, suffix).stream().map(it -> new GitResource(this.readonly(),
+ it, this.rootPath, this.gitRepo)).collect(Collectors.toList());
+ }
+ @Override
+ public boolean renameTo(Resource resource) {
+ if (!this.readonly()) {
+ File target = ((GitResource) resource).file;
+ if (this.file.renameTo(target)) {
+ this.file = target;
+ // 更新两次,新增文件和删除文件都要更新
+ this.update(false);
+ this.update(true);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return this.gitRepo.getProperties().getUrl();
+ }
+}
diff --git a/magic-api-plugins/magic-api-plugin-git-store/src/main/java/org/ssssssss/magicapi/git/GitStoreProperties.java b/magic-api-plugins/magic-api-plugin-git-store/src/main/java/org/ssssssss/magicapi/git/GitStoreProperties.java
new file mode 100644
index 00000000..ff7db46a
--- /dev/null
+++ b/magic-api-plugins/magic-api-plugin-git-store/src/main/java/org/ssssssss/magicapi/git/GitStoreProperties.java
@@ -0,0 +1,68 @@
+package org.ssssssss.magicapi.git;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(prefix = "magic-api.resource.git")
+public class GitStoreProperties {
+ /**
+ * git仓库地址
+ */
+ private String url;
+ /**
+ * git分支
+ */
+ private String branch;
+ /**
+ * ssh 密钥地址
+ * 仅支持-m PEM参数生产的ssh key
+ */
+ private String privateKey;
+ /**
+ * git账号
+ */
+ private String username;
+ /**
+ * git密码
+ */
+ private String password;
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getBranch() {
+ return branch;
+ }
+
+ public void setBranch(String branch) {
+ this.branch = branch;
+ }
+
+ public String getPrivateKey() {
+ return privateKey;
+ }
+
+ public void setPrivateKey(String privateKey) {
+ this.privateKey = privateKey;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+}
diff --git a/magic-api-plugins/magic-api-plugin-git-store/src/main/java/org/ssssssss/magicapi/git/MagicGitStoreConfiguration.java b/magic-api-plugins/magic-api-plugin-git-store/src/main/java/org/ssssssss/magicapi/git/MagicGitStoreConfiguration.java
new file mode 100644
index 00000000..d8aab900
--- /dev/null
+++ b/magic-api-plugins/magic-api-plugin-git-store/src/main/java/org/ssssssss/magicapi/git/MagicGitStoreConfiguration.java
@@ -0,0 +1,48 @@
+package org.ssssssss.magicapi.git;
+
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.ssssssss.magicapi.core.config.MagicAPIProperties;
+import org.ssssssss.magicapi.core.config.MagicPluginConfiguration;
+import org.ssssssss.magicapi.core.config.Resource;
+import org.ssssssss.magicapi.core.model.Plugin;
+import org.ssssssss.magicapi.git.GitStoreProperties;
+
+import java.io.IOException;
+
+@Configuration
+@EnableConfigurationProperties(GitStoreProperties.class)
+public class MagicGitStoreConfiguration implements MagicPluginConfiguration {
+
+ private final MagicAPIProperties properties;
+ private final GitStoreProperties gitStoreProperties;
+
+ public MagicGitStoreConfiguration(MagicAPIProperties properties, GitStoreProperties gitStoreProperties) {
+ this.properties = properties;
+ this.gitStoreProperties = gitStoreProperties;
+ }
+
+ /**
+ * git存储
+ * @author soriee
+ * @date 2022/2/28 19:50
+ * @return
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = "magic-api", name = "resource.type", havingValue = "git")
+ public org.ssssssss.magicapi.core.resource.Resource magicGitResource() throws IOException, GitAPIException {
+ Resource resourceConfig = properties.getResource();
+ return GitResource.of(resourceConfig, this.gitStoreProperties);
+ }
+
+
+ @Override
+ public Plugin plugin() {
+ return new Plugin("Git");
+ }
+}
diff --git a/magic-api-plugins/magic-api-plugin-git-store/src/main/resources/META-INF/spring.factories b/magic-api-plugins/magic-api-plugin-git-store/src/main/resources/META-INF/spring.factories
new file mode 100644
index 00000000..83c6a893
--- /dev/null
+++ b/magic-api-plugins/magic-api-plugin-git-store/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+ org.ssssssss.magicapi.git.MagicGitStoreConfiguration
diff --git a/magic-api-plugins/pom.xml b/magic-api-plugins/pom.xml
index 241b3d96..11ee112b 100644
--- a/magic-api-plugins/pom.xml
+++ b/magic-api-plugins/pom.xml
@@ -20,6 +20,7 @@
magic-api-plugin-mongo
magic-api-plugin-elasticsearch
magic-api-plugin-cluster
+ magic-api-plugin-git-store