Introduce Git sync service
This commit is contained in:
		
							parent
							
								
									c88a05ef5c
								
							
						
					
					
						commit
						7248279a88
					
				@ -0,0 +1,164 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2024 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.service.sync;
 | 
			
		||||
 | 
			
		||||
import jakarta.annotation.PreDestroy;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.apache.commons.lang3.StringUtils;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Value;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
import org.thingsboard.common.util.ThingsBoardThreadFactory;
 | 
			
		||||
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
 | 
			
		||||
import org.thingsboard.server.service.sync.vc.GitRepository;
 | 
			
		||||
import org.thingsboard.server.service.sync.vc.GitRepository.FileType;
 | 
			
		||||
import org.thingsboard.server.service.sync.vc.GitRepository.RepoFile;
 | 
			
		||||
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
import java.util.concurrent.ScheduledExecutorService;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
 | 
			
		||||
@Service
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class DefaultGitSyncService implements GitSyncService {
 | 
			
		||||
 | 
			
		||||
    @Value("${vc.git.repositories-folder:${java.io.tmpdir}/repositories}")
 | 
			
		||||
    private String repositoriesFolder;
 | 
			
		||||
 | 
			
		||||
    private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("git-sync"));
 | 
			
		||||
    private final Map<String, GitRepository> repositories = new ConcurrentHashMap<>();
 | 
			
		||||
    private final Map<String, Runnable> updateListeners = new ConcurrentHashMap<>();
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void registerSync(String key, String repoUri, String branch, long fetchFrequencyMs, Runnable onUpdate) {
 | 
			
		||||
        RepositorySettings settings = new RepositorySettings();
 | 
			
		||||
        settings.setRepositoryUri(repoUri);
 | 
			
		||||
        settings.setDefaultBranch(branch);
 | 
			
		||||
        if (onUpdate != null) {
 | 
			
		||||
            updateListeners.put(key, onUpdate);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        executor.execute(() -> {
 | 
			
		||||
            initRepository(key, settings);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        executor.scheduleWithFixedDelay(() -> {
 | 
			
		||||
            GitRepository repository = repositories.get(key);
 | 
			
		||||
            if (repository == null || Files.notExists(Path.of(repository.getDirectory()))) {
 | 
			
		||||
                initRepository(key, settings);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                log.debug("Fetching {} repository", key);
 | 
			
		||||
                repository.fetch();
 | 
			
		||||
                onUpdate(key);
 | 
			
		||||
            } catch (Throwable e) {
 | 
			
		||||
                log.error("Failed to fetch {} repository", key, e);
 | 
			
		||||
            }
 | 
			
		||||
        }, fetchFrequencyMs, fetchFrequencyMs, TimeUnit.MILLISECONDS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<RepoFile> listFiles(String key, String path, int depth, FileType type) {
 | 
			
		||||
        GitRepository repository = getRepository(key);
 | 
			
		||||
        return repository.listFilesAtCommit(getBranchRef(repository), path, depth).stream()
 | 
			
		||||
                .filter(file -> type == null || file.type() == type)
 | 
			
		||||
                .toList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getFileContent(String key, String path) {
 | 
			
		||||
        GitRepository repository = getRepository(key);
 | 
			
		||||
        try {
 | 
			
		||||
            return repository.getFileContentAtCommit(path, getBranchRef(repository));
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            log.warn("Failed to get file content for path {} for {} repository: {}", path, key, e.getMessage());
 | 
			
		||||
            return "{}";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getGithubRawContentUrl(String key, String path) {
 | 
			
		||||
        if (path == null) {
 | 
			
		||||
            return "";
 | 
			
		||||
        }
 | 
			
		||||
        RepositorySettings settings = getRepository(key).getSettings();
 | 
			
		||||
        return StringUtils.removeEnd(settings.getRepositoryUri(), ".git") + "/blob/" + settings.getDefaultBranch() + "/" + path + "?raw=true";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private GitRepository getRepository(String key) {
 | 
			
		||||
        GitRepository repository = repositories.get(key);
 | 
			
		||||
        if (repository != null) {
 | 
			
		||||
            if (Files.notExists(Path.of(repository.getDirectory()))) {
 | 
			
		||||
                // reinitializing the repository because folder was deleted
 | 
			
		||||
                initRepository(key, repository.getSettings());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        repository = repositories.get(key);
 | 
			
		||||
        if (repository == null) {
 | 
			
		||||
            throw new IllegalStateException("{} repository is not initialized");
 | 
			
		||||
        }
 | 
			
		||||
        return repository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void initRepository(String key, RepositorySettings settings) {
 | 
			
		||||
        try {
 | 
			
		||||
            repositories.remove(key);
 | 
			
		||||
            Path directory = getRepoDirectory(settings);
 | 
			
		||||
 | 
			
		||||
            GitRepository repository = GitRepository.openOrClone(directory, settings, true);
 | 
			
		||||
            repositories.put(key, repository);
 | 
			
		||||
            log.info("Initialized {} repository", key);
 | 
			
		||||
 | 
			
		||||
            onUpdate(key);
 | 
			
		||||
        } catch (Throwable e) {
 | 
			
		||||
            log.error("Failed to init {} repository for settings {}", key, settings, e);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void onUpdate(String key) {
 | 
			
		||||
        Runnable listener = updateListeners.get(key);
 | 
			
		||||
        if (listener != null) {
 | 
			
		||||
            listener.run();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Path getRepoDirectory(RepositorySettings settings) {
 | 
			
		||||
        // using uri to define folder name in case repo url is changed
 | 
			
		||||
        String name = URI.create(settings.getRepositoryUri()).getPath().replaceAll("[^a-zA-Z]", "");
 | 
			
		||||
        return Path.of(repositoriesFolder, name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private String getBranchRef(GitRepository repository) {
 | 
			
		||||
        return "refs/remotes/origin/" + repository.getSettings().getDefaultBranch();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @PreDestroy
 | 
			
		||||
    private void preDestroy() {
 | 
			
		||||
        executor.shutdownNow();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,33 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2024 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.service.sync;
 | 
			
		||||
 | 
			
		||||
import org.thingsboard.server.service.sync.vc.GitRepository.FileType;
 | 
			
		||||
import org.thingsboard.server.service.sync.vc.GitRepository.RepoFile;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
public interface GitSyncService {
 | 
			
		||||
 | 
			
		||||
    void registerSync(String key, String repoUri, String branch, long fetchFrequencyMs, Runnable onUpdate);
 | 
			
		||||
 | 
			
		||||
    List<RepoFile> listFiles(String key, String path, int depth, FileType type);
 | 
			
		||||
 | 
			
		||||
    String getFileContent(String key, String path);
 | 
			
		||||
 | 
			
		||||
    String getGithubRawContentUrl(String key, String path);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -283,22 +283,7 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
 | 
			
		||||
    private GitRepository openOrCloneRepository(TenantId tenantId, RepositorySettings settings, boolean fetch) throws Exception {
 | 
			
		||||
        log.debug("[{}] Init tenant repository started.", tenantId);
 | 
			
		||||
        Path repositoryDirectory = Path.of(repositoriesFolder, settings.isLocalOnly() ? "local_" + settings.getRepositoryUri() : tenantId.getId().toString());
 | 
			
		||||
 | 
			
		||||
        GitRepository repository;
 | 
			
		||||
        if (Files.exists(repositoryDirectory)) {
 | 
			
		||||
            repository = GitRepository.open(repositoryDirectory.toFile(), settings);
 | 
			
		||||
            if (fetch) {
 | 
			
		||||
                repository.fetch();
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Files.createDirectories(repositoryDirectory);
 | 
			
		||||
            if (settings.isLocalOnly()) {
 | 
			
		||||
                repository = GitRepository.create(settings, repositoryDirectory.toFile());
 | 
			
		||||
            } else {
 | 
			
		||||
                repository = GitRepository.clone(settings, repositoryDirectory.toFile());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        GitRepository repository = GitRepository.openOrClone(repositoryDirectory, settings, fetch);
 | 
			
		||||
        repositories.put(tenantId, repository);
 | 
			
		||||
        log.debug("[{}] Init tenant repository completed.", tenantId);
 | 
			
		||||
        return repository;
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ import com.google.common.collect.Streams;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import lombok.SneakyThrows;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.apache.commons.io.FileUtils;
 | 
			
		||||
import org.apache.commons.lang3.StringUtils;
 | 
			
		||||
@ -75,6 +76,7 @@ import java.io.IOException;
 | 
			
		||||
import java.net.InetSocketAddress;
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
import java.security.KeyPair;
 | 
			
		||||
import java.security.PublicKey;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
@ -136,6 +138,24 @@ public class GitRepository {
 | 
			
		||||
        return new GitRepository(git, settings, authHandler, directory.getAbsolutePath());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static GitRepository openOrClone(Path directory, RepositorySettings settings, boolean fetch) throws IOException, GitAPIException {
 | 
			
		||||
        GitRepository repository;
 | 
			
		||||
        if (Files.exists(directory)) {
 | 
			
		||||
            repository = GitRepository.open(directory.toFile(), settings);
 | 
			
		||||
            if (fetch) {
 | 
			
		||||
                repository.fetch();
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Files.createDirectories(directory);
 | 
			
		||||
            if (settings.isLocalOnly()) {
 | 
			
		||||
                repository = GitRepository.create(settings, directory.toFile());
 | 
			
		||||
            } else {
 | 
			
		||||
                repository = GitRepository.clone(settings, directory.toFile());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return repository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void test(RepositorySettings settings, File directory) throws Exception {
 | 
			
		||||
        if (settings.isLocalOnly()) {
 | 
			
		||||
            return;
 | 
			
		||||
@ -233,22 +253,29 @@ public class GitRepository {
 | 
			
		||||
        return iterableToPageData(commits, this::toCommit, pageLink, revCommitComparatorFunction);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public List<String> listFilesAtCommit(String commitId) throws IOException {
 | 
			
		||||
        return listFilesAtCommit(commitId, null);
 | 
			
		||||
    public List<String> listFilesAtCommit(String commitId, String path) {
 | 
			
		||||
        return listFilesAtCommit(commitId, path, -1).stream().map(RepoFile::path).toList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public List<String> listFilesAtCommit(String commitId, String path) throws IOException {
 | 
			
		||||
    @SneakyThrows
 | 
			
		||||
    public List<RepoFile> listFilesAtCommit(String commitId, String path, int depth) {
 | 
			
		||||
        log.debug("Executing listFilesAtCommit [{}][{}][{}]", settings.getRepositoryUri(), commitId, path);
 | 
			
		||||
        List<String> files = new ArrayList<>();
 | 
			
		||||
        List<RepoFile> files = new ArrayList<>();
 | 
			
		||||
        RevCommit revCommit = resolveCommit(commitId);
 | 
			
		||||
        try (TreeWalk treeWalk = new TreeWalk(git.getRepository())) {
 | 
			
		||||
            treeWalk.reset(revCommit.getTree().getId());
 | 
			
		||||
            if (StringUtils.isNotEmpty(path)) {
 | 
			
		||||
                treeWalk.setFilter(PathFilter.create(path));
 | 
			
		||||
            }
 | 
			
		||||
            treeWalk.setRecursive(true);
 | 
			
		||||
            boolean fixedDepth = depth != -1;
 | 
			
		||||
            treeWalk.setRecursive(!fixedDepth);
 | 
			
		||||
            while (treeWalk.next()) {
 | 
			
		||||
                files.add(treeWalk.getPathString());
 | 
			
		||||
                if (!fixedDepth || treeWalk.getDepth() == depth) {
 | 
			
		||||
                    files.add(new RepoFile(treeWalk.getPathString(), treeWalk.getNameString(), treeWalk.isSubtree() ? FileType.DIRECTORY : FileType.FILE));
 | 
			
		||||
                }
 | 
			
		||||
                if (fixedDepth && treeWalk.getDepth() < depth) {
 | 
			
		||||
                    treeWalk.enterSubtree();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return files;
 | 
			
		||||
@ -584,4 +611,10 @@ public class GitRepository {
 | 
			
		||||
        private String diffStringValue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public record RepoFile(String path, String name, FileType type) {}
 | 
			
		||||
 | 
			
		||||
    public enum FileType {
 | 
			
		||||
        FILE, DIRECTORY
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user